From b2bc608ec83095e725c918c79de121dc1a632d11 Mon Sep 17 00:00:00 2001 From: Sam Date: Sat, 18 Mar 2023 16:54:31 +0100 Subject: [PATCH] feat: allow unlinking auth providers --- backend/db/user.go | 43 ++++++++++ backend/routes/auth/discord.go | 33 +++++++ backend/routes/auth/fedi_mastodon.go | 33 +++++++ backend/routes/auth/routes.go | 3 + backend/server/errors.go | 3 + frontend/src/lib/api/entities.ts | 5 ++ .../src/routes/settings/auth/+page.svelte | 85 ++++++++++++++++++- 7 files changed, 201 insertions(+), 4 deletions(-) diff --git a/backend/db/user.go b/backend/db/user.go index facaf09..20af570 100644 --- a/backend/db/user.go +++ b/backend/db/user.go @@ -141,6 +141,28 @@ func (u *User) UpdateFromFedi(ctx context.Context, ex Execer, userID, username s 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. 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"). @@ -182,6 +204,27 @@ func (u *User) UpdateFromDiscord(ctx context.Context, ex Execer, du *discordgo.U 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. 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"). diff --git a/backend/routes/auth/discord.go b/backend/routes/auth/discord.go index 8d3f088..466bb4b 100644 --- a/backend/routes/auth/discord.go +++ b/backend/routes/auth/discord.go @@ -203,6 +203,39 @@ func (s *Server) discordLink(w http.ResponseWriter, r *http.Request) error { 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 { Ticket string `json:"ticket"` Username string `json:"username"` diff --git a/backend/routes/auth/fedi_mastodon.go b/backend/routes/auth/fedi_mastodon.go index 413bab8..02f6523 100644 --- a/backend/routes/auth/fedi_mastodon.go +++ b/backend/routes/auth/fedi_mastodon.go @@ -230,6 +230,39 @@ func (s *Server) mastodonLink(w http.ResponseWriter, r *http.Request) error { 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 { Instance string `json:"instance"` Ticket string `json:"ticket"` diff --git a/backend/routes/auth/routes.go b/backend/routes/auth/routes.go index f8640fb..79366b9 100644 --- a/backend/routes/auth/routes.go +++ b/backend/routes/auth/routes.go @@ -80,12 +80,15 @@ func Mount(srv *server.Server, r chi.Router) { r.Post("/signup", server.WrapHandler(s.discordSignup)) // takes discord signup ticket to link to existing account 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.Post("/callback", server.WrapHandler(s.mastodonCallback)) r.Post("/signup", server.WrapHandler(s.mastodonSignup)) r.With(server.MustAuth).Post("/add-provider", server.WrapHandler(s.mastodonLink)) + r.With(server.MustAuth).Post("/remove-provider", server.WrapHandler(s.mastodonUnlink)) }) // invite routes diff --git a/backend/server/errors.go b/backend/server/errors.go index 4ae73a9..df31ca2 100644 --- a/backend/server/errors.go +++ b/backend/server/errors.go @@ -95,6 +95,7 @@ const ( ErrRecentExport = 1012 // latest export is too recent ErrUnsupportedInstance = 1013 // unsupported fediverse software 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 ErrUserNotFound = 2001 @@ -132,6 +133,7 @@ var errCodeMessages = map[int]string{ ErrRecentExport: "Your latest data export is less than 1 day old", ErrUnsupportedInstance: "Unsupported instance software", 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", @@ -166,6 +168,7 @@ var errCodeStatuses = map[int]int{ ErrRecentExport: http.StatusBadRequest, ErrUnsupportedInstance: http.StatusBadRequest, ErrAlreadyLinked: http.StatusBadRequest, + ErrNotLinked: http.StatusBadRequest, ErrUserNotFound: http.StatusNotFound, diff --git a/frontend/src/lib/api/entities.ts b/frontend/src/lib/api/entities.ts index 7171ed5..e45ebf3 100644 --- a/frontend/src/lib/api/entities.ts +++ b/frontend/src/lib/api/entities.ts @@ -106,11 +106,16 @@ export enum ErrorCode { InviteLimitReached = 1009, InviteAlreadyUsed = 1010, RecentExport = 1012, + UnsupportedInstance = 1013, + AlreadyLinked = 1014, + NotLinked = 1015, UserNotFound = 2001, MemberNotFound = 3001, MemberLimitReached = 3002, + MemberNameInUse = 3003, + NotOwnMember = 3004, RequestTooBig = 4001, MissingPermissions = 4002, diff --git a/frontend/src/routes/settings/auth/+page.svelte b/frontend/src/routes/settings/auth/+page.svelte index 4fa904d..a84facf 100644 --- a/frontend/src/routes/settings/auth/+page.svelte +++ b/frontend/src/routes/settings/auth/+page.svelte @@ -1,7 +1,8 @@
@@ -66,7 +95,9 @@ {/if} {#if data.user.fediverse} - + {:else} {/if} @@ -86,7 +117,9 @@ {/if} {#if data.user.discord} - + {:else} {/if} @@ -111,5 +144,49 @@ > + + + +

+ Are you sure you want to unlink your fediverse account? You will no longer be able to use + it to log in. +

+ {#if error} +
+ +
+ {/if} +
+ + + + +
+ + + +

+ Are you sure you want to unlink your Discord account? You will no longer be able to use it + to log in. +

+ {#if error} +
+ +
+ {/if} +
+ + + + +