user.go (7300B)
1 package backend 2 3 import ( 4 "errors" 5 "fmt" 6 "log" 7 "crypto/rand" 8 "math/big" 9 "net/smtp" 10 "database/sql" 11 "golang.org/x/crypto/bcrypt" 12 "github.com/stripe/stripe-go/v72" 13 ) 14 15 // Database representation of user 16 type User struct { 17 Name string `json:"name"` 18 hash string 19 stripeCustomerID string 20 } 21 22 type UserEmails struct { 23 Verified *string 24 Setting *string 25 } 26 27 func (e *Env) CreateUser(username string, password string) (*User, error) { 28 if username == "" { 29 return nil, errors.New("Empty username") 30 } 31 if len(password) < 8 { 32 return nil, errors.New("Password must be at least 8 characters long") 33 } 34 35 hash, err := bcrypt.GenerateFromPassword([]byte(password), 12) 36 if err != nil { 37 return nil, err 38 } 39 40 tx, err := e.DB.Begin() 41 if err != nil { 42 return nil, err 43 } 44 defer tx.Rollback() 45 46 c, err := e.Stripe.Customers.New(&stripe.CustomerParams{}) 47 if err != nil { 48 return nil, err 49 } 50 51 user := User{ Name: username, hash: string(hash), stripeCustomerID: c.ID } 52 if err := e.insertUser(tx, user); err != nil { 53 if _, err := e.Stripe.Customers.Del(c.ID, nil); err != nil { 54 log.Printf("%s: Unable to delete customer on error", c.ID) 55 } 56 return nil, err 57 } 58 return &user, tx.Commit() 59 } 60 61 func (e *Env) DeleteUser(username string) error { 62 tx, err := e.DB.Begin() 63 if err != nil { 64 return err 65 } 66 defer tx.Rollback() 67 68 del, err := e.CacheTxStmt(tx, "delete_user", "DELETE FROM spaceplanner.users WHERE users.name = $1") 69 if err != nil { 70 return err 71 } 72 73 user, err := e.GetUser(tx, username) 74 if err != nil { 75 return err 76 } 77 78 if _, err := del.Exec(username); err != nil { 79 return err 80 } 81 82 if _, err := e.Stripe.Customers.Del(user.stripeCustomerID, nil); err != nil { 83 return err 84 } 85 86 return tx.Commit() 87 } 88 89 func (e *Env) GetUser(tx *sql.Tx, username string) (User, error) { 90 if username == "" { 91 return User{}, errors.New("Empty username") 92 } 93 94 stmt, err := e.CacheTxStmt(tx, "get_user", "SELECT name, hash, stripe_customer_id FROM spaceplanner.users WHERE users.name = $1") 95 if err != nil { 96 return User{}, err 97 } 98 return scanUser(stmt.QueryRow(username)) 99 } 100 101 func (e *Env) LoginUser(username string, password string) (User, error) { 102 user, err := e.GetUser(nil, username) 103 if err != nil { 104 return User{}, err; 105 } 106 107 err = bcrypt.CompareHashAndPassword([]byte(user.hash), []byte(password)) 108 if err != nil { 109 return User{}, err 110 } 111 return user, nil; 112 } 113 114 // In the future this may return a Service 115 func (e *Env) UserService(username string) (*string, error) { 116 service, err := e.CacheStmt("get_user_service", 117 `SELECT product_id 118 FROM spaceplanner.subscriptions 119 WHERE active = true AND customer_id = ( 120 SELECT stripe_customer_id 121 FROM spaceplanner.users 122 WHERE name = $1 123 )`) 124 if err != nil { 125 return nil, err 126 } 127 128 var prod *string 129 err = service.QueryRow(username).Scan(&prod) 130 if err == sql.ErrNoRows { 131 return nil, nil 132 } 133 return prod, err 134 } 135 136 func (e *Env) VerifyEmail(username string, code *string) (bool, error) { 137 emails, err := e.UserEmails(username) 138 if err != nil { 139 return false, err 140 } 141 142 /* Unchanged; emails.Verified */ 143 if (emails.Setting == nil || emails.Verified == emails.Setting) { 144 return true, nil 145 } 146 147 if code == nil { 148 /* There is unemails.Verified email */ 149 return false, nil 150 } 151 152 target, err := e.emailCode(username, *emails.Setting, nil) 153 if err != nil { 154 return false, err 155 } 156 157 /* No match; unemails.Verified */ 158 if *code != target { 159 return false, nil 160 } 161 162 tx, err := e.DB.Begin() 163 if err != nil { 164 return false, err 165 } 166 defer tx.Rollback() 167 168 _, err = tx.Exec(`DELETE FROM spaceplanner.email_codes WHERE email = $1`, emails.Setting) 169 if err != nil { 170 return false, err 171 } 172 _, err = tx.Exec(`UPDATE spaceplanner.users SET verified_email = $2 WHERE name = $1`, 173 username, emails.Setting) 174 if err != nil { 175 return false, errors.New("UPDATE verified_email: " + err.Error()) 176 } 177 178 if err := e.updateStripeEmail(username, *emails.Setting); err != nil { 179 return false, err 180 } 181 182 /* Match; emails.Verified if successful db update */ 183 return true, tx.Commit() 184 } 185 186 func (e *Env) SendVerificationEmail(username string) error { 187 emails, err := e.UserEmails(username) 188 if err != nil { 189 return err 190 } 191 192 // In the future there may be more reasons to send a code (though I doubt it) 193 // but for now only if Setting differs from Verified and is not nil 194 if emails.Setting == nil { 195 return errors.New("No email to verify") 196 } 197 if emails.Verified != nil && emails.Verified == emails.Setting { 198 return errors.New("Email already verified") 199 } 200 201 code, err := genCode() 202 if err != nil { 203 return err 204 } 205 206 if _, err := e.emailCode(username, *emails.Setting, &code); err != nil { 207 return err 208 } 209 210 body := fmt.Sprintf( 211 `From: %s <%s@%s> 212 To: %s 213 Subject: Your Spaceplanner.app verification code 214 215 Your email verification code is 216 217 %s 218 219 It expires after 15 minutes. 220 221 If you did not try and set your email for spaceplanner.app, you can 222 safely ignore this message (and sorry to bother).`, e.Config.SMTP.Name, e.Config.SMTP.User, 223 e.Config.SMTP.Server, *emails.Setting, code) 224 225 addrs := []string{*emails.Setting} 226 log.Printf("Sending authentication code to %s at request of %s", *emails.Setting, username) 227 return smtp.SendMail(e.Config.SMTP.Server + ":" + e.Config.SMTP.Port, 228 e.SMTPAuth, e.Config.SMTP.User + "@" + e.Config.SMTP.Server, addrs, []byte(body)) 229 } 230 231 func (e *Env) UserEmails(username string) (UserEmails, error) { 232 get, err := e.CacheTxStmt(nil, "get_both_emails", ` 233 SELECT users.verified_email, user_settings.strval AS setting 234 FROM spaceplanner.users LEFT OUTER JOIN spaceplanner.user_settings 235 ON users.name = user_settings.username AND user_settings.name = 'email' 236 WHERE users.name = $1`) 237 if err != nil { 238 return UserEmails{}, err 239 } 240 241 var r UserEmails 242 return r, get.QueryRow(username).Scan(&r.Verified, &r.Setting) 243 } 244 245 func (e *Env) updateStripeEmail(username, email string) error { 246 user, err := e.GetUser(nil, username) 247 if err != nil { 248 return err 249 } 250 251 params := &stripe.CustomerParams{Email: stripe.String(email)} 252 _, err = e.Stripe.Customers.Update(user.stripeCustomerID, params) 253 return err 254 } 255 256 func (e *Env) emailCode(username, email string, code *string) (string, error) { 257 _, err := e.DB.Exec("DELETE FROM spaceplanner.email_codes WHERE age(created) > '15 minutes'") 258 if err != nil { 259 return "", err 260 } 261 262 if code != nil { 263 _, err := e.DB.Exec(`INSERT INTO spaceplanner.email_codes (username, email, code) 264 VALUES ($1, $2, $3) 265 ON CONFLICT (username, email) DO UPDATE 266 SET code = EXCLUDED.code`, username, email, code) 267 return "", err 268 } 269 270 var c string 271 err = e.DB.QueryRow(`SELECT code FROM spaceplanner.email_codes 272 WHERE username = $1 AND email = $2`, username, email).Scan(&c) 273 return c, err 274 } 275 276 func genCode() (string, error) { 277 n, err := rand.Int(rand.Reader, big.NewInt(999999)) 278 if err != nil { 279 return "", err 280 } 281 return fmt.Sprintf("%.6d", n), nil 282 } 283 284 func scanUser(s Scanner) (User, error) { 285 var u User 286 return u, s.Scan(&u.Name, &u.hash, &u.stripeCustomerID) 287 } 288 289 func (e *Env) insertUser(tx *sql.Tx, user User) error { 290 stmt, err := e.CacheTxStmt(tx, "insert_user", "INSERT INTO spaceplanner.users (name, hash, stripe_customer_id) VALUES ($1, $2, $3)") 291 if err != nil { 292 return err 293 } 294 _, err = stmt.Exec(user.Name, user.hash, user.stripeCustomerID) 295 return err 296 }