From 1cfe28cd5958fc8514b2c0693f7fb182c00ac66e Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 16 Feb 2024 14:50:41 +0100 Subject: [PATCH] feat: add email/password login --- backend/db/email.go | 2 +- backend/db/tokens.go | 4 +- backend/db/user.go | 4 +- backend/routes/v1/auth/discord.go | 16 ++--- backend/routes/v1/auth/fedi_mastodon.go | 16 ++--- backend/routes/v1/auth/fedi_misskey.go | 16 ++--- backend/routes/v1/auth/google.go | 16 ++--- backend/routes/v1/auth/tokens.go | 2 +- backend/routes/v1/auth/tumblr.go | 16 ++--- backend/routes/v2/auth/email_signup.go | 9 ++- backend/routes/v2/auth/login.go | 62 +++++++++++++++++++ backend/routes/v2/auth/routes.go | 2 +- backend/routes/v2/auth/send_email.go | 4 +- frontend/src/lib/api/responses.ts | 1 + .../src/routes/auth/login/+page.server.ts | 27 ++++++++ frontend/src/routes/auth/login/+page.svelte | 47 +++++++++++--- frontend/svelte.config.js | 3 + 17 files changed, 188 insertions(+), 59 deletions(-) create mode 100644 backend/routes/v2/auth/login.go diff --git a/backend/db/email.go b/backend/db/email.go index 8525ddd..d26e01e 100644 --- a/backend/db/email.go +++ b/backend/db/email.go @@ -55,7 +55,7 @@ func (db *DB) UserByEmail(ctx context.Context, email string) (u User, err error) // EmailExists returns whether an email address already exists. It does not need to be comfirmed. func (db *DB) EmailExists(ctx context.Context, email string) (exists bool, err error) { - err = db.QueryRow(ctx, "select exists(SELECT * FROM user_emails WHERE email = $1)", email).Scan(&exists) + err = db.QueryRow(ctx, "select exists(SELECT * FROM user_emails WHERE email_address = $1)", email).Scan(&exists) return exists, err } diff --git a/backend/db/tokens.go b/backend/db/tokens.go index 367c492..a032c8d 100644 --- a/backend/db/tokens.go +++ b/backend/db/tokens.go @@ -64,7 +64,7 @@ func (db *DB) Tokens(ctx context.Context, userID xid.ID) (ts []Token, err error) const TokenExpiryTime = 3 * 30 * 24 * time.Hour // SaveToken saves a token to the database. -func (db *DB) SaveToken(ctx context.Context, userID xid.ID, tokenID xid.ID, apiOnly, readOnly bool) (t Token, err error) { +func (db *DB) SaveToken(ctx context.Context, q pgxscan.Querier, userID xid.ID, tokenID xid.ID, apiOnly, readOnly bool) (t Token, err error) { sql, args, err := sq.Insert("tokens"). SetMap(map[string]any{ "user_id": userID, @@ -79,7 +79,7 @@ func (db *DB) SaveToken(ctx context.Context, userID xid.ID, tokenID xid.ID, apiO return t, errors.Wrap(err, "building sql") } - err = pgxscan.Get(ctx, db, &t, sql, args...) + err = pgxscan.Get(ctx, q, &t, sql, args...) if err != nil { return t, errors.Wrap(err, "inserting token") } diff --git a/backend/db/user.go b/backend/db/user.go index 2137d55..055c06c 100644 --- a/backend/db/user.go +++ b/backend/db/user.go @@ -142,9 +142,7 @@ func (u User) VerifyPassword(input string) bool { } inputHash := hashPassword([]byte(input), u.Salt) - verifiedHash := hashPassword(u.Password, u.Salt) - - return subtle.ConstantTimeCompare(inputHash, verifiedHash) == 1 + return subtle.ConstantTimeCompare(inputHash, u.Password) == 1 } func hashPassword(password, salt []byte) []byte { diff --git a/backend/routes/v1/auth/discord.go b/backend/routes/v1/auth/discord.go index f814cd9..342bbc0 100644 --- a/backend/routes/v1/auth/discord.go +++ b/backend/routes/v1/auth/discord.go @@ -120,7 +120,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error { } // save token to database - _, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false) + _, err = s.DB.SaveToken(ctx, s.DB, u.ID, tokenID, false, false) if err != nil { return errors.Wrap(err, "saving token to database") } @@ -361,12 +361,6 @@ func (s *Server) discordSignup(w http.ResponseWriter, r *http.Request) error { return errors.Wrap(err, "deleting signup ticket") } - // commit transaction - err = tx.Commit(ctx) - if err != nil { - return errors.Wrap(err, "committing transaction") - } - // create token // TODO: implement user + token permissions tokenID := xid.New() @@ -376,11 +370,17 @@ func (s *Server) discordSignup(w http.ResponseWriter, r *http.Request) error { } // save token to database - _, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false) + _, err = s.DB.SaveToken(ctx, tx, u.ID, tokenID, false, false) if err != nil { return errors.Wrap(err, "saving token to database") } + // commit transaction + err = tx.Commit(ctx) + if err != nil { + return errors.Wrap(err, "committing transaction") + } + // return user render.JSON(w, r, signupResponse{ User: *dbUserToUserResponse(u, nil), diff --git a/backend/routes/v1/auth/fedi_mastodon.go b/backend/routes/v1/auth/fedi_mastodon.go index f7fd0f2..28dee0f 100644 --- a/backend/routes/v1/auth/fedi_mastodon.go +++ b/backend/routes/v1/auth/fedi_mastodon.go @@ -141,7 +141,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error } // save token to database - _, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false) + _, err = s.DB.SaveToken(ctx, s.DB, u.ID, tokenID, false, false) if err != nil { return errors.Wrap(err, "saving token to database") } @@ -389,12 +389,6 @@ func (s *Server) mastodonSignup(w http.ResponseWriter, r *http.Request) error { return errors.Wrap(err, "deleting signup ticket") } - // commit transaction - err = tx.Commit(ctx) - if err != nil { - return errors.Wrap(err, "committing transaction") - } - // create token // TODO: implement user + token permissions tokenID := xid.New() @@ -404,11 +398,17 @@ func (s *Server) mastodonSignup(w http.ResponseWriter, r *http.Request) error { } // save token to database - _, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false) + _, err = s.DB.SaveToken(ctx, tx, u.ID, tokenID, false, false) if err != nil { return errors.Wrap(err, "saving token to database") } + // commit transaction + err = tx.Commit(ctx) + if err != nil { + return errors.Wrap(err, "committing transaction") + } + // return user render.JSON(w, r, signupResponse{ User: *dbUserToUserResponse(u, nil), diff --git a/backend/routes/v1/auth/fedi_misskey.go b/backend/routes/v1/auth/fedi_misskey.go index 5e12ca9..a9c1b87 100644 --- a/backend/routes/v1/auth/fedi_misskey.go +++ b/backend/routes/v1/auth/fedi_misskey.go @@ -120,7 +120,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error { } // save token to database - _, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false) + _, err = s.DB.SaveToken(ctx, s.DB, u.ID, tokenID, false, false) if err != nil { return errors.Wrap(err, "saving token to database") } @@ -317,12 +317,6 @@ func (s *Server) misskeySignup(w http.ResponseWriter, r *http.Request) error { return errors.Wrap(err, "deleting signup ticket") } - // commit transaction - err = tx.Commit(ctx) - if err != nil { - return errors.Wrap(err, "committing transaction") - } - // create token // TODO: implement user + token permissions tokenID := xid.New() @@ -332,11 +326,17 @@ func (s *Server) misskeySignup(w http.ResponseWriter, r *http.Request) error { } // save token to database - _, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false) + _, err = s.DB.SaveToken(ctx, tx, u.ID, tokenID, false, false) if err != nil { return errors.Wrap(err, "saving token to database") } + // commit transaction + err = tx.Commit(ctx) + if err != nil { + return errors.Wrap(err, "committing transaction") + } + // return user render.JSON(w, r, signupResponse{ User: *dbUserToUserResponse(u, nil), diff --git a/backend/routes/v1/auth/google.go b/backend/routes/v1/auth/google.go index 0ef113f..166080c 100644 --- a/backend/routes/v1/auth/google.go +++ b/backend/routes/v1/auth/google.go @@ -139,7 +139,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error { } // save token to database - _, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false) + _, err = s.DB.SaveToken(ctx, s.DB, u.ID, tokenID, false, false) if err != nil { return errors.Wrap(err, "saving token to database") } @@ -364,12 +364,6 @@ func (s *Server) googleSignup(w http.ResponseWriter, r *http.Request) error { return errors.Wrap(err, "deleting signup ticket") } - // commit transaction - err = tx.Commit(ctx) - if err != nil { - return errors.Wrap(err, "committing transaction") - } - // create token // TODO: implement user + token permissions tokenID := xid.New() @@ -379,11 +373,17 @@ func (s *Server) googleSignup(w http.ResponseWriter, r *http.Request) error { } // save token to database - _, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false) + _, err = s.DB.SaveToken(ctx, tx, u.ID, tokenID, false, false) if err != nil { return errors.Wrap(err, "saving token to database") } + // commit transaction + err = tx.Commit(ctx) + if err != nil { + return errors.Wrap(err, "committing transaction") + } + // return user render.JSON(w, r, signupResponse{ User: *dbUserToUserResponse(u, nil), diff --git a/backend/routes/v1/auth/tokens.go b/backend/routes/v1/auth/tokens.go index 0f8b8b8..27a7127 100644 --- a/backend/routes/v1/auth/tokens.go +++ b/backend/routes/v1/auth/tokens.go @@ -115,7 +115,7 @@ func (s *Server) createToken(w http.ResponseWriter, r *http.Request) error { return errors.Wrap(err, "creating token") } - t, err := s.DB.SaveToken(ctx, claims.UserID, tokenID, true, readOnly) + t, err := s.DB.SaveToken(ctx, s.DB, claims.UserID, tokenID, true, readOnly) if err != nil { return errors.Wrap(err, "saving token") } diff --git a/backend/routes/v1/auth/tumblr.go b/backend/routes/v1/auth/tumblr.go index 30edcd5..5c372b6 100644 --- a/backend/routes/v1/auth/tumblr.go +++ b/backend/routes/v1/auth/tumblr.go @@ -172,7 +172,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error { } // save token to database - _, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false) + _, err = s.DB.SaveToken(ctx, s.DB, u.ID, tokenID, false, false) if err != nil { return errors.Wrap(err, "saving token to database") } @@ -397,12 +397,6 @@ func (s *Server) tumblrSignup(w http.ResponseWriter, r *http.Request) error { return errors.Wrap(err, "deleting signup ticket") } - // commit transaction - err = tx.Commit(ctx) - if err != nil { - return errors.Wrap(err, "committing transaction") - } - // create token // TODO: implement user + token permissions tokenID := xid.New() @@ -412,11 +406,17 @@ func (s *Server) tumblrSignup(w http.ResponseWriter, r *http.Request) error { } // save token to database - _, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false) + _, err = s.DB.SaveToken(ctx, s.DB, u.ID, tokenID, false, false) if err != nil { return errors.Wrap(err, "saving token to database") } + // commit transaction + err = tx.Commit(ctx) + if err != nil { + return errors.Wrap(err, "committing transaction") + } + // return user render.JSON(w, r, signupResponse{ User: *dbUserToUserResponse(u, nil), diff --git a/backend/routes/v2/auth/email_signup.go b/backend/routes/v2/auth/email_signup.go index 63cd471..28c4022 100644 --- a/backend/routes/v2/auth/email_signup.go +++ b/backend/routes/v2/auth/email_signup.go @@ -1,6 +1,7 @@ package auth import ( + "fmt" "net/http" "strings" @@ -72,10 +73,14 @@ func (s *Server) postEmailSignupConfirm(w http.ResponseWriter, r *http.Request) } var email string - err = s.DB.Redis.Do(ctx, radix.Cmd(&email, "GET", emailSignupTicketKey(email))) + fmt.Println(emailSignupTicketKey(req.Ticket)) + err = s.DB.Redis.Do(ctx, radix.Cmd(&email, "GET", emailSignupTicketKey(req.Ticket))) if err != nil { return errors.Wrap(err, "getting email signup key") } + if email == "" { + return server.APIError{Code: server.ErrBadRequest, Details: "Unknown ticket"} + } tx, err := s.DB.Begin(ctx) if err != nil { @@ -125,7 +130,7 @@ func (s *Server) postEmailSignupConfirm(w http.ResponseWriter, r *http.Request) } // save token to database - _, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false) + _, err = s.DB.SaveToken(ctx, tx, u.ID, tokenID, false, false) if err != nil { return errors.Wrap(err, "saving token to database") } diff --git a/backend/routes/v2/auth/login.go b/backend/routes/v2/auth/login.go new file mode 100644 index 0000000..aead6b1 --- /dev/null +++ b/backend/routes/v2/auth/login.go @@ -0,0 +1,62 @@ +package auth + +import ( + "net/http" + + "codeberg.org/pronounscc/pronouns.cc/backend/server" + "emperror.dev/errors" + "github.com/go-chi/render" + "github.com/rs/xid" +) + +type postLoginRequest struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type postLoginResponse struct { + User *userResponse `json:"user,omitempty"` + Token string `json:"token,omitempty"` +} + +func (s *Server) postLogin(w http.ResponseWriter, r *http.Request) (err error) { + ctx := r.Context() + var req postLoginRequest + err = render.Decode(r, &req) + if err != nil { + return server.APIError{Code: server.ErrBadRequest} + } + + u, err := s.DB.UserByEmail(ctx, req.Email) + if err != nil { + return server.APIError{Code: server.ErrForbidden, Details: "Invalid email or password"} + } + + if !u.VerifyPassword(req.Password) { + return server.APIError{Code: server.ErrForbidden, Details: "Invalid email or password"} + } + + tokenID := xid.New() + token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true) + if err != nil { + return errors.Wrap(err, "creating token") + } + + // save token to database + _, err = s.DB.SaveToken(ctx, s.DB, u.ID, tokenID, false, false) + if err != nil { + return errors.Wrap(err, "saving token to database") + } + + fields, err := s.DB.UserFields(ctx, u.ID) + if err != nil { + return errors.Wrap(err, "querying fields") + } + + render.JSON(w, r, postLoginResponse{ + Token: token, + User: dbUserToUserResponse(u, fields), + }) + + return nil +} diff --git a/backend/routes/v2/auth/routes.go b/backend/routes/v2/auth/routes.go index 5e097dc..1ee3826 100644 --- a/backend/routes/v2/auth/routes.go +++ b/backend/routes/v2/auth/routes.go @@ -40,7 +40,7 @@ func Mount(srv *server.Server, r chi.Router) { r.With(server.MustAuth).Post("/", nil) // Add/update email to existing account, { email } r.With(server.MustAuth).Delete("/{id}", nil) // Remove existing email from account, - r.Post("/login", nil) // Log in to account, { username, password } + r.Post("/login", server.WrapHandler(s.postLogin)) // Log in to account, { username, password } r.Post("/signup", server.WrapHandler(s.postEmailSignup)) // Create account, { email } r.Post("/signup/confirm", server.WrapHandler(s.postEmailSignupConfirm)) // Create account, { ticket, username, password } r.Post("/confirm", nil) // Confirm email address, { ticket } diff --git a/backend/routes/v2/auth/send_email.go b/backend/routes/v2/auth/send_email.go index 763f1d4..e327333 100644 --- a/backend/routes/v2/auth/send_email.go +++ b/backend/routes/v2/auth/send_email.go @@ -40,12 +40,12 @@ func (s *Server) Template(template string, data map[string]any) (text, html []by textWriter := new(bytes.Buffer) htmlWriter := new(bytes.Buffer) - err = tmpl.ExecuteTemplate(textWriter, "templates/"+template+".txt", data) + err = tmpl.ExecuteTemplate(textWriter, template+".txt", data) if err != nil { return nil, nil, errors.Wrap(err, "executing text template") } - err = tmpl.ExecuteTemplate(htmlWriter, "templates/"+template+".html", data) + err = tmpl.ExecuteTemplate(htmlWriter, template+".html", data) if err != nil { return nil, nil, errors.Wrap(err, "executing HTML template") } diff --git a/frontend/src/lib/api/responses.ts b/frontend/src/lib/api/responses.ts index 7730b6e..7364bee 100644 --- a/frontend/src/lib/api/responses.ts +++ b/frontend/src/lib/api/responses.ts @@ -11,6 +11,7 @@ export interface MetaResponse { users: MetaUsers; members: number; require_invite: boolean; + email_signup: boolean; notice: { id: number; notice: string } | null; } diff --git a/frontend/src/routes/auth/login/+page.server.ts b/frontend/src/routes/auth/login/+page.server.ts index e9e880a..b4e9a25 100644 --- a/frontend/src/routes/auth/login/+page.server.ts +++ b/frontend/src/routes/auth/login/+page.server.ts @@ -1,6 +1,7 @@ import { apiFetch } from "$lib/api/fetch"; import { PUBLIC_BASE_URL } from "$env/static/public"; import type { UrlsResponse } from "$lib/api/responses"; +import type { APIError, MeUser } from "$lib/api/entities"; export const load = async () => { const resp = await apiFetch("/auth/urls", { @@ -12,3 +13,29 @@ export const load = async () => { return resp; }; + +interface LoginResponse { + user?: MeUser; + token: string; +} + +export const actions = { + default: async ({ request }) => { + const data = await request.formData(); + + try { + const resp = await apiFetch("/auth/email/login", { + method: "POST", + version: 2, + body: { + email: data.get("email"), + password: data.get("password"), + }, + }); + + return { data: resp }; + } catch (e) { + return { error: e as APIError }; + } + }, +}; diff --git a/frontend/src/routes/auth/login/+page.svelte b/frontend/src/routes/auth/login/+page.svelte index 9fce921..6189f45 100644 --- a/frontend/src/routes/auth/login/+page.svelte +++ b/frontend/src/routes/auth/login/+page.svelte @@ -1,4 +1,5 @@