feat: allow unlinking auth providers
This commit is contained in:
parent
8f6e280367
commit
b2bc608ec8
|
@ -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").
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue