forked from mirrors/pronouns.cc
Compare commits
6 commits
feature/em
...
main
Author | SHA1 | Date | |
---|---|---|---|
8f34367d1a | |||
|
5fcd87a94a | ||
|
0633a32f64 | ||
|
623cdb545e | ||
|
4745a1c04b | ||
|
4e78d36eff |
44 changed files with 111 additions and 1378 deletions
|
@ -1,11 +1,7 @@
|
||||||
// Package common contains functions and types common to all (or most) packages.
|
// Package common contains functions and types common to all (or most) packages.
|
||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import "unicode/utf8"
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
func StringLength(s *string) int {
|
func StringLength(s *string) int {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
|
@ -13,14 +9,3 @@ func StringLength(s *string) int {
|
||||||
}
|
}
|
||||||
return utf8.RuneCountInString(*s)
|
return utf8.RuneCountInString(*s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RandBase64 returns a string of random bytes encoded in raw base 64.
|
|
||||||
func RandBase64(size int) string {
|
|
||||||
b := make([]byte, size)
|
|
||||||
_, err := rand.Read(b)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return base64.RawURLEncoding.EncodeToString(b)
|
|
||||||
}
|
|
||||||
|
|
|
@ -37,15 +37,3 @@ func (id FlagID) Increment() uint16 { return Snowflake(id).Increment() }
|
||||||
|
|
||||||
func (id FlagID) MarshalJSON() ([]byte, error) { return Snowflake(id).MarshalJSON() }
|
func (id FlagID) MarshalJSON() ([]byte, error) { return Snowflake(id).MarshalJSON() }
|
||||||
func (id *FlagID) UnmarshalJSON(src []byte) error { return (*Snowflake)(id).UnmarshalJSON(src) }
|
func (id *FlagID) UnmarshalJSON(src []byte) error { return (*Snowflake)(id).UnmarshalJSON(src) }
|
||||||
|
|
||||||
type EmailID Snowflake
|
|
||||||
|
|
||||||
func (id EmailID) String() string { return Snowflake(id).String() }
|
|
||||||
func (id EmailID) Time() time.Time { return Snowflake(id).Time() }
|
|
||||||
func (id EmailID) IsValid() bool { return Snowflake(id).IsValid() }
|
|
||||||
func (id EmailID) Worker() uint8 { return Snowflake(id).Worker() }
|
|
||||||
func (id EmailID) PID() uint8 { return Snowflake(id).PID() }
|
|
||||||
func (id EmailID) Increment() uint16 { return Snowflake(id).Increment() }
|
|
||||||
|
|
||||||
func (id EmailID) MarshalJSON() ([]byte, error) { return Snowflake(id).MarshalJSON() }
|
|
||||||
func (id *EmailID) UnmarshalJSON(src []byte) error { return (*Snowflake)(id).UnmarshalJSON(src) }
|
|
||||||
|
|
|
@ -1,125 +0,0 @@
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"github.com/jackc/pgx/v5/pgconn"
|
|
||||||
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
|
||||||
"emperror.dev/errors"
|
|
||||||
"github.com/georgysavva/scany/v2/pgxscan"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserEmail struct {
|
|
||||||
ID common.EmailID
|
|
||||||
UserID common.UserID
|
|
||||||
EmailAddress string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) UserEmails(ctx context.Context, userID common.UserID) (es []UserEmail, err error) {
|
|
||||||
return db.UserEmailsTx(ctx, db, userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) UserEmailsTx(ctx context.Context, q pgxscan.Querier, userID common.UserID) (es []UserEmail, err error) {
|
|
||||||
sql, args, err := sq.Select("*").From("user_emails").Where("user_id = ?", userID).OrderBy("id").ToSql()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "building query")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = pgxscan.Select(ctx, q, &es, sql, args...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "executing query")
|
|
||||||
}
|
|
||||||
|
|
||||||
return NotNull(es), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserByEmail gets a user by their email address.
|
|
||||||
func (db *DB) UserByEmail(ctx context.Context, email string) (u User, err error) {
|
|
||||||
sql, args, err := sq.Select("users.*").
|
|
||||||
From("user_emails").
|
|
||||||
Join("users ON user_emails.user_id = users.snowflake_id").
|
|
||||||
Where("email_address = ?", email).
|
|
||||||
ToSql()
|
|
||||||
if err != nil {
|
|
||||||
return u, errors.Wrap(err, "building query")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = pgxscan.Get(ctx, db, &u, sql, args...)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Cause(err) == pgx.ErrNoRows {
|
|
||||||
return u, ErrUserNotFound
|
|
||||||
}
|
|
||||||
return u, errors.Wrap(err, "executing query")
|
|
||||||
}
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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_address = $1)", email).Scan(&exists)
|
|
||||||
return exists, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) EmailExistsTx(ctx context.Context, tx pgx.Tx, email string) (exists bool, err error) {
|
|
||||||
err = tx.QueryRow(ctx, "select exists(SELECT * FROM user_emails WHERE email_address = $1)", email).Scan(&exists)
|
|
||||||
return exists, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const ErrEmailInUse = errors.Sentinel("email already in use")
|
|
||||||
|
|
||||||
// AddEmail adds a new email to the database.
|
|
||||||
func (db *DB) AddEmail(ctx context.Context, q pgxscan.Querier, userID common.UserID, email string) (e UserEmail, err error) {
|
|
||||||
sql, args, err := sq.Insert("user_emails").SetMap(map[string]any{
|
|
||||||
"id": common.GenerateID(),
|
|
||||||
"user_id": userID,
|
|
||||||
"email_address": email,
|
|
||||||
}).Suffix("RETURNING *").ToSql()
|
|
||||||
if err != nil {
|
|
||||||
return e, errors.Wrap(err, "building query")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = pgxscan.Get(ctx, q, &e, sql, args...)
|
|
||||||
if err != nil {
|
|
||||||
pge := &pgconn.PgError{}
|
|
||||||
if errors.As(err, &pge) {
|
|
||||||
// unique constraint violation
|
|
||||||
if pge.Code == uniqueViolation {
|
|
||||||
return e, ErrEmailInUse
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return e, errors.Wrap(err, "executing query")
|
|
||||||
}
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) RemoveEmails(ctx context.Context, tx pgx.Tx, userID common.UserID) (err error) {
|
|
||||||
sql, args, err := sq.Delete("user_emails").Where("user_id = ?", userID).ToSql()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "building sql")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = tx.Exec(ctx, sql, args...)
|
|
||||||
return errors.Wrap(err, "executing query")
|
|
||||||
}
|
|
|
@ -48,11 +48,11 @@ func (f FediverseApp) ClientConfig() *oauth2.Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f FediverseApp) MastodonCompatible() bool {
|
func (f FediverseApp) MastodonCompatible() bool {
|
||||||
return f.InstanceType == "mastodon" || f.InstanceType == "pleroma" || f.InstanceType == "akkoma" || f.InstanceType == "pixelfed" || f.InstanceType == "gotosocial"
|
return f.InstanceType == "mastodon" || f.InstanceType == "pleroma" || f.InstanceType == "akkoma" || f.InstanceType == "incestoma" || f.InstanceType == "pixelfed" || f.InstanceType == "gotosocial"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f FediverseApp) Misskey() bool {
|
func (f FediverseApp) Misskey() bool {
|
||||||
return f.InstanceType == "misskey" || f.InstanceType == "foundkey" || f.InstanceType == "calckey" || f.InstanceType == "firefish"
|
return f.InstanceType == "misskey" || f.InstanceType == "foundkey" || f.InstanceType == "calckey" || f.InstanceType == "firefish" || f.InstanceType == "sharkey"
|
||||||
}
|
}
|
||||||
|
|
||||||
const ErrNoInstanceApp = errors.Sentinel("instance doesn't have an app")
|
const ErrNoInstanceApp = errors.Sentinel("instance doesn't have an app")
|
||||||
|
|
|
@ -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
|
const TokenExpiryTime = 3 * 30 * 24 * time.Hour
|
||||||
|
|
||||||
// SaveToken saves a token to the database.
|
// SaveToken saves a token to the database.
|
||||||
func (db *DB) SaveToken(ctx context.Context, q pgxscan.Querier, userID xid.ID, tokenID xid.ID, apiOnly, readOnly bool) (t Token, err error) {
|
func (db *DB) SaveToken(ctx context.Context, userID xid.ID, tokenID xid.ID, apiOnly, readOnly bool) (t Token, err error) {
|
||||||
sql, args, err := sq.Insert("tokens").
|
sql, args, err := sq.Insert("tokens").
|
||||||
SetMap(map[string]any{
|
SetMap(map[string]any{
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
|
@ -79,7 +79,7 @@ func (db *DB) SaveToken(ctx context.Context, q pgxscan.Querier, userID xid.ID, t
|
||||||
return t, errors.Wrap(err, "building sql")
|
return t, errors.Wrap(err, "building sql")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = pgxscan.Get(ctx, q, &t, sql, args...)
|
err = pgxscan.Get(ctx, db, &t, sql, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return t, errors.Wrap(err, "inserting token")
|
return t, errors.Wrap(err, "inserting token")
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package db
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/subtle"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
@ -19,7 +18,6 @@ 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 {
|
||||||
|
@ -58,8 +56,6 @@ 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 []byte
|
|
||||||
Salt []byte
|
|
||||||
|
|
||||||
DeletedAt *time.Time
|
DeletedAt *time.Time
|
||||||
SelfDelete *bool
|
SelfDelete *bool
|
||||||
|
@ -105,10 +101,7 @@ const (
|
||||||
PreferenceSizeSmall PreferenceSize = "small"
|
PreferenceSizeSmall PreferenceSize = "small"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (u User) NumProviders(emails []UserEmail) (numProviders int) {
|
func (u User) NumProviders() (numProviders int) {
|
||||||
if len(emails) > 0 {
|
|
||||||
numProviders++
|
|
||||||
}
|
|
||||||
if u.Discord != nil {
|
if u.Discord != nil {
|
||||||
numProviders++
|
numProviders++
|
||||||
}
|
}
|
||||||
|
@ -139,19 +132,6 @@ 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)
|
|
||||||
return subtle.ConstantTimeCompare(inputHash, u.Password) == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func hashPassword(password, salt []byte) []byte {
|
|
||||||
return argon2.IDKey(password, salt, 1, 65536, 4, 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Badge int32
|
type Badge int32
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -6,10 +6,11 @@ import (
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/meta"
|
"codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/meta"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/mod"
|
"codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/mod"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/user"
|
"codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/user"
|
||||||
auth2 "codeberg.org/pronounscc/pronouns.cc/backend/routes/v2/auth"
|
|
||||||
user2 "codeberg.org/pronounscc/pronouns.cc/backend/routes/v2/user"
|
user2 "codeberg.org/pronounscc/pronouns.cc/backend/routes/v2/user"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
|
_ "embed"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mountRoutes mounts all API routes on the server's router.
|
// mountRoutes mounts all API routes on the server's router.
|
||||||
|
@ -25,6 +26,5 @@ func mountRoutes(s *server.Server) {
|
||||||
|
|
||||||
s.Router.Route("/v2", func(r chi.Router) {
|
s.Router.Route("/v2", func(r chi.Router) {
|
||||||
user2.Mount(s, r)
|
user2.Mount(s, r)
|
||||||
auth2.Mount(s, r)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||||
|
@ -120,7 +119,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// save token to database
|
// save token to database
|
||||||
_, err = s.DB.SaveToken(ctx, s.DB, u.ID, tokenID, false, false)
|
_, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "saving token to database")
|
return errors.Wrap(err, "saving token to database")
|
||||||
}
|
}
|
||||||
|
@ -143,7 +142,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// no user found, so save a ticket + save their Discord info in Redis
|
// no user found, so save a ticket + save their Discord info in Redis
|
||||||
ticket := common.RandBase64(32)
|
ticket := RandBase64(32)
|
||||||
err = s.DB.SetJSON(ctx, "discord:"+ticket, du, "EX", "600")
|
err = s.DB.SetJSON(ctx, "discord:"+ticket, du, "EX", "600")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("setting Discord user for ticket %q: %v", ticket, err)
|
log.Errorf("setting Discord user for ticket %q: %v", ticket, err)
|
||||||
|
@ -235,13 +234,8 @@ func (s *Server) discordUnlink(w http.ResponseWriter, r *http.Request) error {
|
||||||
return server.APIError{Code: server.ErrNotLinked}
|
return server.APIError{Code: server.ErrNotLinked}
|
||||||
}
|
}
|
||||||
|
|
||||||
emails, err := s.DB.UserEmails(ctx, u.SnowflakeID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "getting user emails")
|
|
||||||
}
|
|
||||||
|
|
||||||
// cannot unlink last auth provider
|
// cannot unlink last auth provider
|
||||||
if u.NumProviders(emails) <= 1 {
|
if u.NumProviders() <= 1 {
|
||||||
return server.APIError{Code: server.ErrLastProvider}
|
return server.APIError{Code: server.ErrLastProvider}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -366,6 +360,12 @@ func (s *Server) discordSignup(w http.ResponseWriter, r *http.Request) error {
|
||||||
return errors.Wrap(err, "deleting signup ticket")
|
return errors.Wrap(err, "deleting signup ticket")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// commit transaction
|
||||||
|
err = tx.Commit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "committing transaction")
|
||||||
|
}
|
||||||
|
|
||||||
// create token
|
// create token
|
||||||
// TODO: implement user + token permissions
|
// TODO: implement user + token permissions
|
||||||
tokenID := xid.New()
|
tokenID := xid.New()
|
||||||
|
@ -375,17 +375,11 @@ func (s *Server) discordSignup(w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// save token to database
|
// save token to database
|
||||||
_, err = s.DB.SaveToken(ctx, tx, u.ID, tokenID, false, false)
|
_, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "saving token to database")
|
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
|
// return user
|
||||||
render.JSON(w, r, signupResponse{
|
render.JSON(w, r, signupResponse{
|
||||||
User: *dbUserToUserResponse(u, nil),
|
User: *dbUserToUserResponse(u, nil),
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||||
|
@ -141,7 +140,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// save token to database
|
// save token to database
|
||||||
_, err = s.DB.SaveToken(ctx, s.DB, u.ID, tokenID, false, false)
|
_, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "saving token to database")
|
return errors.Wrap(err, "saving token to database")
|
||||||
}
|
}
|
||||||
|
@ -164,7 +163,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// no user found, so save a ticket + save their Mastodon info in Redis
|
// no user found, so save a ticket + save their Mastodon info in Redis
|
||||||
ticket := common.RandBase64(32)
|
ticket := RandBase64(32)
|
||||||
err = s.DB.SetJSON(ctx, "mastodon:"+ticket, mu, "EX", "600")
|
err = s.DB.SetJSON(ctx, "mastodon:"+ticket, mu, "EX", "600")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("setting mastoAPI user for ticket %q: %v", ticket, err)
|
log.Errorf("setting mastoAPI user for ticket %q: %v", ticket, err)
|
||||||
|
@ -262,13 +261,8 @@ func (s *Server) mastodonUnlink(w http.ResponseWriter, r *http.Request) error {
|
||||||
return server.APIError{Code: server.ErrNotLinked}
|
return server.APIError{Code: server.ErrNotLinked}
|
||||||
}
|
}
|
||||||
|
|
||||||
emails, err := s.DB.UserEmails(ctx, u.SnowflakeID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "getting user emails")
|
|
||||||
}
|
|
||||||
|
|
||||||
// cannot unlink last auth provider
|
// cannot unlink last auth provider
|
||||||
if u.NumProviders(emails) <= 1 {
|
if u.NumProviders() <= 1 {
|
||||||
return server.APIError{Code: server.ErrLastProvider}
|
return server.APIError{Code: server.ErrLastProvider}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -394,6 +388,12 @@ func (s *Server) mastodonSignup(w http.ResponseWriter, r *http.Request) error {
|
||||||
return errors.Wrap(err, "deleting signup ticket")
|
return errors.Wrap(err, "deleting signup ticket")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// commit transaction
|
||||||
|
err = tx.Commit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "committing transaction")
|
||||||
|
}
|
||||||
|
|
||||||
// create token
|
// create token
|
||||||
// TODO: implement user + token permissions
|
// TODO: implement user + token permissions
|
||||||
tokenID := xid.New()
|
tokenID := xid.New()
|
||||||
|
@ -403,17 +403,11 @@ func (s *Server) mastodonSignup(w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// save token to database
|
// save token to database
|
||||||
_, err = s.DB.SaveToken(ctx, tx, u.ID, tokenID, false, false)
|
_, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "saving token to database")
|
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
|
// return user
|
||||||
render.JSON(w, r, signupResponse{
|
render.JSON(w, r, signupResponse{
|
||||||
User: *dbUserToUserResponse(u, nil),
|
User: *dbUserToUserResponse(u, nil),
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||||
|
@ -120,7 +119,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// save token to database
|
// save token to database
|
||||||
_, err = s.DB.SaveToken(ctx, s.DB, u.ID, tokenID, false, false)
|
_, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "saving token to database")
|
return errors.Wrap(err, "saving token to database")
|
||||||
}
|
}
|
||||||
|
@ -143,7 +142,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// no user found, so save a ticket + save their Misskey info in Redis
|
// no user found, so save a ticket + save their Misskey info in Redis
|
||||||
ticket := common.RandBase64(32)
|
ticket := RandBase64(32)
|
||||||
err = s.DB.SetJSON(ctx, "misskey:"+ticket, mu.User, "EX", "600")
|
err = s.DB.SetJSON(ctx, "misskey:"+ticket, mu.User, "EX", "600")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("setting misskey user for ticket %q: %v", ticket, err)
|
log.Errorf("setting misskey user for ticket %q: %v", ticket, err)
|
||||||
|
@ -317,6 +316,12 @@ func (s *Server) misskeySignup(w http.ResponseWriter, r *http.Request) error {
|
||||||
return errors.Wrap(err, "deleting signup ticket")
|
return errors.Wrap(err, "deleting signup ticket")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// commit transaction
|
||||||
|
err = tx.Commit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "committing transaction")
|
||||||
|
}
|
||||||
|
|
||||||
// create token
|
// create token
|
||||||
// TODO: implement user + token permissions
|
// TODO: implement user + token permissions
|
||||||
tokenID := xid.New()
|
tokenID := xid.New()
|
||||||
|
@ -326,17 +331,11 @@ func (s *Server) misskeySignup(w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// save token to database
|
// save token to database
|
||||||
_, err = s.DB.SaveToken(ctx, tx, u.ID, tokenID, false, false)
|
_, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "saving token to database")
|
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
|
// return user
|
||||||
render.JSON(w, r, signupResponse{
|
render.JSON(w, r, signupResponse{
|
||||||
User: *dbUserToUserResponse(u, nil),
|
User: *dbUserToUserResponse(u, nil),
|
||||||
|
|
|
@ -68,13 +68,10 @@ func (s *Server) noAppFediverseURL(ctx context.Context, w http.ResponseWriter, r
|
||||||
case "iceshrimp":
|
case "iceshrimp":
|
||||||
softwareName = "firefish"
|
softwareName = "firefish"
|
||||||
fallthrough
|
fallthrough
|
||||||
case "misskey", "foundkey", "calckey", "firefish":
|
case "misskey", "foundkey", "calckey", "firefish", "sharkey":
|
||||||
return s.noAppMisskeyURL(ctx, w, r, softwareName, instance)
|
return s.noAppMisskeyURL(ctx, w, r, softwareName, instance)
|
||||||
case "mastodon", "pleroma", "akkoma", "pixelfed", "gotosocial":
|
case "mastodon", "pleroma", "akkoma", "incestoma", "pixelfed", "gotosocial":
|
||||||
case "glitchcafe", "hometown":
|
case "glitchcafe", "hometown":
|
||||||
// plural.cafe (potentially other instances too?) runs Mastodon but changes the software name
|
|
||||||
// Hometown is a lightweight fork of Mastodon so we can just treat it the same
|
|
||||||
// changing it back to mastodon here for consistency
|
|
||||||
softwareName = "mastodon"
|
softwareName = "mastodon"
|
||||||
default:
|
default:
|
||||||
return server.APIError{Code: server.ErrUnsupportedInstance}
|
return server.APIError{Code: server.ErrUnsupportedInstance}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||||
|
@ -139,7 +138,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// save token to database
|
// save token to database
|
||||||
_, err = s.DB.SaveToken(ctx, s.DB, u.ID, tokenID, false, false)
|
_, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "saving token to database")
|
return errors.Wrap(err, "saving token to database")
|
||||||
}
|
}
|
||||||
|
@ -162,7 +161,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// no user found, so save a ticket + save their Google info in Redis
|
// no user found, so save a ticket + save their Google info in Redis
|
||||||
ticket := common.RandBase64(32)
|
ticket := RandBase64(32)
|
||||||
err = s.DB.SetJSON(ctx, "google:"+ticket, partialGoogleUser{ID: googleID, Email: googleUsername}, "EX", "600")
|
err = s.DB.SetJSON(ctx, "google:"+ticket, partialGoogleUser{ID: googleID, Email: googleUsername}, "EX", "600")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("setting Google user for ticket %q: %v", ticket, err)
|
log.Errorf("setting Google user for ticket %q: %v", ticket, err)
|
||||||
|
@ -250,13 +249,8 @@ func (s *Server) googleUnlink(w http.ResponseWriter, r *http.Request) error {
|
||||||
return server.APIError{Code: server.ErrNotLinked}
|
return server.APIError{Code: server.ErrNotLinked}
|
||||||
}
|
}
|
||||||
|
|
||||||
emails, err := s.DB.UserEmails(ctx, u.SnowflakeID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "getting user emails")
|
|
||||||
}
|
|
||||||
|
|
||||||
// cannot unlink last auth provider
|
// cannot unlink last auth provider
|
||||||
if u.NumProviders(emails) <= 1 {
|
if u.NumProviders() <= 1 {
|
||||||
return server.APIError{Code: server.ErrLastProvider}
|
return server.APIError{Code: server.ErrLastProvider}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -369,6 +363,12 @@ func (s *Server) googleSignup(w http.ResponseWriter, r *http.Request) error {
|
||||||
return errors.Wrap(err, "deleting signup ticket")
|
return errors.Wrap(err, "deleting signup ticket")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// commit transaction
|
||||||
|
err = tx.Commit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "committing transaction")
|
||||||
|
}
|
||||||
|
|
||||||
// create token
|
// create token
|
||||||
// TODO: implement user + token permissions
|
// TODO: implement user + token permissions
|
||||||
tokenID := xid.New()
|
tokenID := xid.New()
|
||||||
|
@ -378,17 +378,11 @@ func (s *Server) googleSignup(w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// save token to database
|
// save token to database
|
||||||
_, err = s.DB.SaveToken(ctx, tx, u.ID, tokenID, false, false)
|
_, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "saving token to database")
|
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
|
// return user
|
||||||
render.JSON(w, r, signupResponse{
|
render.JSON(w, r, signupResponse{
|
||||||
User: *dbUserToUserResponse(u, nil),
|
User: *dbUserToUserResponse(u, nil),
|
||||||
|
|
|
@ -2,8 +2,9 @@ package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
|
||||||
"github.com/mediocregopher/radix/v4"
|
"github.com/mediocregopher/radix/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -13,7 +14,7 @@ const numStates = "1000"
|
||||||
|
|
||||||
// setCSRFState generates a random string to use as state, then stores that in Redis.
|
// setCSRFState generates a random string to use as state, then stores that in Redis.
|
||||||
func (s *Server) setCSRFState(ctx context.Context) (string, error) {
|
func (s *Server) setCSRFState(ctx context.Context) (string, error) {
|
||||||
state := common.RandBase64(32)
|
state := RandBase64(32)
|
||||||
|
|
||||||
err := s.DB.MultiCmd(ctx,
|
err := s.DB.MultiCmd(ctx,
|
||||||
radix.Cmd(nil, "LPUSH", "csrf", state),
|
radix.Cmd(nil, "LPUSH", "csrf", state),
|
||||||
|
@ -31,3 +32,14 @@ func (s *Server) validateCSRFState(ctx context.Context, state string) (matched b
|
||||||
}
|
}
|
||||||
return num > 0, nil
|
return num > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RandBase64 returns a string of random bytes encoded in raw base 64.
|
||||||
|
func RandBase64(size int) string {
|
||||||
|
b := make([]byte, size)
|
||||||
|
_, err := rand.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64.RawURLEncoding.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
|
@ -115,7 +115,7 @@ func (s *Server) createToken(w http.ResponseWriter, r *http.Request) error {
|
||||||
return errors.Wrap(err, "creating token")
|
return errors.Wrap(err, "creating token")
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := s.DB.SaveToken(ctx, s.DB, claims.UserID, tokenID, true, readOnly)
|
t, err := s.DB.SaveToken(ctx, claims.UserID, tokenID, true, readOnly)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "saving token")
|
return errors.Wrap(err, "saving token")
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||||
|
@ -172,7 +171,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// save token to database
|
// save token to database
|
||||||
_, err = s.DB.SaveToken(ctx, s.DB, u.ID, tokenID, false, false)
|
_, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "saving token to database")
|
return errors.Wrap(err, "saving token to database")
|
||||||
}
|
}
|
||||||
|
@ -195,7 +194,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// no user found, so save a ticket + save their Tumblr info in Redis
|
// no user found, so save a ticket + save their Tumblr info in Redis
|
||||||
ticket := common.RandBase64(32)
|
ticket := RandBase64(32)
|
||||||
err = s.DB.SetJSON(ctx, "tumblr:"+ticket, tumblrUserInfo{ID: tumblrID, Name: tumblrName}, "EX", "600")
|
err = s.DB.SetJSON(ctx, "tumblr:"+ticket, tumblrUserInfo{ID: tumblrID, Name: tumblrName}, "EX", "600")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("setting Tumblr user for ticket %q: %v", ticket, err)
|
log.Errorf("setting Tumblr user for ticket %q: %v", ticket, err)
|
||||||
|
@ -283,13 +282,8 @@ func (s *Server) tumblrUnlink(w http.ResponseWriter, r *http.Request) error {
|
||||||
return server.APIError{Code: server.ErrNotLinked}
|
return server.APIError{Code: server.ErrNotLinked}
|
||||||
}
|
}
|
||||||
|
|
||||||
emails, err := s.DB.UserEmails(ctx, u.SnowflakeID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "getting user emails")
|
|
||||||
}
|
|
||||||
|
|
||||||
// cannot unlink last auth provider
|
// cannot unlink last auth provider
|
||||||
if u.NumProviders(emails) <= 1 {
|
if u.NumProviders() <= 1 {
|
||||||
return server.APIError{Code: server.ErrLastProvider}
|
return server.APIError{Code: server.ErrLastProvider}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -402,6 +396,12 @@ func (s *Server) tumblrSignup(w http.ResponseWriter, r *http.Request) error {
|
||||||
return errors.Wrap(err, "deleting signup ticket")
|
return errors.Wrap(err, "deleting signup ticket")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// commit transaction
|
||||||
|
err = tx.Commit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "committing transaction")
|
||||||
|
}
|
||||||
|
|
||||||
// create token
|
// create token
|
||||||
// TODO: implement user + token permissions
|
// TODO: implement user + token permissions
|
||||||
tokenID := xid.New()
|
tokenID := xid.New()
|
||||||
|
@ -411,17 +411,11 @@ func (s *Server) tumblrSignup(w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// save token to database
|
// save token to database
|
||||||
_, err = s.DB.SaveToken(ctx, s.DB, u.ID, tokenID, false, false)
|
_, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "saving token to database")
|
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
|
// return user
|
||||||
render.JSON(w, r, signupResponse{
|
render.JSON(w, r, signupResponse{
|
||||||
User: *dbUserToUserResponse(u, nil),
|
User: *dbUserToUserResponse(u, nil),
|
||||||
|
|
|
@ -13,17 +13,10 @@ import (
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
*server.Server
|
*server.Server
|
||||||
|
|
||||||
requireInvite bool
|
|
||||||
emailSignup bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Mount(srv *server.Server, r chi.Router) {
|
func Mount(srv *server.Server, r chi.Router) {
|
||||||
s := &Server{
|
s := &Server{Server: srv}
|
||||||
Server: srv,
|
|
||||||
requireInvite: os.Getenv("REQUIRE_INVITE") == "true",
|
|
||||||
emailSignup: os.Getenv("EMAIL_SMTP_ADDRESS") != "",
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Get("/meta", server.WrapHandler(s.meta))
|
r.Get("/meta", server.WrapHandler(s.meta))
|
||||||
}
|
}
|
||||||
|
@ -34,7 +27,6 @@ type MetaResponse struct {
|
||||||
Users MetaUsers `json:"users"`
|
Users MetaUsers `json:"users"`
|
||||||
Members int64 `json:"members"`
|
Members int64 `json:"members"`
|
||||||
RequireInvite bool `json:"require_invite"`
|
RequireInvite bool `json:"require_invite"`
|
||||||
EmailSignup bool `json:"email_signup"`
|
|
||||||
Notice *MetaNotice `json:"notice"`
|
Notice *MetaNotice `json:"notice"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,8 +69,7 @@ func (s *Server) meta(w http.ResponseWriter, r *http.Request) error {
|
||||||
ActiveDay: activeDay,
|
ActiveDay: activeDay,
|
||||||
},
|
},
|
||||||
Members: numMembers,
|
Members: numMembers,
|
||||||
RequireInvite: s.requireInvite,
|
RequireInvite: os.Getenv("REQUIRE_INVITE") == "true",
|
||||||
EmailSignup: s.emailSignup,
|
|
||||||
Notice: notice,
|
Notice: notice,
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -1,192 +0,0 @@
|
||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
|
||||||
"emperror.dev/errors"
|
|
||||||
"github.com/go-chi/render"
|
|
||||||
"github.com/mediocregopher/radix/v4"
|
|
||||||
"github.com/rs/xid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type postEmailSignupRequest struct {
|
|
||||||
Email string `json:"email"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) postEmailSignup(w http.ResponseWriter, r *http.Request) (err error) {
|
|
||||||
ctx := r.Context()
|
|
||||||
var req postEmailSignupRequest
|
|
||||||
err = render.Decode(r, &req)
|
|
||||||
if err != nil {
|
|
||||||
return server.APIError{Code: server.ErrBadRequest}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(req.Email, "@") {
|
|
||||||
return server.APIError{Code: server.ErrBadRequest, Details: "Email seems to be invalid"}
|
|
||||||
}
|
|
||||||
|
|
||||||
ticket := common.RandBase64(48)
|
|
||||||
err = s.DB.Redis.Do(ctx, radix.Cmd(nil, "SET", emailSignupTicketKey(ticket), req.Email, "EX", "3600"))
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "setting email signup key")
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the email address already exists, pretend we sent an email and return
|
|
||||||
// to prevent people from discovering valid email addresses
|
|
||||||
exists, err := s.DB.EmailExists(ctx, req.Email)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "checking if email exists")
|
|
||||||
}
|
|
||||||
if exists {
|
|
||||||
render.NoContent(w, r)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
go s.SendEmail(req.Email, "Confirm your email address", "signup", map[string]any{
|
|
||||||
"Ticket": ticket,
|
|
||||||
})
|
|
||||||
|
|
||||||
render.NoContent(w, r)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func emailSignupTicketKey(ticket string) string {
|
|
||||||
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(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 {
|
|
||||||
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, tx, 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"`
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
|
||||||
"emperror.dev/errors"
|
|
||||||
"github.com/go-chi/render"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EmailResponse struct {
|
|
||||||
ID common.EmailID `json:"id"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func dbEmailToResponse(e db.UserEmail) EmailResponse {
|
|
||||||
return EmailResponse{
|
|
||||||
ID: e.ID,
|
|
||||||
Email: e.EmailAddress,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) getEmails(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
ctx := r.Context()
|
|
||||||
claims, _ := server.ClaimsFromContext(ctx)
|
|
||||||
if claims.APIToken {
|
|
||||||
return server.APIError{Code: server.ErrMissingPermissions, Details: "This endpoint cannot be used by API tokens"}
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := s.DB.User(ctx, claims.UserID)
|
|
||||||
if err != nil {
|
|
||||||
// this should never fail
|
|
||||||
log.Errorf("getting user: %v", err)
|
|
||||||
return errors.Wrap(err, "getting user")
|
|
||||||
}
|
|
||||||
|
|
||||||
emails, err := s.DB.UserEmails(ctx, u.SnowflakeID)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("getting user emails: %v", err)
|
|
||||||
return errors.Wrap(err, "getting user emails")
|
|
||||||
}
|
|
||||||
|
|
||||||
out := make([]EmailResponse, len(emails))
|
|
||||||
for i := range emails {
|
|
||||||
out[i] = dbEmailToResponse(emails[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
render.JSON(w, r, out)
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,225 +0,0 @@
|
||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
|
||||||
"emperror.dev/errors"
|
|
||||||
"github.com/go-chi/render"
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"github.com/mediocregopher/radix/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
type putEmailRequest struct {
|
|
||||||
Email string `json:"email"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) putEmail(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
ctx := r.Context()
|
|
||||||
claims, _ := server.ClaimsFromContext(ctx)
|
|
||||||
if claims.APIToken {
|
|
||||||
return server.APIError{Code: server.ErrMissingPermissions, Details: "This endpoint cannot be used by API tokens"}
|
|
||||||
}
|
|
||||||
|
|
||||||
var req putEmailRequest
|
|
||||||
err := render.Decode(r, &req)
|
|
||||||
if err != nil {
|
|
||||||
return server.APIError{Code: server.ErrBadRequest}
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := s.DB.User(ctx, claims.UserID)
|
|
||||||
if err != nil {
|
|
||||||
// this should never fail
|
|
||||||
log.Errorf("getting user: %v", err)
|
|
||||||
return errors.Wrap(err, "getting user")
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := s.DB.Begin(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "beginning transaction")
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = tx.Rollback(ctx)
|
|
||||||
}()
|
|
||||||
|
|
||||||
emails, err := s.DB.UserEmailsTx(ctx, tx, u.SnowflakeID)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("getting user emails: %v", err)
|
|
||||||
return errors.Wrap(err, "getting user emails")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(emails) > 0 {
|
|
||||||
if emails[0].EmailAddress == req.Email {
|
|
||||||
return server.APIError{Code: server.ErrBadRequest, Details: "New email address cannot be the same as the current one"}
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.putEmailExisting(w, r, tx, u, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
ticket := common.RandBase64(48)
|
|
||||||
err = s.DB.Redis.Do(ctx, radix.Cmd(nil, "SET",
|
|
||||||
emailChangeTicketKey(ticket), emailChangeTicketValue(req.Email, u.SnowflakeID), "EX", "3600"))
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "setting email change key")
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the email address already exists, pretend we sent an email and return
|
|
||||||
exists, err := s.DB.EmailExistsTx(ctx, tx, req.Email)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "checking if email exists")
|
|
||||||
}
|
|
||||||
if exists {
|
|
||||||
render.NoContent(w, r)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the user's password, this won't do anything unless the email address is actually confirmed
|
|
||||||
err = s.DB.SetPassword(ctx, tx, u.SnowflakeID, req.Password)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "setting user password")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tx.Commit(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "committing transaction")
|
|
||||||
}
|
|
||||||
|
|
||||||
// send the email
|
|
||||||
go s.SendEmail(req.Email, "Confirm your email address", "change", map[string]any{
|
|
||||||
"Ticket": ticket,
|
|
||||||
"Username": u.Username,
|
|
||||||
})
|
|
||||||
|
|
||||||
render.NoContent(w, r)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) putEmailExisting(
|
|
||||||
w http.ResponseWriter, r *http.Request, tx pgx.Tx, u db.User, req putEmailRequest,
|
|
||||||
) error {
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
if !u.VerifyPassword(req.Password) {
|
|
||||||
return server.APIError{Code: server.ErrForbidden, Details: "Invalid password"}
|
|
||||||
}
|
|
||||||
|
|
||||||
ticket := common.RandBase64(48)
|
|
||||||
err := s.DB.Redis.Do(ctx, radix.Cmd(nil, "SET",
|
|
||||||
emailChangeTicketKey(ticket), emailChangeTicketValue(req.Email, u.SnowflakeID), "EX", "3600"))
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "setting email change key")
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the email address already exists, pretend we sent an email and return
|
|
||||||
exists, err := s.DB.EmailExistsTx(ctx, tx, req.Email)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "checking if email exists")
|
|
||||||
}
|
|
||||||
if exists {
|
|
||||||
render.NoContent(w, r)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
go s.SendEmail(req.Email, "Confirm your email address", "change", map[string]any{
|
|
||||||
"Ticket": ticket,
|
|
||||||
"Username": u.Username,
|
|
||||||
})
|
|
||||||
|
|
||||||
render.NoContent(w, r)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type putEmailConfirmRequest struct {
|
|
||||||
Ticket string `json:"ticket"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type putEmailConfirmResponse struct {
|
|
||||||
Email string `json:"email"`
|
|
||||||
User userResponse `json:"user"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) putEmailConfim(w http.ResponseWriter, r *http.Request) (err error) {
|
|
||||||
ctx := r.Context()
|
|
||||||
var req putEmailConfirmRequest
|
|
||||||
err = render.Decode(r, &req)
|
|
||||||
if err != nil {
|
|
||||||
return server.APIError{Code: server.ErrBadRequest}
|
|
||||||
}
|
|
||||||
|
|
||||||
var ticket string
|
|
||||||
err = s.DB.Redis.Do(ctx, radix.Cmd(&ticket, "GET", emailChangeTicketKey(req.Ticket)))
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "getting email change key")
|
|
||||||
}
|
|
||||||
if ticket == "" {
|
|
||||||
return server.APIError{Code: server.ErrBadRequest, Details: "Unknown ticket"}
|
|
||||||
}
|
|
||||||
|
|
||||||
email, userID, ok := parseEmailChangeTicket(ticket)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid email change ticket %q", ticket)
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := s.DB.UserBySnowflake(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "getting user")
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := s.DB.Begin(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "beginning transaction")
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = tx.Rollback(ctx)
|
|
||||||
}()
|
|
||||||
|
|
||||||
err = s.DB.RemoveEmails(ctx, tx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "removing existing email addresses for user %v", userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
dbEmail, err := s.DB.AddEmail(ctx, s.DB, userID, 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")
|
|
||||||
}
|
|
||||||
|
|
||||||
render.JSON(w, r, putEmailConfirmResponse{
|
|
||||||
Email: dbEmail.EmailAddress,
|
|
||||||
User: *dbUserToUserResponse(u, nil),
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func emailChangeTicketKey(ticket string) string {
|
|
||||||
return "email-change:" + ticket
|
|
||||||
}
|
|
||||||
|
|
||||||
func emailChangeTicketValue(email string, userID common.UserID) string {
|
|
||||||
return email + ":" + userID.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseEmailChangeTicket(v string) (email string, userID common.UserID, ok bool) {
|
|
||||||
before, after, ok := strings.Cut(v, ":")
|
|
||||||
if !ok {
|
|
||||||
return "", common.UserID(common.NullSnowflake), false
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := common.ParseSnowflake(after)
|
|
||||||
if err != nil {
|
|
||||||
return "", common.UserID(common.NullSnowflake), false
|
|
||||||
}
|
|
||||||
|
|
||||||
return before, common.UserID(id), true
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/smtp"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/jordan-wright/email"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Server struct {
|
|
||||||
*server.Server
|
|
||||||
|
|
||||||
emailPool *email.Pool
|
|
||||||
emailAddress string
|
|
||||||
baseURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
func Mount(srv *server.Server, r chi.Router) {
|
|
||||||
s := &Server{
|
|
||||||
Server: srv,
|
|
||||||
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
|
|
||||||
if os.Getenv("EMAIL_SMTP_ADDRESS") != "" {
|
|
||||||
// This function will never return an error
|
|
||||||
s.emailPool, _ = email.NewPool(
|
|
||||||
os.Getenv("EMAIL_SMTP_ADDRESS"), // This should be the SMTP server host, like "example.com:587"
|
|
||||||
4, smtp.PlainAuth("",
|
|
||||||
os.Getenv("EMAIL_USERNAME"), // This should be the account name, like "noreply@pronouns.cc"
|
|
||||||
os.Getenv("EMAIL_PASSWORD"), // This should be the account pass, like "6GzW3dY6"
|
|
||||||
os.Getenv("EMAIL_HOST"), // This should be the email host (seems to be SMTP server without the port?)
|
|
||||||
))
|
|
||||||
|
|
||||||
r.Route("/auth/email", func(r chi.Router) {
|
|
||||||
r.With(server.MustAuth).Get("/", server.WrapHandler(s.getEmails))
|
|
||||||
r.With(server.MustAuth).Put("/", server.WrapHandler(s.putEmail))
|
|
||||||
r.With(server.MustAuth).Delete("/{id}", nil) // Remove existing email from account, <no body>
|
|
||||||
|
|
||||||
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", server.WrapHandler(s.putEmailConfim)) // Confirm email address, { ticket }
|
|
||||||
|
|
||||||
r.Patch("/password", nil) // Update password
|
|
||||||
r.Post("/password/forgot", nil) // Forgot/reset password, { email }
|
|
||||||
r.Post("/password/confirm", nil) // Confirm resetting password, { ticket, new_password }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"embed"
|
|
||||||
"html/template"
|
|
||||||
"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) {
|
|
||||||
e := email.NewEmail()
|
|
||||||
e.From = s.emailAddress
|
|
||||||
e.To = []string{to}
|
|
||||||
e.Subject = title
|
|
||||||
|
|
||||||
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 {
|
|
||||||
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, template+".txt", data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, errors.Wrap(err, "executing text template")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tmpl.ExecuteTemplate(htmlWriter, template+".html", data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, errors.Wrap(err, "executing HTML template")
|
|
||||||
}
|
|
||||||
|
|
||||||
return textWriter.Bytes(), htmlWriter.Bytes(), nil
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
<!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>
|
|
||||||
To change <b>@{{.Username}}</b>'s email address, press the following link:
|
|
||||||
<br />
|
|
||||||
<a href="{{.BaseURL}}/auth/email/confirm/{{.Ticket}}">Confirm your new email address</a>
|
|
||||||
<br />
|
|
||||||
Note that this link will expire in one hour.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
If you didn't mean to change your email address, feel free to ignore this email.
|
|
||||||
</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,5 +0,0 @@
|
||||||
To change @{{.Username}}'s email address, press the following link:
|
|
||||||
{{.BaseURL}}/auth/email/confirm/{{.Ticket}}
|
|
||||||
This link will expire in one hour.
|
|
||||||
|
|
||||||
If you didn't mean to change your email address, feel free to ignore this email.
|
|
|
@ -1,24 +0,0 @@
|
||||||
<!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:
|
|
||||||
<br />
|
|
||||||
<a href="{{.BaseURL}}/auth/signup/confirm/{{.Ticket}}">Confirm your email address</a>
|
|
||||||
<br />
|
|
||||||
Note that this link will expire in one hour.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
If you didn't mean to create a new account, feel free to ignore this email.
|
|
||||||
</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,5 +0,0 @@
|
||||||
Please continue creating a new pronouns.cc account by using the following link:
|
|
||||||
{{.BaseURL}}/auth/signup/confirm/{{.Ticket}}
|
|
||||||
This link will expire in one hour.
|
|
||||||
|
|
||||||
If you didn't mean to create a new account, feel free to ignore this email.
|
|
|
@ -151,11 +151,6 @@ export interface Warning {
|
||||||
read: boolean;
|
read: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Email {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface APIError {
|
export interface APIError {
|
||||||
code: ErrorCode;
|
code: ErrorCode;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
|
|
@ -11,7 +11,6 @@ export interface MetaResponse {
|
||||||
users: MetaUsers;
|
users: MetaUsers;
|
||||||
members: number;
|
members: number;
|
||||||
require_invite: boolean;
|
require_invite: boolean;
|
||||||
email_signup: boolean;
|
|
||||||
notice: { id: number; notice: string } | null;
|
notice: { id: number; notice: string } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
export let urls: string[];
|
export let urls: string[];
|
||||||
export let alt: string;
|
export let alt: string;
|
||||||
export let width = 300;
|
export let width = 300;
|
||||||
|
export let lazyLoad = false;
|
||||||
|
|
||||||
const contentTypeFor = (url: string) => {
|
const contentTypeFor = (url: string) => {
|
||||||
if (url.endsWith(".webp")) {
|
if (url.endsWith(".webp")) {
|
||||||
|
@ -31,6 +32,7 @@
|
||||||
src={urls[0] || defaultAvatars[0]}
|
src={urls[0] || defaultAvatars[0]}
|
||||||
{alt}
|
{alt}
|
||||||
class="rounded-circle img-fluid"
|
class="rounded-circle img-fluid"
|
||||||
|
loading={lazyLoad ? "lazy" : "eager"}
|
||||||
/>
|
/>
|
||||||
</picture>
|
</picture>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
@ -46,7 +46,12 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<a href="/@{user.name}/{member.name}">
|
<a href="/@{user.name}/{member.name}">
|
||||||
<FallbackImage urls={memberAvatars(member)} width={200} alt="Avatar for {member.name}" />
|
<FallbackImage
|
||||||
|
urls={memberAvatars(member)}
|
||||||
|
width={200}
|
||||||
|
lazyLoad
|
||||||
|
alt="Avatar for {member.name}"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
<p class="m-2">
|
<p class="m-2">
|
||||||
<a class="text-reset fs-5 text-break" href="/@{user.name}/{member.name}">
|
<a class="text-reset fs-5 text-break" href="/@{user.name}/{member.name}">
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
import type { APIError, User } from "$lib/api/entities";
|
|
||||||
import { apiFetch } from "$lib/api/fetch";
|
|
||||||
|
|
||||||
export const load = async ({ params }) => {
|
|
||||||
try {
|
|
||||||
const resp = await apiFetch<{ email: string; user: User }>("/auth/email/confirm", {
|
|
||||||
method: "POST",
|
|
||||||
version: 2,
|
|
||||||
body: { ticket: params.ticket },
|
|
||||||
});
|
|
||||||
return { error: null, email: resp.email, user: resp.user };
|
|
||||||
} catch (e) {
|
|
||||||
return { error: e as APIError, email: null, user: null };
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,25 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
// TODO: improve this page's layout, it's very empty right now
|
|
||||||
|
|
||||||
import type { PageData } from "./$types";
|
|
||||||
|
|
||||||
export let data: PageData;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if !data.error}
|
|
||||||
<h1>Email confirmed!</h1>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
The email address <strong>{data.email}</strong> is now linked to
|
|
||||||
<strong>@{data.user.name}</strong>, and can be used to log in.
|
|
||||||
</p>
|
|
||||||
{:else}
|
|
||||||
<h1>Error confirming email</h1>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>{data.error.code}</strong>: {data.error.message}
|
|
||||||
{#if data.error.details}{data.error.details}{/if}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>Please <a href="/settings/auth">try again</a>.</p>
|
|
||||||
{/if}
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { apiFetch } from "$lib/api/fetch";
|
import { apiFetch } from "$lib/api/fetch";
|
||||||
import { PUBLIC_BASE_URL } from "$env/static/public";
|
import { PUBLIC_BASE_URL } from "$env/static/public";
|
||||||
import type { UrlsResponse } from "$lib/api/responses";
|
import type { UrlsResponse } from "$lib/api/responses";
|
||||||
import type { APIError, MeUser } from "$lib/api/entities";
|
|
||||||
|
|
||||||
export const load = async () => {
|
export const load = async () => {
|
||||||
const resp = await apiFetch<UrlsResponse>("/auth/urls", {
|
const resp = await apiFetch<UrlsResponse>("/auth/urls", {
|
||||||
|
@ -13,29 +12,3 @@ export const load = async () => {
|
||||||
|
|
||||||
return resp;
|
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,5 +1,4 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from "$app/forms";
|
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import type { APIError } from "$lib/api/entities";
|
import type { APIError } from "$lib/api/entities";
|
||||||
import { apiFetch } from "$lib/api/fetch";
|
import { apiFetch } from "$lib/api/fetch";
|
||||||
|
@ -9,7 +8,6 @@
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
FormGroup,
|
|
||||||
Icon,
|
Icon,
|
||||||
Input,
|
Input,
|
||||||
ListGroup,
|
ListGroup,
|
||||||
|
@ -18,21 +16,9 @@
|
||||||
ModalBody,
|
ModalBody,
|
||||||
ModalFooter,
|
ModalFooter,
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import type { PageData, ActionData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
export let data: PageData;
|
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 error: APIError | null = null;
|
||||||
let instance = "";
|
let instance = "";
|
||||||
|
@ -73,31 +59,7 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Log in or sign up</h1>
|
<h1>Log in or sign up</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md-4 mb-1">
|
||||||
{#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" required />
|
|
||||||
</FormGroup>
|
|
||||||
<FormGroup class="m-1" floating label="Password">
|
|
||||||
<Input name="password" type="password" required />
|
|
||||||
</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>
|
<ListGroup>
|
||||||
<ListGroupItem tag="button" on:click={toggleModal}>Log in with the Fediverse</ListGroupItem>
|
<ListGroupItem tag="button" on:click={toggleModal}>Log in with the Fediverse</ListGroupItem>
|
||||||
{#if data.discord}
|
{#if data.discord}
|
||||||
|
@ -139,5 +101,10 @@
|
||||||
a token in your browser to identify your account.
|
a token in your browser to identify your account.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
import { PUBLIC_BASE_URL } from "$env/static/public";
|
|
||||||
import { ErrorCode, type APIError } from "$lib/api/entities";
|
|
||||||
import { apiFetch, fastFetch } from "$lib/api/fetch";
|
|
||||||
import type { UrlsResponse } from "$lib/api/responses";
|
|
||||||
import { error, redirect } from "@sveltejs/kit";
|
|
||||||
|
|
||||||
export const load = async ({ parent }) => {
|
|
||||||
const data = await parent();
|
|
||||||
if (!data.email_signup) {
|
|
||||||
redirect(303, "/auth/login");
|
|
||||||
}
|
|
||||||
|
|
||||||
const resp = await apiFetch<UrlsResponse>("/auth/urls", {
|
|
||||||
method: "POST",
|
|
||||||
body: {
|
|
||||||
callback_domain: PUBLIC_BASE_URL,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return resp;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const actions = {
|
|
||||||
signup: async ({ request }) => {
|
|
||||||
const data = await request.formData();
|
|
||||||
const email = data.get("email");
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fastFetch("/auth/email/signup", {
|
|
||||||
method: "POST",
|
|
||||||
version: 2,
|
|
||||||
body: { email },
|
|
||||||
});
|
|
||||||
|
|
||||||
return { status: "ok", email };
|
|
||||||
} catch (e) {
|
|
||||||
return { status: "error", error: e as APIError };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
fedi: async ({ request }) => {
|
|
||||||
const data = await request.formData();
|
|
||||||
const instance = data.get("instance");
|
|
||||||
if (!instance) error(400, { code: ErrorCode.BadRequest, message: "Empty instance domain" });
|
|
||||||
|
|
||||||
let resp: { url: string };
|
|
||||||
try {
|
|
||||||
resp = await apiFetch<{ url: string }>(
|
|
||||||
`/auth/urls/fediverse?instance=${encodeURIComponent(instance as string)}`,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
return { status: "error", error: e as APIError };
|
|
||||||
}
|
|
||||||
redirect(303, resp.url);
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,83 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { enhance } from "$app/forms";
|
|
||||||
import { Alert, Button, FormGroup, Input, InputGroup } from "@sveltestrap/sveltestrap";
|
|
||||||
import type { ActionData, PageData } from "./$types";
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
import { userStore } from "$lib/store";
|
|
||||||
import { addToast } from "$lib/toast";
|
|
||||||
import { goto } from "$app/navigation";
|
|
||||||
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
|
|
||||||
|
|
||||||
export let data: PageData;
|
|
||||||
export let form: ActionData;
|
|
||||||
$: ok = form?.status === "ok";
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if ($userStore) {
|
|
||||||
addToast({ header: "Error", body: "You are already logged in." });
|
|
||||||
goto("/");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>Sign up - pronouns.ccc</title>
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<h1>Sign up</h1>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Enter your email address to get started. After confirming it, you can choose a username and
|
|
||||||
password.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form method="POST" use:enhance action="?/signup">
|
|
||||||
{#if form?.error}
|
|
||||||
<ErrorAlert error={form.error} />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if ok}
|
|
||||||
<Alert class="mx-1">
|
|
||||||
<h4 class="alert-heading">Check your email</h4>
|
|
||||||
Check your email to continue signing up!
|
|
||||||
</Alert>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<FormGroup class="m-1" floating label="Email">
|
|
||||||
{#if !ok}
|
|
||||||
<Input name="email" type="email" required />
|
|
||||||
{:else}
|
|
||||||
<Input name="email" type="email" disabled value={form?.email || undefined} />
|
|
||||||
{/if}
|
|
||||||
</FormGroup>
|
|
||||||
<p class="mt-3 mx-1">
|
|
||||||
<Button color="primary" type="submit" disabled={ok}>Sign up</Button>
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<p>Or, if you would rather sign up using another provider:</p>
|
|
||||||
|
|
||||||
<div class="d-flex flex-column flex-lg-row">
|
|
||||||
<div class="d-flex flex-row my-1">
|
|
||||||
{#if data.discord}
|
|
||||||
<Button class="mx-1" href={data.discord}>Log in with Discord</Button>
|
|
||||||
{/if}
|
|
||||||
{#if data.tumblr}
|
|
||||||
<Button class="mx-1" href={data.tumblr}>Log in with Tumblr</Button>
|
|
||||||
{/if}
|
|
||||||
{#if data.google}
|
|
||||||
<Button class="mx-1" href={data.google}>Log in with Google</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="m-1">
|
|
||||||
<form method="POST" use:enhance action="?/fedi">
|
|
||||||
<InputGroup>
|
|
||||||
<Input style="max-width: 200px" name="instance" placeholder="Server" required />
|
|
||||||
<Button type="submit">Log in with the fediverse</Button>
|
|
||||||
</InputGroup>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { ErrorCode, type APIError, type MeUser } from "$lib/api/entities";
|
|
||||||
import { apiFetch } from "$lib/api/fetch";
|
|
||||||
|
|
||||||
interface SignupConfirmData {
|
|
||||||
user: MeUser;
|
|
||||||
token: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const actions = {
|
|
||||||
default: async ({ request, params }) => {
|
|
||||||
const data = await request.formData();
|
|
||||||
const username = data.get("username");
|
|
||||||
const password = data.get("password");
|
|
||||||
const password2 = data.get("password2");
|
|
||||||
|
|
||||||
if (password !== password2) {
|
|
||||||
return {
|
|
||||||
status: "error",
|
|
||||||
error: { code: ErrorCode.BadRequest, message: "Passwords do not match" } as APIError,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await apiFetch<SignupConfirmData>("/auth/email/signup/confirm", {
|
|
||||||
method: "POST",
|
|
||||||
version: 2,
|
|
||||||
body: { username, password, ticket: params.ticket },
|
|
||||||
});
|
|
||||||
|
|
||||||
return { status: "ok", data: resp };
|
|
||||||
} catch (e) {
|
|
||||||
return { status: "error", error: e as APIError };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,47 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { enhance } from "$app/forms";
|
|
||||||
import { Button, FormGroup, Input } from "@sveltestrap/sveltestrap";
|
|
||||||
import type { ActionData } from "./$types";
|
|
||||||
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
|
|
||||||
import { userStore } from "$lib/store";
|
|
||||||
import { addToast } from "$lib/toast";
|
|
||||||
import { goto } from "$app/navigation";
|
|
||||||
|
|
||||||
export let form: ActionData;
|
|
||||||
|
|
||||||
$: signupCallback(form);
|
|
||||||
const signupCallback = (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: "Signed up", body: "Successfully created account!" });
|
|
||||||
goto(`/@${form.data.user.name}`);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>Sign up - pronouns.cc</title>
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<h1>Sign up</h1>
|
|
||||||
|
|
||||||
<form method="POST" use:enhance>
|
|
||||||
{#if form?.error}
|
|
||||||
<ErrorAlert error={form.error} />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<FormGroup class="m-1" floating label="Username">
|
|
||||||
<Input name="username" type="text" required />
|
|
||||||
</FormGroup>
|
|
||||||
<FormGroup class="m-1" floating label="Password">
|
|
||||||
<Input name="password" type="password" required />
|
|
||||||
</FormGroup>
|
|
||||||
<FormGroup class="m-1" floating label="Password (again)">
|
|
||||||
<Input name="password2" type="password" required />
|
|
||||||
</FormGroup>
|
|
||||||
<p class="m-1">
|
|
||||||
<Button color="primary" type="submit">Sign up</Button>
|
|
||||||
</p>
|
|
||||||
</form>
|
|
|
@ -21,7 +21,7 @@
|
||||||
let canUnlink = false;
|
let canUnlink = false;
|
||||||
|
|
||||||
$: canUnlink =
|
$: canUnlink =
|
||||||
[data.emails?.[0] || null, data.user.discord, data.user.fediverse, data.user.tumblr, data.user.google]
|
[data.user.discord, data.user.fediverse, data.user.tumblr, data.user.google]
|
||||||
.map<number>((entry) => (entry === null ? 0 : 1))
|
.map<number>((entry) => (entry === null ? 0 : 1))
|
||||||
.reduce((prev, current) => prev + current) >= 2;
|
.reduce((prev, current) => prev + current) >= 2;
|
||||||
|
|
||||||
|
@ -30,19 +30,19 @@
|
||||||
let fediDisabled = false;
|
let fediDisabled = false;
|
||||||
|
|
||||||
let fediLinkModalOpen = false;
|
let fediLinkModalOpen = false;
|
||||||
const toggleFediLinkModal = () => (fediLinkModalOpen = !fediLinkModalOpen);
|
let toggleFediLinkModal = () => (fediLinkModalOpen = !fediLinkModalOpen);
|
||||||
|
|
||||||
let fediUnlinkModalOpen = false;
|
let fediUnlinkModalOpen = false;
|
||||||
const toggleFediUnlinkModal = () => (fediUnlinkModalOpen = !fediUnlinkModalOpen);
|
let toggleFediUnlinkModal = () => (fediUnlinkModalOpen = !fediUnlinkModalOpen);
|
||||||
|
|
||||||
let discordUnlinkModalOpen = false;
|
let discordUnlinkModalOpen = false;
|
||||||
const toggleDiscordUnlinkModal = () => (discordUnlinkModalOpen = !discordUnlinkModalOpen);
|
let toggleDiscordUnlinkModal = () => (discordUnlinkModalOpen = !discordUnlinkModalOpen);
|
||||||
|
|
||||||
let tumblrUnlinkModalOpen = false;
|
let tumblrUnlinkModalOpen = false;
|
||||||
const toggleTumblrUnlinkModal = () => (tumblrUnlinkModalOpen = !tumblrUnlinkModalOpen);
|
let toggleTumblrUnlinkModal = () => (tumblrUnlinkModalOpen = !tumblrUnlinkModalOpen);
|
||||||
|
|
||||||
let googleUnlinkModalOpen = false;
|
let googleUnlinkModalOpen = false;
|
||||||
const toggleGoogleUnlinkModal = () => (googleUnlinkModalOpen = !googleUnlinkModalOpen);
|
let toggleGoogleUnlinkModal = () => (googleUnlinkModalOpen = !googleUnlinkModalOpen);
|
||||||
|
|
||||||
const fediLogin = async (e: Event) => {
|
const fediLogin = async (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -107,26 +107,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h1>Authentication</h1>
|
<h1>Authentication providers</h1>
|
||||||
|
|
||||||
{#if data.email_signup}
|
|
||||||
<h2>Email address</h2>
|
|
||||||
{#if data.emails && data.emails.length > 0}
|
|
||||||
<p>Your current email address is <strong>{data.emails[0].email}</strong>.</p>
|
|
||||||
<p>
|
|
||||||
<Button>Change email address</Button>
|
|
||||||
<Button>Change password</Button>
|
|
||||||
<Button outline color="danger" disabled={!canUnlink}>Remove email address</Button>
|
|
||||||
</p>
|
|
||||||
{:else}
|
|
||||||
<p>You currently do not have an email address set.</p>
|
|
||||||
<p>
|
|
||||||
<Button>Add email address</Button>
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<h2>Third-party providers</h2>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
|
|
|
@ -1,15 +1,8 @@
|
||||||
import { PUBLIC_BASE_URL } from "$env/static/public";
|
import { PUBLIC_BASE_URL } from "$env/static/public";
|
||||||
import type { Email } from "$lib/api/entities";
|
import { apiFetch } from "$lib/api/fetch";
|
||||||
import { apiFetch, apiFetchClient } from "$lib/api/fetch";
|
|
||||||
import type { UrlsResponse } from "$lib/api/responses";
|
import type { UrlsResponse } from "$lib/api/responses";
|
||||||
|
|
||||||
export const load = async ({ parent }) => {
|
export const load = async () => {
|
||||||
const data = await parent();
|
|
||||||
let emails: Array<Email> | undefined = undefined;
|
|
||||||
if (data.email_signup) {
|
|
||||||
emails = await apiFetchClient<Array<Email>>("/auth/email", "GET", undefined, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
const resp = await apiFetch<UrlsResponse>("/auth/urls", {
|
const resp = await apiFetch<UrlsResponse>("/auth/urls", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
|
@ -17,5 +10,5 @@ export const load = async ({ parent }) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return { urls: resp, emails };
|
return { urls: resp };
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,9 +13,6 @@ const config = {
|
||||||
version: {
|
version: {
|
||||||
name: child_process.execSync("git describe --tags --long --always").toString().trim(),
|
name: child_process.execSync("git describe --tags --long --always").toString().trim(),
|
||||||
},
|
},
|
||||||
csrf: {
|
|
||||||
checkOrigin: process.env.NODE_ENV !== "development",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
7
go.mod
7
go.mod
|
@ -19,7 +19,6 @@ require (
|
||||||
github.com/google/uuid v1.4.0
|
github.com/google/uuid v1.4.0
|
||||||
github.com/jackc/pgx/v5 v5.4.3
|
github.com/jackc/pgx/v5 v5.4.3
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
|
|
||||||
github.com/mediocregopher/radix/v4 v4.1.4
|
github.com/mediocregopher/radix/v4 v4.1.4
|
||||||
github.com/minio/minio-go/v7 v7.0.63
|
github.com/minio/minio-go/v7 v7.0.63
|
||||||
github.com/prometheus/client_golang v1.17.0
|
github.com/prometheus/client_golang v1.17.0
|
||||||
|
@ -28,7 +27,6 @@ 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
|
||||||
|
@ -72,11 +70,12 @@ 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.17.0 // indirect
|
golang.org/x/sys v0.13.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.13.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
|
||||||
|
|
16
go.sum
16
go.sum
|
@ -109,8 +109,6 @@ github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk
|
||||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
|
|
||||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
|
github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
|
||||||
|
@ -207,8 +205,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.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
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 +246,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.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
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.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
|
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||||
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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
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=
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
-- 2023-10-28: Add email/password login
|
|
||||||
|
|
||||||
-- +migrate Up
|
|
||||||
|
|
||||||
alter table users add column password bytea null;
|
|
||||||
alter table users add column salt bytea null;
|
|
||||||
|
|
||||||
create table user_emails (
|
|
||||||
id bigint primary key,
|
|
||||||
user_id bigint not null references users (snowflake_id) on delete cascade,
|
|
||||||
email_address text not null unique
|
|
||||||
);
|
|
||||||
|
|
||||||
-- +migrate Down
|
|
||||||
|
|
||||||
alter table users drop column password;
|
|
||||||
alter table users drop column salt;
|
|
||||||
|
|
||||||
drop table user_emails;
|
|
Loading…
Reference in a new issue