@@ -0,0 +1,250 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gardomatic.kleiax.de/internal/auth"
|
||||
"gardomatic.kleiax.de/internal/platform/validate"
|
||||
"gardomatic.kleiax.de/internal/storage"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
type accountSession struct {
|
||||
ID string `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
Current bool `json:"current"`
|
||||
}
|
||||
|
||||
func (app *application) listAccountSessionsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
user, _ := app.contextGetAuthenticatedUser(r)
|
||||
currentID := app.sessions.GetString(r.Context(), accountSessionIDKey)
|
||||
sessions := make([]accountSession, 0)
|
||||
err := app.sessions.Iterate(r.Context(), func(ctx context.Context) error {
|
||||
if app.sessions.GetInt(ctx, authenticatedUserIDSessionKey) != user.ID {
|
||||
return nil
|
||||
}
|
||||
id := app.sessions.GetString(ctx, accountSessionIDKey)
|
||||
if id == "" {
|
||||
return nil
|
||||
}
|
||||
sessions = append(sessions, accountSession{
|
||||
ID: id,
|
||||
CreatedAt: time.Unix(app.sessions.GetInt64(ctx, accountSessionCreatedAtKey), 0).UTC(),
|
||||
ExpiresAt: app.sessions.Deadline(ctx),
|
||||
Current: id == currentID,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
if err = app.writeJSON(w, http.StatusOK, envelope{"sessions": sessions}, nil); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) deleteAccountSessionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
user, _ := app.contextGetAuthenticatedUser(r)
|
||||
targetID := httprouter.ParamsFromContext(r.Context()).ByName("sessionID")
|
||||
if targetID == app.sessions.GetString(r.Context(), accountSessionIDKey) {
|
||||
if err := app.sessions.Destroy(r.Context()); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
found := false
|
||||
err := app.sessions.Iterate(r.Context(), func(ctx context.Context) error {
|
||||
if app.sessions.GetInt(ctx, authenticatedUserIDSessionKey) == user.ID && app.sessions.GetString(ctx, accountSessionIDKey) == targetID {
|
||||
found = true
|
||||
return app.sessions.Destroy(ctx)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
if !found {
|
||||
app.notFoundResponse(w, r)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (app *application) updateAccountProfileHandler(w http.ResponseWriter, r *http.Request) {
|
||||
user, _ := app.contextGetAuthenticatedUser(r)
|
||||
var input struct {
|
||||
Name string `json:"name"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
if err := app.readJSON(w, r, &input); err != nil {
|
||||
app.badRequestResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
user.Name = strings.TrimSpace(input.Name)
|
||||
if color := strings.TrimSpace(input.Color); color != "" {
|
||||
user.Color = color
|
||||
}
|
||||
v := validate.New()
|
||||
storage.ValidateUser(v, user)
|
||||
if !v.Valid() {
|
||||
app.failedValidationResponse(w, r, v.Errors)
|
||||
return
|
||||
}
|
||||
updated, err := app.models.Users.Update(user)
|
||||
if err != nil {
|
||||
app.respondToAccountError(w, r, err)
|
||||
return
|
||||
}
|
||||
if err = app.writeJSON(w, http.StatusOK, envelope{"user": updated}, nil); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) updateAccountPasswordHandler(w http.ResponseWriter, r *http.Request) {
|
||||
user, _ := app.contextGetAuthenticatedUser(r)
|
||||
var input struct {
|
||||
CurrentPassword string `json:"current_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
if err := app.readJSON(w, r, &input); err != nil {
|
||||
app.badRequestResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
v := validate.New()
|
||||
auth.ValidatePasswordPlaintext(v, input.CurrentPassword)
|
||||
auth.ValidatePasswordPlaintext(v, input.NewPassword)
|
||||
if !v.Valid() {
|
||||
app.failedValidationResponse(w, r, v.Errors)
|
||||
return
|
||||
}
|
||||
match, err := user.Password.Matches(input.CurrentPassword)
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
if !match {
|
||||
app.invalidCredentialsResponse(w, r)
|
||||
return
|
||||
}
|
||||
if err = user.Password.Set(input.NewPassword); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
if _, err = app.models.Users.Update(user); err != nil {
|
||||
app.respondToAccountError(w, r, err)
|
||||
return
|
||||
}
|
||||
if err = app.models.Tokens.DeleteAllForUser(auth.ScopeAuthentication, user.ID); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
if err = app.models.Tokens.DeleteAllForUser(auth.ScopePasswordReset, user.ID); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
if err = app.writeJSON(w, http.StatusOK, envelope{"message": "password updated"}, nil); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) requestAccountEmailChangeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
user, _ := app.contextGetAuthenticatedUser(r)
|
||||
var input struct {
|
||||
Email string `json:"email"`
|
||||
CurrentPassword string `json:"current_password"`
|
||||
}
|
||||
if err := app.readJSON(w, r, &input); err != nil {
|
||||
app.badRequestResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
input.Email = strings.ToLower(strings.TrimSpace(input.Email))
|
||||
v := validate.New()
|
||||
storage.ValidateEmail(v, input.Email)
|
||||
auth.ValidatePasswordPlaintext(v, input.CurrentPassword)
|
||||
if !v.Valid() {
|
||||
app.failedValidationResponse(w, r, v.Errors)
|
||||
return
|
||||
}
|
||||
match, err := user.Password.Matches(input.CurrentPassword)
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
if !match {
|
||||
app.invalidCredentialsResponse(w, r)
|
||||
return
|
||||
}
|
||||
if existing, lookupErr := app.models.Users.GetByEmail(input.Email); lookupErr == nil && existing.ID != user.ID {
|
||||
v.AddError("email", "email address is already registered")
|
||||
app.failedValidationResponse(w, r, v.Errors)
|
||||
return
|
||||
} else if lookupErr != nil && !errors.Is(lookupErr, storage.ErrRecordNotFound) {
|
||||
app.serverErrorResponse(w, r, lookupErr)
|
||||
return
|
||||
}
|
||||
token, err := app.models.Users.CreateEmailChange(user.ID, input.Email, 45*time.Minute)
|
||||
if err != nil {
|
||||
app.respondToAccountError(w, r, err)
|
||||
return
|
||||
}
|
||||
app.background(func() {
|
||||
data := map[string]any{"confirmationURL": strings.TrimRight(app.config.WebBaseURL, "/") + "/account/email-confirm?token=" + token}
|
||||
if sendErr := app.mailer.Send(input.Email, "email_change.tmpl", data); sendErr != nil {
|
||||
app.logger.Error(sendErr.Error())
|
||||
}
|
||||
})
|
||||
if err = app.writeJSON(w, http.StatusAccepted, envelope{"message": "confirmation email sent"}, nil); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) confirmAccountEmailHandler(w http.ResponseWriter, r *http.Request) {
|
||||
user, _ := app.contextGetAuthenticatedUser(r)
|
||||
var input struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
if err := app.readJSON(w, r, &input); err != nil {
|
||||
app.badRequestResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
v := validate.New()
|
||||
auth.ValidateTokenPlaintext(v, input.Token)
|
||||
if !v.Valid() {
|
||||
app.failedValidationResponse(w, r, v.Errors)
|
||||
return
|
||||
}
|
||||
updated, err := app.models.Users.ConfirmEmailChange(input.Token, user.ID)
|
||||
if err != nil {
|
||||
app.respondToAccountError(w, r, err)
|
||||
return
|
||||
}
|
||||
if err = app.models.Tokens.DeleteAllForUser(auth.ScopeAuthentication, user.ID); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
if err = app.writeJSON(w, http.StatusOK, envelope{"user": updated}, nil); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) respondToAccountError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
switch {
|
||||
case errors.Is(err, storage.ErrDuplicateEmail):
|
||||
app.failedValidationResponse(w, r, map[string]string{"email": "email address is already registered"})
|
||||
case errors.Is(err, storage.ErrRecordNotFound):
|
||||
app.notFoundResponse(w, r)
|
||||
case errors.Is(err, storage.ErrEditConflict):
|
||||
app.editConflictResponse(w, r)
|
||||
default:
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user