forked from mirrors/pronouns.cc
feat: add email/password login
This commit is contained in:
parent
12ed7fb5bb
commit
1cfe28cd59
17 changed files with 188 additions and 59 deletions
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
62
backend/routes/v2/auth/login.go
Normal file
62
backend/routes/v2/auth/login.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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, <no body>
|
||||
|
||||
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 }
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ export interface MetaResponse {
|
|||
users: MetaUsers;
|
||||
members: number;
|
||||
require_invite: boolean;
|
||||
email_signup: boolean;
|
||||
notice: { id: number; notice: string } | null;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<UrlsResponse>("/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<LoginResponse>("/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 };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from "$app/forms";
|
||||
import { goto } from "$app/navigation";
|
||||
import type { APIError } from "$lib/api/entities";
|
||||
import { apiFetch } from "$lib/api/fetch";
|
||||
|
@ -8,6 +9,7 @@
|
|||
import { onMount } from "svelte";
|
||||
import {
|
||||
Button,
|
||||
FormGroup,
|
||||
Icon,
|
||||
Input,
|
||||
ListGroup,
|
||||
|
@ -16,9 +18,21 @@
|
|||
ModalBody,
|
||||
ModalFooter,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import type { PageData } from "./$types";
|
||||
import type { PageData, ActionData } from "./$types";
|
||||
|
||||
export let data: PageData;
|
||||
export let form: ActionData;
|
||||
|
||||
$: loginCallback(form);
|
||||
const loginCallback = (form: ActionData) => {
|
||||
if (!form?.data?.user) return;
|
||||
|
||||
localStorage.setItem("pronouns-token", form.data.token);
|
||||
localStorage.setItem("pronouns-user", JSON.stringify(form.data.user));
|
||||
userStore.set(form.data.user);
|
||||
addToast({ header: "Logged in", body: "Successfully logged in!" });
|
||||
goto(`/@${form.data.user.name}`);
|
||||
};
|
||||
|
||||
let error: APIError | null = null;
|
||||
let instance = "";
|
||||
|
@ -59,7 +73,31 @@
|
|||
<div class="container">
|
||||
<h1>Log in or sign up</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-1">
|
||||
<div class="col-md">
|
||||
{#if data.email_signup}
|
||||
{#if form?.error}
|
||||
<ErrorAlert error={form?.error} />
|
||||
{/if}
|
||||
|
||||
<form method="POST" use:enhance>
|
||||
<FormGroup class="m-1" floating label="Email">
|
||||
<Input name="email" type="email" />
|
||||
</FormGroup>
|
||||
<FormGroup class="m-1" floating label="Password">
|
||||
<Input name="password" type="password" />
|
||||
</FormGroup>
|
||||
<p class="m-1">
|
||||
<Button color="primary" type="submit">Log in</Button>
|
||||
<Button href="/auth/signup">Sign up</Button>
|
||||
</p>
|
||||
</form>
|
||||
{:else}
|
||||
<p>
|
||||
<b>Choose an authentication provider to get started.</b> You can add more providers later.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<ListGroup>
|
||||
<ListGroupItem tag="button" on:click={toggleModal}>Log in with the Fediverse</ListGroupItem>
|
||||
{#if data.discord}
|
||||
|
@ -101,10 +139,5 @@
|
|||
a token in your browser to identify your account.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<p>
|
||||
<b>Choose an authentication provider to get started.</b> You can add more providers later.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -13,6 +13,9 @@ const config = {
|
|||
version: {
|
||||
name: child_process.execSync("git describe --tags --long --always").toString().trim(),
|
||||
},
|
||||
csrf: {
|
||||
checkOrigin: process.env.NODE_ENV !== "development",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue