feat: allow linking fediverse account to existing user

This commit is contained in:
Sam 2023-03-18 15:19:53 +01:00
parent d6bb2f7743
commit 97191933cb
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
14 changed files with 306 additions and 93 deletions

View file

@ -31,6 +31,7 @@ type User struct {
Fediverse *string
FediverseUsername *string
FediverseAppID *int64
FediverseInstance *string
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) {
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).
ToSql()
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.
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 {
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.
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 {
return u, errors.Wrap(err, "building sql")
}

View file

@ -203,15 +203,15 @@ func (s *Server) discordSignup(w http.ResponseWriter, r *http.Request) error {
u, err := s.DB.CreateUser(ctx, tx, req.Username)
if err != nil {
if errors.Cause(err) == db.ErrUsernameTaken {
return server.APIError{Code: server.ErrUsernameTaken}
}
return errors.Wrap(err, "creating user")
}
err = u.UpdateFromDiscord(ctx, tx, du)
if err != nil {
if errors.Cause(err) == db.ErrUsernameTaken {
return server.APIError{Code: server.ErrUsernameTaken}
}
return errors.Wrap(err, "updating user from discord")
}

View file

@ -174,6 +174,57 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error
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 {
Instance string `json:"instance"`
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)
if err != nil {
if errors.Cause(err) == db.ErrUsernameTaken {
return server.APIError{Code: server.ErrUsernameTaken}
}
return errors.Wrap(err, "creating user")
}
err = u.UpdateFromFedi(ctx, tx, mu.ID, mu.Username, app.ID)
if err != nil {
if errors.Cause(err) == db.ErrUsernameTaken {
return server.APIError{Code: server.ErrUsernameTaken}
}
return errors.Wrap(err, "updating user from mastoAPI")
}

View file

@ -33,21 +33,28 @@ type userResponse struct {
Discord *string `json:"discord"`
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 {
return &userResponse{
ID: u.ID,
Username: u.Username,
DisplayName: u.DisplayName,
Bio: u.Bio,
Avatar: u.Avatar,
Links: db.NotNull(u.Links),
Names: db.NotNull(u.Names),
Pronouns: db.NotNull(u.Pronouns),
Fields: db.NotNull(fields),
Discord: u.Discord,
DiscordUsername: u.DiscordUsername,
ID: u.ID,
Username: u.Username,
DisplayName: u.DisplayName,
Bio: u.Bio,
Avatar: u.Avatar,
Links: db.NotNull(u.Links),
Names: db.NotNull(u.Names),
Pronouns: db.NotNull(u.Pronouns),
Fields: db.NotNull(fields),
Discord: u.Discord,
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.Post("/callback", server.WrapHandler(s.mastodonCallback))
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

View file

@ -159,15 +159,6 @@ func (s *Server) getMeUser(w http.ResponseWriter, r *http.Request) error {
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{
GetUserResponse: dbUserToResponse(u, fields, members),
MaxInvites: u.MaxInvites,
@ -175,7 +166,7 @@ func (s *Server) getMeUser(w http.ResponseWriter, r *http.Request) error {
DiscordUsername: u.DiscordUsername,
Fediverse: u.Fediverse,
FediverseUsername: u.FediverseUsername,
FediverseInstance: fediInstance,
FediverseInstance: u.FediverseInstance,
})
return nil
}

View file

@ -94,6 +94,7 @@ const (
ErrDeletionPending = 1011 // own user deletion pending, returned with undo code
ErrRecentExport = 1012 // latest export is too recent
ErrUnsupportedInstance = 1013 // unsupported fediverse software
ErrAlreadyLinked = 1014 // user already has linked account of the same type
// User-related error codes
ErrUserNotFound = 2001
@ -130,6 +131,7 @@ var errCodeMessages = map[int]string{
ErrDeletionPending: "Your account is pending deletion",
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",
ErrUserNotFound: "User not found",
@ -163,6 +165,7 @@ var errCodeStatuses = map[int]int{
ErrDeletionPending: http.StatusBadRequest,
ErrRecentExport: http.StatusBadRequest,
ErrUnsupportedInstance: http.StatusBadRequest,
ErrAlreadyLinked: http.StatusBadRequest,
ErrUserNotFound: http.StatusNotFound,