feat(backend): add DELETE /users/@me endpoint
This commit is contained in:
parent
c4b8b26ec7
commit
ff3d612b06
|
@ -5,23 +5,27 @@ package queries
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/jackc/pgtype"
|
||||||
"github.com/jackc/pgx/v4"
|
"github.com/jackc/pgx/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
const getUserByIDSQL = `SELECT * FROM users WHERE id = $1;`
|
const getUserByIDSQL = `SELECT * FROM users WHERE id = $1;`
|
||||||
|
|
||||||
type GetUserByIDRow struct {
|
type GetUserByIDRow struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
DisplayName *string `json:"display_name"`
|
DisplayName *string `json:"display_name"`
|
||||||
Bio *string `json:"bio"`
|
Bio *string `json:"bio"`
|
||||||
AvatarUrls []string `json:"avatar_urls"`
|
AvatarUrls []string `json:"avatar_urls"`
|
||||||
Links []string `json:"links"`
|
Links []string `json:"links"`
|
||||||
Discord *string `json:"discord"`
|
Discord *string `json:"discord"`
|
||||||
DiscordUsername *string `json:"discord_username"`
|
DiscordUsername *string `json:"discord_username"`
|
||||||
MaxInvites int32 `json:"max_invites"`
|
MaxInvites int32 `json:"max_invites"`
|
||||||
Names []FieldEntry `json:"names"`
|
Names []FieldEntry `json:"names"`
|
||||||
Pronouns []PronounEntry `json:"pronouns"`
|
Pronouns []PronounEntry `json:"pronouns"`
|
||||||
|
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||||
|
SelfDelete *bool `json:"self_delete"`
|
||||||
|
DeleteReason *string `json:"delete_reason"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByID implements Querier.GetUserByID.
|
// GetUserByID implements Querier.GetUserByID.
|
||||||
|
@ -31,7 +35,7 @@ func (q *DBQuerier) GetUserByID(ctx context.Context, id string) (GetUserByIDRow,
|
||||||
var item GetUserByIDRow
|
var item GetUserByIDRow
|
||||||
namesArray := q.types.newFieldEntryArray()
|
namesArray := q.types.newFieldEntryArray()
|
||||||
pronounsArray := q.types.newPronounEntryArray()
|
pronounsArray := q.types.newPronounEntryArray()
|
||||||
if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray); err != nil {
|
if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray, &item.DeletedAt, &item.SelfDelete, &item.DeleteReason); err != nil {
|
||||||
return item, fmt.Errorf("query GetUserByID: %w", err)
|
return item, fmt.Errorf("query GetUserByID: %w", err)
|
||||||
}
|
}
|
||||||
if err := namesArray.AssignTo(&item.Names); err != nil {
|
if err := namesArray.AssignTo(&item.Names); err != nil {
|
||||||
|
@ -54,7 +58,7 @@ func (q *DBQuerier) GetUserByIDScan(results pgx.BatchResults) (GetUserByIDRow, e
|
||||||
var item GetUserByIDRow
|
var item GetUserByIDRow
|
||||||
namesArray := q.types.newFieldEntryArray()
|
namesArray := q.types.newFieldEntryArray()
|
||||||
pronounsArray := q.types.newPronounEntryArray()
|
pronounsArray := q.types.newPronounEntryArray()
|
||||||
if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray); err != nil {
|
if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray, &item.DeletedAt, &item.SelfDelete, &item.DeleteReason); err != nil {
|
||||||
return item, fmt.Errorf("scan GetUserByIDBatch row: %w", err)
|
return item, fmt.Errorf("scan GetUserByIDBatch row: %w", err)
|
||||||
}
|
}
|
||||||
if err := namesArray.AssignTo(&item.Names); err != nil {
|
if err := namesArray.AssignTo(&item.Names); err != nil {
|
||||||
|
@ -69,17 +73,20 @@ func (q *DBQuerier) GetUserByIDScan(results pgx.BatchResults) (GetUserByIDRow, e
|
||||||
const getUserByUsernameSQL = `SELECT * FROM users WHERE username = $1;`
|
const getUserByUsernameSQL = `SELECT * FROM users WHERE username = $1;`
|
||||||
|
|
||||||
type GetUserByUsernameRow struct {
|
type GetUserByUsernameRow struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
DisplayName *string `json:"display_name"`
|
DisplayName *string `json:"display_name"`
|
||||||
Bio *string `json:"bio"`
|
Bio *string `json:"bio"`
|
||||||
AvatarUrls []string `json:"avatar_urls"`
|
AvatarUrls []string `json:"avatar_urls"`
|
||||||
Links []string `json:"links"`
|
Links []string `json:"links"`
|
||||||
Discord *string `json:"discord"`
|
Discord *string `json:"discord"`
|
||||||
DiscordUsername *string `json:"discord_username"`
|
DiscordUsername *string `json:"discord_username"`
|
||||||
MaxInvites int32 `json:"max_invites"`
|
MaxInvites int32 `json:"max_invites"`
|
||||||
Names []FieldEntry `json:"names"`
|
Names []FieldEntry `json:"names"`
|
||||||
Pronouns []PronounEntry `json:"pronouns"`
|
Pronouns []PronounEntry `json:"pronouns"`
|
||||||
|
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||||
|
SelfDelete *bool `json:"self_delete"`
|
||||||
|
DeleteReason *string `json:"delete_reason"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByUsername implements Querier.GetUserByUsername.
|
// GetUserByUsername implements Querier.GetUserByUsername.
|
||||||
|
@ -89,7 +96,7 @@ func (q *DBQuerier) GetUserByUsername(ctx context.Context, username string) (Get
|
||||||
var item GetUserByUsernameRow
|
var item GetUserByUsernameRow
|
||||||
namesArray := q.types.newFieldEntryArray()
|
namesArray := q.types.newFieldEntryArray()
|
||||||
pronounsArray := q.types.newPronounEntryArray()
|
pronounsArray := q.types.newPronounEntryArray()
|
||||||
if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray); err != nil {
|
if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray, &item.DeletedAt, &item.SelfDelete, &item.DeleteReason); err != nil {
|
||||||
return item, fmt.Errorf("query GetUserByUsername: %w", err)
|
return item, fmt.Errorf("query GetUserByUsername: %w", err)
|
||||||
}
|
}
|
||||||
if err := namesArray.AssignTo(&item.Names); err != nil {
|
if err := namesArray.AssignTo(&item.Names); err != nil {
|
||||||
|
@ -112,7 +119,7 @@ func (q *DBQuerier) GetUserByUsernameScan(results pgx.BatchResults) (GetUserByUs
|
||||||
var item GetUserByUsernameRow
|
var item GetUserByUsernameRow
|
||||||
namesArray := q.types.newFieldEntryArray()
|
namesArray := q.types.newFieldEntryArray()
|
||||||
pronounsArray := q.types.newPronounEntryArray()
|
pronounsArray := q.types.newPronounEntryArray()
|
||||||
if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray); err != nil {
|
if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray, &item.DeletedAt, &item.SelfDelete, &item.DeleteReason); err != nil {
|
||||||
return item, fmt.Errorf("scan GetUserByUsernameBatch row: %w", err)
|
return item, fmt.Errorf("scan GetUserByUsernameBatch row: %w", err)
|
||||||
}
|
}
|
||||||
if err := namesArray.AssignTo(&item.Names); err != nil {
|
if err := namesArray.AssignTo(&item.Names); err != nil {
|
||||||
|
@ -137,17 +144,20 @@ type UpdateUserNamesPronounsParams struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateUserNamesPronounsRow struct {
|
type UpdateUserNamesPronounsRow struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
DisplayName *string `json:"display_name"`
|
DisplayName *string `json:"display_name"`
|
||||||
Bio *string `json:"bio"`
|
Bio *string `json:"bio"`
|
||||||
AvatarUrls []string `json:"avatar_urls"`
|
AvatarUrls []string `json:"avatar_urls"`
|
||||||
Links []string `json:"links"`
|
Links []string `json:"links"`
|
||||||
Discord *string `json:"discord"`
|
Discord *string `json:"discord"`
|
||||||
DiscordUsername *string `json:"discord_username"`
|
DiscordUsername *string `json:"discord_username"`
|
||||||
MaxInvites int32 `json:"max_invites"`
|
MaxInvites int32 `json:"max_invites"`
|
||||||
Names []FieldEntry `json:"names"`
|
Names []FieldEntry `json:"names"`
|
||||||
Pronouns []PronounEntry `json:"pronouns"`
|
Pronouns []PronounEntry `json:"pronouns"`
|
||||||
|
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||||
|
SelfDelete *bool `json:"self_delete"`
|
||||||
|
DeleteReason *string `json:"delete_reason"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUserNamesPronouns implements Querier.UpdateUserNamesPronouns.
|
// UpdateUserNamesPronouns implements Querier.UpdateUserNamesPronouns.
|
||||||
|
@ -157,7 +167,7 @@ func (q *DBQuerier) UpdateUserNamesPronouns(ctx context.Context, params UpdateUs
|
||||||
var item UpdateUserNamesPronounsRow
|
var item UpdateUserNamesPronounsRow
|
||||||
namesArray := q.types.newFieldEntryArray()
|
namesArray := q.types.newFieldEntryArray()
|
||||||
pronounsArray := q.types.newPronounEntryArray()
|
pronounsArray := q.types.newPronounEntryArray()
|
||||||
if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray); err != nil {
|
if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray, &item.DeletedAt, &item.SelfDelete, &item.DeleteReason); err != nil {
|
||||||
return item, fmt.Errorf("query UpdateUserNamesPronouns: %w", err)
|
return item, fmt.Errorf("query UpdateUserNamesPronouns: %w", err)
|
||||||
}
|
}
|
||||||
if err := namesArray.AssignTo(&item.Names); err != nil {
|
if err := namesArray.AssignTo(&item.Names); err != nil {
|
||||||
|
@ -180,7 +190,7 @@ func (q *DBQuerier) UpdateUserNamesPronounsScan(results pgx.BatchResults) (Updat
|
||||||
var item UpdateUserNamesPronounsRow
|
var item UpdateUserNamesPronounsRow
|
||||||
namesArray := q.types.newFieldEntryArray()
|
namesArray := q.types.newFieldEntryArray()
|
||||||
pronounsArray := q.types.newPronounEntryArray()
|
pronounsArray := q.types.newPronounEntryArray()
|
||||||
if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray); err != nil {
|
if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray, &item.DeletedAt, &item.SelfDelete, &item.DeleteReason); err != nil {
|
||||||
return item, fmt.Errorf("scan UpdateUserNamesPronounsBatch row: %w", err)
|
return item, fmt.Errorf("scan UpdateUserNamesPronounsBatch row: %w", err)
|
||||||
}
|
}
|
||||||
if err := namesArray.AssignTo(&item.Names); err != nil {
|
if err := namesArray.AssignTo(&item.Names); err != nil {
|
||||||
|
|
|
@ -96,3 +96,16 @@ func (db *DB) InvalidateToken(ctx context.Context, userID xid.ID, tokenID xid.ID
|
||||||
}
|
}
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *DB) InvalidateAllTokens(ctx context.Context, q querier, userID xid.ID) error {
|
||||||
|
sql, args, err := sq.Update("tokens").Where("user_id = ?", userID).Set("invalidated", true).ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "building sql")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = q.Exec(ctx, sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "executing query")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -3,11 +3,13 @@ package db
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"time"
|
||||||
|
|
||||||
"codeberg.org/u1f320/pronouns.cc/backend/db/queries"
|
"codeberg.org/u1f320/pronouns.cc/backend/db/queries"
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
"github.com/jackc/pgconn"
|
"github.com/jackc/pgconn"
|
||||||
|
"github.com/jackc/pgtype"
|
||||||
"github.com/jackc/pgx/v4"
|
"github.com/jackc/pgx/v4"
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
)
|
)
|
||||||
|
@ -28,6 +30,10 @@ type User struct {
|
||||||
DiscordUsername *string
|
DiscordUsername *string
|
||||||
|
|
||||||
MaxInvites int
|
MaxInvites int
|
||||||
|
|
||||||
|
DeletedAt *time.Time
|
||||||
|
SelfDelete *bool
|
||||||
|
DeleteReason *string
|
||||||
}
|
}
|
||||||
|
|
||||||
// usernames must match this regex
|
// usernames must match this regex
|
||||||
|
@ -134,6 +140,11 @@ func (db *DB) getUser(ctx context.Context, q querier, id xid.ID) (u User, err er
|
||||||
return u, errors.Wrap(err, "getting user from database")
|
return u, errors.Wrap(err, "getting user from database")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var deletedAt *time.Time
|
||||||
|
if qu.DeletedAt.Status == pgtype.Present {
|
||||||
|
deletedAt = &qu.DeletedAt.Time
|
||||||
|
}
|
||||||
|
|
||||||
u = User{
|
u = User{
|
||||||
ID: id,
|
ID: id,
|
||||||
Username: qu.Username,
|
Username: qu.Username,
|
||||||
|
@ -146,6 +157,9 @@ func (db *DB) getUser(ctx context.Context, q querier, id xid.ID) (u User, err er
|
||||||
Discord: qu.Discord,
|
Discord: qu.Discord,
|
||||||
DiscordUsername: qu.DiscordUsername,
|
DiscordUsername: qu.DiscordUsername,
|
||||||
MaxInvites: int(qu.MaxInvites),
|
MaxInvites: int(qu.MaxInvites),
|
||||||
|
DeletedAt: deletedAt,
|
||||||
|
SelfDelete: qu.SelfDelete,
|
||||||
|
DeleteReason: qu.DeleteReason,
|
||||||
}
|
}
|
||||||
|
|
||||||
return u, nil
|
return u, nil
|
||||||
|
@ -283,3 +297,20 @@ func (db *DB) UpdateUser(
|
||||||
}
|
}
|
||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *DB) DeleteUser(ctx context.Context, q querier, id xid.ID, selfDelete bool, reason string) error {
|
||||||
|
builder := sq.Update("users").Set("deleted_at", time.Now().UTC()).Set("self_delete", selfDelete).Where("id = ?", id)
|
||||||
|
if !selfDelete {
|
||||||
|
builder = builder.Set("delete_reason", reason)
|
||||||
|
}
|
||||||
|
sql, args, err := builder.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "building sql")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = q.Exec(ctx, sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "executing query")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -84,7 +84,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
|
||||||
// TODO: implement user + token permissions
|
// TODO: implement user + token permissions
|
||||||
tokenID := xid.New()
|
tokenID := xid.New()
|
||||||
token, err := s.Auth.CreateToken(u.ID, tokenID, false, true)
|
token, err := s.Auth.CreateToken(u.ID, tokenID, false, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -217,7 +217,7 @@ func (s *Server) discordSignup(w http.ResponseWriter, r *http.Request) error {
|
||||||
// create token
|
// create token
|
||||||
// TODO: implement user + token permissions
|
// TODO: implement user + token permissions
|
||||||
tokenID := xid.New()
|
tokenID := xid.New()
|
||||||
token, err := s.Auth.CreateToken(u.ID, tokenID, false, true)
|
token, err := s.Auth.CreateToken(u.ID, tokenID, false, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "creating token")
|
return errors.Wrap(err, "creating token")
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"codeberg.org/u1f320/pronouns.cc/backend/server"
|
||||||
|
"emperror.dev/errors"
|
||||||
|
"github.com/go-chi/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) deleteUser(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
ctx := r.Context()
|
||||||
|
claims, _ := server.ClaimsFromContext(ctx)
|
||||||
|
|
||||||
|
if claims.APIToken || !claims.TokenWrite {
|
||||||
|
return server.APIError{Code: server.ErrMissingPermissions}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := s.DB.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "creating transaction")
|
||||||
|
}
|
||||||
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
|
err = s.DB.DeleteUser(ctx, tx, claims.UserID, true, "")
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "setting user as deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.DB.InvalidateAllTokens(ctx, tx, claims.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "invalidating tokens")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "committing transaction")
|
||||||
|
}
|
||||||
|
|
||||||
|
render.NoContent(w, r)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ func Mount(srv *server.Server, r chi.Router) {
|
||||||
r.With(server.MustAuth).Group(func(r chi.Router) {
|
r.With(server.MustAuth).Group(func(r chi.Router) {
|
||||||
r.Get("/@me", server.WrapHandler(s.getMeUser))
|
r.Get("/@me", server.WrapHandler(s.getMeUser))
|
||||||
r.Patch("/@me", server.WrapHandler(s.patchUser))
|
r.Patch("/@me", server.WrapHandler(s.patchUser))
|
||||||
|
r.Delete("/@me", server.WrapHandler(s.deleteUser))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,9 @@ type Claims struct {
|
||||||
TokenID xid.ID `json:"jti"`
|
TokenID xid.ID `json:"jti"`
|
||||||
UserIsAdmin bool `json:"adm"`
|
UserIsAdmin bool `json:"adm"`
|
||||||
|
|
||||||
|
// APIToken specifies whether this token was generated for the API or for the website.
|
||||||
|
// API tokens cannot perform some destructive actions, such as DELETE /users/@me.
|
||||||
|
APIToken bool `json:"atn"`
|
||||||
// TokenWrite specifies whether this token can be used for write actions.
|
// TokenWrite specifies whether this token can be used for write actions.
|
||||||
// If set to false, this token can only be used for read actions.
|
// If set to false, this token can only be used for read actions.
|
||||||
TokenWrite bool `json:"twr"`
|
TokenWrite bool `json:"twr"`
|
||||||
|
@ -48,7 +51,7 @@ const ExpireDays = 30
|
||||||
|
|
||||||
// CreateToken creates a token for the given user ID.
|
// CreateToken creates a token for the given user ID.
|
||||||
// It expires after 30 days.
|
// It expires after 30 days.
|
||||||
func (v *Verifier) CreateToken(userID, tokenID xid.ID, isAdmin bool, isWriteToken bool) (token string, err error) {
|
func (v *Verifier) CreateToken(userID, tokenID xid.ID, isAdmin bool, isAPIToken bool, isWriteToken bool) (token string, err error) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
expires := now.Add(ExpireDays * 24 * time.Hour)
|
expires := now.Add(ExpireDays * 24 * time.Hour)
|
||||||
|
|
||||||
|
@ -56,6 +59,7 @@ func (v *Verifier) CreateToken(userID, tokenID xid.ID, isAdmin bool, isWriteToke
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
TokenID: tokenID,
|
TokenID: tokenID,
|
||||||
UserIsAdmin: isAdmin,
|
UserIsAdmin: isAdmin,
|
||||||
|
APIToken: isAPIToken,
|
||||||
TokenWrite: isWriteToken,
|
TokenWrite: isWriteToken,
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
Issuer: "pronouns",
|
Issuer: "pronouns",
|
||||||
|
|
|
@ -83,6 +83,7 @@ const (
|
||||||
ErrInvitesDisabled = 1008 // invites are disabled (unneeded)
|
ErrInvitesDisabled = 1008 // invites are disabled (unneeded)
|
||||||
ErrInviteLimitReached = 1009 // invite limit reached (when creating invites)
|
ErrInviteLimitReached = 1009 // invite limit reached (when creating invites)
|
||||||
ErrInviteAlreadyUsed = 1010 // invite already used (when signing up)
|
ErrInviteAlreadyUsed = 1010 // invite already used (when signing up)
|
||||||
|
ErrDeletionPending = 1011 // own user deletion pending, returned with undo code
|
||||||
|
|
||||||
// User-related error codes
|
// User-related error codes
|
||||||
ErrUserNotFound = 2001
|
ErrUserNotFound = 2001
|
||||||
|
@ -94,7 +95,8 @@ const (
|
||||||
ErrNotOwnMember = 3004
|
ErrNotOwnMember = 3004
|
||||||
|
|
||||||
// General request error codes
|
// General request error codes
|
||||||
ErrRequestTooBig = 4001
|
ErrRequestTooBig = 4001
|
||||||
|
ErrMissingPermissions = 4002
|
||||||
)
|
)
|
||||||
|
|
||||||
var errCodeMessages = map[int]string{
|
var errCodeMessages = map[int]string{
|
||||||
|
@ -115,6 +117,7 @@ var errCodeMessages = map[int]string{
|
||||||
ErrInvitesDisabled: "Invites are disabled",
|
ErrInvitesDisabled: "Invites are disabled",
|
||||||
ErrInviteLimitReached: "Your account has reached the invite limit",
|
ErrInviteLimitReached: "Your account has reached the invite limit",
|
||||||
ErrInviteAlreadyUsed: "That invite code has already been used",
|
ErrInviteAlreadyUsed: "That invite code has already been used",
|
||||||
|
ErrDeletionPending: "Your account is pending deletion",
|
||||||
|
|
||||||
ErrUserNotFound: "User not found",
|
ErrUserNotFound: "User not found",
|
||||||
|
|
||||||
|
@ -123,7 +126,8 @@ var errCodeMessages = map[int]string{
|
||||||
ErrMemberNameInUse: "Member name already in use",
|
ErrMemberNameInUse: "Member name already in use",
|
||||||
ErrNotOwnMember: "Not your member",
|
ErrNotOwnMember: "Not your member",
|
||||||
|
|
||||||
ErrRequestTooBig: "Request too big (max 2 MB)",
|
ErrRequestTooBig: "Request too big (max 2 MB)",
|
||||||
|
ErrMissingPermissions: "Your account or current token is missing required permissions for this action",
|
||||||
}
|
}
|
||||||
|
|
||||||
var errCodeStatuses = map[int]int{
|
var errCodeStatuses = map[int]int{
|
||||||
|
@ -144,6 +148,7 @@ var errCodeStatuses = map[int]int{
|
||||||
ErrInvitesDisabled: http.StatusForbidden,
|
ErrInvitesDisabled: http.StatusForbidden,
|
||||||
ErrInviteLimitReached: http.StatusForbidden,
|
ErrInviteLimitReached: http.StatusForbidden,
|
||||||
ErrInviteAlreadyUsed: http.StatusBadRequest,
|
ErrInviteAlreadyUsed: http.StatusBadRequest,
|
||||||
|
ErrDeletionPending: http.StatusBadRequest,
|
||||||
|
|
||||||
ErrUserNotFound: http.StatusNotFound,
|
ErrUserNotFound: http.StatusNotFound,
|
||||||
|
|
||||||
|
@ -152,5 +157,6 @@ var errCodeStatuses = map[int]int{
|
||||||
ErrMemberNameInUse: http.StatusBadRequest,
|
ErrMemberNameInUse: http.StatusBadRequest,
|
||||||
ErrNotOwnMember: http.StatusForbidden,
|
ErrNotOwnMember: http.StatusForbidden,
|
||||||
|
|
||||||
ErrRequestTooBig: http.StatusBadRequest,
|
ErrRequestTooBig: http.StatusBadRequest,
|
||||||
|
ErrMissingPermissions: http.StatusForbidden,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
-- +migrate Up
|
||||||
|
|
||||||
|
-- 2023-03-07: add delete functionality
|
||||||
|
|
||||||
|
-- if not null, the user is soft deleted
|
||||||
|
alter table users add column deleted_at timestamptz;
|
||||||
|
-- if true, the user deleted their account themselves + should have option to reactivate; should also be deleted after 30 days
|
||||||
|
alter table users add column self_delete boolean;
|
||||||
|
-- delete reason if the user was deleted by a moderator
|
||||||
|
alter table users add column delete_reason text;
|
Loading…
Reference in New Issue