package gracha import ( "context" "database/sql" "encoding/hex" "encoding/json" "errors" "fmt" "runtime" "sync" "time" "guuid" "q" "scrypt" g "gobang" ) const ( defaultPrefix = "gracha" rollbackErrorFmt = "rollback error: %w; while executing: %w" NEW_USER = "new-user" SEND_CONFIRMATION_REQUEST = "send-confirmation-request" FORGOT_PASSWORD_REQUEST = "forgot-password-request" day = 24 * time.Hour ) var ( SessionDuration = 7 * day RegisterTimeout = 15 * time.Second ErrPassMismatch = errors.New("gracha: password confirmation mismatch") ErrTooShort = errors.New("gracha: password too short") ErrTimeout = errors.New("gracha: timeout when creating user") ErrRegistered = errors.New("gracha: user already registered") ErrBadCombo = errors.New("gracha: bad username/passphrase combo") ErrUnconfirmed = errors.New("gracha: email is not confirmed") ErrRevokedSession = errors.New("gracha: this session was revoked") ErrSessionExpired = errors.New("gracha: session expired") ) type queryT struct{ write string read string } type queriesT struct{ byEmail func(string) (userT, error) byToken func(guuid.UUID) (userT, error) register func(string, []byte, []byte) (userT, error) confirm func(guuid.UUID) (sessionT, error) login func(string, string) (sessionT, error) refresh func(guuid.UUID) (sessionT, error) reset func(int64, []byte, guuid.UUID) (sessionT, error) change func(int64, []byte) (sessionT, error) byUUID func(guuid.UUID) (sessionT, error) logout func(guuid.UUID) error outOthers func(guuid.UUID) error outAll func(guuid.UUID) error close func() error } type confirmationT struct{ } type userT struct{ id int64 timestamp time.Time uuid guuid.UUID email string username *string salt []byte pwhash []byte confirmed_at *time.Time metadata map[string]interface{} } type sessionT struct{ id int64 timestr string timestamp time.Time uuid guuid.UUID user_id int64 type_ string revoked_at *time.Time metadata map[string]interface{} } type consumerT struct{ topic string handlerFn func(authT) func(q.Message) error } type authT struct{ queries queriesT queue q.IQueue hasher func(scrypt.HashInput) resultT[[]byte] checker func(scrypt.CheckInput) resultT[bool] close func() } type IAuth interface{ Register(string, string, string) (userT, error) ResendConfirmation(string) error ConfirmEmail(guuid.UUID) (sessionT, error) LoginEmail(string, string) (sessionT, error) ForgotPassword(string) error Refresh(sessionT) (sessionT, error) ResetPassword(guuid.UUID, string, string) (sessionT, error) ChangePassword(userT, string, string, string) (sessionT, error) Logout(sessionT) error LogoutOthers(sessionT) error LogoutAll(sessionT) error Close() error } func tryRollback(db *sql.DB, ctx context.Context, err error) error { _, rollbackErr := db.ExecContext(ctx, "ROLLBACK;") if rollbackErr != nil { return fmt.Errorf( rollbackErrorFmt, rollbackErr, err, ) } return err } func inTx(db *sql.DB, fn func(context.Context) error) error { ctx := context.Background() _, err := db.ExecContext(ctx, "BEGIN IMMEDIATE;") if err != nil { return err } err = fn(ctx) if err != nil { return tryRollback(db, ctx, err) } _, err = db.ExecContext(ctx, "COMMIT;") if err != nil { return tryRollback(db, ctx, err) } return nil } func createTablesSQL(prefix string) queryT { const tmpl_write = ` CREATE TABLE IF NOT EXISTS "%s_users" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL DEFAULT (%s), uuid BLOB NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE, username TEXT UNIQUE, salt BLOB NOT NULL UNIQUE, pwhash BLOB NOT NULL, confirmed_at TEXT, confirmer_id INTEGER REFERENCES "%s"(id), metadata TEXT ); CREATE TABLE IF NOT EXISTS "%s_user_changes" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL DEFAULT (%s), user_id INTEGER NOT NULL REFERENCES "%s_users"(id), attribute TEXT NOT NULL, value TEXT NOT NULL, op BOOLEAN NOT NULL ); CREATE TABLE IF NOT EXISTS "%s_tokens" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL DEFAULT (%s), uuid BLOB NOT NULL UNIQUE, type TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS "%s_roles" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES "%s_users"(id), role TEXT NOT NULL, metadata TEXT, UNIQUE (user_id, role) ); CREATE TABLE IF NOT EXISTS "%s_role_changes" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL DEFAULT (%s), user_id INTEGER NOT NULL REFERENCES "%s_roles"(id), role TEXT NOT NULL, op BOOLEAN NOT NULL ); CREATE TABLE IF NOT EXISTS "%s_sessions" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL DEFAULT (%s), uuid BLOB NOT NULL UNIQUE, user_id INTEGER NOT NULL REFERENCES "%s_users"(id), type TEXT NOT NULL, revoked_at TEXT, revoker_id INTEGER REFERENCES "%s_users"(id), metadata TEXT ); CREATE TABLE IF NOT EXISTS "%s_attempts" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL DEFAULT (%s), user_id INTEGER REFERENCES "%s_users"(id), session_id INTEGER REFERENCES "%s_sessions"(id), metadata TEXT ); CREATE TABLE IF NOT EXISTS "%s_audit" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL DEFAULT (%s), uuid BLOB NOT NULL UNIQUE, attribute TEXT NOT NULL, value TEXT NOT NULL, op BOOLEAN NOT NULL, metadata TEXT ); ` return queryT{ write: fmt.Sprintf( tmpl_write, prefix, g.SQLiteNow, prefix, prefix, g.SQLiteNow, prefix, prefix, g.SQLiteNow, prefix, prefix, prefix, g.SQLiteNow, prefix, prefix, g.SQLiteNow, prefix, prefix, prefix, g.SQLiteNow, prefix, prefix, prefix, g.SQLiteNow, ), } } func createTables(db *sql.DB, prefix string) error { q := createTablesSQL(prefix) return inTx(db, func(ctx context.Context) error { _, err := db.ExecContext(ctx, q.write) return err }) } func byEmailSQL(prefix string) queryT { const tmpl_read = ` SELECT id, timestamp, uuid, email, username, pwhash, metadata FROM "%s_users" WHERE email = ?; ` return queryT{ read: fmt.Sprintf(tmpl_read, prefix), } } func byEmailStmt( db *sql.DB, prefix string, ) (func(string) (userT, error), func() error, error) { q := byEmailSQL(prefix) readStmt, err := db.Prepare(q.read) if err != nil { return nil, nil, err } fn := func(email string) (userT, error) { user := userT{ email: email, } var ( timestr string uuid_bytes []byte ) err := readStmt.QueryRow(email).Scan( &user.id, ×tr, &uuid_bytes, &user.username, &user.pwhash, &user.metadata, ) if err != nil { return userT{}, err } user.uuid = guuid.UUID(uuid_bytes) user.timestamp, err = time.Parse(time.RFC3339Nano,timestr) if err != nil { return userT{}, err } return user, nil } return fn, readStmt.Close, nil } func byTokenSQL(prefix string) queryT{ const tmpl_read = ` SELECT id, timestamp, uuid, email, username, pwhash, metadata FROM "%s" WHERE email = ?; ` return queryT{ read: fmt.Sprintf(tmpl_read, prefix), } } func byTokenStmt( db *sql.DB, prefix string, ) (func(guuid.UUID) (userT, error), func() error, error) { q := byTokenSQL(prefix) readStmt, err := db.Prepare(q.read) if err != nil { return nil, nil, err } fn := func(token guuid.UUID) (userT, error) { var user userT // FIXME: build user err := readStmt.QueryRow(token).Scan(&user.id) return user, err } return fn, readStmt.Close, nil } func registerSQL(prefix string) queryT { const tmpl_write = ` INSERT INTO "%s" (uuid, email, username, salt, pwhash, metadata) VALUES (?, ?, ?, ?, ?, ?); ` return queryT{ write: fmt.Sprintf(tmpl_write, prefix), } } func registerStmt( db *sql.DB, prefix string, ) (func(string, []byte, []byte) (userT, error), func() error, error) { q := registerSQL(prefix) writeStmt, err := db.Prepare(q.write) if err != nil { return nil, nil, err } fn := func(email string, salt []byte, pwhash []byte) (userT, error) { /* timestamp TEXT NOT NULL DEFAULT (%s), uuid BLOB NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE, username TEXT UNIQUE, pwhash TEXT NOT NULL, metadata TEXT */ var user userT // err := stmt.QueryRow( ret, err := writeStmt.Exec( guuid.New(), email, "credentials.username", salt, pwhash, "credentials.metadata", // ).Scan(&user.email) // FIXME: finish ) if false { fmt.Printf("ret: %#v\n", ret) fmt.Printf("user: %#v\n", user) } return user, err } return fn, writeStmt.Close, nil } func confirmSQL(prefix string) queryT { const tmpl_write = ` -- INSERT SOMETHING %s ` return queryT{ write: fmt.Sprintf(tmpl_write, prefix), } } func confirmStmt( db *sql.DB, prefix string, ) (func(guuid.UUID) (sessionT, error), func() error, error) { q := confirmSQL(prefix) writeStmt, err := db.Prepare(q.write) if err != nil { return nil, nil, err } fn := func(token guuid.UUID) (sessionT, error) { var session sessionT err := writeStmt.QueryRow(token).Scan(&session) return session, err } return fn, writeStmt.Close, nil } func loginSQL(prefix string) queryT { const tmpl_write = ` -- INSERT INTO "%s" (t3, t4) VALUES (?, ?); ` return queryT{ write: fmt.Sprintf(tmpl_write, prefix), } } func loginStmt( db *sql.DB, prefix string, ) (func(string, string) (sessionT, error), func() error, error) { q := loginSQL(prefix) writeStmt, err := db.Prepare(q.write) if err != nil { return nil, nil, err } fn := func(email string, pwhash string) (sessionT, error) { var session sessionT err := writeStmt.QueryRow(email, pwhash).Scan(session) // FIXME: finish return session, err } return fn, writeStmt.Close, nil } func refreshSQL(prefix string) queryT { const tmpl_write = ` -- INSERT SOMETHING %s ` return queryT{ write: fmt.Sprintf(tmpl_write, prefix), } } func refreshStmt( db *sql.DB, prefix string, ) (func(guuid.UUID) (sessionT, error), func() error, error) { q := refreshSQL(prefix) writeStmt, err := db.Prepare(q.write) if err != nil { return nil, nil, err } fn := func(uuid guuid.UUID) (sessionT, error) { var session sessionT err := writeStmt.QueryRow(uuid).Scan(&session) return session, err } return fn, writeStmt.Close, nil } func resetSQL(prefix string) queryT { const tmpl_write = ` -- INSERT SOMETHING %s ` return queryT{ write: fmt.Sprintf(tmpl_write, prefix), } } func resetStmt( db *sql.DB, prefix string, ) (func(int64, []byte, guuid.UUID) (sessionT, error), func() error, error) { q := resetSQL(prefix) writeStmt, err := db.Prepare(q.write) if err != nil { return nil, nil, err } fn := func(id int64, pwhash []byte, token guuid.UUID) (sessionT, error) { var session sessionT err := writeStmt.QueryRow(id, pwhash, token).Scan(&session) return session, err } return fn, writeStmt.Close, nil } func changeSQL(prefix string) queryT { const tmpl_write = ` -- INSERT SOMETHING %s ` return queryT{ write: fmt.Sprintf(tmpl_write, prefix), } } func changeStmt( db *sql.DB, prefix string, ) (func(int64, []byte) (sessionT, error), func() error, error) { q := changeSQL(prefix) writeStmt, err := db.Prepare(q.write) if err != nil { return nil, nil, err } fn := func(id int64, pwhash []byte) (sessionT, error) { var session sessionT err := writeStmt.QueryRow(id, pwhash).Scan(&session) return session, err } return fn, writeStmt.Close, nil } func byUUIDSQL(prefix string) queryT { const tmpl_read = ` -- INSERT SOMETHING %s ` return queryT{ read: fmt.Sprintf(tmpl_read, prefix), } } func byUUIDStmt( db *sql.DB, prefix string, ) (func(guuid.UUID) (sessionT, error), func() error, error) { q := byUUIDSQL(prefix) readStmt, err := db.Prepare(q.read) if err != nil { return nil, nil, err } fn := func(uuid guuid.UUID) (sessionT, error) { var session sessionT err := readStmt.QueryRow(uuid).Scan(&session) return session, err } return fn, readStmt.Close, nil } func logoutSQL(prefix string) queryT { const tmpl_write = ` -- INSERT SOMETHING %s ` return queryT{ write: fmt.Sprintf(tmpl_write, prefix), } } func logoutStmt( db *sql.DB, prefix string, ) (func(guuid.UUID) error, func() error, error) { q := logoutSQL(prefix) writeStmt, err := db.Prepare(q.write) if err != nil { return nil, nil, err } fn := func(uuid guuid.UUID) error { _, err := writeStmt.Exec(uuid) return err } return fn, writeStmt.Close, nil } func outOthersSQL(prefix string) queryT { const tmpl_write = ` -- INSERT SOMETHING %s ` return queryT{ write: fmt.Sprintf(tmpl_write, prefix), } } func outOthersStmt( db *sql.DB, prefix string, ) (func(guuid.UUID) error, func() error, error) { q := outOthersSQL(prefix) writeStmt, err := db.Prepare(q.write) if err != nil { return nil, nil, err } fn := func(uuid guuid.UUID) error { _, err := writeStmt.Exec(uuid) return err } return fn, writeStmt.Close, nil } func outAllSQL(prefix string) queryT { const tmpl_write = ` -- INSERT SOMETHING %s ` return queryT{ write: fmt.Sprintf(tmpl_write, prefix), } } func outAllStmt( db *sql.DB, prefix string, ) (func(guuid.UUID) error, func() error, error) { q := outAllSQL(prefix) writeStmt, err := db.Prepare(q.write) if err != nil { return nil, nil, err } fn := func(uuid guuid.UUID) error { _, err := writeStmt.Exec(uuid) return err } return fn, writeStmt.Close, nil } func initDB( db *sql.DB, prefix string, ) (queriesT, error) { createTablesErr := createTables(db, prefix) byEmail, byEmailClose, byEmailErr := byEmailStmt(db, prefix) byToken, byTokenClose, byTokenErr := byTokenStmt(db, prefix) register, registerClose, registerErr := registerStmt(db, prefix) confirm, confirmClose, confirmErr := confirmStmt(db, prefix) login, loginClose, loginErr := loginStmt(db, prefix) refresh, refreshClose, refreshErr := refreshStmt(db, prefix) reset, resetClose, resetErr := resetStmt(db, prefix) change, changeClose, changeErr := changeStmt(db, prefix) byUUID, byUUIDClose, byUUIDErr := byUUIDStmt(db, prefix) logout, logoutClose, logoutErr := logoutStmt(db, prefix) outOthers, outOthersClose, outOthersErr := outOthersStmt(db, prefix) outAll, outAllClose, outAllErr := outAllStmt(db, prefix) err := g.SomeError( createTablesErr, byEmailErr, byTokenErr, registerErr, confirmErr, loginErr, refreshErr, resetErr, changeErr, byUUIDErr, logoutErr, outOthersErr, outAllErr, ) if err != nil { return queriesT{}, err } close := func() error { return g.SomeFnError( byEmailClose, byTokenClose, registerClose, confirmClose, loginClose, refreshClose, resetClose, changeClose, byUUIDClose, logoutClose, outOthersClose, outAllClose, ) } // FIXME: lock return queriesT{ byEmail: byEmail, byToken: byToken, register: register, confirm: confirm, login: login, refresh: refresh, reset: reset, change: change, byUUID: byUUID, logout: logout, outOthers: outOthers, outAll: outAll, close: close, }, nil } func newUserHandler(auth authT) func(q.Message) error { return func(message q.Message) error { return nil } } func sendConfirmationRequestHandler(auth authT) func(q.Message) error { return func(message q.Message) error { return nil } } func forgotPasswordRequestHandler(auth authT) func(q.Message) error { return func(message q.Message) error { return nil } } var consumers = []consumerT{ consumerT{ topic: NEW_USER, handlerFn: newUserHandler, }, consumerT{ topic: SEND_CONFIRMATION_REQUEST, handlerFn: sendConfirmationRequestHandler, }, consumerT{ topic: FORGOT_PASSWORD_REQUEST, handlerFn: forgotPasswordRequestHandler, }, } func registerConsumers(auth authT, consumers []consumerT) { for _, consumer := range consumers { auth.queue.Subscribe( consumer.topic, defaultPrefix + "-" + consumer.topic, consumer.handlerFn(auth), ) } } type resultT[T any] struct{ value T err error } type taggedT[T any] struct{ id int value T } func startRunner[A any, B any]( in <-chan taggedT[A], out chan<- taggedT[B], fn func(A) B, done func(), ) { for input := range in { out <- taggedT[B]{ id: input.id, value: fn(input.value), } } done() } func makePoolRunner[A any, B any](count int, fn func(A) B) (func(A) B, func()) { var wg sync.WaitGroup wg.Add(count) in := make(chan taggedT[A]) out := make(chan taggedT[B]) for _ = range count { go startRunner(in, out, fn, wg.Done) } var mutex sync.Mutex m := map[int]chan B{} id := 0 go func() { for output := range out { mutex.Lock() defer mutex.Unlock() m[output.id] <- output.value close(m[output.id]) delete(m, output.id) } }() poolRunFn := func(input A) B { c := make(chan B) { mutex.Lock() defer mutex.Unlock() m[id] = c id++ } in <- taggedT[A]{ id: id, value: input, } return <- c } close := func() { close(in) wg.Wait() close(out) } return poolRunFn, close } func asResult[A any, B any](fn func(A) (B, error)) func(A) resultT[B] { return func(input A) resultT[B] { output, err := fn(input) return resultT[B]{ value: output, err: err, } } } func NewWithPrefix(db *sql.DB, queue q.IQueue, prefix string) (IAuth, error) { queries, err := initDB(db, prefix) if err != nil { return authT{}, err } numCPU := (runtime.NumCPU() / 2) + 1 hasher, closeHasher := makePoolRunner(numCPU, asResult(scrypt.Hash)) checker, closeChecker := makePoolRunner(numCPU, asResult(scrypt.Check)) close := func() { closeHasher() closeChecker() } return authT{ queries: queries, queue: queue, hasher: hasher, checker: checker, close: close, }, nil } func New(db *sql.DB, queue q.IQueue) (IAuth, error) { return NewWithPrefix(db, queue, defaultPrefix) } func newUserPayload(email string, salt []byte, pwhash []byte) ([]byte, error) { data := make(map[string]interface{}) data["email"] = email data["salt"] = hex.EncodeToString(salt) data["pwhash"] = hex.EncodeToString(pwhash) return json.Marshal(data) } func (auth authT) Register( email string, password string, confirmPassword string, ) (userT, error) { if password != confirmPassword { return userT{}, ErrPassMismatch } if len(password) < scrypt.MinimumPasswordLength { return userT{}, ErrTooShort } // special check for sql.ErrNoRows to combat enumeration attacks. // FIXME: how so? _, lookupErr := auth.queries.byEmail(email) if lookupErr != nil && lookupErr != sql.ErrNoRows { return userT{}, lookupErr } salt, err := scrypt.Salt() if err != nil { return userT{}, err } input := scrypt.HashInput{ Password: []byte(password), Salt: salt, } result := auth.hasher(input) /* We also try to register anyway, to prevent disk IO timing attacks. / FIXME: how so? */ flowID := guuid.New() waiter := auth.queue.WaitFor(NEW_USER, flowID, "register") defer waiter.Close() payload, err := newUserPayload(email, salt, result.value) if err != nil { return userT{}, err } unsent := q.UnsentMessage{ Topic: NEW_USER, FlowID: flowID, Payload: payload, } _, err = auth.queue.Publish(unsent) if err != nil { return userT{}, err } user := userT{} select { case <-time.After(RegisterTimeout): err = ErrTimeout case <-waiter.Channel: user, err = auth.queries.byEmail(email) } return user, nil } func sendConfirmationMessage( email string, flowID guuid.UUID, ) (q.UnsentMessage, error) { data := make(map[string]interface{}) data["email"] = email payload, err := json.Marshal(data) if err != nil { return q.UnsentMessage{}, err } return q.UnsentMessage{ Topic: SEND_CONFIRMATION_REQUEST, FlowID: flowID, Payload: payload, }, nil } func (auth authT) ResendConfirmation(email string) error { unsent, err := sendConfirmationMessage(email, guuid.New()) if err != nil { return err } _, err = auth.queue.Publish(unsent) return err } func (auth authT) ConfirmEmail(token guuid.UUID) (sessionT, error) { return auth.queries.confirm(token) } func (auth authT) LoginEmail( email string, password string, ) (sessionT, error) { if len(password) < scrypt.MinimumPasswordLength { return sessionT{}, ErrTooShort } // special check for sql.ErrNoRows to combat enumeration attacks. user, err := auth.queries.byEmail(email) if err != nil && err != sql.ErrNoRows { return sessionT{}, err } input := scrypt.CheckInput{ Password: []byte(password), Salt: user.salt, Hash: user.pwhash, } ok, err := scrypt.Check(input) if err != nil { return sessionT{}, err } if !ok { return sessionT{}, ErrBadCombo } if user.confirmed_at == nil { return sessionT{}, ErrUnconfirmed } dbSession, err := auth.queries.login(email, password) if err != nil { return sessionT{}, err } return dbSession, nil } func forgotPasswordMessage( email string, flowID guuid.UUID, ) (q.UnsentMessage, error) { data := make(map[string]interface{}) data["email"] = email payload, err := json.Marshal(data) if err != nil { return q.UnsentMessage{}, err } return q.UnsentMessage{ Topic: FORGOT_PASSWORD_REQUEST, FlowID: flowID, Payload: payload, }, nil } func (auth authT) ForgotPassword(email string) error { // special check for sql.ErrNoRows to combat enumeration attacks. user, err := auth.queries.byEmail(email) if err != nil && err != sql.ErrNoRows { return err } unsent, err := forgotPasswordMessage(user.email, guuid.New()) if err != nil { return err } _, err = auth.queue.Publish(unsent) return err } func checkSession(session sessionT, now time.Time) error { if session.revoked_at != nil { return ErrRevokedSession } if session.timestamp.Add(SessionDuration).After(now) { return ErrSessionExpired } return nil } func validateSession( lookupFn func(guuid.UUID) (sessionT, error), session sessionT, ) error { dbSession, err := lookupFn(session.uuid) if err != nil { return err } return checkSession(dbSession, time.Now()) } func (auth authT) Refresh(session sessionT) (sessionT, error) { err := validateSession(auth.queries.byUUID, session) if err != nil { return sessionT{}, err } return auth.queries.refresh(session.uuid) } func (auth authT) ResetPassword( token guuid.UUID, password string, confirmPassword string, ) (sessionT, error) { if password != confirmPassword { return sessionT{}, ErrPassMismatch } if len(password) < scrypt.MinimumPasswordLength { return sessionT{}, ErrTooShort } user, err := auth.queries.byToken(token) if err != nil { return sessionT{}, err } input := scrypt.HashInput{ Password: []byte(password), Salt: user.salt, } pwhash, err := scrypt.Hash(input) if err != nil { return sessionT{}, err } if user.confirmed_at != nil { return auth.queries.reset(user.id, pwhash, token) } else { // return auth.queries.confirm(user.id, pwhash, token) return auth.queries.confirm(token) } } func (auth authT) ChangePassword( user userT, currentPassword string, newPassword string, confirmNewPassword string, ) (sessionT, error) { if newPassword != confirmNewPassword { return sessionT{}, ErrPassMismatch } if len(newPassword) < scrypt.MinimumPasswordLength { return sessionT{}, ErrTooShort } // FIXME input := scrypt.HashInput{ Password: []byte(newPassword), Salt: user.salt, } pwhash, err := scrypt.Hash(input) if err != nil { return sessionT{}, err } if user.confirmed_at == nil { return sessionT{}, ErrUnconfirmed } return auth.queries.change(user.id, pwhash) } func runLogout( lookupFn func(guuid.UUID) (sessionT, error), session sessionT, queryFn func(guuid.UUID) error, ) error { err := validateSession(lookupFn, session) if err != nil { return err } return queryFn(session.uuid) } func (auth authT) Logout(session sessionT) error { return runLogout( auth.queries.byUUID, session, auth.queries.logout, ) } func (auth authT) LogoutOthers(session sessionT) error { return runLogout( auth.queries.byUUID, session, auth.queries.outOthers, ) } func (auth authT) LogoutAll(session sessionT) error { return runLogout( auth.queries.byUUID, session, auth.queries.outAll, ) } func (auth authT) Close() error { return auth.queries.close() } func Main() { g.Init() db, err := sql.Open("acude", "file:gracha.db?mode=memory&cache=shared") if err != nil { panic(err) } defer db.Close() queue, err := q.New(db) if err != nil { panic(err) } defer queue.Close() auth, err := New(db, queue) if err != nil { fmt.Println(err) panic(err) } user, err := auth.Register("contact@example.com", "password", "password") if false { fmt.Printf("user: %#v\n", user) fmt.Printf("err: %#v\n", err) } return fmt.Printf("q: %#v\n", queue) fmt.Printf("auth: %#v\n", auth) }