forked from mirrors/pronouns.cc
192 lines
5.4 KiB
Go
192 lines
5.4 KiB
Go
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"`
|
|
}
|