forked from mirrors/pronouns.cc
feat: add POST /api/v2/auth/email/signup/confirm
This commit is contained in:
parent
ae453df77c
commit
12ed7fb5bb
11 changed files with 255 additions and 38 deletions
|
@ -2,6 +2,8 @@ package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgconn"
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
|
||||||
|
@ -14,9 +16,6 @@ type UserEmail struct {
|
||||||
ID common.EmailID
|
ID common.EmailID
|
||||||
UserID common.UserID
|
UserID common.UserID
|
||||||
EmailAddress string
|
EmailAddress string
|
||||||
Confirmed bool
|
|
||||||
|
|
||||||
ConfirmationToken *string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) UserEmails(ctx context.Context, userID common.UserID) (es []UserEmail, err error) {
|
func (db *DB) UserEmails(ctx context.Context, userID common.UserID) (es []UserEmail, err error) {
|
||||||
|
@ -34,13 +33,11 @@ func (db *DB) UserEmails(ctx context.Context, userID common.UserID) (es []UserEm
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserByEmail gets a user by their email address.
|
// UserByEmail gets a user by their email address.
|
||||||
// The email address must be confirmed.
|
|
||||||
func (db *DB) UserByEmail(ctx context.Context, email string) (u User, err error) {
|
func (db *DB) UserByEmail(ctx context.Context, email string) (u User, err error) {
|
||||||
sql, args, err := sq.Select("users.*").
|
sql, args, err := sq.Select("users.*").
|
||||||
From("user_emails").
|
From("user_emails").
|
||||||
Join("users ON user_emails.user_id = users.snowflake_id").
|
Join("users ON user_emails.user_id = users.snowflake_id").
|
||||||
Where("email_address = ?", email).
|
Where("email_address = ?", email).
|
||||||
Where("confirmed = ?", true).
|
|
||||||
ToSql()
|
ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return u, errors.Wrap(err, "building query")
|
return u, errors.Wrap(err, "building query")
|
||||||
|
@ -67,11 +64,9 @@ const ErrEmailInUse = errors.Sentinel("email already in use")
|
||||||
// AddEmail adds a new email to the database, and generates a confirmation token for it.
|
// AddEmail adds a new email to the database, and generates a confirmation token for it.
|
||||||
func (db *DB) AddEmail(ctx context.Context, tx pgx.Tx, userID common.UserID, email string) (e UserEmail, err error) {
|
func (db *DB) AddEmail(ctx context.Context, tx pgx.Tx, userID common.UserID, email string) (e UserEmail, err error) {
|
||||||
sql, args, err := sq.Insert("user_emails").SetMap(map[string]any{
|
sql, args, err := sq.Insert("user_emails").SetMap(map[string]any{
|
||||||
"id": common.GenerateID(),
|
"id": common.GenerateID(),
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"email_address": email,
|
"email_address": email,
|
||||||
"confirmed": false,
|
|
||||||
"confirmation_token": common.RandBase64(48),
|
|
||||||
}).Suffix("RETURNING *").ToSql()
|
}).Suffix("RETURNING *").ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return e, errors.Wrap(err, "building query")
|
return e, errors.Wrap(err, "building query")
|
||||||
|
@ -91,3 +86,21 @@ func (db *DB) AddEmail(ctx context.Context, tx pgx.Tx, userID common.UserID, ema
|
||||||
}
|
}
|
||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *DB) SetPassword(ctx context.Context, tx pgx.Tx, userID common.UserID, password string) (err error) {
|
||||||
|
salt := make([]byte, 16)
|
||||||
|
_, err = rand.Read(salt)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "creating salt")
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := hashPassword([]byte(password), salt)
|
||||||
|
|
||||||
|
sql, args, err := sq.Update("users").Set("password", hash).Set("salt", salt).Where("snowflake_id = ?", userID).ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "building sql")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec(ctx, sql, args...)
|
||||||
|
return errors.Wrap(err, "executing query")
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package db
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"crypto/subtle"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
@ -18,6 +19,7 @@ import (
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgconn"
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
|
"golang.org/x/crypto/argon2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
|
@ -56,7 +58,8 @@ type User struct {
|
||||||
LastSIDReroll time.Time `db:"last_sid_reroll"`
|
LastSIDReroll time.Time `db:"last_sid_reroll"`
|
||||||
Timezone *string
|
Timezone *string
|
||||||
Settings UserSettings
|
Settings UserSettings
|
||||||
Password *string
|
Password []byte
|
||||||
|
Salt []byte
|
||||||
|
|
||||||
DeletedAt *time.Time
|
DeletedAt *time.Time
|
||||||
SelfDelete *bool
|
SelfDelete *bool
|
||||||
|
@ -133,6 +136,21 @@ func (u User) UTCOffset() (offset int, ok bool) {
|
||||||
return offset, true
|
return offset, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u User) VerifyPassword(input string) bool {
|
||||||
|
if u.Password == nil || u.Salt == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
inputHash := hashPassword([]byte(input), u.Salt)
|
||||||
|
verifiedHash := hashPassword(u.Password, u.Salt)
|
||||||
|
|
||||||
|
return subtle.ConstantTimeCompare(inputHash, verifiedHash) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashPassword(password, salt []byte) []byte {
|
||||||
|
return argon2.IDKey(password, salt, 1, 65536, 4, 16)
|
||||||
|
}
|
||||||
|
|
||||||
type Badge int32
|
type Badge int32
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -5,10 +5,12 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
||||||
|
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"github.com/go-chi/render"
|
"github.com/go-chi/render"
|
||||||
"github.com/mediocregopher/radix/v4"
|
"github.com/mediocregopher/radix/v4"
|
||||||
|
"github.com/rs/xid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type postEmailSignupRequest struct {
|
type postEmailSignupRequest struct {
|
||||||
|
@ -45,7 +47,7 @@ func (s *Server) postEmailSignup(w http.ResponseWriter, r *http.Request) (err er
|
||||||
}
|
}
|
||||||
|
|
||||||
go s.SendEmail(req.Email, "Confirm your email address", "signup", map[string]any{
|
go s.SendEmail(req.Email, "Confirm your email address", "signup", map[string]any{
|
||||||
"ticket": ticket,
|
"Ticket": ticket,
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -54,3 +56,133 @@ func (s *Server) postEmailSignup(w http.ResponseWriter, r *http.Request) (err er
|
||||||
func emailSignupTicketKey(ticket string) string {
|
func emailSignupTicketKey(ticket string) string {
|
||||||
return "email-signup:" + ticket
|
return "email-signup:" + ticket
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type postEmailSignupConfirmRequest struct {
|
||||||
|
Ticket string `json:"ticket"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) postEmailSignupConfirm(w http.ResponseWriter, r *http.Request) (err error) {
|
||||||
|
ctx := r.Context()
|
||||||
|
var req postEmailSignupConfirmRequest
|
||||||
|
err = render.Decode(r, &req)
|
||||||
|
if err != nil {
|
||||||
|
return server.APIError{Code: server.ErrBadRequest}
|
||||||
|
}
|
||||||
|
|
||||||
|
var email string
|
||||||
|
err = s.DB.Redis.Do(ctx, radix.Cmd(&email, "GET", emailSignupTicketKey(email)))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "getting email signup key")
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := s.DB.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "starting transaction")
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback(ctx) }()
|
||||||
|
|
||||||
|
u, err := s.DB.CreateUser(ctx, tx, req.Username)
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case db.ErrUsernameTaken:
|
||||||
|
return server.APIError{Code: server.ErrUsernameTaken}
|
||||||
|
case db.ErrUsernameTooShort:
|
||||||
|
return server.APIError{Code: server.ErrBadRequest, Details: "Username is too short"}
|
||||||
|
case db.ErrUsernameTooLong:
|
||||||
|
return server.APIError{Code: server.ErrBadRequest, Details: "Username is too long"}
|
||||||
|
case db.ErrInvalidUsername:
|
||||||
|
return server.APIError{Code: server.ErrBadRequest, Details: "Username contains invalid characters"}
|
||||||
|
case db.ErrBannedUsername:
|
||||||
|
return server.APIError{Code: server.ErrBadRequest, Details: "Username is not allowed"}
|
||||||
|
default:
|
||||||
|
return errors.Wrap(err, "creating user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.DB.AddEmail(ctx, tx, u.SnowflakeID, email)
|
||||||
|
if err != nil {
|
||||||
|
if err == db.ErrEmailInUse {
|
||||||
|
// This should only happen if the email was *not* taken when the ticket was sent, but was taken in the meantime.
|
||||||
|
// i.e. unless another person has access to the mailbox, the user will know what happened
|
||||||
|
return server.APIError{Code: server.ErrBadRequest, Details: "Email is already in use"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Wrap(err, "adding email to user")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.DB.SetPassword(ctx, tx, u.SnowflakeID, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "setting password for user")
|
||||||
|
}
|
||||||
|
|
||||||
|
// create token
|
||||||
|
tokenID := xid.New()
|
||||||
|
token, err := s.Auth.CreateToken(u.ID, tokenID, false, false, true)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "creating token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// save token to database
|
||||||
|
_, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "saving token to database")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "committing transaction")
|
||||||
|
}
|
||||||
|
|
||||||
|
render.JSON(w, r, signupResponse{
|
||||||
|
User: *dbUserToUserResponse(u, nil),
|
||||||
|
Token: token,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type userResponse struct {
|
||||||
|
ID common.UserID `json:"id"`
|
||||||
|
Username string `json:"name"`
|
||||||
|
DisplayName *string `json:"display_name"`
|
||||||
|
Bio *string `json:"bio"`
|
||||||
|
Avatar *string `json:"avatar"`
|
||||||
|
Links []string `json:"links"`
|
||||||
|
Names []db.FieldEntry `json:"names"`
|
||||||
|
Pronouns []db.PronounEntry `json:"pronouns"`
|
||||||
|
Fields []db.Field `json:"fields"`
|
||||||
|
|
||||||
|
Discord *string `json:"discord"`
|
||||||
|
DiscordUsername *string `json:"discord_username"`
|
||||||
|
|
||||||
|
Tumblr *string `json:"tumblr"`
|
||||||
|
TumblrUsername *string `json:"tumblr_username"`
|
||||||
|
|
||||||
|
Google *string `json:"google"`
|
||||||
|
GoogleUsername *string `json:"google_username"`
|
||||||
|
|
||||||
|
Fediverse *string `json:"fediverse"`
|
||||||
|
FediverseUsername *string `json:"fediverse_username"`
|
||||||
|
FediverseInstance *string `json:"fediverse_instance"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbUserToUserResponse(u db.User, fields []db.Field) *userResponse {
|
||||||
|
return &userResponse{
|
||||||
|
ID: u.SnowflakeID,
|
||||||
|
Username: u.Username,
|
||||||
|
DisplayName: u.DisplayName,
|
||||||
|
Bio: u.Bio,
|
||||||
|
Avatar: u.Avatar,
|
||||||
|
Links: db.NotNull(u.Links),
|
||||||
|
Names: db.NotNull(u.Names),
|
||||||
|
Pronouns: db.NotNull(u.Pronouns),
|
||||||
|
Fields: db.NotNull(fields),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type signupResponse struct {
|
||||||
|
User userResponse `json:"user"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
|
@ -12,16 +12,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type EmailResponse struct {
|
type EmailResponse struct {
|
||||||
ID common.EmailID `json:"id"`
|
ID common.EmailID `json:"id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Confirmed bool `json:"confirmed"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func dbEmailToResponse(e db.UserEmail) EmailResponse {
|
func dbEmailToResponse(e db.UserEmail) EmailResponse {
|
||||||
return EmailResponse{
|
return EmailResponse{
|
||||||
ID: e.ID,
|
ID: e.ID,
|
||||||
Email: e.EmailAddress,
|
Email: e.EmailAddress,
|
||||||
Confirmed: e.Confirmed,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,12 +14,14 @@ type Server struct {
|
||||||
|
|
||||||
emailPool *email.Pool
|
emailPool *email.Pool
|
||||||
emailAddress string
|
emailAddress string
|
||||||
|
baseURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Mount(srv *server.Server, r chi.Router) {
|
func Mount(srv *server.Server, r chi.Router) {
|
||||||
s := &Server{
|
s := &Server{
|
||||||
Server: srv,
|
Server: srv,
|
||||||
emailAddress: os.Getenv("EMAIL_ADDRESS"), // The address used for sending email
|
emailAddress: os.Getenv("EMAIL_ADDRESS"), // The address used for sending email
|
||||||
|
baseURL: os.Getenv("BASE_URL"), // The frontend base URL
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only mount email routes if email is set up
|
// Only mount email routes if email is set up
|
||||||
|
@ -38,10 +40,10 @@ 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).Post("/", nil) // Add/update email to existing account, { email }
|
||||||
r.With(server.MustAuth).Delete("/{id}", nil) // Remove existing email from account, <no body>
|
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", nil) // Log in to account, { username, password }
|
||||||
r.Post("/signup", server.WrapHandler(s.postEmailSignup)) // Create account, { email }
|
r.Post("/signup", server.WrapHandler(s.postEmailSignup)) // Create account, { email }
|
||||||
r.Post("/signup/confirm", nil) // Create account, { ticket, username, password }
|
r.Post("/signup/confirm", server.WrapHandler(s.postEmailSignupConfirm)) // Create account, { ticket, username, password }
|
||||||
r.Post("/confirm", nil) // Confirm email address, { ticket }
|
r.Post("/confirm", nil) // Confirm email address, { ticket }
|
||||||
|
|
||||||
r.Patch("/password", nil) // Update password
|
r.Patch("/password", nil) // Update password
|
||||||
r.Post("/password/forgot", nil) // Forgot/reset password, { email }
|
r.Post("/password/forgot", nil) // Forgot/reset password, { email }
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
"bytes"
|
||||||
"github.com/jordan-wright/email"
|
"embed"
|
||||||
|
"html/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||||
|
"emperror.dev/errors"
|
||||||
|
"github.com/jordan-wright/email"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) SendEmail(to, title, template string, data map[string]any) {
|
func (s *Server) SendEmail(to, title, template string, data map[string]any) {
|
||||||
|
@ -11,8 +16,39 @@ func (s *Server) SendEmail(to, title, template string, data map[string]any) {
|
||||||
e.From = s.emailAddress
|
e.From = s.emailAddress
|
||||||
e.To = []string{to}
|
e.To = []string{to}
|
||||||
|
|
||||||
err := s.emailPool.Send(e, 10*time.Second)
|
text, html, err := s.Template(template, data)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("executing templates: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.Text = text
|
||||||
|
e.HTML = html
|
||||||
|
|
||||||
|
err = s.emailPool.Send(e, 10*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("sending email: %v", err)
|
log.Errorf("sending email: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:embed templates/*
|
||||||
|
var fs embed.FS
|
||||||
|
var tmpl = template.Must(template.ParseFS(fs, "templates/*"))
|
||||||
|
|
||||||
|
func (s *Server) Template(template string, data map[string]any) (text, html []byte, err error) {
|
||||||
|
data["BaseURL"] = s.baseURL
|
||||||
|
|
||||||
|
textWriter := new(bytes.Buffer)
|
||||||
|
htmlWriter := new(bytes.Buffer)
|
||||||
|
|
||||||
|
err = tmpl.ExecuteTemplate(textWriter, "templates/"+template+".txt", data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "executing text template")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tmpl.ExecuteTemplate(htmlWriter, "templates/"+template+".html", data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "executing HTML template")
|
||||||
|
}
|
||||||
|
|
||||||
|
return textWriter.Bytes(), htmlWriter.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
16
backend/routes/v2/auth/templates/signup.html
Normal file
16
backend/routes/v2/auth/templates/signup.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<p>Please continue creating a new pronouns.cc account by using the following link:</p>
|
||||||
|
<p><a href="{{.BaseURL}}/auth/email/signup/{{.Ticket}}">Confirm your email address</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,4 @@
|
||||||
|
Please continue creating a new pronouns.cc account by using the following link:
|
||||||
|
{{.BaseURL}}/auth/email/signup/{{.Ticket}}
|
||||||
|
|
||||||
|
If you didn't mean to create a new account, feel free to ignore this email.
|
6
go.mod
6
go.mod
|
@ -28,6 +28,7 @@ require (
|
||||||
github.com/toshi0607/chi-prometheus v0.1.4
|
github.com/toshi0607/chi-prometheus v0.1.4
|
||||||
github.com/urfave/cli/v2 v2.25.7
|
github.com/urfave/cli/v2 v2.25.7
|
||||||
go.uber.org/zap v1.26.0
|
go.uber.org/zap v1.26.0
|
||||||
|
golang.org/x/crypto v0.19.0
|
||||||
golang.org/x/oauth2 v0.13.0
|
golang.org/x/oauth2 v0.13.0
|
||||||
google.golang.org/api v0.148.0
|
google.golang.org/api v0.148.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
@ -71,12 +72,11 @@ require (
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
go.opencensus.io v0.24.0 // indirect
|
go.opencensus.io v0.24.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/crypto v0.14.0 // indirect
|
|
||||||
golang.org/x/image v0.13.0 // indirect
|
golang.org/x/image v0.13.0 // indirect
|
||||||
golang.org/x/net v0.17.0 // indirect
|
golang.org/x/net v0.17.0 // indirect
|
||||||
golang.org/x/sync v0.4.0 // indirect
|
golang.org/x/sync v0.4.0 // indirect
|
||||||
golang.org/x/sys v0.13.0 // indirect
|
golang.org/x/sys v0.17.0 // indirect
|
||||||
golang.org/x/text v0.13.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
google.golang.org/appengine v1.6.8 // indirect
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a // indirect
|
||||||
google.golang.org/grpc v1.59.0 // indirect
|
google.golang.org/grpc v1.59.0 // indirect
|
||||||
|
|
14
go.sum
14
go.sum
|
@ -207,8 +207,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
|
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
|
||||||
golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg=
|
golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg=
|
||||||
|
@ -248,19 +248,19 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
|
|
@ -8,9 +8,7 @@ alter table users add column salt bytea null;
|
||||||
create table user_emails (
|
create table user_emails (
|
||||||
id bigint primary key,
|
id bigint primary key,
|
||||||
user_id bigint not null references users (snowflake_id) on delete cascade,
|
user_id bigint not null references users (snowflake_id) on delete cascade,
|
||||||
email_address text not null unique,
|
email_address text not null unique
|
||||||
confirmed boolean not null default false,
|
|
||||||
confirmation_token text null unique
|
|
||||||
);
|
);
|
||||||
|
|
||||||
-- +migrate Down
|
-- +migrate Down
|
||||||
|
|
Loading…
Reference in a new issue