feat: add captcha when signing up (closes #53)

This commit is contained in:
Sam 2023-04-24 16:51:55 +02:00
parent bb3d56f548
commit 6f7eb5eeee
No known key found for this signature in database
GPG Key ID: B4EF20DDE721CAA1
23 changed files with 316 additions and 61 deletions

View File

@ -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
}

View File

@ -42,6 +42,7 @@ type discordCallbackResponse struct {
Discord string `json:"discord,omitempty"` // username, for UI purposes Discord string `json:"discord,omitempty"` // username, for UI purposes
Ticket string `json:"ticket,omitempty"` Ticket string `json:"ticket,omitempty"`
RequireInvite bool `json:"require_invite"` // require an invite for signing up RequireInvite bool `json:"require_invite"` // require an invite for signing up
RequireCaptcha bool `json:"require_captcha"`
IsDeleted bool `json:"is_deleted"` IsDeleted bool `json:"is_deleted"`
DeletedAt *time.Time `json:"deleted_at,omitempty"` DeletedAt *time.Time `json:"deleted_at,omitempty"`
@ -152,6 +153,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
Discord: du.String(), Discord: du.String(),
Ticket: ticket, Ticket: ticket,
RequireInvite: s.RequireInvite, RequireInvite: s.RequireInvite,
RequireCaptcha: s.hcaptchaSecret != "",
}) })
return nil return nil
@ -254,6 +256,7 @@ type signupRequest struct {
Ticket string `json:"ticket"` Ticket string `json:"ticket"`
Username string `json:"username"` Username string `json:"username"`
InviteCode string `json:"invite_code"` InviteCode string `json:"invite_code"`
CaptchaResponse string `json:"captcha_response"`
} }
type signupResponse struct { type signupResponse struct {
@ -298,6 +301,19 @@ func (s *Server) discordSignup(w http.ResponseWriter, r *http.Request) error {
return server.APIError{Code: server.ErrInvalidTicket} 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) u, err := s.DB.CreateUser(ctx, tx, req.Username)
if err != nil { if err != nil {
if errors.Cause(err) == db.ErrUsernameTaken { if errors.Cause(err) == db.ErrUsernameTaken {

View File

@ -30,6 +30,7 @@ type fediCallbackResponse struct {
Fediverse string `json:"fediverse,omitempty"` // username, for UI purposes Fediverse string `json:"fediverse,omitempty"` // username, for UI purposes
Ticket string `json:"ticket,omitempty"` Ticket string `json:"ticket,omitempty"`
RequireInvite bool `json:"require_invite"` // require an invite for signing up RequireInvite bool `json:"require_invite"` // require an invite for signing up
RequireCaptcha bool `json:"require_captcha"`
IsDeleted bool `json:"is_deleted"` IsDeleted bool `json:"is_deleted"`
DeletedAt *time.Time `json:"deleted_at,omitempty"` DeletedAt *time.Time `json:"deleted_at,omitempty"`
@ -173,6 +174,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error
Fediverse: mu.Username, Fediverse: mu.Username,
Ticket: ticket, Ticket: ticket,
RequireInvite: s.RequireInvite, RequireInvite: s.RequireInvite,
RequireCaptcha: s.hcaptchaSecret != "",
}) })
return nil return nil
@ -282,6 +284,7 @@ type fediSignupRequest struct {
Ticket string `json:"ticket"` Ticket string `json:"ticket"`
Username string `json:"username"` Username string `json:"username"`
InviteCode string `json:"invite_code"` InviteCode string `json:"invite_code"`
CaptchaResponse string `json:"captcha_response"`
} }
func (s *Server) mastodonSignup(w http.ResponseWriter, r *http.Request) error { 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} 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) u, err := s.DB.CreateUser(ctx, tx, req.Username)
if err != nil { if err != nil {
if errors.Cause(err) == db.ErrUsernameTaken { if errors.Cause(err) == db.ErrUsernameTaken {

View File

@ -153,6 +153,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
Fediverse: mu.User.Username, Fediverse: mu.User.Username,
Ticket: ticket, Ticket: ticket,
RequireInvite: s.RequireInvite, RequireInvite: s.RequireInvite,
RequireCaptcha: s.hcaptchaSecret != "",
}) })
return nil return nil
@ -256,6 +257,19 @@ func (s *Server) misskeySignup(w http.ResponseWriter, r *http.Request) error {
return server.APIError{Code: server.ErrInvalidTicket} 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) u, err := s.DB.CreateUser(ctx, tx, req.Username)
if err != nil { if err != nil {
if errors.Cause(err) == db.ErrUsernameTaken { if errors.Cause(err) == db.ErrUsernameTaken {

View File

@ -36,6 +36,7 @@ type googleCallbackResponse struct {
Google string `json:"google,omitempty"` // username, for UI purposes Google string `json:"google,omitempty"` // username, for UI purposes
Ticket string `json:"ticket,omitempty"` Ticket string `json:"ticket,omitempty"`
RequireInvite bool `json:"require_invite"` // require an invite for signing up RequireInvite bool `json:"require_invite"` // require an invite for signing up
RequireCaptcha bool `json:"require_captcha"`
IsDeleted bool `json:"is_deleted"` IsDeleted bool `json:"is_deleted"`
DeletedAt *time.Time `json:"deleted_at,omitempty"` DeletedAt *time.Time `json:"deleted_at,omitempty"`
@ -171,6 +172,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error {
Google: googleUsername, Google: googleUsername,
Ticket: ticket, Ticket: ticket,
RequireInvite: s.RequireInvite, RequireInvite: s.RequireInvite,
RequireCaptcha: s.hcaptchaSecret != "",
}) })
return nil return nil
@ -302,6 +304,19 @@ func (s *Server) googleSignup(w http.ResponseWriter, r *http.Request) error {
return server.APIError{Code: server.ErrInvalidTicket} 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) u, err := s.DB.CreateUser(ctx, tx, req.Username)
if err != nil { if err != nil {
if errors.Cause(err) == db.ErrUsernameTaken { if errors.Cause(err) == db.ErrUsernameTaken {

View File

@ -18,6 +18,9 @@ type Server struct {
RequireInvite bool RequireInvite bool
BaseURL string BaseURL string
hcaptchaSitekey string
hcaptchaSecret string
} }
type userResponse struct { type userResponse struct {
@ -73,6 +76,8 @@ func Mount(srv *server.Server, r chi.Router) {
Server: srv, Server: srv,
RequireInvite: os.Getenv("REQUIRE_INVITE") == "true", RequireInvite: os.Getenv("REQUIRE_INVITE") == "true",
BaseURL: os.Getenv("BASE_URL"), BaseURL: os.Getenv("BASE_URL"),
hcaptchaSitekey: os.Getenv("HCAPTCHA_SITEKEY"),
hcaptchaSecret: os.Getenv("HCAPTCHA_SECRET"),
} }
r.Route("/auth", func(r chi.Router) { r.Route("/auth", func(r chi.Router) {

View File

@ -58,6 +58,7 @@ type tumblrCallbackResponse struct {
Tumblr string `json:"tumblr,omitempty"` // username, for UI purposes Tumblr string `json:"tumblr,omitempty"` // username, for UI purposes
Ticket string `json:"ticket,omitempty"` Ticket string `json:"ticket,omitempty"`
RequireInvite bool `json:"require_invite"` // require an invite for signing up RequireInvite bool `json:"require_invite"` // require an invite for signing up
RequireCaptcha bool `json:"require_captcha"`
IsDeleted bool `json:"is_deleted"` IsDeleted bool `json:"is_deleted"`
DeletedAt *time.Time `json:"deleted_at,omitempty"` DeletedAt *time.Time `json:"deleted_at,omitempty"`
@ -204,6 +205,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error {
Tumblr: tumblrName, Tumblr: tumblrName,
Ticket: ticket, Ticket: ticket,
RequireInvite: s.RequireInvite, RequireInvite: s.RequireInvite,
RequireCaptcha: s.hcaptchaSecret != "",
}) })
return nil return nil
@ -335,6 +337,19 @@ func (s *Server) tumblrSignup(w http.ResponseWriter, r *http.Request) error {
return server.APIError{Code: server.ErrInvalidTicket} 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) u, err := s.DB.CreateUser(ctx, tx, req.Username)
if err != nil { if err != nil {
if errors.Cause(err) == db.ErrUsernameTaken { if errors.Cause(err) == db.ErrUsernameTaken {

View File

@ -97,6 +97,7 @@ const (
ErrAlreadyLinked = 1014 // user already has linked account of the same type ErrAlreadyLinked = 1014 // user already has linked account of the same type
ErrNotLinked = 1015 // user already doesn't have a linked account ErrNotLinked = 1015 // user already doesn't have a linked account
ErrLastProvider = 1016 // unlinking provider would leave account with no authentication method ErrLastProvider = 1016 // unlinking provider would leave account with no authentication method
ErrInvalidCaptcha = 1017 // invalid or missing captcha response
// User-related error codes // User-related error codes
ErrUserNotFound = 2001 ErrUserNotFound = 2001
@ -141,6 +142,7 @@ var errCodeMessages = map[int]string{
ErrAlreadyLinked: "Your account is already linked to an account of this type", 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", ErrNotLinked: "Your account is already not linked to an account of this type",
ErrLastProvider: "This is your account's only authentication provider", ErrLastProvider: "This is your account's only authentication provider",
ErrInvalidCaptcha: "Invalid or missing captcha response",
ErrUserNotFound: "User not found", ErrUserNotFound: "User not found",
ErrMemberListPrivate: "This user's member list is private.", ErrMemberListPrivate: "This user's member list is private.",
@ -181,6 +183,7 @@ var errCodeStatuses = map[int]int{
ErrAlreadyLinked: http.StatusBadRequest, ErrAlreadyLinked: http.StatusBadRequest,
ErrNotLinked: http.StatusBadRequest, ErrNotLinked: http.StatusBadRequest,
ErrLastProvider: http.StatusBadRequest, ErrLastProvider: http.StatusBadRequest,
ErrInvalidCaptcha: http.StatusBadRequest,
ErrUserNotFound: http.StatusNotFound, ErrUserNotFound: http.StatusNotFound,
ErrMemberListPrivate: http.StatusForbidden, ErrMemberListPrivate: http.StatusForbidden,

View File

@ -28,6 +28,7 @@
"prettier-plugin-svelte": "^2.10.0", "prettier-plugin-svelte": "^2.10.0",
"svelte": "^3.58.0", "svelte": "^3.58.0",
"svelte-check": "^3.1.4", "svelte-check": "^3.1.4",
"svelte-hcaptcha": "^0.1.1",
"sveltestrap": "^5.10.0", "sveltestrap": "^5.10.0",
"tslib": "^2.5.0", "tslib": "^2.5.0",
"typescript": "^4.9.5", "typescript": "^4.9.5",

View File

@ -27,6 +27,7 @@ specifiers:
sanitize-html: ^2.10.0 sanitize-html: ^2.10.0
svelte: ^3.58.0 svelte: ^3.58.0
svelte-check: ^3.1.4 svelte-check: ^3.1.4
svelte-hcaptcha: ^0.1.1
sveltestrap: ^5.10.0 sveltestrap: ^5.10.0
tslib: ^2.5.0 tslib: ^2.5.0
typescript: ^4.9.5 typescript: ^4.9.5
@ -62,6 +63,7 @@ devDependencies:
prettier-plugin-svelte: 2.10.0_ur5pqdgn24bclu6l6i7qojopk4 prettier-plugin-svelte: 2.10.0_ur5pqdgn24bclu6l6i7qojopk4
svelte: 3.58.0 svelte: 3.58.0
svelte-check: 3.1.4_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 sveltestrap: 5.10.0_svelte@3.58.0
tslib: 2.5.0 tslib: 2.5.0
typescript: 4.9.5 typescript: 4.9.5
@ -2018,6 +2020,10 @@ packages:
- sugarss - sugarss
dev: true 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: /svelte-hmr/0.15.1_svelte@3.58.0:
resolution: {integrity: sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==} resolution: {integrity: sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==}
engines: {node: ^12.20 || ^14.13.1 || >= 16} engines: {node: ^12.20 || ^14.13.1 || >= 16}

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

@ -9,4 +9,23 @@ 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

@ -150,6 +150,7 @@ export enum ErrorCode {
AlreadyLinked = 1014, AlreadyLinked = 1014,
NotLinked = 1015, NotLinked = 1015,
LastProvider = 1016, LastProvider = 1016,
InvalidCaptcha = 1017,
UserNotFound = 2001, UserNotFound = 2001,

View File

@ -4,6 +4,8 @@
import { fastFetch } from "$lib/api/fetch"; import { fastFetch } from "$lib/api/fetch";
import { usernameRegex } from "$lib/api/regex"; import { usernameRegex } from "$lib/api/regex";
import ErrorAlert from "$lib/components/ErrorAlert.svelte"; 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 { userStore } from "$lib/store";
import { addToast } from "$lib/toast"; import { addToast } from "$lib/toast";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
@ -23,6 +25,7 @@
export let remoteName: string | undefined; export let remoteName: string | undefined;
export let error: APIError | undefined; export let error: APIError | undefined;
export let requireInvite: boolean | undefined; export let requireInvite: boolean | undefined;
export let requireCaptcha: boolean | undefined;
export let isDeleted: boolean | undefined; export let isDeleted: boolean | undefined;
export let ticket: string | undefined; export let ticket: string | undefined;
export let token: string | undefined; export let token: string | undefined;
@ -54,7 +57,31 @@
let toggleForceDeleteModal = () => (forceDeleteModalOpen = !forceDeleteModalOpen); let toggleForceDeleteModal = () => (forceDeleteModalOpen = !forceDeleteModalOpen);
export let linkAccount: () => Promise<void>; export let linkAccount: () => Promise<void>;
export let signupForm: (username: string, inviteCode: string) => Promise<void>; export let signupForm: (
username: string,
inviteCode: string,
captchaToken: string,
) => Promise<void>;
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 () => { const forceDeleteAccount = async () => {
try { try {
@ -116,7 +143,7 @@
<Button color="secondary" href="/settings/auth">Cancel</Button> <Button color="secondary" href="/settings/auth">Cancel</Button>
</div> </div>
{:else if ticket} {:else if ticket}
<form on:submit|preventDefault={() => signupForm(username, inviteCode)}> <form on:submit|preventDefault={() => signupForm(username, inviteCode, captchaToken)}>
<div> <div>
<FormGroup floating label="{authType} username"> <FormGroup floating label="{authType} username">
<Input readonly value={remoteName} /> <Input readonly value={remoteName} />
@ -144,12 +171,22 @@
</div> </div>
</div> </div>
{/if} {/if}
{#if requireCaptcha}
<div class="mt-2 mx-2 mb-1">
<HCaptcha
bind:this={captcha}
sitekey={PUBLIC_HCAPTCHA_SITEKEY}
on:success={captchaSuccess}
on:error={captchaError}
/>
</div>
{/if}
<div class="text-muted my-1"> <div class="text-muted my-1">
By signing up, you agree to the <a href="/page/terms">terms of service</a> and the By signing up, you agree to the <a href="/page/terms">terms of service</a> and the
<a href="/page/privacy">privacy policy</a>. <a href="/page/privacy">privacy policy</a>.
</div> </div>
<p> <p>
<Button type="submit" color="primary" disabled={!usernameValid}>Sign up</Button> <Button type="submit" color="primary" disabled={!canSubmit}>Sign up</Button>
{#if !usernameValid && username.length > 0} {#if !usernameValid && username.length > 0}
<span class="text-danger-emphasis mb-2">That username is not valid.</span> <span class="text-danger-emphasis mb-2">That username is not valid.</span>
{/if} {/if}

View File

@ -30,6 +30,7 @@ interface CallbackResponse {
discord?: string; discord?: string;
ticket?: string; ticket?: string;
require_invite: boolean; require_invite: boolean;
require_captcha: boolean;
is_deleted: boolean; is_deleted: boolean;
deleted_at?: string; deleted_at?: string;

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; 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 { apiFetch, apiFetchClient } from "$lib/api/fetch";
import { userStore } from "$lib/store"; import { userStore } from "$lib/store";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
@ -10,7 +10,9 @@
export let data: PageData; export let data: PageData;
const signupForm = async (username: string, invite: string) => { let callbackPage: any;
const signupForm = async (username: string, invite: string, captchaToken: string) => {
try { try {
const resp = await apiFetch<SignupResponse>("/auth/discord/signup", { const resp = await apiFetch<SignupResponse>("/auth/discord/signup", {
method: "POST", method: "POST",
@ -18,6 +20,7 @@
ticket: data.ticket, ticket: data.ticket,
username: username, username: username,
invite_code: invite, invite_code: invite,
captcha_response: captchaToken,
}, },
}); });
@ -27,6 +30,10 @@
addToast({ header: "Welcome!", body: "Signed up successfully!" }); addToast({ header: "Welcome!", body: "Signed up successfully!" });
goto(`/@${resp.user.name}`); goto(`/@${resp.user.name}`);
} catch (e) { } catch (e) {
if ((e as APIError).code === ErrorCode.InvalidCaptcha) {
callbackPage.resetCaptcha();
}
data.error = e as APIError; data.error = e as APIError;
} }
}; };
@ -48,10 +55,12 @@
</script> </script>
<CallbackPage <CallbackPage
bind:this={callbackPage}
authType="Discord" authType="Discord"
remoteName={data.discord} remoteName={data.discord}
error={data.error} error={data.error}
requireInvite={data.require_invite} requireInvite={data.require_invite}
requireCaptcha={data.require_captcha}
isDeleted={data.is_deleted} isDeleted={data.is_deleted}
ticket={data.ticket} ticket={data.ticket}
token={data.token} token={data.token}

View File

@ -30,6 +30,7 @@ interface CallbackResponse {
google?: string; google?: string;
ticket?: string; ticket?: string;
require_invite: boolean; require_invite: boolean;
require_captcha: boolean;
is_deleted: boolean; is_deleted: boolean;
deleted_at?: string; deleted_at?: string;

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; 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 { apiFetch, apiFetchClient } from "$lib/api/fetch";
import { userStore } from "$lib/store"; import { userStore } from "$lib/store";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
@ -10,7 +10,9 @@
export let data: PageData; export let data: PageData;
const signupForm = async (username: string, invite: string) => { let callbackPage: any;
const signupForm = async (username: string, invite: string, captchaToken: string) => {
try { try {
const resp = await apiFetch<SignupResponse>("/auth/google/signup", { const resp = await apiFetch<SignupResponse>("/auth/google/signup", {
method: "POST", method: "POST",
@ -18,6 +20,7 @@
ticket: data.ticket, ticket: data.ticket,
username: username, username: username,
invite_code: invite, invite_code: invite,
captcha_response: captchaToken,
}, },
}); });
@ -27,6 +30,10 @@
addToast({ header: "Welcome!", body: "Signed up successfully!" }); addToast({ header: "Welcome!", body: "Signed up successfully!" });
goto(`/@${resp.user.name}`); goto(`/@${resp.user.name}`);
} catch (e) { } catch (e) {
if ((e as APIError).code === ErrorCode.InvalidCaptcha) {
callbackPage.resetCaptcha();
}
data.error = e as APIError; data.error = e as APIError;
} }
}; };
@ -48,10 +55,12 @@
</script> </script>
<CallbackPage <CallbackPage
bind:this={callbackPage}
authType="Google" authType="Google"
remoteName={data.google} remoteName={data.google}
error={data.error} error={data.error}
requireInvite={data.require_invite} requireInvite={data.require_invite}
requireCaptcha={data.require_captcha}
isDeleted={data.is_deleted} isDeleted={data.is_deleted}
ticket={data.ticket} ticket={data.ticket}
token={data.token} token={data.token}

View File

@ -30,6 +30,7 @@ interface CallbackResponse {
fediverse?: string; fediverse?: string;
ticket?: string; ticket?: string;
require_invite: boolean; require_invite: boolean;
require_captcha: boolean;
is_deleted: boolean; is_deleted: boolean;
deleted_at?: string; deleted_at?: string;

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; 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 { apiFetch, apiFetchClient } from "$lib/api/fetch";
import { userStore } from "$lib/store"; import { userStore } from "$lib/store";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
@ -10,7 +10,9 @@
export let data: PageData; export let data: PageData;
const signupForm = async (username: string, invite: string) => { let callbackPage: any;
const signupForm = async (username: string, invite: string, captchaToken: string) => {
try { try {
const resp = await apiFetch<SignupResponse>("/auth/mastodon/signup", { const resp = await apiFetch<SignupResponse>("/auth/mastodon/signup", {
method: "POST", method: "POST",
@ -19,6 +21,7 @@
ticket: data.ticket, ticket: data.ticket,
username: username, username: username,
invite_code: invite, invite_code: invite,
captcha_response: captchaToken,
}, },
}); });
@ -28,6 +31,10 @@
addToast({ header: "Welcome!", body: "Signed up successfully!" }); addToast({ header: "Welcome!", body: "Signed up successfully!" });
goto(`/@${resp.user.name}`); goto(`/@${resp.user.name}`);
} catch (e) { } catch (e) {
if ((e as APIError).code === ErrorCode.InvalidCaptcha) {
callbackPage.resetCaptcha();
}
data.error = e as APIError; data.error = e as APIError;
} }
}; };
@ -50,10 +57,12 @@
</script> </script>
<CallbackPage <CallbackPage
bind:this={callbackPage}
authType="Fediverse" authType="Fediverse"
remoteName="{data.fediverse}@{data.instance}" remoteName="{data.fediverse}@{data.instance}"
error={data.error} error={data.error}
requireInvite={data.require_invite} requireInvite={data.require_invite}
requireCaptcha={data.require_captcha}
isDeleted={data.is_deleted} isDeleted={data.is_deleted}
ticket={data.ticket} ticket={data.ticket}
token={data.token} token={data.token}

View File

@ -29,6 +29,7 @@ interface CallbackResponse {
fediverse?: string; fediverse?: string;
ticket?: string; ticket?: string;
require_invite: boolean; require_invite: boolean;
require_captcha: boolean;
is_deleted: boolean; is_deleted: boolean;
deleted_at?: string; deleted_at?: string;

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; 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 { apiFetch, apiFetchClient } from "$lib/api/fetch";
import { userStore } from "$lib/store"; import { userStore } from "$lib/store";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
@ -10,7 +10,9 @@
export let data: PageData; export let data: PageData;
const signupForm = async (username: string, invite: string) => { let callbackPage: any;
const signupForm = async (username: string, invite: string, captchaToken: string) => {
try { try {
const resp = await apiFetch<SignupResponse>("/auth/misskey/signup", { const resp = await apiFetch<SignupResponse>("/auth/misskey/signup", {
method: "POST", method: "POST",
@ -19,6 +21,7 @@
ticket: data.ticket, ticket: data.ticket,
username: username, username: username,
invite_code: invite, invite_code: invite,
captcha_response: captchaToken,
}, },
}); });
@ -28,6 +31,10 @@
addToast({ header: "Welcome!", body: "Signed up successfully!" }); addToast({ header: "Welcome!", body: "Signed up successfully!" });
goto(`/@${resp.user.name}`); goto(`/@${resp.user.name}`);
} catch (e) { } catch (e) {
if ((e as APIError).code === ErrorCode.InvalidCaptcha) {
callbackPage.resetCaptcha();
}
data.error = e as APIError; data.error = e as APIError;
} }
}; };
@ -54,6 +61,7 @@
remoteName="{data.fediverse}@{data.instance}" remoteName="{data.fediverse}@{data.instance}"
error={data.error} error={data.error}
requireInvite={data.require_invite} requireInvite={data.require_invite}
requireCaptcha={data.require_captcha}
isDeleted={data.is_deleted} isDeleted={data.is_deleted}
ticket={data.ticket} ticket={data.ticket}
token={data.token} token={data.token}

View File

@ -30,6 +30,7 @@ interface CallbackResponse {
tumblr?: string; tumblr?: string;
ticket?: string; ticket?: string;
require_invite: boolean; require_invite: boolean;
require_captcha: boolean;
is_deleted: boolean; is_deleted: boolean;
deleted_at?: string; deleted_at?: string;

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; 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 { apiFetch, apiFetchClient } from "$lib/api/fetch";
import { userStore } from "$lib/store"; import { userStore } from "$lib/store";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
@ -10,7 +10,9 @@
export let data: PageData; export let data: PageData;
const signupForm = async (username: string, invite: string) => { let callbackPage: any;
const signupForm = async (username: string, invite: string, captchaToken: string) => {
try { try {
const resp = await apiFetch<SignupResponse>("/auth/tumblr/signup", { const resp = await apiFetch<SignupResponse>("/auth/tumblr/signup", {
method: "POST", method: "POST",
@ -18,6 +20,7 @@
ticket: data.ticket, ticket: data.ticket,
username: username, username: username,
invite_code: invite, invite_code: invite,
captcha_response: captchaToken,
}, },
}); });
@ -27,6 +30,10 @@
addToast({ header: "Welcome!", body: "Signed up successfully!" }); addToast({ header: "Welcome!", body: "Signed up successfully!" });
goto(`/@${resp.user.name}`); goto(`/@${resp.user.name}`);
} catch (e) { } catch (e) {
if ((e as APIError).code === ErrorCode.InvalidCaptcha) {
callbackPage.resetCaptcha();
}
data.error = e as APIError; data.error = e as APIError;
} }
}; };
@ -48,10 +55,12 @@
</script> </script>
<CallbackPage <CallbackPage
bind:this={callbackPage}
authType="Tumblr" authType="Tumblr"
remoteName={data.tumblr} remoteName={data.tumblr}
error={data.error} error={data.error}
requireInvite={data.require_invite} requireInvite={data.require_invite}
requireCaptcha={data.require_captcha}
isDeleted={data.is_deleted} isDeleted={data.is_deleted}
ticket={data.ticket} ticket={data.ticket}
token={data.token} token={data.token}