feat: allow unlinking auth providers

This commit is contained in:
Sam 2023-03-18 16:54:31 +01:00
parent 8f6e280367
commit b2bc608ec8
No known key found for this signature in database
GPG Key ID: B4EF20DDE721CAA1
7 changed files with 201 additions and 4 deletions

View File

@ -141,6 +141,28 @@ func (u *User) UpdateFromFedi(ctx context.Context, ex Execer, userID, username s
return nil return nil
} }
func (u *User) UnlinkFedi(ctx context.Context, ex Execer) error {
sql, args, err := sq.Update("users").
Set("fediverse", nil).
Set("fediverse_username", nil).
Set("fediverse_app_id", nil).
Where("id = ?", u.ID).
ToSql()
if err != nil {
return errors.Wrap(err, "building sql")
}
_, err = ex.Exec(ctx, sql, args...)
if err != nil {
return errors.Wrap(err, "executing query")
}
u.Fediverse = nil
u.FediverseUsername = nil
u.FediverseAppID = nil
return nil
}
// 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("*", "(SELECT instance FROM fediverse_apps WHERE id = users.fediverse_app_id) AS fediverse_instance"). sql, args, err := sq.Select("*", "(SELECT instance FROM fediverse_apps WHERE id = users.fediverse_app_id) AS fediverse_instance").
@ -182,6 +204,27 @@ func (u *User) UpdateFromDiscord(ctx context.Context, ex Execer, du *discordgo.U
return nil return nil
} }
func (u *User) UnlinkDiscord(ctx context.Context, ex Execer) error {
sql, args, err := sq.Update("users").
Set("discord", nil).
Set("discord_username", nil).
Where("id = ?", u.ID).
ToSql()
if err != nil {
return errors.Wrap(err, "building sql")
}
_, err = ex.Exec(ctx, sql, args...)
if err != nil {
return errors.Wrap(err, "executing query")
}
u.Discord = nil
u.DiscordUsername = nil
return nil
}
// 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("*", "(SELECT instance FROM fediverse_apps WHERE id = users.fediverse_app_id) AS fediverse_instance"). sql, args, err := sq.Select("*", "(SELECT instance FROM fediverse_apps WHERE id = users.fediverse_app_id) AS fediverse_instance").

View File

@ -203,6 +203,39 @@ func (s *Server) discordLink(w http.ResponseWriter, r *http.Request) error {
return nil return nil
} }
func (s *Server) discordUnlink(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
claims, _ := server.ClaimsFromContext(ctx)
// only site tokens can be used for this endpoint
if claims.APIToken || !claims.TokenWrite {
return server.APIError{Code: server.ErrInvalidToken}
}
u, err := s.DB.User(ctx, claims.UserID)
if err != nil {
return errors.Wrap(err, "getting user")
}
if u.Discord == nil {
return server.APIError{Code: server.ErrNotLinked}
}
err = u.UnlinkDiscord(ctx, s.DB)
if err != nil {
return errors.Wrap(err, "updating user in db")
}
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 discordSignupRequest struct { type discordSignupRequest struct {
Ticket string `json:"ticket"` Ticket string `json:"ticket"`
Username string `json:"username"` Username string `json:"username"`

View File

@ -230,6 +230,39 @@ func (s *Server) mastodonLink(w http.ResponseWriter, r *http.Request) error {
return nil return nil
} }
func (s *Server) mastodonUnlink(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
claims, _ := server.ClaimsFromContext(ctx)
// only site tokens can be used for this endpoint
if claims.APIToken || !claims.TokenWrite {
return server.APIError{Code: server.ErrInvalidToken}
}
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.ErrNotLinked}
}
err = u.UnlinkFedi(ctx, s.DB)
if err != nil {
return errors.Wrap(err, "updating user in db")
}
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"`

View File

@ -80,12 +80,15 @@ func Mount(srv *server.Server, r chi.Router) {
r.Post("/signup", server.WrapHandler(s.discordSignup)) r.Post("/signup", server.WrapHandler(s.discordSignup))
// takes discord signup ticket to link to existing account // takes discord signup ticket to link to existing account
r.With(server.MustAuth).Post("/add-provider", server.WrapHandler(s.discordLink)) r.With(server.MustAuth).Post("/add-provider", server.WrapHandler(s.discordLink))
// removes discord link from existing account
r.With(server.MustAuth).Post("/remove-provider", server.WrapHandler(s.discordUnlink))
}) })
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(s.mastodonLink)) r.With(server.MustAuth).Post("/add-provider", server.WrapHandler(s.mastodonLink))
r.With(server.MustAuth).Post("/remove-provider", server.WrapHandler(s.mastodonUnlink))
}) })
// invite routes // invite routes

View File

