Merge branch 'main' into feature/email

This commit is contained in:
sam 2024-02-01 00:47:54 +01:00
commit ae453df77c
No known key found for this signature in database
GPG key ID: B4EF20DDE721CAA1
104 changed files with 1809 additions and 449 deletions

13
.woodpecker/.backend.yml Normal file
View file

@ -0,0 +1,13 @@
when:
branch:
exclude: stable
steps:
check:
image: golang:alpine
commands:
- apk update && apk add curl vips-dev build-base
- make backend
# Install golangci-lint
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.55.2
- golangci-lint run

20
.woodpecker/.frontend.yml Normal file
View file

@ -0,0 +1,20 @@
when:
branch:
exclude: stable
steps:
check:
image: node
directory: frontend
environment: # SvelteKit expects these in the environment during build time.
- PRIVATE_SENTRY_DSN=
- PUBLIC_BASE_URL=http://pronouns.localhost
- PUBLIC_MEDIA_URL=http://pronouns.localhost/media
- PUBLIC_SHORT_BASE=http://prns.localhost
- PUBLIC_HCAPTCHA_SITEKEY=non_existent_sitekey
commands:
- corepack enable
- pnpm install
- pnpm check
- pnpm lint
- pnpm build

View file

@ -2,7 +2,7 @@ all: generate backend frontend
.PHONY: backend .PHONY: backend
backend: backend:
go build -v -o pronouns -ldflags="-buildid= -X codeberg.org/pronounscc/pronouns.cc/backend/server.Revision=`git rev-parse --short HEAD` -X codeberg.org/pronounscc/pronouns.cc/backend/server.Tag=`git describe --tags --long`" . go build -v -o pronouns -ldflags="-buildid= -X codeberg.org/pronounscc/pronouns.cc/backend/server.Revision=`git rev-parse --short HEAD` -X codeberg.org/pronounscc/pronouns.cc/backend/server.Tag=`git describe --tags --long --always`" .
.PHONY: generate .PHONY: generate
generate: generate:

View file

@ -79,7 +79,7 @@ func (db *DB) CreateExport(ctx context.Context, userID xid.ID, filename string,
return de, errors.Wrap(err, "building query") return de, errors.Wrap(err, "building query")
} }
pgxscan.Get(ctx, db, &de, sql, args...) err = pgxscan.Get(ctx, db, &de, sql, args...)
if err != nil { if err != nil {
return de, errors.Wrap(err, "executing sql") return de, errors.Wrap(err, "executing sql")
} }

View file

