api.spaceplanner.app

Spaceplanner API
git clone git://jacobedwards.org/api.spaceplanner.app
Log | Files | Refs

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 }