From 6f7eb5eeeecaa56fe16b8755c5024be5e2479900 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 24 Apr 2023 16:51:55 +0200 Subject: [PATCH] feat: add captcha when signing up (closes #53) --- backend/routes/auth/captcha.go | 58 +++++++++++++++++++ backend/routes/auth/discord.go | 36 ++++++++---- backend/routes/auth/fedi_mastodon.go | 38 ++++++++---- backend/routes/auth/fedi_misskey.go | 22 +++++-- backend/routes/auth/google.go | 29 +++++++--- backend/routes/auth/routes.go | 11 +++- backend/routes/auth/tumblr.go | 29 +++++++--- backend/server/errors.go | 3 + frontend/package.json | 1 + frontend/pnpm-lock.yaml | 6 ++ frontend/src/app.d.ts | 31 ++++++++-- frontend/src/lib/api/entities.ts | 1 + .../src/routes/auth/login/CallbackPage.svelte | 43 +++++++++++++- .../routes/auth/login/discord/+page.server.ts | 1 + .../routes/auth/login/discord/+page.svelte | 13 ++++- .../routes/auth/login/google/+page.server.ts | 1 + .../src/routes/auth/login/google/+page.svelte | 13 ++++- .../login/mastodon/[instance]/+page.server.ts | 1 + .../login/mastodon/[instance]/+page.svelte | 13 ++++- .../login/misskey/[instance]/+page.server.ts | 1 + .../login/misskey/[instance]/+page.svelte | 12 +++- .../routes/auth/login/tumblr/+page.server.ts | 1 + .../src/routes/auth/login/tumblr/+page.svelte | 13 ++++- 23 files changed, 316 insertions(+), 61 deletions(-) create mode 100644 backend/routes/auth/captcha.go diff --git a/backend/routes/auth/captcha.go b/backend/routes/auth/captcha.go new file mode 100644 index 0000000..d7ef04b --- /dev/null +++ b/backend/routes/auth/captcha.go @@ -0,0 +1,58 @@ +package auth + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "codeberg.org/u1f320/pronouns.cc/backend/server" + "emperror.dev/errors" +) + +const hcaptchaURL = "https://hcaptcha.com/siteverify" + +type hcaptchaResponse struct { + Success bool `json:"success"` +} + +// verifyCaptcha verifies a captcha response. +func (s *Server) verifyCaptcha(ctx context.Context, response string) (ok bool, err error) { + vals := url.Values{ + "response": []string{response}, + "secret": []string{s.hcaptchaSecret}, + "sitekey": []string{s.hcaptchaSitekey}, + } + + req, err := http.NewRequestWithContext(ctx, "POST", hcaptchaURL, strings.NewReader(vals.Encode())) + if err != nil { + return false, errors.Wrap(err, "creating request") + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("User-Agent", "pronouns.cc/"+server.Tag) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false, errors.Wrap(err, "sending request") + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 400 { + return false, errors.Sentinel("error status code") + } + b, err := io.ReadAll(resp.Body) + if err != nil { + return false, errors.Wrap(err, "reading body") + } + fmt.Println(string(b)) + var hr hcaptchaResponse + err = json.Unmarshal(b, &hr) + if err != nil { + return false, errors.Wrap(err, "unmarshaling json") + } + + return hr.Success, nil +} diff --git a/backend/routes/auth/discord.go b/backend/routes/auth/discord.go index 34b0f93..2432b98 100644 --- a/backend/routes/auth/discord.go +++ b/backend/routes/auth/discord.go @@ -39,9 +39,10 @@ type discordCallbackResponse struct { Token string `json:"token,omitempty"` User *userResponse `json:"user,omitempty"` - Discord string `json:"discord,omitempty"` // username, for UI purposes - Ticket string `json:"ticket,omitempty"` - RequireInvite bool `json:"require_invite"` // require an invite for signing up + Discord string `json:"discord,omitempty"` // username, for UI purposes + Ticket string `json:"ticket,omitempty"` + RequireInvite bool `json:"require_invite"` // require an invite for signing up + RequireCaptcha bool `json:"require_captcha"` IsDeleted bool `json:"is_deleted"` DeletedAt *time.Time `json:"deleted_at,omitempty"` @@ -148,10 +149,11 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error { } render.JSON(w, r, discordCallbackResponse{ - HasAccount: false, - Discord: du.String(), - Ticket: ticket, - RequireInvite: s.RequireInvite, + HasAccount: false, + Discord: du.String(), + Ticket: ticket, + RequireInvite: s.RequireInvite, + RequireCaptcha: s.hcaptchaSecret != "", }) return nil @@ -251,9 +253,10 @@ func (s *Server) discordUnlink(w http.ResponseWriter, r *http.Request) error { } type signupRequest struct { - Ticket string `json:"ticket"` - Username string `json:"username"` - InviteCode string `json:"invite_code"` + Ticket string `json:"ticket"` + Username string `json:"username"` + InviteCode string `json:"invite_code"` + CaptchaResponse string `json:"captcha_response"` } type signupResponse struct { @@ -298,6 +301,19 @@ func (s *Server) discordSignup(w http.ResponseWriter, r *http.Request) error { return server.APIError{Code: server.ErrInvalidTicket} } + // check captcha + if s.hcaptchaSecret != "" { + ok, err := s.verifyCaptcha(ctx, req.CaptchaResponse) + if err != nil { + log.Errorf("verifying captcha: %v", err) + return server.APIError{Code: server.ErrInternalServerError} + } + + if !ok { + return server.APIError{Code: server.ErrInvalidCaptcha} + } + } + u, err := s.DB.CreateUser(ctx, tx, req.Username) if err != nil { if errors.Cause(err) == db.ErrUsernameTaken { diff --git a/backend/routes/auth/fedi_mastodon.go b/backend/routes/auth/fedi_mastodon.go index 85bab13..7c44922 100644 --- a/backend/routes/auth/fedi_mastodon.go +++ b/backend/routes/auth/fedi_mastodon.go @@ -27,9 +27,10 @@ type fediCallbackResponse struct { Token string `json:"token,omitempty"` User *userResponse `json:"user,omitempty"` - Fediverse string `json:"fediverse,omitempty"` // username, for UI purposes - Ticket string `json:"ticket,omitempty"` - RequireInvite bool `json:"require_invite"` // require an invite for signing up + Fediverse string `json:"fediverse,omitempty"` // username, for UI purposes + Ticket string `json:"ticket,omitempty"` + RequireInvite bool `json:"require_invite"` // require an invite for signing up + RequireCaptcha bool `json:"require_captcha"` IsDeleted bool `json:"is_deleted"` DeletedAt *time.Time `json:"deleted_at,omitempty"` @@ -169,10 +170,11 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error } render.JSON(w, r, fediCallbackResponse{ - HasAccount: false, - Fediverse: mu.Username, - Ticket: ticket, - RequireInvite: s.RequireInvite, + HasAccount: false, + Fediverse: mu.Username, + Ticket: ticket, + RequireInvite: s.RequireInvite, + RequireCaptcha: s.hcaptchaSecret != "", }) return nil @@ -278,10 +280,11 @@ func (s *Server) mastodonUnlink(w http.ResponseWriter, r *http.Request) error { } type fediSignupRequest struct { - Instance string `json:"instance"` - Ticket string `json:"ticket"` - Username string `json:"username"` - InviteCode string `json:"invite_code"` + Instance string `json:"instance"` + Ticket string `json:"ticket"` + Username string `json:"username"` + InviteCode string `json:"invite_code"` + CaptchaResponse string `json:"captcha_response"` } func (s *Server) mastodonSignup(w http.ResponseWriter, r *http.Request) error { @@ -326,6 +329,19 @@ func (s *Server) mastodonSignup(w http.ResponseWriter, r *http.Request) error { return server.APIError{Code: server.ErrInvalidTicket} } + // check captcha + if s.hcaptchaSecret != "" { + ok, err := s.verifyCaptcha(ctx, req.CaptchaResponse) + if err != nil { + log.Errorf("verifying captcha: %v", err) + return server.APIError{Code: server.ErrInternalServerError} + } + + if !ok { + return server.APIError{Code: server.ErrInvalidCaptcha} + } + } + u, err := s.DB.CreateUser(ctx, tx, req.Username) if err != nil { if errors.Cause(err) == db.ErrUsernameTaken { diff --git a/backend/routes/auth/fedi_misskey.go b/backend/routes/auth/fedi_misskey.go index 69b0d94..a7c28c3 100644 --- a/backend/routes/auth/fedi_misskey.go +++ b/backend/routes/auth/fedi_misskey.go @@ -149,10 +149,11 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error { } render.JSON(w, r, fediCallbackResponse{ - HasAccount: false, - Fediverse: mu.User.Username, - Ticket: ticket, - RequireInvite: s.RequireInvite, + HasAccount: false, + Fediverse: mu.User.Username, + Ticket: ticket, + RequireInvite: s.RequireInvite, + RequireCaptcha: s.hcaptchaSecret != "", }) return nil @@ -256,6 +257,19 @@ func (s *Server) misskeySignup(w http.ResponseWriter, r *http.Request) error { return server.APIError{Code: server.ErrInvalidTicket} } + // check captcha + if s.hcaptchaSecret != "" { + ok, err := s.verifyCaptcha(ctx, req.CaptchaResponse) + if err != nil { + log.Errorf("verifying captcha: %v", err) + return server.APIError{Code: server.ErrInternalServerError} + } + + if !ok { + return server.APIError{Code: server.ErrInvalidCaptcha} + } + } + u, err := s.DB.CreateUser(ctx, tx, req.Username) if err != nil { if errors.Cause(err) == db.ErrUsernameTaken { diff --git a/backend/routes/auth/google.go b/backend/routes/auth/google.go index 9edb21e..38896e8 100644 --- a/backend/routes/auth/google.go +++ b/backend/routes/auth/google.go @@ -33,9 +33,10 @@ type googleCallbackResponse struct { Token string `json:"token,omitempty"` User *userResponse `json:"user,omitempty"` - Google string `json:"google,omitempty"` // username, for UI purposes - Ticket string `json:"ticket,omitempty"` - RequireInvite bool `json:"require_invite"` // require an invite for signing up + Google string `json:"google,omitempty"` // username, for UI purposes + Ticket string `json:"ticket,omitempty"` + RequireInvite bool `json:"require_invite"` // require an invite for signing up + RequireCaptcha bool `json:"require_captcha"` IsDeleted bool `json:"is_deleted"` DeletedAt *time.Time `json:"deleted_at,omitempty"` @@ -167,10 +168,11 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error { } render.JSON(w, r, googleCallbackResponse{ - HasAccount: false, - Google: googleUsername, - Ticket: ticket, - RequireInvite: s.RequireInvite, + HasAccount: false, + Google: googleUsername, + Ticket: ticket, + RequireInvite: s.RequireInvite, + RequireCaptcha: s.hcaptchaSecret != "", }) return nil @@ -302,6 +304,19 @@ func (s *Server) googleSignup(w http.ResponseWriter, r *http.Request) error { return server.APIError{Code: server.ErrInvalidTicket} } + // check captcha + if s.hcaptchaSecret != "" { + ok, err := s.verifyCaptcha(ctx, req.CaptchaResponse) + if err != nil { + log.Errorf("verifying captcha: %v", err) + return server.APIError{Code: server.ErrInternalServerError} + } + + if !ok { + return server.APIError{Code: server.ErrInvalidCaptcha} + } + } + u, err := s.DB.CreateUser(ctx, tx, req.Username) if err != nil { if errors.Cause(err) == db.ErrUsernameTaken { diff --git a/backend/routes/auth/routes.go b/backend/routes/auth/routes.go index 1b7ad53..ad5c92d 100644 --- a/backend/routes/auth/routes.go +++ b/backend/routes/auth/routes.go @@ -18,6 +18,9 @@ type Server struct { RequireInvite bool BaseURL string + + hcaptchaSitekey string + hcaptchaSecret string } type userResponse struct { @@ -70,9 +73,11 @@ func dbUserToUserResponse(u db.User, fields []db.Field) *userResponse { func Mount(srv *server.Server, r chi.Router) { s := &Server{ - Server: srv, - RequireInvite: os.Getenv("REQUIRE_INVITE") == "true", - BaseURL: os.Getenv("BASE_URL"), + Server: srv, + RequireInvite: os.Getenv("REQUIRE_INVITE") == "true", + BaseURL: os.Getenv("BASE_URL"), + hcaptchaSitekey: os.Getenv("HCAPTCHA_SITEKEY"), + hcaptchaSecret: os.Getenv("HCAPTCHA_SECRET"), } r.Route("/auth", func(r chi.Router) { diff --git a/backend/routes/auth/tumblr.go b/backend/routes/auth/tumblr.go index 8fdddf1..c896e76 100644 --- a/backend/routes/auth/tumblr.go +++ b/backend/routes/auth/tumblr.go @@ -55,9 +55,10 @@ type tumblrCallbackResponse struct { Token string `json:"token,omitempty"` User *userResponse `json:"user,omitempty"` - Tumblr string `json:"tumblr,omitempty"` // username, for UI purposes - Ticket string `json:"ticket,omitempty"` - RequireInvite bool `json:"require_invite"` // require an invite for signing up + Tumblr string `json:"tumblr,omitempty"` // username, for UI purposes + Ticket string `json:"ticket,omitempty"` + RequireInvite bool `json:"require_invite"` // require an invite for signing up + RequireCaptcha bool `json:"require_captcha"` IsDeleted bool `json:"is_deleted"` DeletedAt *time.Time `json:"deleted_at,omitempty"` @@ -200,10 +201,11 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error { } render.JSON(w, r, tumblrCallbackResponse{ - HasAccount: false, - Tumblr: tumblrName, - Ticket: ticket, - RequireInvite: s.RequireInvite, + HasAccount: false, + Tumblr: tumblrName, + Ticket: ticket, + RequireInvite: s.RequireInvite, + RequireCaptcha: s.hcaptchaSecret != "", }) return nil @@ -335,6 +337,19 @@ func (s *Server) tumblrSignup(w http.ResponseWriter, r *http.Request) error { return server.APIError{Code: server.ErrInvalidTicket} } + // check captcha + if s.hcaptchaSecret != "" { + ok, err := s.verifyCaptcha(ctx, req.CaptchaResponse) + if err != nil { + log.Errorf("verifying captcha: %v", err) + return server.APIError{Code: server.ErrInternalServerError} + } + + if !ok { + return server.APIError{Code: server.ErrInvalidCaptcha} + } + } + u, err := s.DB.CreateUser(ctx, tx, req.Username) if err != nil { if errors.Cause(err) == db.ErrUsernameTaken { diff --git a/backend/server/errors.go b/backend/server/errors.go index 0b0ae3d..18bec03 100644 --- a/backend/server/errors.go +++ b/backend/server/errors.go @@ -97,6 +97,7 @@ const ( ErrAlreadyLinked = 1014 // user already has linked account of the same type ErrNotLinked = 1015 // user already doesn't have a linked account ErrLastProvider = 1016 // unlinking provider would leave account with no authentication method + ErrInvalidCaptcha = 1017 // invalid or missing captcha response // User-related error codes ErrUserNotFound = 2001 @@ -141,6 +142,7 @@ var errCodeMessages = map[int]string{ ErrAlreadyLinked: "Your account is already linked to an account of this type", ErrNotLinked: "Your account is already not linked to an account of this type", ErrLastProvider: "This is your account's only authentication provider", + ErrInvalidCaptcha: "Invalid or missing captcha response", ErrUserNotFound: "User not found", ErrMemberListPrivate: "This user's member list is private.", @@ -181,6 +183,7 @@ var errCodeStatuses = map[int]int{ ErrAlreadyLinked: http.StatusBadRequest, ErrNotLinked: http.StatusBadRequest, ErrLastProvider: http.StatusBadRequest, + ErrInvalidCaptcha: http.StatusBadRequest, ErrUserNotFound: http.StatusNotFound, ErrMemberListPrivate: http.StatusForbidden, diff --git a/frontend/package.json b/frontend/package.json index 1755f23..54aa8ac 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "prettier-plugin-svelte": "^2.10.0", "svelte": "^3.58.0", "svelte-check": "^3.1.4", + "svelte-hcaptcha": "^0.1.1", "sveltestrap": "^5.10.0", "tslib": "^2.5.0", "typescript": "^4.9.5", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 42f86fc..248aa30 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -27,6 +27,7 @@ specifiers: sanitize-html: ^2.10.0 svelte: ^3.58.0 svelte-check: ^3.1.4 + svelte-hcaptcha: ^0.1.1 sveltestrap: ^5.10.0 tslib: ^2.5.0 typescript: ^4.9.5 @@ -62,6 +63,7 @@ devDependencies: prettier-plugin-svelte: 2.10.0_ur5pqdgn24bclu6l6i7qojopk4 svelte: 3.58.0 svelte-check: 3.1.4_svelte@3.58.0 + svelte-hcaptcha: 0.1.1 sveltestrap: 5.10.0_svelte@3.58.0 tslib: 2.5.0 typescript: 4.9.5 @@ -2018,6 +2020,10 @@ packages: - sugarss dev: true + /svelte-hcaptcha/0.1.1: + resolution: {integrity: sha512-iFF3HwfrCRciJnDs4Y9/rpP/BM2U/5zt+vh+9d4tALPAHVkcANiJIKqYuS835pIaTm6gt+xOzjfFI3cgiRI29A==} + dev: true + /svelte-hmr/0.15.1_svelte@3.58.0: resolution: {integrity: sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==} engines: {node: ^12.20 || ^14.13.1 || >= 16} diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts index f59b884..9176095 100644 --- a/frontend/src/app.d.ts +++ b/frontend/src/app.d.ts @@ -1,12 +1,31 @@ // See https://kit.svelte.dev/docs/types#app // for information about these interfaces declare global { - namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface Platform {} - } + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface Platform {} + } +} + +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 {}; diff --git a/frontend/src/lib/api/entities.ts b/frontend/src/lib/api/entities.ts index 267061e..22fe815 100644 --- a/frontend/src/lib/api/entities.ts +++ b/frontend/src/lib/api/entities.ts @@ -150,6 +150,7 @@ export enum ErrorCode { AlreadyLinked = 1014, NotLinked = 1015, LastProvider = 1016, + InvalidCaptcha = 1017, UserNotFound = 2001, diff --git a/frontend/src/routes/auth/login/CallbackPage.svelte b/frontend/src/routes/auth/login/CallbackPage.svelte index 6a9b27d..0138dff 100644 --- a/frontend/src/routes/auth/login/CallbackPage.svelte +++ b/frontend/src/routes/auth/login/CallbackPage.svelte @@ -4,6 +4,8 @@ import { fastFetch } from "$lib/api/fetch"; import { usernameRegex } from "$lib/api/regex"; import ErrorAlert from "$lib/components/ErrorAlert.svelte"; + import { PUBLIC_HCAPTCHA_SITEKEY } from "$env/static/public"; + import HCaptcha from "svelte-hcaptcha"; import { userStore } from "$lib/store"; import { addToast } from "$lib/toast"; import { DateTime } from "luxon"; @@ -23,6 +25,7 @@ export let remoteName: string | undefined; export let error: APIError | undefined; export let requireInvite: boolean | undefined; + export let requireCaptcha: boolean | undefined; export let isDeleted: boolean | undefined; export let ticket: string | undefined; export let token: string | undefined; @@ -54,7 +57,31 @@ let toggleForceDeleteModal = () => (forceDeleteModalOpen = !forceDeleteModalOpen); export let linkAccount: () => Promise; - export let signupForm: (username: string, inviteCode: string) => Promise; + export let signupForm: ( + username: string, + inviteCode: string, + captchaToken: string, + ) => Promise; + + let captchaToken = ""; + let captcha: any; + + const captchaSuccess = (token: any) => { + captchaToken = token.detail.token; + }; + + let canSubmit = false; + $: canSubmit = usernameValid && (!!captchaToken || !requireCaptcha); + + const captchaError = () => { + addToast({ + header: "Captcha failed", + body: "There was an error verifying the captcha, please try again.", + }); + captcha.reset(); + }; + + export const resetCaptcha = (): void => captcha.reset(); const forceDeleteAccount = async () => { try { @@ -116,7 +143,7 @@ {:else if ticket} -
signupForm(username, inviteCode)}> + signupForm(username, inviteCode, captchaToken)}>
@@ -144,12 +171,22 @@
{/if} + {#if requireCaptcha} +
+ +
+ {/if}
By signing up, you agree to the terms of service and the privacy policy.

- + {#if !usernameValid && username.length > 0} That username is not valid. {/if} diff --git a/frontend/src/routes/auth/login/discord/+page.server.ts b/frontend/src/routes/auth/login/discord/+page.server.ts index b1df685..59ca72e 100644 --- a/frontend/src/routes/auth/login/discord/+page.server.ts +++ b/frontend/src/routes/auth/login/discord/+page.server.ts @@ -30,6 +30,7 @@ interface CallbackResponse { discord?: string; ticket?: string; require_invite: boolean; + require_captcha: boolean; is_deleted: boolean; deleted_at?: string; diff --git a/frontend/src/routes/auth/login/discord/+page.svelte b/frontend/src/routes/auth/login/discord/+page.svelte index 703e10c..66feeb8 100644 --- a/frontend/src/routes/auth/login/discord/+page.svelte +++ b/frontend/src/routes/auth/login/discord/+page.svelte @@ -1,6 +1,6 @@ import { goto } from "$app/navigation"; - import type { APIError, MeUser } from "$lib/api/entities"; + import { ErrorCode, type APIError, type MeUser } from "$lib/api/entities"; import { apiFetch, apiFetchClient } from "$lib/api/fetch"; import { userStore } from "$lib/store"; import type { PageData } from "./$types"; @@ -10,7 +10,9 @@ export let data: PageData; - const signupForm = async (username: string, invite: string) => { + let callbackPage: any; + + const signupForm = async (username: string, invite: string, captchaToken: string) => { try { const resp = await apiFetch("/auth/google/signup", { method: "POST", @@ -18,6 +20,7 @@ ticket: data.ticket, username: username, invite_code: invite, + captcha_response: captchaToken, }, }); @@ -27,6 +30,10 @@ addToast({ header: "Welcome!", body: "Signed up successfully!" }); goto(`/@${resp.user.name}`); } catch (e) { + if ((e as APIError).code === ErrorCode.InvalidCaptcha) { + callbackPage.resetCaptcha(); + } + data.error = e as APIError; } }; @@ -48,10 +55,12 @@ import { goto } from "$app/navigation"; - import type { APIError, MeUser } from "$lib/api/entities"; + import { ErrorCode, type APIError, type MeUser } from "$lib/api/entities"; import { apiFetch, apiFetchClient } from "$lib/api/fetch"; import { userStore } from "$lib/store"; import type { PageData } from "./$types"; @@ -10,7 +10,9 @@ export let data: PageData; - const signupForm = async (username: string, invite: string) => { + let callbackPage: any; + + const signupForm = async (username: string, invite: string, captchaToken: string) => { try { const resp = await apiFetch("/auth/mastodon/signup", { method: "POST", @@ -19,6 +21,7 @@ ticket: data.ticket, username: username, invite_code: invite, + captcha_response: captchaToken, }, }); @@ -28,6 +31,10 @@ addToast({ header: "Welcome!", body: "Signed up successfully!" }); goto(`/@${resp.user.name}`); } catch (e) { + if ((e as APIError).code === ErrorCode.InvalidCaptcha) { + callbackPage.resetCaptcha(); + } + data.error = e as APIError; } }; @@ -50,10 +57,12 @@ import { goto } from "$app/navigation"; - import type { APIError, MeUser } from "$lib/api/entities"; + import { ErrorCode, type APIError, type MeUser } from "$lib/api/entities"; import { apiFetch, apiFetchClient } from "$lib/api/fetch"; import { userStore } from "$lib/store"; import type { PageData } from "./$types"; @@ -10,7 +10,9 @@ export let data: PageData; - const signupForm = async (username: string, invite: string) => { + let callbackPage: any; + + const signupForm = async (username: string, invite: string, captchaToken: string) => { try { const resp = await apiFetch("/auth/misskey/signup", { method: "POST", @@ -19,6 +21,7 @@ ticket: data.ticket, username: username, invite_code: invite, + captcha_response: captchaToken, }, }); @@ -28,6 +31,10 @@ addToast({ header: "Welcome!", body: "Signed up successfully!" }); goto(`/@${resp.user.name}`); } catch (e) { + if ((e as APIError).code === ErrorCode.InvalidCaptcha) { + callbackPage.resetCaptcha(); + } + data.error = e as APIError; } }; @@ -54,6 +61,7 @@ remoteName="{data.fediverse}@{data.instance}" error={data.error} requireInvite={data.require_invite} + requireCaptcha={data.require_captcha} isDeleted={data.is_deleted} ticket={data.ticket} token={data.token} diff --git a/frontend/src/routes/auth/login/tumblr/+page.server.ts b/frontend/src/routes/auth/login/tumblr/+page.server.ts index 48a0b8c..c045030 100644 --- a/frontend/src/routes/auth/login/tumblr/+page.server.ts +++ b/frontend/src/routes/auth/login/tumblr/+page.server.ts @@ -30,6 +30,7 @@ interface CallbackResponse { tumblr?: string; ticket?: string; require_invite: boolean; + require_captcha: boolean; is_deleted: boolean; deleted_at?: string; diff --git a/frontend/src/routes/auth/login/tumblr/+page.svelte b/frontend/src/routes/auth/login/tumblr/+page.svelte index c64dcef..1316f33 100644 --- a/frontend/src/routes/auth/login/tumblr/+page.svelte +++ b/frontend/src/routes/auth/login/tumblr/+page.svelte @@ -1,6 +1,6 @@