@ -6,6 +6,7 @@ import (
"encoding/base64" "encoding/base64"
"time" "time"
"codeberg.org/pronounscc/pronouns.cc/backend/log"
"emperror.dev/errors" "emperror.dev/errors"
"github.com/georgysavva/scany/v2/pgxscan" "github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
@ -43,7 +44,12 @@ func (db *DB) CreateInvite(ctx context.Context, userID xid.ID) (i Invite, err er
if err != nil { if err != nil {
return i, errors.Wrap(err, "beginning transaction") return i, errors.Wrap(err, "beginning transaction")
} }
defer tx.Rollback(ctx) defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
var maxInvites, inviteCount int var maxInvites, inviteCount int
err = tx.QueryRow(ctx, "SELECT max_invites FROM users WHERE id = $1", userID).Scan(&maxInvites) err = tx.QueryRow(ctx, "SELECT max_invites FROM users WHERE id = $1", userID).Scan(&maxInvites)

View file

@ -7,6 +7,7 @@ import (
"time" "time"
"codeberg.org/pronounscc/pronouns.cc/backend/common" "codeberg.org/pronounscc/pronouns.cc/backend/common"
"codeberg.org/pronounscc/pronouns.cc/backend/log"
"emperror.dev/errors" "emperror.dev/errors"
"github.com/Masterminds/squirrel" "github.com/Masterminds/squirrel"
"github.com/georgysavva/scany/v2/pgxscan" "github.com/georgysavva/scany/v2/pgxscan"
@ -287,7 +288,12 @@ func (db *DB) RerollMemberSID(ctx context.Context, userID, memberID xid.ID) (new
if err != nil { if err != nil {
return "", errors.Wrap(err, "beginning transaction") return "", errors.Wrap(err, "beginning transaction")
} }
defer tx.Rollback(ctx) defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
sql, args, err := sq.Update("members"). sql, args, err := sq.Update("members").
Set("sid", squirrel.Expr("find_free_member_sid()")). Set("sid", squirrel.Expr("find_free_member_sid()")).

View file

@ -26,7 +26,8 @@ var Command = &cli.Command{
func run(c *cli.Context) error { func run(c *cli.Context) error {
// initialize sentry // initialize sentry
if dsn := os.Getenv("SENTRY_DSN"); dsn != "" { if dsn := os.Getenv("SENTRY_DSN"); dsn != "" {
sentry.Init(sentry.ClientOptions{ // We don't need to check the error here--it's fine if no DSN is set.
_ = sentry.Init(sentry.ClientOptions{
Dsn: dsn, Dsn: dsn,
Debug: os.Getenv("DEBUG") == "true", Debug: os.Getenv("DEBUG") == "true",
Release: server.Tag, Release: server.Tag,

View file

@ -12,6 +12,7 @@ import (
"emperror.dev/errors" "emperror.dev/errors"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/jackc/pgx/v5"
"github.com/mediocregopher/radix/v4" "github.com/mediocregopher/radix/v4"
"github.com/rs/xid" "github.com/rs/xid"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@ -292,7 +293,12 @@ func (s *Server) discordSignup(w http.ResponseWriter, r *http.Request) error {
if err != nil { if err != nil {
return errors.Wrap(err, "beginning transaction") return errors.Wrap(err, "beginning transaction")
} }
defer tx.Rollback(ctx) defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
du := new(discordgo.User) du := new(discordgo.User)
err = s.DB.GetJSON(ctx, "discord:"+req.Ticket, &du) err = s.DB.GetJSON(ctx, "discord:"+req.Ticket, &du)

View file

@ -12,6 +12,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/server" "codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors" "emperror.dev/errors"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/jackc/pgx/v5"
"github.com/mediocregopher/radix/v4" "github.com/mediocregopher/radix/v4"
"github.com/rs/xid" "github.com/rs/xid"
) )
@ -320,7 +321,12 @@ func (s *Server) mastodonSignup(w http.ResponseWriter, r *http.Request) error {
if err != nil { if err != nil {
return errors.Wrap(err, "beginning transaction") return errors.Wrap(err, "beginning transaction")
} }
defer tx.Rollback(ctx) defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
mu := new(partialMastodonAccount) mu := new(partialMastodonAccount)
err = s.DB.GetJSON(ctx, "mastodon:"+req.Ticket, &mu) err = s.DB.GetJSON(ctx, "mastodon:"+req.Ticket, &mu)

View file

@ -13,6 +13,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/server" "codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors" "emperror.dev/errors"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/jackc/pgx/v5"
"github.com/mediocregopher/radix/v4" "github.com/mediocregopher/radix/v4"
"github.com/rs/xid" "github.com/rs/xid"
) )
@ -248,7 +249,12 @@ func (s *Server) misskeySignup(w http.ResponseWriter, r *http.Request) error {
if err != nil { if err != nil {
return errors.Wrap(err, "beginning transaction") return errors.Wrap(err, "beginning transaction")
} }
defer tx.Rollback(ctx) defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
mu := new(partialMisskeyAccount) mu := new(partialMisskeyAccount)
err = s.DB.GetJSON(ctx, "misskey:"+req.Ticket, &mu) err = s.DB.GetJSON(ctx, "misskey:"+req.Ticket, &mu)

View file

@ -11,6 +11,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/server" "codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors" "emperror.dev/errors"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/jackc/pgx/v5"
"github.com/mediocregopher/radix/v4" "github.com/mediocregopher/radix/v4"
"github.com/rs/xid" "github.com/rs/xid"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@ -295,7 +296,12 @@ func (s *Server) googleSignup(w http.ResponseWriter, r *http.Request) error {
if err != nil { if err != nil {
return errors.Wrap(err, "beginning transaction") return errors.Wrap(err, "beginning transaction")
} }
defer tx.Rollback(ctx) defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
gu := new(partialGoogleUser) gu := new(partialGoogleUser)
err = s.DB.GetJSON(ctx, "google:"+req.Ticket, &gu) err = s.DB.GetJSON(ctx, "google:"+req.Ticket, &gu)

View file

@ -185,7 +185,7 @@ func (s *Server) oauthURLs(w http.ResponseWriter, r *http.Request) error {
if googleOAuthConfig.ClientID != "" { if googleOAuthConfig.ClientID != "" {
googleCfg := googleOAuthConfig googleCfg := googleOAuthConfig
googleCfg.RedirectURL = req.CallbackDomain + "/auth/login/google" googleCfg.RedirectURL = req.CallbackDomain + "/auth/login/google"
resp.Google = googleCfg.AuthCodeURL(state) resp.Google = googleCfg.AuthCodeURL(state) + "&prompt=select_account"
} }
render.JSON(w, r, resp) render.JSON(w, r, resp)

View file

@ -5,9 +5,11 @@ import (
"time" "time"
"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/server" "codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors" "emperror.dev/errors"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/jackc/pgx/v5"
"github.com/rs/xid" "github.com/rs/xid"
) )
@ -63,7 +65,12 @@ func (s *Server) deleteToken(w http.ResponseWriter, r *http.Request) error {
if err != nil { if err != nil {
return errors.Wrap(err, "beginning transaction") return errors.Wrap(err, "beginning transaction")
} }
defer tx.Rollback(ctx) defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
err = s.DB.InvalidateAllTokens(ctx, tx, claims.UserID) err = s.DB.InvalidateAllTokens(ctx, tx, claims.UserID)
if err != nil { if err != nil {

View file

@ -13,6 +13,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/server" "codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors" "emperror.dev/errors"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/jackc/pgx/v5"
"github.com/mediocregopher/radix/v4" "github.com/mediocregopher/radix/v4"
"github.com/rs/xid" "github.com/rs/xid"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@ -328,7 +329,12 @@ func (s *Server) tumblrSignup(w http.ResponseWriter, r *http.Request) error {
if err != nil { if err != nil {
return errors.Wrap(err, "beginning transaction") return errors.Wrap(err, "beginning transaction")
} }
defer tx.Rollback(ctx) defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
tui := new(tumblrUserInfo) tui := new(tumblrUserInfo)
err = s.DB.GetJSON(ctx, "tumblr:"+req.Ticket, &tui) err = s.DB.GetJSON(ctx, "tumblr:"+req.Ticket, &tui)

View file

@ -11,6 +11,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/server" "codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors" "emperror.dev/errors"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/jackc/pgx/v5"
) )
type CreateMemberRequest struct { type CreateMemberRequest struct {
@ -119,7 +120,12 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error
if err != nil { if err != nil {
return errors.Wrap(err, "starting transaction") return errors.Wrap(err, "starting transaction")
} }
defer tx.Rollback(ctx) defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
m, err := s.DB.CreateMember(ctx, tx, claims.UserID, cmr.Name, cmr.DisplayName, cmr.Bio, cmr.Links) m, err := s.DB.CreateMember(ctx, tx, claims.UserID, cmr.Name, cmr.DisplayName, cmr.Bio, cmr.Links)
if err != nil { if err != nil {

View file

@ -13,6 +13,7 @@ import (
"emperror.dev/errors" "emperror.dev/errors"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/jackc/pgx/v5"
"github.com/rs/xid" "github.com/rs/xid"
) )
@ -246,7 +247,12 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
log.Errorf("creating transaction: %v", err) log.Errorf("creating transaction: %v", err)
return errors.Wrap(err, "creating transaction") return errors.Wrap(err, "creating transaction")
} }
defer tx.Rollback(ctx) defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
m, err = s.DB.UpdateMember(ctx, tx, m.ID, req.Name, req.DisplayName, req.Bio, req.Unlisted, req.Links, avatarHash) m, err = s.DB.UpdateMember(ctx, tx, m.ID, req.Name, req.DisplayName, req.Bio, req.Unlisted, req.Links, avatarHash)
if err != nil { if err != nil {

View file

@ -10,6 +10,7 @@ import (
"emperror.dev/errors" "emperror.dev/errors"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/jackc/pgx/v5"
) )
type resolveReportRequest struct { type resolveReportRequest struct {
@ -43,7 +44,12 @@ func (s *Server) resolveReport(w http.ResponseWriter, r *http.Request) error {
log.Errorf("creating transaction: %v", err) log.Errorf("creating transaction: %v", err)
return errors.Wrap(err, "creating transaction") return errors.Wrap(err, "creating transaction")
} }
defer tx.Rollback(ctx) defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
report, err := s.DB.Report(ctx, tx, id) report, err := s.DB.Report(ctx, tx, id)
if err != nil { if err != nil {

View file

@ -3,9 +3,11 @@ package user
import ( import (
"net/http" "net/http"
"codeberg.org/pronounscc/pronouns.cc/backend/log"
"codeberg.org/pronounscc/pronouns.cc/backend/server" "codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors" "emperror.dev/errors"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/jackc/pgx/v5"
) )
func (s *Server) deleteUser(w http.ResponseWriter, r *http.Request) error { func (s *Server) deleteUser(w http.ResponseWriter, r *http.Request) error {
@ -20,7 +22,12 @@ func (s *Server) deleteUser(w http.ResponseWriter, r *http.Request) error {
if err != nil { if err != nil {
return errors.Wrap(err, "creating transaction") return errors.Wrap(err, "creating transaction")
} }
defer tx.Rollback(ctx) defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
err = s.DB.DeleteUser(ctx, tx, claims.UserID, true, "") err = s.DB.DeleteUser(ctx, tx, claims.UserID, true, "")
if err != nil { if err != nil {

View file

@ -13,6 +13,7 @@ import (
"emperror.dev/errors" "emperror.dev/errors"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/jackc/pgx/v5"
"github.com/rs/xid" "github.com/rs/xid"
) )
@ -80,7 +81,12 @@ func (s *Server) postUserFlag(w http.ResponseWriter, r *http.Request) error {
if err != nil { if err != nil {
return errors.Wrap(err, "starting transaction") return errors.Wrap(err, "starting transaction")
} }
defer tx.Rollback(ctx) defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
flag, err := s.DB.CreateFlag(ctx, tx, claims.UserID, req.Name, req.Description) flag, err := s.DB.CreateFlag(ctx, tx, claims.UserID, req.Name, req.Description)
if err != nil { if err != nil {
@ -192,7 +198,12 @@ func (s *Server) patchUserFlag(w http.ResponseWriter, r *http.Request) error {
if err != nil { if err != nil {
return errors.Wrap(err, "beginning transaction") return errors.Wrap(err, "beginning transaction")
} }
defer tx.Rollback(ctx) defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
flag, err = s.DB.EditFlag(ctx, tx, flag.ID, req.Name, req.Description, nil) flag, err = s.DB.EditFlag(ctx, tx, flag.ID, req.Name, req.Description, nil)
if err != nil { if err != nil {

View file

@ -12,6 +12,7 @@ import (
"emperror.dev/errors" "emperror.dev/errors"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/rs/xid" "github.com/rs/xid"
) )
@ -221,7 +222,12 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
log.Errorf("creating transaction: %v", err) log.Errorf("creating transaction: %v", err)
return errors.Wrap(err, "creating transaction") return errors.Wrap(err, "creating transaction")
} }
defer tx.Rollback(ctx) defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
// update username // update username
if req.Username != nil && *req.Username != u.Username { if req.Username != nil && *req.Username != u.Username {

View file

@ -100,23 +100,23 @@ func New() (*Server, error) {
// set scopes // set scopes
// users // users
rateLimiter.Scope("GET", "/users/*", 60) _ = rateLimiter.Scope("GET", "/users/*", 60)
rateLimiter.Scope("PATCH", "/users/@me", 10) _ = rateLimiter.Scope("PATCH", "/users/@me", 10)
// members // members
rateLimiter.Scope("GET", "/users/*/members", 60) _ = rateLimiter.Scope("GET", "/users/*/members", 60)
rateLimiter.Scope("GET", "/users/*/members/*", 60) _ = rateLimiter.Scope("GET", "/users/*/members/*", 60)
rateLimiter.Scope("POST", "/members", 10) _ = rateLimiter.Scope("POST", "/members", 10)
rateLimiter.Scope("GET", "/members/*", 60) _ = rateLimiter.Scope("GET", "/members/*", 60)
rateLimiter.Scope("PATCH", "/members/*", 20) _ = rateLimiter.Scope("PATCH", "/members/*", 20)
rateLimiter.Scope("DELETE", "/members/*", 5) _ = rateLimiter.Scope("DELETE", "/members/*", 5)
// auth // auth
rateLimiter.Scope("*", "/auth/*", 20) _ = rateLimiter.Scope("*", "/auth/*", 20)
rateLimiter.Scope("*", "/auth/tokens", 10) _ = rateLimiter.Scope("*", "/auth/tokens", 10)
rateLimiter.Scope("*", "/auth/invites", 10) _ = rateLimiter.Scope("*", "/auth/invites", 10)
rateLimiter.Scope("POST", "/auth/discord/*", 10) _ = rateLimiter.Scope("POST", "/auth/discord/*", 10)
s.Router.Use(rateLimiter.Handler()) s.Router.Use(rateLimiter.Handler())

View file

@ -17,4 +17,15 @@ module.exports = {
es2017: true, es2017: true,
node: true, node: true,
}, },
rules: {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
varsIgnorePattern: "^_",
},
],
},
}; };

View file

@ -12,11 +12,13 @@
"format": "prettier --plugin-search-dir . --write ." "format": "prettier --plugin-search-dir . --write ."
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0", "@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-node": "^1.2.3", "@sveltejs/adapter-node": "^2.0.0",
"@sveltejs/kit": "^1.15.0", "@sveltejs/kit": "^2.0.0",
"@types/luxon": "^3.2.2", "@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/markdown-it": "^12.2.3", "@sveltestrap/sveltestrap": "^6.0.5",
"@types/luxon": "^3.3.7",
"@types/markdown-it": "^13.0.7",
"@types/node": "^18.15.11", "@types/node": "^18.15.11",
"@types/sanitize-html": "^2.9.0", "@types/sanitize-html": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^5.57.1", "@typescript-eslint/eslint-plugin": "^5.57.1",
@ -25,14 +27,13 @@
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-svelte3": "^4.0.0", "eslint-plugin-svelte3": "^4.0.0",
"prettier": "^2.8.7", "prettier": "^2.8.7",
"prettier-plugin-svelte": "^2.10.0", "prettier-plugin-svelte": "^2.10.1",
"svelte": "^3.58.0", "svelte": "^4.0.0",
"svelte-check": "^3.1.4", "svelte-check": "^3.4.3",
"svelte-hcaptcha": "^0.1.1", "svelte-hcaptcha": "^0.1.1",
"sveltestrap": "^5.10.0",
"tslib": "^2.5.0", "tslib": "^2.5.0",
"typescript": "^4.9.5", "typescript": "^5.0.0",
"vite": "^4.2.1", "vite": "^5.0.0",
"vite-plugin-markdown": "^2.1.0" "vite-plugin-markdown": "^2.1.0"
}, },
"type": "module", "type": "module",
@ -41,8 +42,8 @@
"@popperjs/core": "^2.11.7", "@popperjs/core": "^2.11.7",
"@sentry/node": "^7.46.0", "@sentry/node": "^7.46.0",
"base64-arraybuffer": "^1.0.2", "base64-arraybuffer": "^1.0.2",
"bootstrap": "5.3.0-alpha1", "bootstrap": "^5.3.2",
"bootstrap-icons": "^1.10.4", "bootstrap-icons": "^1.11.2",
"jose": "^4.13.1", "jose": "^4.13.1",
"luxon": "^3.3.0", "luxon": "^3.3.0",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",

19
frontend/src/app.d.ts vendored
View file

@ -16,23 +16,4 @@ declare global {
} }
} }
declare module "svelte-hcaptcha" {
import type { SvelteComponent } from "svelte";
export interface HCaptchaProps {
sitekey?: string;
apihost?: string;
hl?: string;
reCaptchaCompat?: boolean;
theme?: CaptchaTheme;
size?: string;
}
declare class HCaptcha extends SvelteComponent {
$$prop_def: HCaptchaProps;
}
export default HCaptcha;
}
export {}; export {};

View file

@ -2,7 +2,9 @@ import { PRIVATE_SENTRY_DSN } from "$env/static/private";
import * as Sentry from "@sentry/node"; import * as Sentry from "@sentry/node";
import type { HandleServerError } from "@sveltejs/kit"; import type { HandleServerError } from "@sveltejs/kit";
Sentry.init({ dsn: PRIVATE_SENTRY_DSN }); if (PRIVATE_SENTRY_DSN) {
Sentry.init({ dsn: PRIVATE_SENTRY_DSN });
}
export const handleError = (({ error, event }) => { export const handleError = (({ error, event }) => {
console.log(error); console.log(error);

View file

@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
import { PUBLIC_BASE_URL, PUBLIC_MEDIA_URL } from "$env/static/public"; import { PUBLIC_BASE_URL, PUBLIC_MEDIA_URL } from "$env/static/public";
export const MAX_MEMBERS = 500; export const MAX_MEMBERS = 500;

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { NavLink } from "sveltestrap"; import { NavLink } from "@sveltestrap/sveltestrap";
import { page } from "$app/stores"; import { page } from "$app/stores";
export let href: string; export let href: string;

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { APIError } from "$lib/api/entities"; import type { APIError } from "$lib/api/entities";
import { Alert } from "sveltestrap"; import { Alert } from "@sveltestrap/sveltestrap";
export let error: APIError; export let error: APIError;
</script> </script>

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Button, Icon, Tooltip } from "sveltestrap"; import { Button, Icon, Tooltip } from "@sveltestrap/sveltestrap";
export let icon: string; export let icon: string;
export let color: "primary" | "secondary" | "success" | "danger"; export let color: "primary" | "secondary" | "success" | "danger";

View file

@ -6,12 +6,12 @@
type User, type User,
type CustomPreferences, type CustomPreferences,
} from "$lib/api/entities"; } from "$lib/api/entities";
import { Icon, Tooltip } from "sveltestrap"; import { Icon, Tooltip } from "@sveltestrap/sveltestrap";
import FallbackImage from "./FallbackImage.svelte"; import FallbackImage from "./FallbackImage.svelte";
export let user: User; export let user: User;
export let member: PartialMember & { export let member: PartialMember & {
unlisted?: boolean unlisted?: boolean;
}; };
let pronouns: string | undefined; let pronouns: string | undefined;
@ -52,7 +52,7 @@
<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}">
{member.display_name ?? member.name} {member.display_name ?? member.name}
{#if member.unlisted === true} {#if member.unlisted === true}
<span bind:this={iconElement} tabindex={0}><Icon name="lock"/></span> <span bind:this={iconElement} tabindex={0}><Icon name="lock" /></span>
<Tooltip target={iconElement} placement="top">This member is hidden</Tooltip> <Tooltip target={iconElement} placement="top">This member is hidden</Tooltip>
{/if} {/if}
</a> </a>

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Icon, Tooltip } from "sveltestrap"; import { Icon, Tooltip } from "@sveltestrap/sveltestrap";
import type { CustomPreference, CustomPreferences } from "$lib/api/entities"; import type { CustomPreference, CustomPreferences } from "$lib/api/entities";
import defaultPreferences from "$lib/api/default_preferences"; import defaultPreferences from "$lib/api/default_preferences";

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Toast } from "sveltestrap"; import { Toast } from "@sveltestrap/sveltestrap";
export let header: string | undefined = undefined; export let header: string | undefined = undefined;
export let body: string; export let body: string;

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { Field, CustomPreferences } from "$lib/api/entities"; import type { Field, CustomPreferences } from "$lib/api/entities";
import IconButton from "$lib/components/IconButton.svelte"; import IconButton from "$lib/components/IconButton.svelte";
import { Button, Input, InputGroup } from "sveltestrap"; import { Button, Input, InputGroup } from "@sveltestrap/sveltestrap";
import FieldEntry from "./FieldEntry.svelte"; import FieldEntry from "./FieldEntry.svelte";
export let field: Field; export let field: Field;

View file

@ -9,7 +9,7 @@
DropdownToggle, DropdownToggle,
Icon, Icon,
Tooltip, Tooltip,
} from "sveltestrap"; } from "@sveltestrap/sveltestrap";
export let value: string; export let value: string;
export let status: string; export let status: string;

View file

@ -12,7 +12,7 @@
InputGroupText, InputGroupText,
Popover, Popover,
Tooltip, Tooltip,
} from "sveltestrap"; } from "@sveltestrap/sveltestrap";
export let pronoun: Pronoun; export let pronoun: Pronoun;
export let preferences: CustomPreferences; export let preferences: CustomPreferences;

View file

@ -9,7 +9,7 @@
DropdownToggle, DropdownToggle,
Icon, Icon,
Tooltip, Tooltip,
} from "sveltestrap"; } from "@sveltestrap/sveltestrap";
export let value: string; export let value: string;
export let status: string; export let status: string;

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { flagURL, type PrideFlag } from "$lib/api/entities"; import { flagURL, type PrideFlag } from "$lib/api/entities";
import { Button, Tooltip } from "sveltestrap"; import { Button, Tooltip } from "@sveltestrap/sveltestrap";
export let flag: PrideFlag; export let flag: PrideFlag;
export let tooltip: string; export let tooltip: string;

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Icon, Modal } from "sveltestrap"; import { Icon, Modal } from "@sveltestrap/sveltestrap";
let isOpen = false; let isOpen = false;
const toggle = () => (isOpen = !isOpen); const toggle = () => (isOpen = !isOpen);

View file

@ -13,7 +13,7 @@
import { settingsStore } from "$lib/store"; import { settingsStore } from "$lib/store";
import { toastStore } from "$lib/toast"; import { toastStore } from "$lib/toast";
import Toast from "$lib/components/Toast.svelte"; import Toast from "$lib/components/Toast.svelte";
import { Alert, Icon } from "sveltestrap"; import { Alert, Icon } from "@sveltestrap/sveltestrap";
import { apiFetchClient } from "$lib/api/fetch"; import { apiFetchClient } from "$lib/api/fetch";
import type { Settings } from "$lib/api/entities"; import type { Settings } from "$lib/api/entities";
import { renderUnsafeMarkdown } from "$lib/utils"; import { renderUnsafeMarkdown } from "$lib/utils";
@ -31,6 +31,8 @@
const resp = await apiFetchClient<Settings>( const resp = await apiFetchClient<Settings>(
"/users/@me/settings", "/users/@me/settings",
"PATCH", "PATCH",
// If this function is run, notice will always be non-null
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
{ read_global_notice: data.notice!.id }, { read_global_notice: data.notice!.id },
2, 2,
); );

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { PUBLIC_BASE_URL } from "$env/static/public"; import { PUBLIC_BASE_URL } from "$env/static/public";
import { Button } from "sveltestrap"; import { Button } from "@sveltestrap/sveltestrap";
import { userStore } from "$lib/store"; import { userStore } from "$lib/store";
</script> </script>

View file

@ -11,7 +11,7 @@ export const load = async ({ params }) => {
return resp; return resp;
} catch (e) { } catch (e) {
if ((e as APIError).code === ErrorCode.UserNotFound) { if ((e as APIError).code === ErrorCode.UserNotFound) {
throw error(404, e as APIError); error(404, e as App.Error);
} }
throw e; throw e;

View file

@ -4,7 +4,6 @@
import { import {
Alert, Alert,
Badge,
Button, Button,
ButtonGroup, ButtonGroup,
Icon, Icon,
@ -14,8 +13,8 @@
ModalBody, ModalBody,
ModalFooter, ModalFooter,
Tooltip, Tooltip,
} from "sveltestrap"; } from "@sveltestrap/sveltestrap";
import { DateTime, Duration, FixedOffsetZone, Zone } from "luxon"; import { DateTime, FixedOffsetZone } from "luxon";
import FieldCard from "$lib/components/FieldCard.svelte"; import FieldCard from "$lib/components/FieldCard.svelte";
import PronounLink from "$lib/components/PronounLink.svelte"; import PronounLink from "$lib/components/PronounLink.svelte";
import PartialMemberCard from "$lib/components/PartialMemberCard.svelte"; import PartialMemberCard from "$lib/components/PartialMemberCard.svelte";
@ -46,6 +45,7 @@
import ProfileFlag from "./ProfileFlag.svelte"; import ProfileFlag from "./ProfileFlag.svelte";
import IconButton from "$lib/components/IconButton.svelte"; import IconButton from "$lib/components/IconButton.svelte";
import Badges from "./badges/Badges.svelte"; import Badges from "./badges/Badges.svelte";
import PreferencesCheatsheet from "./PreferencesCheatsheet.svelte";
export let data: PageData; export let data: PageData;
@ -190,14 +190,15 @@
{/if} {/if}
{#if data.utc_offset} {#if data.utc_offset}
<Tooltip target="user-clock" placement="top">Current time</Tooltip> <Tooltip target="user-clock" placement="top">Current time</Tooltip>
<Icon id="user-clock" name="clock" aria-label="This user's current time" /> {currentTime} <span class="text-body-secondary">(UTC{timezone})</span> <Icon id="user-clock" name="clock" aria-label="This user's current time" />
{currentTime} <span class="text-body-secondary">(UTC{timezone})</span>
{/if} {/if}
{#if profileEmpty && $userStore?.id === data.id} {#if profileEmpty && $userStore?.id === data.id}
<hr /> <hr />
<p> <p>
<em> <em>
Your profile is empty! You can customize it by going to the <a href="/@{data.name}/edit" Your profile is empty! You can customize it by going to the <a
>edit profile</a href="/@{data.name}/edit">edit profile</a
> page.</em > page.</em
> <span class="text-muted">(only you can see this)</span> > <span class="text-muted">(only you can see this)</span>
</p> </p>
@ -258,6 +259,12 @@
</div> </div>
{/each} {/each}
</div> </div>
<PreferencesCheatsheet
preferences={data.custom_preferences}
names={data.names}
pronouns={data.pronouns}
fields={data.fields}
/>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<InputGroup> <InputGroup>

View file

@ -0,0 +1,65 @@
<script lang="ts">
import type {
CustomPreferences,
CustomPreference,
Field,
FieldEntry,
Pronoun,
} from "$lib/api/entities";
import defaultPreferences from "$lib/api/default_preferences";
import StatusIcon from "$lib/components/StatusIcon.svelte";
export let preferences: CustomPreferences;
export let names: FieldEntry[];
export let pronouns: Pronoun[];
export let fields: Field[];
let mergedPreferences: CustomPreferences;
$: mergedPreferences = Object.assign({}, defaultPreferences, preferences);
// Filter default preferences to the ones the user/member has used
// This is done separately from custom preferences to make the shown list cleaner
let usedDefaultPreferences: Array<{ id: string; preference: CustomPreference }>;
$: usedDefaultPreferences = Object.keys(defaultPreferences)
.filter(
(pref) =>
names.some((entry) => entry.status === pref) ||
pronouns.some((entry) => entry.status === pref) ||
fields.some((field) => field.entries.some((entry) => entry.status === pref)),
)
.map((key) => ({
id: key,
preference: defaultPreferences[key],
}));
// Do the same for custom preferences
let usedCustomPreferences: Array<{ id: string; preference: CustomPreference }>;
$: usedCustomPreferences = Object.keys(preferences)
.filter(
(pref) =>
names.some((entry) => entry.status === pref) ||
pronouns.some((entry) => entry.status === pref) ||
fields.some((field) => field.entries.some((entry) => entry.status === pref)),
)
.map((pref) => ({ id: pref, preference: mergedPreferences[pref] }));
</script>
<div class="text-center">
<ul class="list-inline text-body-secondary">
{#each usedDefaultPreferences as pref (pref.id)}
<li class="list-inline-item mx-2">
<StatusIcon {preferences} status={pref.id} />
{pref.preference.tooltip}
</li>
{/each}
</ul>
{#if usedCustomPreferences}
<ul class="list-inline text-body-secondary">
{#each usedCustomPreferences as pref (pref.id)}
<li class="list-inline-item mx-2">
<StatusIcon {preferences} status={pref.id} />
{pref.preference.tooltip}
</li>
{/each}
</ul>
{/if}
</div>

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { flagURL, type PrideFlag } from "$lib/api/entities"; import { flagURL, type PrideFlag } from "$lib/api/entities";
import { Tooltip } from "sveltestrap"; import { Tooltip } from "@sveltestrap/sveltestrap";
export let flag: PrideFlag; export let flag: PrideFlag;

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Icon } from "sveltestrap"; import { Icon } from "@sveltestrap/sveltestrap";
export let link: string; export let link: string;

View file

@ -3,7 +3,7 @@
import { fastFetchClient } from "$lib/api/fetch"; import { fastFetchClient } from "$lib/api/fetch";
import ErrorAlert from "$lib/components/ErrorAlert.svelte"; import ErrorAlert from "$lib/components/ErrorAlert.svelte";
import { addToast } from "$lib/toast"; import { addToast } from "$lib/toast";
import { Button, FormGroup, Icon, Modal, ModalBody, ModalFooter } from "sveltestrap"; import { Button, FormGroup, Icon, Modal, ModalBody, ModalFooter } from "@sveltestrap/sveltestrap";
export let subject: string; export let subject: string;
export let reportUrl: string; export let reportUrl: string;

View file

@ -14,9 +14,9 @@ export const load = async ({ params }) => {
(e as APIError).code === ErrorCode.UserNotFound || (e as APIError).code === ErrorCode.UserNotFound ||
(e as APIError).code === ErrorCode.MemberNotFound (e as APIError).code === ErrorCode.MemberNotFound
) { ) {
throw error(404, e as APIError); error(404, e as App.Error);
} }
throw error(500, e as APIError); error(500, e as App.Error);
} }
}; };

View file

@ -4,7 +4,7 @@
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import PronounLink from "$lib/components/PronounLink.svelte"; import PronounLink from "$lib/components/PronounLink.svelte";
import FallbackImage from "$lib/components/FallbackImage.svelte"; import FallbackImage from "$lib/components/FallbackImage.svelte";
import { Alert, Button, Icon, InputGroup } from "sveltestrap"; import { Alert, Button, Icon, InputGroup } from "@sveltestrap/sveltestrap";
import { import {
memberAvatars, memberAvatars,
pronounDisplay, pronounDisplay,
@ -22,6 +22,7 @@
import { addToast } from "$lib/toast"; import { addToast } from "$lib/toast";
import ProfileFlag from "../ProfileFlag.svelte"; import ProfileFlag from "../ProfileFlag.svelte";
import IconButton from "$lib/components/IconButton.svelte"; import IconButton from "$lib/components/IconButton.svelte";
import PreferencesCheatsheet from "../PreferencesCheatsheet.svelte";
export let data: PageData; export let data: PageData;
@ -154,6 +155,12 @@
</div> </div>
{/each} {/each}
</div> </div>
<PreferencesCheatsheet
preferences={data.user.custom_preferences}
names={data.names}
pronouns={data.pronouns}
fields={data.fields}
/>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<InputGroup> <InputGroup>

View file

@ -5,7 +5,15 @@
import type { LayoutData } from "./$types"; import type { LayoutData } from "./$types";
import { addToast, delToast } from "$lib/toast"; import { addToast, delToast } from "$lib/toast";
import { apiFetchClient, fastFetchClient } from "$lib/api/fetch"; import { apiFetchClient, fastFetchClient } from "$lib/api/fetch";
import { Button, ButtonGroup, Modal, ModalBody, ModalFooter, Nav, NavItem } from "sveltestrap"; import {
Button,
ButtonGroup,
Modal,
ModalBody,
ModalFooter,
Nav,
NavItem,
} from "@sveltestrap/sveltestrap";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import ErrorAlert from "$lib/components/ErrorAlert.svelte"; import ErrorAlert from "$lib/components/ErrorAlert.svelte";
import IconButton from "$lib/components/IconButton.svelte"; import IconButton from "$lib/components/IconButton.svelte";

View file

@ -32,7 +32,7 @@ export const load = (async ({ params }) => {
member.user.name !== params.username || member.user.name !== params.username ||
member.name !== params.memberName member.name !== params.memberName
) { ) {
throw redirect(303, `/@${user.name}/${member.name}`); redirect(303, `/@${user.name}/${member.name}`);
} }
return { return {
@ -41,8 +41,9 @@ export const load = (async ({ params }) => {
pronouns: pronouns.autocomplete, pronouns: pronouns.autocomplete,
flags, flags,
}; };
} catch (e) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
if ("code" in e) throw error(500, e as APIError); } catch (e: any) {
if ("code" in e) error(500, e as App.Error);
throw e; throw e;
} }
}) satisfies LayoutLoad; }) satisfies LayoutLoad;

View file

@ -3,7 +3,7 @@
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import { encode } from "base64-arraybuffer"; import { encode } from "base64-arraybuffer";
import { FormGroup, Icon, Input } from "sveltestrap"; import { FormGroup, Icon, Input } from "@sveltestrap/sveltestrap";
import { memberAvatars, type Member } from "$lib/api/entities"; import { memberAvatars, type Member } from "$lib/api/entities";
import FallbackImage from "$lib/components/FallbackImage.svelte"; import FallbackImage from "$lib/components/FallbackImage.svelte";
import EditableName from "$lib/components/edit/EditableName.svelte"; import EditableName from "$lib/components/edit/EditableName.svelte";

View file

@ -4,7 +4,7 @@
import { MAX_DESCRIPTION_LENGTH, type Member } from "$lib/api/entities"; import { MAX_DESCRIPTION_LENGTH, type Member } from "$lib/api/entities";
import { charCount, renderMarkdown } from "$lib/utils"; import { charCount, renderMarkdown } from "$lib/utils";
import MarkdownHelp from "$lib/components/edit/MarkdownHelp.svelte"; import MarkdownHelp from "$lib/components/edit/MarkdownHelp.svelte";
import { Card, CardBody, CardHeader } from "sveltestrap"; import { Card, CardBody, CardHeader } from "@sveltestrap/sveltestrap";
const member = getContext<Writable<Member>>("member"); const member = getContext<Writable<Member>>("member");
</script> </script>

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { getContext } from "svelte"; import { getContext } from "svelte";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { Button, Icon } from "sveltestrap"; import { Button, Icon } from "@sveltestrap/sveltestrap";
import type { Member } from "$lib/api/entities"; import type { Member } from "$lib/api/entities";
import EditableField from "$lib/components/edit/EditableField.svelte"; import EditableField from "$lib/components/edit/EditableField.svelte";
@ -45,9 +45,7 @@
</div> </div>
</div> </div>
<div> <div>
<Button <Button on:click={() => ($member.fields = [...$member.fields, { name: null, entries: [] }])}>
on:click={() => ($member.fields = [...$member.fields, { name: null, entries: [] }])}
>
<Icon name="plus" aria-hidden /> Add new field <Icon name="plus" aria-hidden /> Add new field
</Button> </Button>
</div> </div>

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { getContext } from "svelte"; import { getContext } from "svelte";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { Alert, ButtonGroup, Input } from "sveltestrap"; import { Alert, ButtonGroup, Input } from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import type { Member, PrideFlag } from "$lib/api/entities"; import type { Member, PrideFlag } from "$lib/api/entities";

View file

@ -2,7 +2,7 @@
import { getContext } from "svelte"; import { getContext } from "svelte";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { Button, ButtonGroup, Icon } from "sveltestrap"; import { Button, ButtonGroup, Icon } from "@sveltestrap/sveltestrap";
import type { APIError, Member } from "$lib/api/entities"; import type { APIError, Member } from "$lib/api/entities";
import { PUBLIC_SHORT_BASE } from "$env/static/public"; import { PUBLIC_SHORT_BASE } from "$env/static/public";

View file

@ -2,7 +2,7 @@
import { getContext } from "svelte"; import { getContext } from "svelte";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import type { Member } from "$lib/api/entities"; import type { Member } from "$lib/api/entities";
import { Button, Icon, Popover } from "sveltestrap"; import { Button, Icon, Popover } from "@sveltestrap/sveltestrap";
import EditablePronouns from "$lib/components/edit/EditablePronouns.svelte"; import EditablePronouns from "$lib/components/edit/EditablePronouns.svelte";
import IconButton from "$lib/components/IconButton.svelte"; import IconButton from "$lib/components/IconButton.svelte";
import type { PageData } from "./$types"; import type { PageData } from "./$types";

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Tooltip } from "sveltestrap"; import { Tooltip } from "@sveltestrap/sveltestrap";
let icon: HTMLElement; let icon: HTMLElement;
</script> </script>

View file

@ -2,7 +2,7 @@
import { setContext } from "svelte"; import { setContext } from "svelte";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import type { LayoutData } from "./$types"; import type { LayoutData } from "./$types";
import { Button, ButtonGroup, Icon, Nav, NavItem } from "sveltestrap"; import { Button, ButtonGroup, Icon, Nav, NavItem } from "@sveltestrap/sveltestrap";
import type { MeUser, APIError } from "$lib/api/entities"; import type { MeUser, APIError } from "$lib/api/entities";
import ErrorAlert from "$lib/components/ErrorAlert.svelte"; import ErrorAlert from "$lib/components/ErrorAlert.svelte";
import { addToast, delToast } from "$lib/toast"; import { addToast, delToast } from "$lib/toast";

View file

@ -1,6 +1,6 @@
import type { PrideFlag, APIError, MeUser, PronounsJson } from "$lib/api/entities"; import type { PrideFlag, MeUser, PronounsJson } from "$lib/api/entities";
import { apiFetchClient } from "$lib/api/fetch"; import { apiFetchClient } from "$lib/api/fetch";
import { error, redirect, type Redirect } from "@sveltejs/kit"; import { error, redirect } from "@sveltejs/kit";
import pronounsRaw from "$lib/pronouns.json"; import pronounsRaw from "$lib/pronouns.json";
const pronouns = pronounsRaw as PronounsJson; const pronouns = pronounsRaw as PronounsJson;
@ -13,7 +13,7 @@ export const load = async ({ params }) => {
const flags = await apiFetchClient<PrideFlag[]>("/users/@me/flags"); const flags = await apiFetchClient<PrideFlag[]>("/users/@me/flags");
if (params.username !== user.name) { if (params.username !== user.name) {
throw redirect(303, `/@${user.name}/edit`); redirect(303, `/@${user.name}/edit`);
} }
return { return {
@ -21,8 +21,9 @@ export const load = async ({ params }) => {
pronouns: pronouns.autocomplete, pronouns: pronouns.autocomplete,
flags, flags,
}; };
} catch (e) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
if ("code" in e) throw error(500, e as APIError); } catch (e: any) {
if ("code" in e) error(500, e as App.Error);
throw e; throw e;
} }
}; };

View file

@ -3,7 +3,7 @@
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import { encode } from "base64-arraybuffer"; import { encode } from "base64-arraybuffer";
import { FormGroup, Icon, Input } from "sveltestrap"; import { FormGroup, Icon, Input } from "@sveltestrap/sveltestrap";
import { userAvatars, type MeUser } from "$lib/api/entities"; import { userAvatars, type MeUser } from "$lib/api/entities";
import FallbackImage from "$lib/components/FallbackImage.svelte"; import FallbackImage from "$lib/components/FallbackImage.svelte";
import EditableName from "$lib/components/edit/EditableName.svelte"; import EditableName from "$lib/components/edit/EditableName.svelte";

View file

@ -4,7 +4,7 @@
import { MAX_DESCRIPTION_LENGTH, type MeUser } from "$lib/api/entities"; import { MAX_DESCRIPTION_LENGTH, type MeUser } from "$lib/api/entities";
import { charCount, renderMarkdown } from "$lib/utils"; import { charCount, renderMarkdown } from "$lib/utils";
import MarkdownHelp from "$lib/components/edit/MarkdownHelp.svelte"; import MarkdownHelp from "$lib/components/edit/MarkdownHelp.svelte";
import { Card, CardBody, CardHeader } from "sveltestrap"; import { Card, CardBody, CardHeader } from "@sveltestrap/sveltestrap";
const user = getContext<Writable<MeUser>>("user"); const user = getContext<Writable<MeUser>>("user");
</script> </script>

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { getContext } from "svelte"; import { getContext } from "svelte";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { Button, Icon } from "sveltestrap"; import { Button, Icon } from "@sveltestrap/sveltestrap";
import type { MeUser } from "$lib/api/entities"; import type { MeUser } from "$lib/api/entities";
import EditableField from "$lib/components/edit/EditableField.svelte"; import EditableField from "$lib/components/edit/EditableField.svelte";

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { getContext } from "svelte"; import { getContext } from "svelte";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { Alert, ButtonGroup, Input } from "sveltestrap"; import { Alert, ButtonGroup, Input } from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import type { MeUser, PrideFlag } from "$lib/api/entities"; import type { MeUser, PrideFlag } from "$lib/api/entities";

View file

@ -4,7 +4,14 @@
import { PreferenceSize, type APIError, type MeUser } from "$lib/api/entities"; import { PreferenceSize, type APIError, type MeUser } from "$lib/api/entities";
import IconButton from "$lib/components/IconButton.svelte"; import IconButton from "$lib/components/IconButton.svelte";
import { Button, ButtonGroup, FormGroup, Icon, Input, InputGroup } from "sveltestrap"; import {
Button,
ButtonGroup,
FormGroup,
Icon,
Input,
InputGroup,
} from "@sveltestrap/sveltestrap";
import { PUBLIC_SHORT_BASE } from "$env/static/public"; import { PUBLIC_SHORT_BASE } from "$env/static/public";
import CustomPreference from "./CustomPreference.svelte"; import CustomPreference from "./CustomPreference.svelte";
import { DateTime, FixedOffsetZone } from "luxon"; import { DateTime, FixedOffsetZone } from "luxon";

View file

@ -9,7 +9,7 @@
Icon, Icon,
InputGroup, InputGroup,
Tooltip, Tooltip,
} from "sveltestrap"; } from "@sveltestrap/sveltestrap";
import IconPicker from "./IconPicker.svelte"; import IconPicker from "./IconPicker.svelte";
export let preference: CustomPreference; export let preference: CustomPreference;

View file

@ -7,7 +7,7 @@
Icon, Icon,
Input, Input,
Tooltip, Tooltip,
} from "sveltestrap"; } from "@sveltestrap/sveltestrap";
import icons from "../../../../icons"; import icons from "../../../../icons";
import IconButton from "$lib/components/IconButton.svelte"; import IconButton from "$lib/components/IconButton.svelte";

View file

@ -2,7 +2,7 @@
import { getContext } from "svelte"; import { getContext } from "svelte";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import type { MeUser } from "$lib/api/entities"; import type { MeUser } from "$lib/api/entities";
import { Button, Icon, Popover } from "sveltestrap"; import { Button, Icon, Popover } from "@sveltestrap/sveltestrap";
import EditablePronouns from "$lib/components/edit/EditablePronouns.svelte"; import EditablePronouns from "$lib/components/edit/EditablePronouns.svelte";
import IconButton from "$lib/components/IconButton.svelte"; import IconButton from "$lib/components/IconButton.svelte";
import type { PageData } from "./$types"; import type { PageData } from "./$types";

View file

@ -8,10 +8,10 @@ export const load = async ({ params }) => {
method: "GET", method: "GET",
}); });
throw redirect(303, `/@${resp.name}`); redirect(303, `/@${resp.name}`);
} catch (e) { } catch (e) {
if ((e as APIError).code === ErrorCode.UserNotFound) { if ((e as APIError).code === ErrorCode.UserNotFound) {
throw error(404, e as APIError); error(404, e as App.Error);
} }
throw e; throw e;

View file

@ -15,7 +15,7 @@
Modal, Modal,
ModalBody, ModalBody,
ModalFooter, ModalFooter,
} from "sveltestrap"; } from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
export let data: PageData; export let data: PageData;