@ -95,6 +95,7 @@ const (
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 ErrAlreadyLinked = 1014 // user already has linked account of the same type
ErrNotLinked = 1015 // user already doesn't have a linked account
// User-related error codes // User-related error codes
ErrUserNotFound = 2001 ErrUserNotFound = 2001
@ -132,6 +133,7 @@ var errCodeMessages = map[int]string{
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", 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",
ErrUserNotFound: "User not found", ErrUserNotFound: "User not found",
@ -166,6 +168,7 @@ var errCodeStatuses = map[int]int{
ErrRecentExport: http.StatusBadRequest, ErrRecentExport: http.StatusBadRequest,
ErrUnsupportedInstance: http.StatusBadRequest, ErrUnsupportedInstance: http.StatusBadRequest,
ErrAlreadyLinked: http.StatusBadRequest, ErrAlreadyLinked: http.StatusBadRequest,
ErrNotLinked: http.StatusBadRequest,
ErrUserNotFound: http.StatusNotFound, ErrUserNotFound: http.StatusNotFound,

View File

@ -106,11 +106,16 @@ export enum ErrorCode {
InviteLimitReached = 1009, InviteLimitReached = 1009,
InviteAlreadyUsed = 1010, InviteAlreadyUsed = 1010,
RecentExport = 1012, RecentExport = 1012,
UnsupportedInstance = 1013,
AlreadyLinked = 1014,
NotLinked = 1015,
UserNotFound = 2001, UserNotFound = 2001,
MemberNotFound = 3001, MemberNotFound = 3001,
MemberLimitReached = 3002, MemberLimitReached = 3002,
MemberNameInUse = 3003,
NotOwnMember = 3004,
RequestTooBig = 4001, RequestTooBig = 4001,
MissingPermissions = 4002, MissingPermissions = 4002,

View File

@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import type { APIError } 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 ErrorAlert from "$lib/components/ErrorAlert.svelte"; import ErrorAlert from "$lib/components/ErrorAlert.svelte";
import { addToast } from "$lib/toast";
import { import {
Button, Button,
Card, Card,
@ -31,6 +32,12 @@
let fediLinkModalOpen = false; let fediLinkModalOpen = false;
let toggleFediLinkModal = () => (fediLinkModalOpen = !fediLinkModalOpen); let toggleFediLinkModal = () => (fediLinkModalOpen = !fediLinkModalOpen);
let fediUnlinkModalOpen = false;
let toggleFediUnlinkModal = () => (fediUnlinkModalOpen = !fediUnlinkModalOpen);
let discordUnlinkModalOpen = false;
let toggleDiscordUnlinkModal = () => (discordUnlinkModalOpen = !discordUnlinkModalOpen);
const fediLogin = async () => { const fediLogin = async () => {
fediDisabled = true; fediDisabled = true;
try { try {
@ -45,6 +52,28 @@
fediDisabled = false; fediDisabled = false;
} }
}; };
const fediUnlink = async () => {
try {
const resp = await apiFetchClient<MeUser>("/auth/mastodon/remove-provider", "POST");
data.user = resp;
addToast({ header: "Unlinked account", body: "Successfully unlinked fediverse account!" });
toggleFediUnlinkModal();
} catch (e) {
error = e as APIError;
}
};
const discordUnlink = async () => {
try {
const resp = await apiFetchClient<MeUser>("/auth/discord/remove-provider", "POST");
data.user = resp;
addToast({ header: "Unlinked account", body: "Successfully unlinked Discord account!" });
toggleDiscordUnlinkModal();
} catch (e) {
error = e as APIError;
}
};
</script> </script>
<div> <div>
@ -66,7 +95,9 @@
{/if} {/if}
</CardText> </CardText>
{#if data.user.fediverse} {#if data.user.fediverse}
<Button color="danger" disabled={!canUnlink}>Unlink account</Button> <Button color="danger" disabled={!canUnlink} on:click={toggleFediUnlinkModal}
>Unlink account</Button
>
{:else} {:else}
<Button color="secondary" on:click={toggleFediLinkModal}>Link account</Button> <Button color="secondary" on:click={toggleFediLinkModal}>Link account</Button>
{/if} {/if}
@ -86,7 +117,9 @@
{/if} {/if}
</CardText> </CardText>
{#if data.user.discord} {#if data.user.discord}
<Button color="danger" disabled={!canUnlink}>Unlink account</Button> <Button color="danger" disabled={!canUnlink} on:click={toggleDiscordUnlinkModal}
>Unlink account</Button
>
{:else} {:else}
<Button color="secondary" href={data.urls.discord}>Link account</Button> <Button color="secondary" href={data.urls.discord}>Link account</Button>
{/if} {/if}
@ -111,5 +144,49 @@
> >
</ModalFooter> </ModalFooter>
</Modal> </Modal>
<Modal
header="Unlink fediverse account"
isOpen={fediUnlinkModalOpen}
toggle={toggleFediUnlinkModal}
>
<ModalBody>
<p>
Are you sure you want to unlink your fediverse account? You will no longer be able to use
it to log in.
</p>
{#if error}
<div class="mt-2">
<ErrorAlert {error} />
</div>
{/if}
</ModalBody>
<ModalFooter>
<Button color="danger" on:click={fediUnlink}>Unlink account</Button>
<Button color="secondary" on:click={toggleFediUnlinkModal}>Cancel</Button>
</ModalFooter>
</Modal>
<Modal
header="Unlink Discord account"
isOpen={discordUnlinkModalOpen}
toggle={toggleDiscordUnlinkModal}
>
<ModalBody>
<p>
Are you sure you want to unlink your Discord account? You will no longer be able to use it
to log in.
</p>
{#if error}
<div class="mt-2">
<ErrorAlert {error} />
</div>
{/if}
</ModalBody>
<ModalFooter>
<Button color="danger" on:click={discordUnlink}>Unlink account</Button>
<Button color="secondary" on:click={toggleDiscordUnlinkModal}>Cancel</Button>
</ModalFooter>
</Modal>
</div> </div>
</div> </div>