feat: allow linking fediverse account to existing user
This commit is contained in:
parent
d6bb2f7743
commit
97191933cb
14 changed files with 306 additions and 93 deletions
|
@ -31,6 +31,7 @@ type User struct {
|
||||||
Fediverse *string
|
Fediverse *string
|
||||||
FediverseUsername *string
|
FediverseUsername *string
|
||||||
FediverseAppID *int64
|
FediverseAppID *int64
|
||||||
|
FediverseInstance *string
|
||||||
|
|
||||||
MaxInvites int
|
MaxInvites int
|
||||||
|
|
||||||
|
@ -99,7 +100,8 @@ func (db *DB) CreateUser(ctx context.Context, tx pgx.Tx, username string) (u Use
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) FediverseUser(ctx context.Context, userID string, instanceAppID int64) (u User, err error) {
|
func (db *DB) FediverseUser(ctx context.Context, userID string, instanceAppID int64) (u User, err error) {
|
||||||
sql, args, err := sq.Select("*").From("users").
|
sql, args, err := sq.Select("*", "(SELECT instance FROM fediverse_apps WHERE id = users.fediverse_app_id) AS fediverse_instance").
|
||||||
|
From("users").
|
||||||
Where("fediverse = ?", userID).Where("fediverse_app_id = ?", instanceAppID).
|
Where("fediverse = ?", userID).Where("fediverse_app_id = ?", instanceAppID).
|
||||||
ToSql()
|
ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -141,7 +143,8 @@ func (u *User) UpdateFromFedi(ctx context.Context, ex Execer, userID, username s
|
||||||
|
|
||||||
// DiscordUser fetches a user by Discord user ID.
|
// DiscordUser fetches a user by Discord user ID.
|
||||||
func (db *DB) DiscordUser(ctx context.Context, discordID string) (u User, err error) {
|
func (db *DB) DiscordUser(ctx context.Context, discordID string) (u User, err error) {
|
||||||
sql, args, err := sq.Select("*").From("users").Where("discord = ?", discordID).ToSql()
|
sql, args, err := sq.Select("*", "(SELECT instance FROM fediverse_apps WHERE id = users.fediverse_app_id) AS fediverse_instance").
|
||||||
|
From("users").Where("discord = ?", discordID).ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return u, errors.Wrap(err, "building sql")
|
return u, errors.Wrap(err, "building sql")
|
||||||
}
|
}
|
||||||
|
@ -181,7 +184,8 @@ func (u *User) UpdateFromDiscord(ctx context.Context, ex Execer, du *discordgo.U
|
||||||
|
|
||||||
// User gets a user by ID.
|
// User gets a user by ID.
|
||||||
func (db *DB) User(ctx context.Context, id xid.ID) (u User, err error) {
|
func (db *DB) User(ctx context.Context, id xid.ID) (u User, err error) {
|
||||||
sql, args, err := sq.Select("*").From("users").Where("id = ?", id).ToSql()
|
sql, args, err := sq.Select("*", "(SELECT instance FROM fediverse_apps WHERE id = users.fediverse_app_id) AS fediverse_instance").
|
||||||
|
From("users").Where("id = ?", id).ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return u, errors.Wrap(err, "building sql")
|
return u, errors.Wrap(err, "building sql")
|
||||||
}
|
}
|
||||||
|
|
|
@ -203,15 +203,15 @@ func (s *Server) discordSignup(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
|
||||||
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 {
|
||||||
|
return server.APIError{Code: server.ErrUsernameTaken}
|
||||||
|
}
|
||||||
|
|
||||||
return errors.Wrap(err, "creating user")
|
return errors.Wrap(err, "creating user")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = u.UpdateFromDiscord(ctx, tx, du)
|
err = u.UpdateFromDiscord(ctx, tx, du)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Cause(err) == db.ErrUsernameTaken {
|
|
||||||
return server.APIError{Code: server.ErrUsernameTaken}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrap(err, "updating user from discord")
|
return errors.Wrap(err, "updating user from discord")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -174,6 +174,57 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fediLinkRequest struct {
|
||||||
|
Instance string `json:"instance"`
|
||||||
|
Ticket string `json:"ticket"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) mastodonLink(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
claims, _ := server.ClaimsFromContext(ctx)
|
||||||
|
|
||||||
|
req, err := Decode[fediLinkRequest](r)
|
||||||
|
if err != nil {
|
||||||
|
return server.APIError{Code: server.ErrBadRequest}
|
||||||
|
}
|
||||||
|
|
||||||
|
app, err := s.DB.FediverseApp(ctx, req.Instance)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "getting instance application")
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := s.DB.User(ctx, claims.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "getting user")
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Fediverse != nil {
|
||||||
|
return server.APIError{Code: server.ErrAlreadyLinked}
|
||||||
|
}
|
||||||
|
|
||||||
|
mu := new(partialMastodonAccount)
|
||||||
|
err = s.DB.GetJSON(ctx, "mastodon:"+req.Ticket, &mu)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("getting mastoAPI user for ticket: %v", err)
|
||||||
|
|
||||||
|
return server.APIError{Code: server.ErrInvalidTicket}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = u.UpdateFromFedi(ctx, s.DB, mu.ID, mu.Username, app.ID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "updating user from mastoAPI")
|
||||||
|
}
|
||||||
|
|
||||||
|
fields, err := s.DB.UserFields(ctx, u.ID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "getting user fields")
|
||||||
|
}
|
||||||
|
|
||||||
|
render.JSON(w, r, dbUserToUserResponse(u, fields))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type fediSignupRequest struct {
|
type fediSignupRequest struct {
|
||||||
Instance string `json:"instance"`
|
Instance string `json:"instance"`
|
||||||
Ticket string `json:"ticket"`
|
Ticket string `json:"ticket"`
|
||||||
|
@ -225,15 +276,15 @@ func (s *Server) mastodonSignup(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
|
||||||
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 {
|
||||||
|
return server.APIError{Code: server.ErrUsernameTaken}
|
||||||
|
}
|
||||||
|
|
||||||
return errors.Wrap(err, "creating user")
|
return errors.Wrap(err, "creating user")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = u.UpdateFromFedi(ctx, tx, mu.ID, mu.Username, app.ID)
|
err = u.UpdateFromFedi(ctx, tx, mu.ID, mu.Username, app.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Cause(err) == db.ErrUsernameTaken {
|
|
||||||
return server.APIError{Code: server.ErrUsernameTaken}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrap(err, "updating user from mastoAPI")
|
return errors.Wrap(err, "updating user from mastoAPI")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,21 +33,28 @@ type userResponse struct {
|
||||||
|
|
||||||
Discord *string `json:"discord"`
|
Discord *string `json:"discord"`
|
||||||
DiscordUsername *string `json:"discord_username"`
|
DiscordUsername *string `json:"discord_username"`
|
||||||
|
|
||||||
|
Fediverse *string `json:"fediverse"`
|
||||||
|
FediverseUsername *string `json:"fediverse_username"`
|
||||||
|
FediverseInstance *string `json:"fediverse_instance"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func dbUserToUserResponse(u db.User, fields []db.Field) *userResponse {
|
func dbUserToUserResponse(u db.User, fields []db.Field) *userResponse {
|
||||||
return &userResponse{
|
return &userResponse{
|
||||||
ID: u.ID,
|
ID: u.ID,
|
||||||
Username: u.Username,
|
Username: u.Username,
|
||||||
DisplayName: u.DisplayName,
|
DisplayName: u.DisplayName,
|
||||||
Bio: u.Bio,
|
Bio: u.Bio,
|
||||||
Avatar: u.Avatar,
|
Avatar: u.Avatar,
|
||||||
Links: db.NotNull(u.Links),
|
Links: db.NotNull(u.Links),
|
||||||
Names: db.NotNull(u.Names),
|
Names: db.NotNull(u.Names),
|
||||||
Pronouns: db.NotNull(u.Pronouns),
|
Pronouns: db.NotNull(u.Pronouns),
|
||||||
Fields: db.NotNull(fields),
|
Fields: db.NotNull(fields),
|
||||||
Discord: u.Discord,
|
Discord: u.Discord,
|
||||||
DiscordUsername: u.DiscordUsername,
|
DiscordUsername: u.DiscordUsername,
|
||||||
|
Fediverse: u.Fediverse,
|
||||||
|
FediverseUsername: u.FediverseUsername,
|
||||||
|
FediverseInstance: u.FediverseInstance,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +85,7 @@ func Mount(srv *server.Server, r chi.Router) {
|
||||||
r.Route("/mastodon", func(r chi.Router) {
|
r.Route("/mastodon", func(r chi.Router) {
|
||||||
r.Post("/callback", server.WrapHandler(s.mastodonCallback))
|
r.Post("/callback", server.WrapHandler(s.mastodonCallback))
|
||||||
r.Post("/signup", server.WrapHandler(s.mastodonSignup))
|
r.Post("/signup", server.WrapHandler(s.mastodonSignup))
|
||||||
r.With(server.MustAuth).Post("/add-provider", server.WrapHandler(nil))
|
r.With(server.MustAuth).Post("/add-provider", server.WrapHandler(s.mastodonLink))
|
||||||
})
|
})
|
||||||
|
|
||||||
// invite routes
|
// invite routes
|
||||||
|
|
|
@ -159,15 +159,6 @@ func (s *Server) getMeUser(w http.ResponseWriter, r *http.Request) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// get fedi instance name if the user has a linked fedi account
|
|
||||||
var fediInstance *string
|
|
||||||
if u.FediverseAppID != nil {
|
|
||||||
app, err := s.DB.FediverseAppByID(ctx, *u.FediverseAppID)
|
|
||||||
if err == nil {
|
|
||||||
fediInstance = &app.Instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render.JSON(w, r, GetMeResponse{
|
render.JSON(w, r, GetMeResponse{
|
||||||
GetUserResponse: dbUserToResponse(u, fields, members),
|
GetUserResponse: dbUserToResponse(u, fields, members),
|
||||||
MaxInvites: u.MaxInvites,
|
MaxInvites: u.MaxInvites,
|
||||||
|
@ -175,7 +166,7 @@ func (s *Server) getMeUser(w http.ResponseWriter, r *http.Request) error {
|
||||||
DiscordUsername: u.DiscordUsername,
|
DiscordUsername: u.DiscordUsername,
|
||||||
Fediverse: u.Fediverse,
|
Fediverse: u.Fediverse,
|
||||||
FediverseUsername: u.FediverseUsername,
|
FediverseUsername: u.FediverseUsername,
|
||||||
FediverseInstance: fediInstance,
|
FediverseInstance: u.FediverseInstance,
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,6 +94,7 @@ const (
|
||||||
ErrDeletionPending = 1011 // own user deletion pending, returned with undo code
|
ErrDeletionPending = 1011 // own user deletion pending, returned with undo code
|
||||||
ErrRecentExport = 1012 // latest export is too recent
|
ErrRecentExport = 1012 // latest export is too recent
|
||||||
ErrUnsupportedInstance = 1013 // unsupported fediverse software
|
ErrUnsupportedInstance = 1013 // unsupported fediverse software
|
||||||
|
ErrAlreadyLinked = 1014 // user already has linked account of the same type
|
||||||
|
|
||||||
// User-related error codes
|
// User-related error codes
|
||||||
ErrUserNotFound = 2001
|
ErrUserNotFound = 2001
|
||||||
|
@ -130,6 +131,7 @@ var errCodeMessages = map[int]string{
|
||||||
ErrDeletionPending: "Your account is pending deletion",
|
ErrDeletionPending: "Your account is pending deletion",
|
||||||
ErrRecentExport: "Your latest data export is less than 1 day old",
|
ErrRecentExport: "Your latest data export is less than 1 day old",
|
||||||
ErrUnsupportedInstance: "Unsupported instance software",
|
ErrUnsupportedInstance: "Unsupported instance software",
|
||||||
|
ErrAlreadyLinked: "Your account is already linked to an account of this type",
|
||||||
|
|
||||||
ErrUserNotFound: "User not found",
|
ErrUserNotFound: "User not found",
|
||||||
|
|
||||||
|
@ -163,6 +165,7 @@ var errCodeStatuses = map[int]int{
|
||||||
ErrDeletionPending: http.StatusBadRequest,
|
ErrDeletionPending: http.StatusBadRequest,
|
||||||
ErrRecentExport: http.StatusBadRequest,
|
ErrRecentExport: http.StatusBadRequest,
|
||||||
ErrUnsupportedInstance: http.StatusBadRequest,
|
ErrUnsupportedInstance: http.StatusBadRequest,
|
||||||
|
ErrAlreadyLinked: http.StatusBadRequest,
|
||||||
|
|
||||||
ErrUserNotFound: http.StatusNotFound,
|
ErrUserNotFound: http.StatusNotFound,
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,9 @@ export interface MeUser extends User {
|
||||||
max_invites: number;
|
max_invites: number;
|
||||||
discord: string | null;
|
discord: string | null;
|
||||||
discord_username: string | null;
|
discord_username: string | null;
|
||||||
|
fediverse: string | null;
|
||||||
|
fediverse_username: string | null;
|
||||||
|
fediverse_instance: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Field {
|
export interface Field {
|
||||||
|
|
|
@ -4,10 +4,11 @@
|
||||||
|
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import type { APIError, MeUser } from "$lib/api/entities";
|
import type { APIError, MeUser } from "$lib/api/entities";
|
||||||
import { apiFetch } 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";
|
||||||
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
|
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
|
||||||
|
import { addToast } from "$lib/toast";
|
||||||
|
|
||||||
interface SignupResponse {
|
interface SignupResponse {
|
||||||
user: MeUser;
|
user: MeUser;
|
||||||
|
@ -67,6 +68,22 @@
|
||||||
deleteError = e as APIError;
|
deleteError = e as APIError;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const linkAccount = async () => {
|
||||||
|
try {
|
||||||
|
const resp = await apiFetchClient<MeUser>("/auth/mastodon/add-provider", "POST", {
|
||||||
|
instance: data.instance,
|
||||||
|
ticket: data.ticket,
|
||||||
|
});
|
||||||
|
|
||||||
|
localStorage.setItem("pronouns-user", JSON.stringify(resp));
|
||||||
|
userStore.set(resp);
|
||||||
|
addToast({ header: "Linked account", body: "Successfully linked account!" });
|
||||||
|
goto("/settings/auth");
|
||||||
|
} catch (e) {
|
||||||
|
data.error = e as APIError;
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
@ -78,7 +95,32 @@
|
||||||
{#if data.error}
|
{#if data.error}
|
||||||
<ErrorAlert error={data.error} />
|
<ErrorAlert error={data.error} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if data.ticket}
|
{#if data.ticket && $userStore}
|
||||||
|
<div>
|
||||||
|
<label for="fediverse">Fediverse username</label>
|
||||||
|
<input
|
||||||
|
id="fediverse"
|
||||||
|
class="form-control"
|
||||||
|
name="fediverse"
|
||||||
|
readonly
|
||||||
|
value="{data.fediverse}@{data.instance}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="fediverse">pronouns.cc username</label>
|
||||||
|
<input
|
||||||
|
id="pronounscc"
|
||||||
|
class="form-control"
|
||||||
|
name="pronounscc"
|
||||||
|
readonly
|
||||||
|
value={$userStore.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button on:click={linkAccount}>Link account</Button>
|
||||||
|
<Button color="secondary" href="/settings/auth">Cancel</Button>
|
||||||
|
</div>
|
||||||
|
{:else if data.ticket}
|
||||||
<form on:submit|preventDefault={signupForm}>
|
<form on:submit|preventDefault={signupForm}>
|
||||||
<div>
|
<div>
|
||||||
<label for="fediverse">Fediverse username</label>
|
<label for="fediverse">Fediverse username</label>
|
||||||
|
@ -86,7 +128,7 @@
|
||||||
id="fediverse"
|
id="fediverse"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
name="fediverse"
|
name="fediverse"
|
||||||
disabled
|
readonly
|
||||||
value="{data.fediverse}@{data.instance}"
|
value="{data.fediverse}@{data.instance}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -35,7 +35,14 @@
|
||||||
<ListGroupItem tag="a" active={$page.url.pathname === "/settings"} href="/settings">
|
<ListGroupItem tag="a" active={$page.url.pathname === "/settings"} href="/settings">
|
||||||
Your profile
|
Your profile
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
{#if data.require_invite}
|
<ListGroupItem
|
||||||
|
tag="a"
|
||||||
|
active={$page.url.pathname === "/settings/auth"}
|
||||||
|
href="/settings/auth"
|
||||||
|
>
|
||||||
|
Authentication
|
||||||
|
</ListGroupItem>
|
||||||
|
{#if data.invitesEnabled}
|
||||||
<ListGroupItem
|
<ListGroupItem
|
||||||
tag="a"
|
tag="a"
|
||||||
active={$page.url.pathname === "/settings/invites"}
|
active={$page.url.pathname === "/settings/invites"}
|
||||||
|
|
|
@ -1,8 +1,36 @@
|
||||||
|
import {
|
||||||
|
ErrorCode,
|
||||||
|
type APIError,
|
||||||
|
type Invite,
|
||||||
|
type MeUser,
|
||||||
|
type PartialMember,
|
||||||
|
} from "$lib/api/entities";
|
||||||
|
import { apiFetchClient } from "$lib/api/fetch";
|
||||||
import type { LayoutLoad } from "./$types";
|
import type { LayoutLoad } from "./$types";
|
||||||
|
|
||||||
export const ssr = false;
|
export const ssr = false;
|
||||||
|
|
||||||
export const load = (async ({ parent }) => {
|
export const load = (async ({ parent }) => {
|
||||||
|
const user = await apiFetchClient<MeUser>("/users/@me");
|
||||||
|
const members = await apiFetchClient<PartialMember[]>("/users/@me/members");
|
||||||
|
|
||||||
|
let invites: Invite[] = [];
|
||||||
|
let invitesEnabled = true;
|
||||||
|
try {
|
||||||
|
invites = await apiFetchClient<Invite[]>("/auth/invites");
|
||||||
|
} catch (e) {
|
||||||
|
if ((e as APIError).code === ErrorCode.InvitesDisabled) {
|
||||||
|
invitesEnabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const data = await parent();
|
const data = await parent();
|
||||||
return data;
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
user,
|
||||||
|
members,
|
||||||
|
invites,
|
||||||
|
invitesEnabled,
|
||||||
|
};
|
||||||
}) satisfies LayoutLoad;
|
}) satisfies LayoutLoad;
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
import {
|
|
||||||
type Invite,
|
|
||||||
type APIError,
|
|
||||||
type MeUser,
|
|
||||||
type PartialMember,
|
|
||||||
ErrorCode,
|
|
||||||
} from "$lib/api/entities";
|
|
||||||
import { apiFetchClient } from "$lib/api/fetch";
|
|
||||||
import { error } from "@sveltejs/kit";
|
|
||||||
|
|
||||||
export const load = async () => {
|
|
||||||
try {
|
|
||||||
const user = await apiFetchClient<MeUser>("/users/@me");
|
|
||||||
const members = await apiFetchClient<PartialMember[]>("/users/@me/members");
|
|
||||||
|
|
||||||
let invites: Invite[] = [];
|
|
||||||
let invitesEnabled = true;
|
|
||||||
try {
|
|
||||||
invites = await apiFetchClient<Invite[]>("/auth/invites");
|
|
||||||
} catch (e) {
|
|
||||||
if ((e as APIError).code === ErrorCode.InvitesDisabled) {
|
|
||||||
invitesEnabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { user, members, invites, invitesEnabled };
|
|
||||||
} catch (e) {
|
|
||||||
throw error(500, (e as APIError).message);
|
|
||||||
}
|
|
||||||
};
|
|
115
frontend/src/routes/settings/auth/+page.svelte
Normal file
115
frontend/src/routes/settings/auth/+page.svelte
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { APIError } from "$lib/api/entities";
|
||||||
|
import { apiFetch } from "$lib/api/fetch";
|
||||||
|
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
CardText,
|
||||||
|
CardTitle,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
} from "sveltestrap";
|
||||||
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
|
||||||
|
let canUnlink = false;
|
||||||
|
|
||||||
|
$: canUnlink =
|
||||||
|
[data.user.discord, data.user.fediverse]
|
||||||
|
.map<number>((entry) => (entry === null ? 0 : 1))
|
||||||
|
.reduce((prev, current) => prev + current) >= 2;
|
||||||
|
|
||||||
|
let error: APIError | null = null;
|
||||||
|
let instance = "";
|
||||||
|
let fediDisabled = false;
|
||||||
|
|
||||||
|
let fediLinkModalOpen = false;
|
||||||
|
let toggleFediLinkModal = () => (fediLinkModalOpen = !fediLinkModalOpen);
|
||||||
|
|
||||||
|
const fediLogin = async () => {
|
||||||
|
fediDisabled = true;
|
||||||
|
try {
|
||||||
|
const resp = await apiFetch<{ url: string }>(
|
||||||
|
`/auth/urls/fediverse?instance=${encodeURIComponent(instance)}`,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
window.location.assign(resp.url);
|
||||||
|
} catch (e) {
|
||||||
|
error = e as APIError;
|
||||||
|
} finally {
|
||||||
|
fediDisabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1>Authentication providers</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="my-2">
|
||||||
|
<Card>
|
||||||
|
<CardBody>
|
||||||
|
<CardTitle>Fediverse</CardTitle>
|
||||||
|
<CardText>
|
||||||
|
{#if data.user.fediverse}
|
||||||
|
Your currently linked Fediverse account is <b
|
||||||
|
>{data.user.fediverse_username}@{data.user.fediverse_instance}</b
|
||||||
|
>
|
||||||
|
(<code>{data.user.fediverse}</code>).
|
||||||
|
{:else}
|
||||||
|
You do not have a linked Fediverse account.
|
||||||
|
{/if}
|
||||||
|
</CardText>
|
||||||
|
{#if data.user.fediverse}
|
||||||
|
<Button color="danger" disabled={!canUnlink}>Unlink account</Button>
|
||||||
|
{:else}
|
||||||
|
<Button color="secondary" on:click={toggleFediLinkModal}>Link account</Button>
|
||||||
|
{/if}
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div class="my-2">
|
||||||
|
<Card>
|
||||||
|
<CardBody>
|
||||||
|
<CardTitle>Discord</CardTitle>
|
||||||
|
<CardText>
|
||||||
|
{#if data.user.discord}
|
||||||
|
Your currently linked Discord account is <b>{data.user.discord_username}</b>
|
||||||
|
(<code>{data.user.discord}</code>).
|
||||||
|
{:else}
|
||||||
|
You do not have a linked Discord account.
|
||||||
|
{/if}
|
||||||
|
</CardText>
|
||||||
|
{#if data.user.discord}
|
||||||
|
<Button color="danger" disabled={!canUnlink}>Unlink account</Button>
|
||||||
|
{:else}
|
||||||
|
<Button color="secondary" href={data.urls.discord}>Link account</Button>
|
||||||
|
{/if}
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<Modal header="Pick an instance" isOpen={fediLinkModalOpen} toggle={toggleFediLinkModal}>
|
||||||
|
<ModalBody>
|
||||||
|
<p>
|
||||||
|
<strong>Note:</strong> Misskey (and derivatives) are not supported yet, sorry.
|
||||||
|
</p>
|
||||||
|
<Input placeholder="Instance (e.g. mastodon.social)" bind:value={instance} />
|
||||||
|
{#if error}
|
||||||
|
<div class="mt-2">
|
||||||
|
<ErrorAlert {error} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button color="primary" disabled={fediDisabled || instance === ""} on:click={fediLogin}
|
||||||
|
>Log in</Button
|
||||||
|
>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</div>
|
17
frontend/src/routes/settings/auth/+page.ts
Normal file
17
frontend/src/routes/settings/auth/+page.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { PUBLIC_BASE_URL } from "$env/static/public";
|
||||||
|
import { apiFetch } from "$lib/api/fetch";
|
||||||
|
|
||||||
|
export const load = async () => {
|
||||||
|
const resp = await apiFetch<UrlsResponse>("/auth/urls", {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
callback_domain: PUBLIC_BASE_URL,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { urls: resp };
|
||||||
|
};
|
||||||
|
|
||||||
|
interface UrlsResponse {
|
||||||
|
discord: string;
|
||||||
|
}
|
|
@ -1,25 +0,0 @@
|
||||||
import { ErrorCode, type APIError, type Invite } from "$lib/api/entities";
|
|
||||||
import { apiFetchClient } from "$lib/api/fetch";
|
|
||||||
import { error } from "@sveltejs/kit";
|
|
||||||
import type { PageLoad } from "../$types";
|
|
||||||
|
|
||||||
export const load = (async () => {
|
|
||||||
const data = {
|
|
||||||
invitesEnabled: true,
|
|
||||||
invites: [] as Invite[],
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const invites = await apiFetchClient<Invite[]>("/auth/invites");
|
|
||||||
data.invites = invites;
|
|
||||||
} catch (e) {
|
|
||||||
if ((e as APIError).code === ErrorCode.InvitesDisabled) {
|
|
||||||
data.invitesEnabled = false;
|
|
||||||
data.invites = [];
|
|
||||||
} else {
|
|
||||||
throw error((e as APIError).code, (e as APIError).message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}) satisfies PageLoad;
|
|
Loading…
Reference in a new issue