View file

@ -19,7 +19,7 @@
Modal, Modal,
ModalBody, ModalBody,
ModalFooter, ModalFooter,
} from "sveltestrap"; } from "@sveltestrap/sveltestrap";
export let authType: string; export let authType: string;
export let remoteName: string | undefined; export let remoteName: string | undefined;
@ -64,8 +64,10 @@
) => Promise<void>; ) => Promise<void>;
let captchaToken = ""; let captchaToken = "";
// svelte-hcaptcha doesn't have types, so we can't use anything except `any` here.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let captcha: any; let captcha: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const captchaSuccess = (token: any) => { const captchaSuccess = (token: any) => {
captchaToken = token.detail.token; captchaToken = token.detail.token;
}; };
@ -88,6 +90,8 @@
await fastFetch("/auth/force-delete", { await fastFetch("/auth/force-delete", {
method: "GET", method: "GET",
headers: { headers: {
// We know for sure this value is non-null if this function is run at all
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
"X-Delete-Token": token!, "X-Delete-Token": token!,
}, },
}); });
@ -105,6 +109,8 @@
await fastFetch("/auth/cancel-delete", { await fastFetch("/auth/cancel-delete", {
method: "GET", method: "GET",
headers: { headers: {
// We know for sure this value is non-null if this function is run at all
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
"X-Delete-Token": token!, "X-Delete-Token": token!,
}, },
}); });

