diff --git a/cmd/tulip/serve.go b/cmd/tulip/serve.go index bd3eb7d..5b81c00 100644 --- a/cmd/tulip/serve.go +++ b/cmd/tulip/serve.go @@ -3,13 +3,13 @@ package main import ( "context" "crypto/rand" - "database/sql" "encoding/json" - "errors" "flag" "fmt" "github.com/1f349/mjwt" + "github.com/1f349/tulip" "github.com/1f349/tulip/database" + "github.com/1f349/tulip/database/types" "github.com/1f349/tulip/mail/templates" "github.com/1f349/tulip/pages" "github.com/1f349/tulip/server" @@ -20,6 +20,7 @@ import ( "log" "os" "path/filepath" + "time" ) type serveCmd struct{ configPath string } @@ -78,7 +79,7 @@ func normalLoad(startUp server.Conf, wd string) { log.Fatal("[Tulip] Failed to open signing key file:", err) } - db, err := database.Open(filepath.Join(wd, "tulip.db.sqlite")) + db, err := tulip.InitDB(filepath.Join(wd, "tulip.db.sqlite")) if err != nil { log.Fatal("[Tulip] Failed to open database:", err) } @@ -102,42 +103,30 @@ func normalLoad(startUp server.Conf, wd string) { exit_reload.ExitReload("Tulip", func() {}, func() { // stop http server _ = srv.Close() - _ = db.Close() }) } -func genHmacKey() []byte { - a := make([]byte, 32) - n, err := rand.Reader.Read(a) - if err != nil { - log.Fatal("[Tulip] Failed to generate HMAC key") - } - if n != 32 { - log.Fatal("[Tulip] Failed to generate HMAC key") - } - return a -} - func checkDbHasUser(db *database.Queries) error { - tx, err := db.Begin() + value, err := db.HasUser(context.Background()) if err != nil { - return fmt.Errorf("failed to start transaction: %w", err) + return err } - defer tx.Rollback() - if err := tx.HasUser(); err != nil { - if errors.Is(err, sql.ErrNoRows) { - _, err := tx.InsertUser("Admin", "admin", "admin", "admin@localhost", false, types.RoleAdmin, false) - if err != nil { - return fmt.Errorf("failed to add user: %w", err) - } - if err := tx.Commit(); err != nil { - return fmt.Errorf("failed to commit transaction: %w", err) - } - // continue normal operation now - return nil - } else { - return fmt.Errorf("failed to check if table has a user: %w", err) + + if !value { + _, err := db.AddUser(context.Background(), database.AddUserParams{ + Name: "Admin", + Username: "admin", + Password: "admin", + Email: "admin@localhost", + EmailVerified: false, + Role: types.RoleAdmin, + UpdatedAt: time.Now(), + Active: false, + }) + if err != nil { + return fmt.Errorf("failed to add user: %w", err) } + // continue normal operation now } return nil } diff --git a/database/manage-oauth.sql.go b/database/manage-oauth.sql.go index 7626567..3e8095d 100644 --- a/database/manage-oauth.sql.go +++ b/database/manage-oauth.sql.go @@ -117,6 +117,24 @@ func (q *Queries) InsertClientApp(ctx context.Context, arg InsertClientAppParams return err } +const resetClientAppSecret = `-- name: ResetClientAppSecret :exec +UPDATE client_store +SET secret = ? +WHERE subject = ? + AND owner = ? +` + +type ResetClientAppSecretParams struct { + Secret string `json:"secret"` + Subject string `json:"subject"` + Owner string `json:"owner"` +} + +func (q *Queries) ResetClientAppSecret(ctx context.Context, arg ResetClientAppSecretParams) error { + _, err := q.db.ExecContext(ctx, resetClientAppSecret, arg.Secret, arg.Subject, arg.Owner) + return err +} + const updateClientApp = `-- name: UpdateClientApp :exec UPDATE client_store SET name = ?, @@ -150,21 +168,3 @@ func (q *Queries) UpdateClientApp(ctx context.Context, arg UpdateClientAppParams ) return err } - -const resetClientAppSecret = `-- name: resetClientAppSecret :exec -UPDATE client_store -SET secret = ? -WHERE subject = ? - AND owner = ? -` - -type resetClientAppSecretParams struct { - Secret string `json:"secret"` - Subject string `json:"subject"` - Owner string `json:"owner"` -} - -func (q *Queries) resetClientAppSecret(ctx context.Context, arg resetClientAppSecretParams) error { - _, err := q.db.ExecContext(ctx, resetClientAppSecret, arg.Secret, arg.Subject, arg.Owner) - return err -} diff --git a/database/manage-users.sql.go b/database/manage-users.sql.go index 910218a..3901ec9 100644 --- a/database/manage-users.sql.go +++ b/database/manage-users.sql.go @@ -90,14 +90,14 @@ func (q *Queries) UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) } const userEmailExists = `-- name: UserEmailExists :one -SELECT EXISTS(SELECT 1 FROM users WHERE email = ? AND email_verified = 1) +SELECT CAST(EXISTS(SELECT 1 FROM users WHERE email = ? AND email_verified = 1) AS BOOLEAN) AS email_exists ` -func (q *Queries) UserEmailExists(ctx context.Context, email string) (int64, error) { +func (q *Queries) UserEmailExists(ctx context.Context, email string) (bool, error) { row := q.db.QueryRowContext(ctx, userEmailExists, email) - var column_1 int64 - err := row.Scan(&column_1) - return column_1, err + var email_exists bool + err := row.Scan(&email_exists) + return email_exists, err } const verifyUserEmail = `-- name: VerifyUserEmail :exec diff --git a/database/password-wrapper.go b/database/password-wrapper.go index 6da6b4a..b9aa9dc 100644 --- a/database/password-wrapper.go +++ b/database/password-wrapper.go @@ -40,7 +40,8 @@ func (q *Queries) AddUser(ctx context.Context, arg AddUserParams) (string, error type CheckLoginResult struct { Subject string `json:"subject"` - HasTwoFactor bool `json:"hasTwoFactor"` + Name string `json:"name"` + HasOtp bool `json:"hasTwoFactor"` Email string `json:"email"` EmailVerified bool `json:"email_verified"` } @@ -56,8 +57,26 @@ func (q *Queries) CheckLogin(ctx context.Context, un, pw string) (CheckLoginResu } return CheckLoginResult{ Subject: login.Subject, - HasTwoFactor: login.HasOtp, + Name: login.Name, + HasOtp: login.HasOtp, Email: login.Email, EmailVerified: login.EmailVerified, }, nil } + +func (q *Queries) ChangePassword(ctx context.Context, subject, newPw string) error { + userPassword, err := q.getUserPassword(ctx, subject) + if err != nil { + return err + } + newPwHash, err := password.HashPassword(newPw) + if err != nil { + return err + } + return q.changeUserPassword(ctx, changeUserPasswordParams{ + Password: newPwHash, + UpdatedAt: time.Now(), + Subject: subject, + Password_2: userPassword, + }) +} diff --git a/database/queries/manage-oauth.sql b/database/queries/manage-oauth.sql index 100054c..7d95054 100644 --- a/database/queries/manage-oauth.sql +++ b/database/queries/manage-oauth.sql @@ -25,7 +25,7 @@ SET name = ?, WHERE subject = ? AND owner = ?; --- name: resetClientAppSecret :exec +-- name: ResetClientAppSecret :exec UPDATE client_store SET secret = ? WHERE subject = ? diff --git a/database/queries/manage-users.sql b/database/queries/manage-users.sql index b842424..c92db49 100644 --- a/database/queries/manage-users.sql +++ b/database/queries/manage-users.sql @@ -23,4 +23,4 @@ SET email_verified = 1 WHERE subject = ?; -- name: UserEmailExists :one -SELECT EXISTS(SELECT 1 FROM users WHERE email = ? AND email_verified = 1); +SELECT CAST(EXISTS(SELECT 1 FROM users WHERE email = ? AND email_verified = 1) AS BOOLEAN) AS email_exists; diff --git a/database/queries/users.sql b/database/queries/users.sql index 734292c..4b0a85b 100644 --- a/database/queries/users.sql +++ b/database/queries/users.sql @@ -1,5 +1,5 @@ -- name: HasUser :one -SELECT cast(count(subject) AS BOOLEAN) AS hasUser +SELECT CAST(count(subject) AS BOOLEAN) AS hasUser FROM users; -- name: addUser :exec @@ -7,7 +7,7 @@ INSERT INTO users (subject, name, username, password, email, email_verified, rol VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); -- name: checkLogin :one -SELECT subject, password, cast(EXISTS(SELECT 1 FROM otp WHERE otp.subject = users.subject) AS BOOLEAN) as has_otp, email, email_verified +SELECT subject, name, password, CAST(EXISTS(SELECT 1 FROM otp WHERE otp.subject = users.subject) AS BOOLEAN) AS has_otp, email, email_verified FROM users WHERE username = ? LIMIT 1; @@ -33,14 +33,14 @@ SELECT password FROM users WHERE subject = ?; --- name: changeUserPassword :execrows +-- name: changeUserPassword :exec UPDATE users SET password = ?, updated_at = ? WHERE subject = ? AND password = ?; --- name: ModifyUser :execrows +-- name: ModifyUser :exec UPDATE users SET name = ?, picture = ?, @@ -52,21 +52,26 @@ SET name = ?, updated_at=? WHERE subject = ?; --- name: SetTwoFactor :exec +-- name: SetOtp :exec INSERT OR REPLACE INTO otp (subject, secret, digits) VALUES (?, ?, ?); --- name: DeleteTwoFactor :exec +-- name: DeleteOtp :exec DELETE FROM otp WHERE otp.subject = ?; --- name: GetTwoFactor :one +-- name: GetOtp :one SELECT secret, digits FROM otp WHERE subject = ?; --- name: HasTwoFactor :one -SELECT cast(EXISTS(SELECT 1 FROM otp WHERE subject = ?) AS BOOLEAN); +-- name: HasOtp :one +SELECT CAST(EXISTS(SELECT 1 FROM otp WHERE subject = ?) AS BOOLEAN); + +-- name: GetUserEmail :one +SELECT email +FROM users +WHERE subject = ?; diff --git a/database/users.sql.go b/database/users.sql.go index da956b7..dc0e22a 100644 --- a/database/users.sql.go +++ b/database/users.sql.go @@ -14,31 +14,31 @@ import ( "github.com/1f349/tulip/password" ) -const deleteTwoFactor = `-- name: DeleteTwoFactor :exec +const deleteOtp = `-- name: DeleteOtp :exec DELETE FROM otp WHERE otp.subject = ? ` -func (q *Queries) DeleteTwoFactor(ctx context.Context, subject string) error { - _, err := q.db.ExecContext(ctx, deleteTwoFactor, subject) +func (q *Queries) DeleteOtp(ctx context.Context, subject string) error { + _, err := q.db.ExecContext(ctx, deleteOtp, subject) return err } -const getTwoFactor = `-- name: GetTwoFactor :one +const getOtp = `-- name: GetOtp :one SELECT secret, digits FROM otp WHERE subject = ? ` -type GetTwoFactorRow struct { +type GetOtpRow struct { Secret string `json:"secret"` Digits int64 `json:"digits"` } -func (q *Queries) GetTwoFactor(ctx context.Context, subject string) (GetTwoFactorRow, error) { - row := q.db.QueryRowContext(ctx, getTwoFactor, subject) - var i GetTwoFactorRow +func (q *Queries) GetOtp(ctx context.Context, subject string) (GetOtpRow, error) { + row := q.db.QueryRowContext(ctx, getOtp, subject) + var i GetOtpRow err := row.Scan(&i.Secret, &i.Digits) return i, err } @@ -87,6 +87,19 @@ func (q *Queries) GetUserDisplayName(ctx context.Context, subject string) (strin return name, err } +const getUserEmail = `-- name: GetUserEmail :one +SELECT email +FROM users +WHERE subject = ? +` + +func (q *Queries) GetUserEmail(ctx context.Context, subject string) (string, error) { + row := q.db.QueryRowContext(ctx, getUserEmail, subject) + var email string + err := row.Scan(&email) + return email, err +} + const getUserRole = `-- name: GetUserRole :one SELECT role FROM users @@ -100,19 +113,19 @@ func (q *Queries) GetUserRole(ctx context.Context, subject string) (types.UserRo return role, err } -const hasTwoFactor = `-- name: HasTwoFactor :one -SELECT cast(EXISTS(SELECT 1 FROM otp WHERE subject = ?) AS BOOLEAN) +const hasOtp = `-- name: HasOtp :one +SELECT CAST(EXISTS(SELECT 1 FROM otp WHERE subject = ?) AS BOOLEAN) ` -func (q *Queries) HasTwoFactor(ctx context.Context, subject string) (bool, error) { - row := q.db.QueryRowContext(ctx, hasTwoFactor, subject) +func (q *Queries) HasOtp(ctx context.Context, subject string) (bool, error) { + row := q.db.QueryRowContext(ctx, hasOtp, subject) var column_1 bool err := row.Scan(&column_1) return column_1, err } const hasUser = `-- name: HasUser :one -SELECT cast(count(subject) AS BOOLEAN) AS hasUser +SELECT CAST(count(subject) AS BOOLEAN) AS hasUser FROM users ` @@ -123,7 +136,7 @@ func (q *Queries) HasUser(ctx context.Context) (bool, error) { return hasuser, err } -const modifyUser = `-- name: ModifyUser :execrows +const modifyUser = `-- name: ModifyUser :exec UPDATE users SET name = ?, picture = ?, @@ -148,8 +161,8 @@ type ModifyUserParams struct { Subject string `json:"subject"` } -func (q *Queries) ModifyUser(ctx context.Context, arg ModifyUserParams) (int64, error) { - result, err := q.db.ExecContext(ctx, modifyUser, +func (q *Queries) ModifyUser(ctx context.Context, arg ModifyUserParams) error { + _, err := q.db.ExecContext(ctx, modifyUser, arg.Name, arg.Picture, arg.Website, @@ -160,27 +173,24 @@ func (q *Queries) ModifyUser(ctx context.Context, arg ModifyUserParams) (int64, arg.UpdatedAt, arg.Subject, ) - if err != nil { - return 0, err - } - return result.RowsAffected() + return err } -const setTwoFactor = `-- name: SetTwoFactor :exec +const setOtp = `-- name: SetOtp :exec INSERT OR REPLACE INTO otp (subject, secret, digits) VALUES (?, ?, ?) ` -type SetTwoFactorParams struct { +type SetOtpParams struct { Subject string `json:"subject"` Secret string `json:"secret"` Digits int64 `json:"digits"` } -func (q *Queries) SetTwoFactor(ctx context.Context, arg SetTwoFactorParams) error { - _, err := q.db.ExecContext(ctx, setTwoFactor, arg.Subject, arg.Secret, arg.Digits) +func (q *Queries) SetOtp(ctx context.Context, arg SetOtpParams) error { + _, err := q.db.ExecContext(ctx, setOtp, arg.Subject, arg.Secret, arg.Digits) return err } @@ -216,7 +226,7 @@ func (q *Queries) addUser(ctx context.Context, arg addUserParams) error { return err } -const changeUserPassword = `-- name: changeUserPassword :execrows +const changeUserPassword = `-- name: changeUserPassword :exec UPDATE users SET password = ?, updated_at = ? @@ -231,21 +241,18 @@ type changeUserPasswordParams struct { Password_2 password.HashString `json:"password_2"` } -func (q *Queries) changeUserPassword(ctx context.Context, arg changeUserPasswordParams) (int64, error) { - result, err := q.db.ExecContext(ctx, changeUserPassword, +func (q *Queries) changeUserPassword(ctx context.Context, arg changeUserPasswordParams) error { + _, err := q.db.ExecContext(ctx, changeUserPassword, arg.Password, arg.UpdatedAt, arg.Subject, arg.Password_2, ) - if err != nil { - return 0, err - } - return result.RowsAffected() + return err } const checkLogin = `-- name: checkLogin :one -SELECT subject, password, cast(EXISTS(SELECT 1 FROM otp WHERE otp.subject = users.subject) AS BOOLEAN) as has_otp, email, email_verified +SELECT subject, name, password, CAST(EXISTS(SELECT 1 FROM otp WHERE otp.subject = users.subject) AS BOOLEAN) AS has_otp, email, email_verified FROM users WHERE username = ? LIMIT 1 @@ -253,6 +260,7 @@ LIMIT 1 type checkLoginRow struct { Subject string `json:"subject"` + Name string `json:"name"` Password password.HashString `json:"password"` HasOtp bool `json:"has_otp"` Email string `json:"email"` @@ -264,6 +272,7 @@ func (q *Queries) checkLogin(ctx context.Context, username string) (checkLoginRo var i checkLoginRow err := row.Scan( &i.Subject, + &i.Name, &i.Password, &i.HasOtp, &i.Email, diff --git a/go.mod b/go.mod index a351794..f083f43 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.22 require ( github.com/1f349/cache v0.0.2 - github.com/1f349/mjwt v0.2.2 + github.com/1f349/mjwt v0.2.5 github.com/1f349/overlapfs v0.0.1 github.com/1f349/violet v0.0.13 github.com/MrMelon54/exit-reload v0.0.1 @@ -14,14 +14,15 @@ require ( github.com/emersion/go-smtp v0.20.2 github.com/go-oauth2/oauth2/v4 v4.5.2 github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/golang-migrate/migrate/v4 v4.17.0 github.com/google/subcommands v1.2.0 github.com/google/uuid v1.6.0 github.com/julienschmidt/httprouter v1.3.0 github.com/mattn/go-sqlite3 v1.14.22 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/xlzd/gotp v0.1.0 - golang.org/x/crypto v0.19.0 + golang.org/x/crypto v0.21.0 golang.org/x/text v0.14.0 ) @@ -31,16 +32,19 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/tidwall/btree v1.7.0 // indirect github.com/tidwall/buntdb v1.3.0 // indirect - github.com/tidwall/gjson v1.17.0 // indirect + github.com/tidwall/gjson v1.17.1 // indirect github.com/tidwall/grect v0.1.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/rtred v0.1.2 // indirect github.com/tidwall/tinyqueue v0.1.1 // indirect - golang.org/x/net v0.21.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/net v0.22.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ffb00f5..bedfb92 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/1f349/cache v0.0.2 h1:27QD6zPd9xYyvh9V1qqWq+EAt5+N+qvyGWKfnjMrhP8= github.com/1f349/cache v0.0.2/go.mod h1:LibAMy13dF0KO1fQA9aEjZPBCB6Y4b5kKYEQJUqc2rQ= -github.com/1f349/mjwt v0.2.2 h1:mVw71zcf0D7dWgZXMvjXMq8oNn41V1DFyLY0Ywkq1VQ= -github.com/1f349/mjwt v0.2.2/go.mod h1:KEs6jd9JjWrQW+8feP2pGAU7pdA3aYTqjkT/YQr73PU= +github.com/1f349/mjwt v0.2.5 h1:IxjLaali22ayTzZ628lH7j0JDdYJoj6+CJ/VktCqtXQ= +github.com/1f349/mjwt v0.2.5/go.mod h1:KEs6jd9JjWrQW+8feP2pGAU7pdA3aYTqjkT/YQr73PU= github.com/1f349/overlapfs v0.0.1 h1:LAxBolrXFAgU0yqZtXg/C/aaPq3eoQSPpBc49BHuTp0= github.com/1f349/overlapfs v0.0.1/go.mod h1:I6aItQycr7nrzplmfNXp/QF9tTmKRSgY3fXmu/7Ky2o= github.com/1f349/violet v0.0.13 h1:lJpTz15Ea83Uc1VAISXTjtKuzr8Pe8NM4cMGp3Aiyhk= @@ -46,6 +46,8 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU= +github.com/golang-migrate/migrate/v4 v4.17.0/go.mod h1:+Cp2mtLP4/aXDTKb9wmXYitdrNx2HGs45rbWAo6OsKM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= @@ -57,8 +59,9 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -69,6 +72,11 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= @@ -77,14 +85,17 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= -github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= @@ -116,8 +127,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI= github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8= github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= @@ -128,8 +139,8 @@ github.com/tidwall/buntdb v1.3.0 h1:gdhWO+/YwoB2qZMeAU9JcWWsHSYU3OvcieYgFRS0zwA= github.com/tidwall/buntdb v1.3.0/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU= github.com/tidwall/gjson v1.3.4/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= -github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M= github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg= github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q= @@ -169,11 +180,13 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3Ifn github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -188,8 +201,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/initdb.go b/initdb.go new file mode 100644 index 0000000..de4411f --- /dev/null +++ b/initdb.go @@ -0,0 +1,38 @@ +package tulip + +import ( + "database/sql" + "embed" + "errors" + "github.com/1f349/tulip/database" + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/sqlite3" + "github.com/golang-migrate/migrate/v4/source/iofs" +) + +//go:embed database/migrations/*.sql +var migrations embed.FS + +func InitDB(p string) (*database.Queries, error) { + migDrv, err := iofs.New(migrations, "database/migrations") + if err != nil { + return nil, err + } + dbOpen, err := sql.Open("sqlite3", p) + if err != nil { + return nil, err + } + dbDrv, err := sqlite3.WithInstance(dbOpen, &sqlite3.Config{}) + if err != nil { + return nil, err + } + mig, err := migrate.NewWithInstance("iofs", migDrv, "sqlite3", dbDrv) + if err != nil { + return nil, err + } + err = mig.Up() + if err != nil && !errors.Is(err, migrate.ErrNoChange) { + return nil, err + } + return database.New(dbOpen), nil +} diff --git a/server/edit.go b/server/edit.go index 4c96d5f..2b2ac63 100644 --- a/server/edit.go +++ b/server/edit.go @@ -76,7 +76,7 @@ func (h *HttpServer) EditPost(rw http.ResponseWriter, req *http.Request, _ httpr Subject: auth.ID, } if h.DbTx(rw, func(tx *database.Queries) error { - if _, err := tx.ModifyUser(req.Context(), m); err != nil { + if err := tx.ModifyUser(req.Context(), m); err != nil { return fmt.Errorf("failed to modify user info: %w", err) } return nil diff --git a/server/home.go b/server/home.go index 802fdfa..6cd9a54 100644 --- a/server/home.go +++ b/server/home.go @@ -38,7 +38,7 @@ func (h *HttpServer) Home(rw http.ResponseWriter, req *http.Request, _ httproute if err != nil { return fmt.Errorf("failed to get user display name: %w", err) } - hasTwoFactor, err = tx.HasTwoFactor(req.Context(), auth.ID) + hasTwoFactor, err = tx.HasOtp(req.Context(), auth.ID) if err != nil { return fmt.Errorf("failed to get user two factor state: %w", err) } diff --git a/server/login.go b/server/login.go index 6ea840a..f0c361e 100644 --- a/server/login.go +++ b/server/login.go @@ -58,7 +58,7 @@ func (h *HttpServer) LoginPost(rw http.ResponseWriter, req *http.Request, _ http pw := req.FormValue("password") // flags returned from database call - var userInfo *database.User + var userInfo database.CheckLoginResult var loginMismatch byte var hasOtp bool @@ -74,8 +74,8 @@ func (h *HttpServer) LoginPost(rw http.ResponseWriter, req *http.Request, _ http } userInfo = loginUser - hasOtp = hasOtpRaw - if !hasVerifiedEmail { + hasOtp = loginUser.HasOtp + if !loginUser.EmailVerified { loginMismatch = 2 } return nil @@ -100,7 +100,7 @@ func (h *HttpServer) LoginPost(rw http.ResponseWriter, req *http.Request, _ http } u := uuid.NewString() - h.mailLinkCache.Set(mailLinkKey{mailLinkVerifyEmail, u}, userInfo.Sub, time.Now().Add(10*time.Minute)) + h.mailLinkCache.Set(mailLinkKey{mailLinkVerifyEmail, u}, userInfo.Subject, time.Now().Add(10*time.Minute)) // try to send email err = h.conf.Mail.SendEmailTemplate("mail-verify", "Verify Email", userInfo.Name, address, map[string]any{ @@ -122,7 +122,7 @@ func (h *HttpServer) LoginPost(rw http.ResponseWriter, req *http.Request, _ http // only continues if the above tx succeeds auth = UserAuth{ - ID: userInfo.Sub, + ID: userInfo.Subject, NeedOtp: hasOtp, } @@ -177,7 +177,7 @@ func (h *HttpServer) LoginResetPasswordPost(rw http.ResponseWriter, req *http.Re var emailExists bool if h.DbTx(rw, func(tx *database.Queries) (err error) { - emailExists, err = tx.UserEmailExists(email) + emailExists, err = tx.UserEmailExists(req.Context(), email) return err }) { return @@ -190,4 +190,6 @@ func (h *HttpServer) LoginResetPasswordPost(rw http.ResponseWriter, req *http.Re func (h *HttpServer) possiblySendPasswordResetEmail(email string, exists bool) { // TODO(Melon): Send reset password email template + _ = email + _ = exists } diff --git a/server/mail.go b/server/mail.go index a188c55..de0650d 100644 --- a/server/mail.go +++ b/server/mail.go @@ -2,13 +2,14 @@ package server import ( "github.com/1f349/tulip/database" + "github.com/1f349/tulip/database/types" "github.com/1f349/tulip/pages" "github.com/emersion/go-message/mail" "github.com/julienschmidt/httprouter" "net/http" ) -func (h *HttpServer) MailVerify(rw http.ResponseWriter, _ *http.Request, params httprouter.Params) { +func (h *HttpServer) MailVerify(rw http.ResponseWriter, req *http.Request, params httprouter.Params) { code := params.ByName("code") k := mailLinkKey{mailLinkVerifyEmail, code} @@ -19,7 +20,7 @@ func (h *HttpServer) MailVerify(rw http.ResponseWriter, _ *http.Request, params return } if h.DbTx(rw, func(tx *database.Queries) error { - return tx.VerifyUserEmail(userSub) + return tx.VerifyUserEmail(req.Context(), userSub) }) { return } @@ -76,7 +77,7 @@ func (h *HttpServer) MailPasswordPost(rw http.ResponseWriter, req *http.Request, // reset password database call if h.DbTx(rw, func(tx *database.Queries) error { - return tx.UserResetPassword(userSub, pw) + return tx.ChangePassword(req.Context(), userSub, pw) }) { return } @@ -84,7 +85,7 @@ func (h *HttpServer) MailPasswordPost(rw http.ResponseWriter, req *http.Request, http.Error(rw, "Reset password successfully, you can login now.", http.StatusOK) } -func (h *HttpServer) MailDelete(rw http.ResponseWriter, _ *http.Request, params httprouter.Params) { +func (h *HttpServer) MailDelete(rw http.ResponseWriter, req *http.Request, params httprouter.Params) { code := params.ByName("code") k := mailLinkKey{mailLinkDelete, code} @@ -93,13 +94,17 @@ func (h *HttpServer) MailDelete(rw http.ResponseWriter, _ *http.Request, params http.Error(rw, "Invalid email delete code", http.StatusBadRequest) return } - var userInfo *database.User + var userInfo database.User if h.DbTx(rw, func(tx *database.Queries) (err error) { - userInfo, err = tx.GetUser(userSub) + userInfo, err = tx.GetUser(req.Context(), userSub) if err != nil { return } - return tx.UpdateUser(userSub, types.RoleToDelete, false) + return tx.UpdateUserRole(req.Context(), database.UpdateUserRoleParams{ + Active: false, + Role: types.RoleToDelete, + Subject: userSub, + }) }) { return } diff --git a/server/manage-apps.go b/server/manage-apps.go index 29a554f..42ec0eb 100644 --- a/server/manage-apps.go +++ b/server/manage-apps.go @@ -2,8 +2,10 @@ package server import ( "github.com/1f349/tulip/database" + "github.com/1f349/tulip/database/types" "github.com/1f349/tulip/pages" - "github.com/go-oauth2/oauth2/v4" + "github.com/1f349/tulip/password" + "github.com/google/uuid" "github.com/julienschmidt/httprouter" "net/http" "net/url" @@ -23,13 +25,17 @@ func (h *HttpServer) ManageAppsGet(rw http.ResponseWriter, req *http.Request, _ } var role types.UserRole - var appList []database.ClientStore + var appList []database.GetAppListRow if h.DbTx(rw, func(tx *database.Queries) (err error) { - role, err = tx.GetUserRole(auth.ID) + role, err = tx.GetUserRole(req.Context(), auth.ID) if err != nil { return } - appList, err = tx.GetAppList(auth.ID, role == types.RoleAdmin, offset) + appList, err = tx.GetAppList(req.Context(), database.GetAppListParams{ + Owner: auth.ID, + Column2: role == types.RoleAdmin, + Offset: int64(offset), + }) return }) { return @@ -45,7 +51,7 @@ func (h *HttpServer) ManageAppsGet(rw http.ResponseWriter, req *http.Request, _ } if q.Has("edit") { for _, i := range appList { - if i.Sub == q.Get("edit") { + if i.Subject == q.Get("edit") { m["Edit"] = i goto validEdit } @@ -78,7 +84,7 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _ if sso { var role types.UserRole if h.DbTx(rw, func(tx *database.Queries) (err error) { - role, err = tx.GetUserRole(auth.ID) + role, err = tx.GetUserRole(req.Context(), auth.ID) return }) { return @@ -92,35 +98,61 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _ switch action { case "create": if h.DbTx(rw, func(tx *database.Queries) error { - return tx.InsertClientApp(name, domain, public, sso, active, auth.ID) + secret, err := password.GenerateApiSecret(70) + if err != nil { + return err + } + return tx.InsertClientApp(req.Context(), database.InsertClientAppParams{ + Subject: uuid.NewString(), + Name: name, + Secret: secret, + Domain: domain, + Owner: auth.ID, + Public: public, + Sso: sso, + Active: active, + }) }) { return } case "edit": if h.DbTx(rw, func(tx *database.Queries) error { - return tx.UpdateClientApp(req.Form.Get("subject"), auth.ID, name, domain, public, sso, active) + return tx.UpdateClientApp(req.Context(), database.UpdateClientAppParams{ + Name: name, + Domain: domain, + Public: public, + Sso: sso, + Active: active, + Subject: req.FormValue("subject"), + Owner: auth.ID, + }) }) { return } case "secret": - var info oauth2.ClientInfo + var info database.ClientStore var secret string if h.DbTx(rw, func(tx *database.Queries) error { sub := req.Form.Get("subject") - info, err = tx.GetClientInfo(sub) + info, err = tx.GetClientInfo(req.Context(), sub) if err != nil { return err } - secret, err = tx.ResetClientAppSecret(sub, auth.ID) + secret, err := password.GenerateApiSecret(70) + if err != nil { + return err + } + err = tx.ResetClientAppSecret(req.Context(), database.ResetClientAppSecretParams{ + Secret: secret, + Subject: sub, + Owner: auth.ID, + }) return err }) { return } - appName := "Unknown..." - if getName, ok := info.(interface{ GetName() string }); ok { - appName = getName.GetName() - } + appName := info.GetName() h.ManageAppsGet(rw, &http.Request{ URL: &url.URL{ diff --git a/server/manage-users.go b/server/manage-users.go index c40781c..7717358 100644 --- a/server/manage-users.go +++ b/server/manage-users.go @@ -29,17 +29,18 @@ func (h *HttpServer) ManageUsersGet(rw http.ResponseWriter, req *http.Request, _ } var role types.UserRole - var userList []database.User + var userList []database.GetUserListRow if h.DbTx(rw, func(tx *database.Queries) (err error) { - role, err = tx.GetUserRole(auth.ID) + role, err = tx.GetUserRole(req.Context(), auth.ID) if err != nil { return } - userList, err = tx.GetUserList(offset) + userList, err = tx.GetUserList(req.Context(), int64(offset)) return }) { return } + if role != types.RoleAdmin { http.Error(rw, "403 Forbidden", http.StatusForbidden) return @@ -55,7 +56,7 @@ func (h *HttpServer) ManageUsersGet(rw http.ResponseWriter, req *http.Request, _ } if q.Has("edit") { for _, i := range userList { - if i.Sub == q.Get("edit") { + if i.Subject == q.Get("edit") { m["Edit"] = i goto validEdit } @@ -79,7 +80,7 @@ func (h *HttpServer) ManageUsersPost(rw http.ResponseWriter, req *http.Request, var role types.UserRole if h.DbTx(rw, func(tx *database.Queries) (err error) { - role, err = tx.GetUserRole(auth.ID) + role, err = tx.GetUserRole(req.Context(), auth.ID) return }) { return @@ -116,17 +117,26 @@ func (h *HttpServer) ManageUsersPost(rw http.ResponseWriter, req *http.Request, } addrDomain := address.Address[n+1:] - var userSub uuid.UUID + var userSub string if h.DbTx(rw, func(tx *database.Queries) (err error) { - userSub, err = tx.InsertUser(name, username, "", email, addrDomain == h.conf.Namespace, newRole, active) + userSub, err = tx.AddUser(req.Context(), database.AddUserParams{ + Name: name, + Username: username, + Password: "", + Email: email, + EmailVerified: addrDomain == h.conf.Namespace, + Role: newRole, + UpdatedAt: time.Now(), + Active: active, + }) return err }) { return } u, u2 := uuid.NewString(), uuid.NewString() - h.mailLinkCache.Set(mailLinkKey{mailLinkResetPassword, u}, userSub.String(), time.Now().Add(10*time.Minute)) - h.mailLinkCache.Set(mailLinkKey{mailLinkDelete, u2}, userSub.String(), time.Now().Add(10*time.Minute)) + h.mailLinkCache.Set(mailLinkKey{mailLinkResetPassword, u}, userSub, time.Now().Add(10*time.Minute)) + h.mailLinkCache.Set(mailLinkKey{mailLinkDelete, u2}, userSub, time.Now().Add(10*time.Minute)) err = h.conf.Mail.SendEmailTemplate("mail-register-admin", "Register", name, address, map[string]any{ "RegisterUrl": h.conf.BaseUrl + "/mail/password/" + u, @@ -139,7 +149,11 @@ func (h *HttpServer) ManageUsersPost(rw http.ResponseWriter, req *http.Request, case "edit": if h.DbTx(rw, func(tx *database.Queries) error { sub := req.Form.Get("subject") - return tx.UpdateUser(sub, newRole, active) + return tx.UpdateUserRole(req.Context(), database.UpdateUserRoleParams{ + Active: active, + Role: newRole, + Subject: sub, + }) }) { return } diff --git a/server/oauth.go b/server/oauth.go index 1b9bf88..c69bcbc 100644 --- a/server/oauth.go +++ b/server/oauth.go @@ -77,17 +77,14 @@ func (h *HttpServer) authorizeEndpoint(rw http.ResponseWriter, req *http.Request } } - var user *database.User + var user string var hasOtp bool if h.DbTx(rw, func(tx *database.Queries) (err error) { - user, err = tx.GetUserDisplayName(auth.ID) - if err != nil { - return - } - hasOtp, err = tx.HasTwoFactor(auth.ID) + user, err = tx.GetUserDisplayName(req.Context(), auth.ID) if err != nil { return } + hasOtp, err = tx.HasOtp(req.Context(), auth.ID) return }) { return diff --git a/server/otp.go b/server/otp.go index 17df3c1..7340b20 100644 --- a/server/otp.go +++ b/server/otp.go @@ -2,6 +2,7 @@ package server import ( "bytes" + "context" "encoding/base64" "github.com/1f349/tulip/database" "github.com/1f349/tulip/pages" @@ -45,15 +46,18 @@ func (h *HttpServer) LoginOtpPost(rw http.ResponseWriter, req *http.Request, _ h func (h *HttpServer) fetchAndValidateOtp(rw http.ResponseWriter, sub, code string) bool { var hasOtp bool + var otpRow database.GetOtpRow var secret string - var digits int + var digits int64 if h.DbTx(rw, func(tx *database.Queries) (err error) { - hasOtp, err = tx.HasTwoFactor(sub) + hasOtp, err = tx.HasOtp(context.Background(), sub) if err != nil { return } if hasOtp { - secret, digits, err = tx.GetTwoFactor(sub) + otpRow, err = tx.GetOtp(context.Background(), sub) + secret = otpRow.Secret + digits = otpRow.Digits } return }) { @@ -61,7 +65,7 @@ func (h *HttpServer) fetchAndValidateOtp(rw http.ResponseWriter, sub, code strin } if hasOtp { - totp := gotp.NewTOTP(secret, digits, 30, nil) + totp := gotp.NewTOTP(secret, int(digits), 30, nil) if !verifyTotp(totp, code) { http.Error(rw, "400 Bad Request: Invalid OTP code", http.StatusBadRequest) return true @@ -87,7 +91,11 @@ func (h *HttpServer) EditOtpPost(rw http.ResponseWriter, req *http.Request, _ ht } if h.DbTx(rw, func(tx *database.Queries) error { - return tx.SetTwoFactor(auth.ID, "", 0) + return tx.SetOtp(req.Context(), database.SetOtpParams{ + Subject: auth.ID, + Secret: "", + Digits: 0, + }) }) { return } @@ -120,7 +128,7 @@ func (h *HttpServer) EditOtpPost(rw http.ResponseWriter, req *http.Request, _ ht var email string if h.DbTx(rw, func(tx *database.Queries) error { var err error - email, err = tx.GetUserEmail(auth.ID) + email, err = tx.GetUserEmail(req.Context(), auth.ID) return err }) { return @@ -168,7 +176,11 @@ func (h *HttpServer) EditOtpPost(rw http.ResponseWriter, req *http.Request, _ ht } if h.DbTx(rw, func(tx *database.Queries) error { - return tx.SetTwoFactor(auth.ID, secret, digits) + return tx.SetOtp(req.Context(), database.SetOtpParams{ + Subject: auth.ID, + Secret: secret, + Digits: int64(digits), + }) }) { return } diff --git a/server/server.go b/server/server.go index 5deb729..bc55d40 100644 --- a/server/server.go +++ b/server/server.go @@ -191,7 +191,7 @@ func NewHttpServer(conf Conf, db *database.Queries, signingKey mjwt.Signer) *htt return } - var userData database.GetUserRow + var userData database.User if hs.DbTx(rw, func(tx *database.Queries) (err error) { userData, err = tx.GetUser(req.Context(), userId) @@ -212,21 +212,21 @@ func NewHttpServer(conf Conf, db *database.Queries, signingKey mjwt.Signer) *htt } if claims["profile"] { m["profile"] = conf.BaseUrl + "/user/" + userData.Username - m["picture"] = userData.Picture.String() - m["website"] = userData.Website.String() + m["picture"] = userData.Picture + m["website"] = userData.Website } if claims["email"] { m["email"] = userData.Email m["email_verified"] = userData.EmailVerified } - if claims["birthdate"] { - m["birthdate"] = userData.Birthdate.String() + if claims["birthdate"] && userData.Birthdate.Valid { + m["birthdate"] = userData.Birthdate.Time.String() } if claims["age"] { - m["age"] = CalculateAge(userData.Birthdate.Time.In(userData.ZoneInfo.Location)) + m["age"] = CalculateAge(userData.Birthdate.Time.In(userData.Zoneinfo.Location)) } if claims["zoneinfo"] { - m["zoneinfo"] = userData.ZoneInfo.Location.String() + m["zoneinfo"] = userData.Zoneinfo.Location.String() } if claims["locale"] { m["locale"] = userData.Locale.Tag.String()