View file

@ -10,7 +10,7 @@
export let data: PageData; export let data: PageData;
let callbackPage: any; let callbackPage: CallbackPage;
const signupForm = async (username: string, invite: string, captchaToken: string) => { const signupForm = async (username: string, invite: string, captchaToken: string) => {
try { try {

View file

@ -10,7 +10,7 @@
export let data: PageData; export let data: PageData;
let callbackPage: any; let callbackPage: CallbackPage;
const signupForm = async (username: string, invite: string, captchaToken: string) => { const signupForm = async (username: string, invite: string, captchaToken: string) => {
try { try {

View file

@ -10,7 +10,7 @@
export let data: PageData; export let data: PageData;
let callbackPage: any; let callbackPage: CallbackPage;
const signupForm = async (username: string, invite: string, captchaToken: string) => { const signupForm = async (username: string, invite: string, captchaToken: string) => {
try { try {

View file

@ -10,7 +10,7 @@
export let data: PageData; export let data: PageData;
let callbackPage: any; let callbackPage: CallbackPage;
const signupForm = async (username: string, invite: string, captchaToken: string) => { const signupForm = async (username: string, invite: string, captchaToken: string) => {
try { try {

View file

@ -10,7 +10,7 @@
export let data: PageData; export let data: PageData;
let callbackPage: any; let callbackPage: CallbackPage;
const signupForm = async (username: string, invite: string, captchaToken: string) => { const signupForm = async (username: string, invite: string, captchaToken: string) => {
try { try {

View file

@ -16,14 +16,14 @@ export const load = async ({ params }) => {
} as APIError; } as APIError;
} }
throw redirect(303, `/@${user.name}/${member.name}/edit`); redirect(303, `/@${user.name}/${member.name}/edit`);
} catch (e) { } catch (e) {
if ( if (
(e as APIError).code === ErrorCode.Forbidden || (e as APIError).code === ErrorCode.Forbidden ||
(e as APIError).code === ErrorCode.InvalidToken || (e as APIError).code === ErrorCode.InvalidToken ||
(e as APIError).code === ErrorCode.NotOwnMember (e as APIError).code === ErrorCode.NotOwnMember
) { ) {
throw error(403, e as APIError); error(403, e as App.Error);
} }
throw e; throw e;

View file

@ -8,13 +8,13 @@ export const load = async () => {
try { try {
const resp = await apiFetchClient<MeUser>(`/users/@me`); const resp = await apiFetchClient<MeUser>(`/users/@me`);
throw redirect(303, `/@${resp.name}/edit`); redirect(303, `/@${resp.name}/edit`);
} catch (e) { } catch (e) {
if ( if (
(e as APIError).code === ErrorCode.Forbidden || (e as APIError).code === ErrorCode.Forbidden ||
(e as APIError).code === ErrorCode.InvalidToken (e as APIError).code === ErrorCode.InvalidToken
) { ) {
throw error(403, e as APIError); error(403, e as App.Error);
} }
throw e; throw e;

View file

@ -14,7 +14,7 @@
NavbarToggler, NavbarToggler,
NavItem, NavItem,
NavLink, NavLink,
} from "sveltestrap"; } from "@sveltestrap/sveltestrap";
import Logo from "./Logo.svelte"; import Logo from "./Logo.svelte";
import { userStore, themeStore, CURRENT_CHANGELOG, settingsStore } from "$lib/store"; import { userStore, themeStore, CURRENT_CHANGELOG, settingsStore } from "$lib/store";

View file

@ -1,4 +1,7 @@
<script lang="ts"> <script lang="ts">
// Ignoring the TS error here, because this file imports fine, typescript just chokes on markdown files
// eslint-disable-next-line
//@ts-ignore
import { html } from "./about.md"; import { html } from "./about.md";
</script> </script>

View file

@ -1,5 +1,8 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
// Ignoring the TS error here, because this file imports fine, typescript just chokes on markdown files
// eslint-disable-next-line
//@ts-ignore
import { html } from "./changelog.md"; import { html } from "./changelog.md";
import { CURRENT_CHANGELOG } from "$lib/store"; import { CURRENT_CHANGELOG } from "$lib/store";

View file

@ -1,4 +1,7 @@
<script lang="ts"> <script lang="ts">
// Ignoring the TS error here, because this file imports fine, typescript just chokes on markdown files
// eslint-disable-next-line
//@ts-ignore
import { html } from "./privacy.md"; import { html } from "./privacy.md";
</script> </script>

View file

@ -1,4 +1,7 @@
<script lang="ts"> <script lang="ts">
// Ignoring the TS error here, because this file imports fine, typescript just chokes on markdown files
// eslint-disable-next-line
//@ts-ignore
import { html } from "./terms.md"; import { html } from "./terms.md";
</script> </script>

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { PUBLIC_BASE_URL } from "$env/static/public"; import { PUBLIC_BASE_URL } from "$env/static/public";
import type { PageData } from "../../$types"; import type { PageData } from "./$types";
export let data: PageData; export let data: PageData;

View file

@ -10,7 +10,7 @@ export const load = (async ({ params }) => {
const arr = param.split("/"); const arr = param.split("/");
if (arr.length === 0 || params.pronouns === "") { if (arr.length === 0 || params.pronouns === "") {
throw error(404, { code: ErrorCode.NotFound, message: "Pronouns not found" }); error(404, { code: ErrorCode.NotFound, message: "Pronouns not found" });
} }
if (arr.length === 5) { if (arr.length === 5) {
@ -46,5 +46,5 @@ export const load = (async ({ params }) => {
}; };
} }
throw error(404, { code: ErrorCode.NotFound, message: "Pronouns not found" }); error(404, { code: ErrorCode.NotFound, message: "Pronouns not found" });
}) satisfies PageLoad; }) satisfies PageLoad;

View file

@ -3,7 +3,14 @@
import { fastFetchClient } from "$lib/api/fetch"; import { fastFetchClient } from "$lib/api/fetch";
import ErrorAlert from "$lib/components/ErrorAlert.svelte"; import ErrorAlert from "$lib/components/ErrorAlert.svelte";
import { addToast } from "$lib/toast"; import { addToast } from "$lib/toast";
import { Button, ButtonGroup, FormGroup, Modal, ModalBody, ModalFooter } from "sveltestrap"; import {
Button,
ButtonGroup,
FormGroup,
Modal,
ModalBody,
ModalFooter,
} from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import ReportCard from "./ReportCard.svelte"; import ReportCard from "./ReportCard.svelte";

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { Report } from "$lib/api/entities"; import type { Report } from "$lib/api/entities";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { Card, CardBody, CardFooter, CardHeader } from "sveltestrap"; import { Card, CardBody, CardFooter, CardHeader } from "@sveltestrap/sveltestrap";
export let report: Report; export let report: Report;
</script> </script>

View file

@ -9,7 +9,7 @@
Modal, Modal,
ModalBody, ModalBody,
ModalFooter, ModalFooter,
} from "sveltestrap"; } from "@sveltestrap/sveltestrap";
import { userStore } from "$lib/store"; import { userStore } from "$lib/store";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { addToast } from "$lib/toast"; import { addToast } from "$lib/toast";

View file

@ -40,7 +40,7 @@ export const load = (async ({ parent }) => {
}; };
} catch (e) { } catch (e) {
if ((e as APIError).code !== ErrorCode.InternalServerError) { if ((e as APIError).code !== ErrorCode.InternalServerError) {
throw redirect(303, "/auth/login"); redirect(303, "/auth/login");
} }
throw e; throw e;
} }

View file

@ -23,9 +23,8 @@
ModalFooter, ModalFooter,
ModalHeader, ModalHeader,
Table, Table,
} from "sveltestrap"; } from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import { onMount } from "svelte";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
export let data: PageData; export let data: PageData;

View file

@ -13,7 +13,7 @@
Modal, Modal,
ModalBody, ModalBody,
ModalFooter, ModalFooter,
} from "sveltestrap"; } from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
export let data: PageData; export let data: PageData;

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import { DateTime, Duration } from "luxon"; import { DateTime, Duration } from "luxon";
import { Alert, Button } from "sveltestrap"; import { Alert, Button } from "@sveltestrap/sveltestrap";
import { PUBLIC_MEDIA_URL } from "$env/static/public"; import { PUBLIC_MEDIA_URL } from "$env/static/public";
import { fastFetchClient } from "$lib/api/fetch"; import { fastFetchClient } from "$lib/api/fetch";
import type { APIError } from "$lib/api/entities"; import type { APIError } from "$lib/api/entities";

View file

@ -10,6 +10,6 @@ export const load = async () => {
} catch (e) { } catch (e) {
if ((e as APIError).code === ErrorCode.NotFound) return { exportData: null }; if ((e as APIError).code === ErrorCode.NotFound) return { exportData: null };
throw error(500, e as APIError); error(500, e as App.Error);
} }
}; };

View file

@ -1,6 +1,14 @@
<script lang="ts"> <script lang="ts">
import { MAX_FLAGS, type APIError, type PrideFlag } from "$lib/api/entities"; import { MAX_FLAGS, type APIError, type PrideFlag } from "$lib/api/entities";
import { Button, Icon, Input, Modal, ModalBody, ModalFooter, ModalHeader } from "sveltestrap"; import {
Button,
Icon,
Input,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
} from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import Flag from "./Flag.svelte"; import Flag from "./Flag.svelte";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";

View file

@ -2,7 +2,14 @@
import { flagURL, type APIError, type PrideFlag } from "$lib/api/entities"; import { flagURL, type APIError, type PrideFlag } from "$lib/api/entities";
import { apiFetchClient } from "$lib/api/fetch"; import { apiFetchClient } from "$lib/api/fetch";
import { addToast } from "$lib/toast"; import { addToast } from "$lib/toast";
import { Button, Input, Modal, ModalBody, ModalFooter, ModalHeader } from "sveltestrap"; import {
Button,
Input,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
} from "@sveltestrap/sveltestrap";
export let flag: PrideFlag; export let flag: PrideFlag;
export let deleteFlag: (id: string) => Promise<void>; export let deleteFlag: (id: string) => Promise<void>;

View file

@ -3,7 +3,7 @@
import { apiFetchClient } from "$lib/api/fetch"; import { apiFetchClient } from "$lib/api/fetch";
import ErrorAlert from "$lib/components/ErrorAlert.svelte"; import ErrorAlert from "$lib/components/ErrorAlert.svelte";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { Button, Modal, Table } from "sveltestrap"; import { Button, Modal, Table } from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
export let data: PageData; export let data: PageData;

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { Member } from "$lib/api/entities"; import type { Member } from "$lib/api/entities";
import { Alert, ListGroup, ListGroupItem } from "sveltestrap"; import { Alert, ListGroup, ListGroupItem } from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
export let data: PageData; export let data: PageData;

View file

@ -12,7 +12,7 @@
ModalBody, ModalBody,
ModalFooter, ModalFooter,
ModalHeader, ModalHeader,
} from "sveltestrap"; } from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
interface Token { interface Token {

View file

@ -4,7 +4,7 @@
import ErrorAlert from "$lib/components/ErrorAlert.svelte"; import ErrorAlert from "$lib/components/ErrorAlert.svelte";
import { addToast } from "$lib/toast"; import { addToast } from "$lib/toast";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { Button, Card, CardBody, CardFooter, CardHeader } from "sveltestrap"; import { Button, Card, CardBody, CardFooter, CardHeader } from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
export let data: PageData; export let data: PageData;

18
frontend/src/svelte-hcaptcha.d.ts vendored Normal file
View file

@ -0,0 +1,18 @@
declare module "svelte-hcaptcha" {
import type { SvelteComponent } from "svelte";
export interface HCaptchaProps {
sitekey?: string;
apihost?: string;
hl?: string;
reCaptchaCompat?: boolean;
theme?: CaptchaTheme;
size?: string;
}
declare class HCaptcha extends SvelteComponent {
$$prop_def: HCaptchaProps;
}
export default HCaptcha;
}

View file

@ -1,5 +1,5 @@
import adapter from "@sveltejs/adapter-node"; import adapter from "@sveltejs/adapter-node";
import { vitePreprocess } from "@sveltejs/kit/vite"; import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
import * as child_process from "node:child_process"; import * as child_process from "node:child_process";
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
@ -11,7 +11,7 @@ const config = {
kit: { kit: {
adapter: adapter(), adapter: adapter(),
version: { version: {
name: child_process.execSync("git describe --tags --long").toString().trim(), name: child_process.execSync("git describe --tags --long --always").toString().trim(),
}, },
}, },
}; };

Some files were not shown because too many files have changed in this diff Show more