Monorepo for Tangled tangled.org

appview/settings: add account management UI for tngl.sh users #1151

merged opened by oyster.cafe targeting master from appview-acc-mgmt-ui

tngl.sh users should be able to:

  • change handle
  • deactivate account
  • delete account
  • change password
Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:3fwecdnvtcscjnrx2p4n7alz/sh.tangled.repo.pull/3mgu4bwsml322
+58 -167
Interdiff #3 โ†’ #4
appview/config/config.go

This file has not been changed.

appview/oauth/handler.go

This file has not been changed.

appview/oauth/oauth.go

This file has not been changed.

appview/pages/htmx.go

This file has not been changed.

appview/pages/pages.go

This file has not been changed.

appview/pages/templates/user/settings/fragments/dangerDeleteToken.html

This file has not been changed.

appview/pages/templates/user/settings/fragments/dangerPasswordSuccess.html

This file has not been changed.

appview/pages/templates/user/settings/fragments/dangerPasswordToken.html

This file has not been changed.

appview/pages/templates/user/settings/profile.html

This file has not been changed.

+54 -159
appview/settings/danger.go
··· 1 1 package settings 2 2 3 3 import ( 4 - "bytes" 5 4 "context" 6 - "encoding/json" 7 5 "errors" 8 - "fmt" 9 - "io" 10 6 "net/http" 11 7 "strings" 12 8 "time" ··· 15 11 "github.com/bluesky-social/indigo/xrpc" 16 12 ) 17 13 18 - var pdsClient = &http.Client{Timeout: 15 * time.Second} 19 - 20 - type createSessionResponse struct { 21 - AccessJwt string `json:"accessJwt"` 22 - Did string `json:"did"` 23 - Email string `json:"email"` 14 + type pdsSession struct { 15 + Client *xrpc.Client 16 + Did string 17 + Email string 18 + AccessJwt string 24 19 } 25 20 26 - func (s *Settings) verifyPdsPassword(did, password string) (*createSessionResponse, error) { 27 - body := map[string]string{ 28 - "identifier": did, 29 - "password": password, 30 - } 31 - 32 - jsonData, err := json.Marshal(body) 33 - if err != nil { 34 - return nil, err 35 - } 36 - 37 - url := fmt.Sprintf("%s/xrpc/com.atproto.server.createSession", s.Config.Pds.Host) 38 - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) 39 - if err != nil { 40 - return nil, err 41 - } 42 - req.Header.Set("Content-Type", "application/json") 43 - 44 - resp, err := pdsClient.Do(req) 45 - if err != nil { 46 - return nil, err 47 - } 48 - defer resp.Body.Close() 49 - 50 - if resp.StatusCode != http.StatusOK { 51 - respBody, _ := io.ReadAll(resp.Body) 52 - var errResp struct { 53 - Error string `json:"error"` 54 - Message string `json:"message"` 55 - } 56 - if json.Unmarshal(respBody, &errResp) == nil && errResp.Message != "" { 57 - return nil, fmt.Errorf("%s", errResp.Message) 58 - } 59 - return nil, fmt.Errorf("authentication failed (status %d)", resp.StatusCode) 60 - } 61 - 62 - var session createSessionResponse 63 - if err := json.NewDecoder(resp.Body).Decode(&session); err != nil { 64 - return nil, fmt.Errorf("failed to decode session response: %w", err) 21 + func (s *Settings) pdsClient() *xrpc.Client { 22 + return &xrpc.Client{ 23 + Host: s.Config.Pds.Host, 24 + Client: &http.Client{Timeout: 15 * time.Second}, 65 25 } 66 - 67 - return &session, nil 68 26 } 69 27 70 - func (s *Settings) pdsPost(endpoint string, body any, bearerToken string) (*http.Response, error) { 71 - var bodyReader io.Reader 72 - if body != nil { 73 - jsonData, err := json.Marshal(body) 74 - if err != nil { 75 - return nil, err 76 - } 77 - bodyReader = bytes.NewBuffer(jsonData) 78 - } 79 - 80 - url := fmt.Sprintf("%s/xrpc/%s", s.Config.Pds.Host, endpoint) 81 - req, err := http.NewRequest("POST", url, bodyReader) 28 + func (s *Settings) verifyPdsPassword(did, password string) (*pdsSession, error) { 29 + client := s.pdsClient() 30 + resp, err := comatproto.ServerCreateSession(context.Background(), client, &comatproto.ServerCreateSession_Input{ 31 + Identifier: did, 32 + Password: password, 33 + }) 82 34 if err != nil { 83 35 return nil, err 84 36 } 85 37 86 - if body != nil { 87 - req.Header.Set("Content-Type", "application/json") 88 - } 89 - if bearerToken != "" { 90 - req.Header.Set("Authorization", "Bearer "+bearerToken) 38 + client.Auth = &xrpc.AuthInfo{AccessJwt: resp.AccessJwt} 39 + 40 + var email string 41 + if resp.Email != nil { 42 + email = *resp.Email 91 43 } 92 44 93 - return pdsClient.Do(req) 45 + return &pdsSession{ 46 + Client: client, 47 + Did: resp.Did, 48 + Email: email, 49 + AccessJwt: resp.AccessJwt, 50 + }, nil 94 51 } 95 52 96 - func (s *Settings) revokePdsSession(accessJwt string) { 97 - resp, err := s.pdsPost("com.atproto.server.deleteSession", nil, accessJwt) 98 - if err != nil { 53 + func (s *Settings) revokePdsSession(session *pdsSession) { 54 + if err := comatproto.ServerDeleteSession(context.Background(), session.Client); err != nil { 99 55 s.Logger.Warn("failed to revoke session", "err", err) 100 - return 101 56 } 102 - resp.Body.Close() 103 57 } 104 58 105 59 func (s *Settings) requestPasswordReset(w http.ResponseWriter, r *http.Request) { 106 60 user := s.OAuth.GetMultiAccountUser(r) 107 - if !s.isTnglShUser(user.Pds()) { 61 + if !s.Config.Pds.IsTnglShUser(user.Pds()) { 108 62 s.Pages.Notice(w, "password-error", "Only available for tngl.sh accounts.") 109 63 return 110 64 } ··· 123 77 } 124 78 125 79 if session.Email == "" { 126 - s.revokePdsSession(session.AccessJwt) 80 + s.revokePdsSession(session) 127 81 s.Logger.Error("requesting password reset: no email on account", "did", did) 128 82 s.Pages.Notice(w, "password-error", "No email associated with your account.") 129 83 return 130 84 } 131 85 132 - s.revokePdsSession(session.AccessJwt) 86 + s.revokePdsSession(session) 133 87 134 - resp, err := s.pdsPost("com.atproto.server.requestPasswordReset", map[string]string{ 135 - "email": session.Email, 136 - }, "") 88 + err = comatproto.ServerRequestPasswordReset(context.Background(), s.pdsClient(), &comatproto.ServerRequestPasswordReset_Input{ 89 + Email: session.Email, 90 + }) 137 91 if err != nil { 138 92 s.Logger.Error("requesting password reset", "err", err) 139 93 s.Pages.Notice(w, "password-error", "Failed to request password reset. Try again later.") 140 94 return 141 95 } 142 - defer resp.Body.Close() 143 - 144 - if resp.StatusCode != http.StatusOK { 145 - s.Logger.Error("requesting password reset", "status", resp.StatusCode) 146 - s.Pages.Notice(w, "password-error", "Failed to request password reset. Try again later.") 147 - return 148 - } 149 96 150 97 s.Pages.DangerPasswordTokenStep(w) 151 98 } 152 99 153 100 func (s *Settings) resetPassword(w http.ResponseWriter, r *http.Request) { 154 101 user := s.OAuth.GetMultiAccountUser(r) 155 - if !s.isTnglShUser(user.Pds()) { 102 + if !s.Config.Pds.IsTnglShUser(user.Pds()) { 156 103 s.Pages.Notice(w, "password-error", "Only available for tngl.sh accounts.") 157 104 return 158 105 } ··· 171 118 return 172 119 } 173 120 174 - resp, err := s.pdsPost("com.atproto.server.resetPassword", map[string]string{ 175 - "token": token, 176 - "password": newPassword, 177 - }, "") 121 + err := comatproto.ServerResetPassword(context.Background(), s.pdsClient(), &comatproto.ServerResetPassword_Input{ 122 + Token: token, 123 + Password: newPassword, 124 + }) 178 125 if err != nil { 179 126 s.Logger.Error("resetting password", "err", err) 180 - s.Pages.Notice(w, "password-error", "Failed to reset password. Try again later.") 181 - return 182 - } 183 - defer resp.Body.Close() 184 - 185 - if resp.StatusCode != http.StatusOK { 186 - respBody, _ := io.ReadAll(resp.Body) 187 - var errResp struct { 188 - Message string `json:"message"` 189 - } 190 - if json.Unmarshal(respBody, &errResp) == nil && errResp.Message != "" { 191 - s.Pages.Notice(w, "password-error", errResp.Message) 192 - return 193 - } 194 - s.Logger.Error("resetting password", "status", resp.StatusCode) 195 127 s.Pages.Notice(w, "password-error", "Failed to reset password. The token may have expired.") 196 128 return 197 129 } ··· 201 133 202 134 func (s *Settings) deactivateAccount(w http.ResponseWriter, r *http.Request) { 203 135 user := s.OAuth.GetMultiAccountUser(r) 204 - if !s.isTnglShUser(user.Pds()) { 136 + if !s.Config.Pds.IsTnglShUser(user.Pds()) { 205 137 s.Pages.Notice(w, "deactivate-error", "Only available for tngl.sh accounts.") 206 138 return 207 139 } ··· 220 152 return 221 153 } 222 154 223 - resp, err := s.pdsPost("com.atproto.server.deactivateAccount", map[string]any{}, session.AccessJwt) 224 - s.revokePdsSession(session.AccessJwt) 155 + err = comatproto.ServerDeactivateAccount(context.Background(), session.Client, &comatproto.ServerDeactivateAccount_Input{}) 156 + s.revokePdsSession(session) 225 157 if err != nil { 226 158 s.Logger.Error("deactivating account", "err", err) 227 159 s.Pages.Notice(w, "deactivate-error", "Failed to deactivate account. Try again later.") 228 160 return 229 161 } 230 - defer resp.Body.Close() 231 - 232 - if resp.StatusCode != http.StatusOK { 233 - s.Logger.Error("deactivating account", "status", resp.StatusCode) 234 - s.Pages.Notice(w, "deactivate-error", "Failed to deactivate account. Try again later.") 235 - return 236 - } 237 162 238 163 if err := s.OAuth.DeleteSession(w, r); err != nil { 239 164 s.Logger.Error("clearing session after deactivation", "did", did, "err", err) ··· 246 171 247 172 func (s *Settings) requestAccountDelete(w http.ResponseWriter, r *http.Request) { 248 173 user := s.OAuth.GetMultiAccountUser(r) 249 - if !s.isTnglShUser(user.Pds()) { 174 + if !s.Config.Pds.IsTnglShUser(user.Pds()) { 250 175 s.Pages.Notice(w, "delete-error", "Only available for tngl.sh accounts.") 251 176 return 252 177 } ··· 265 190 return 266 191 } 267 192 268 - resp, err := s.pdsPost("com.atproto.server.requestAccountDelete", nil, session.AccessJwt) 269 - s.revokePdsSession(session.AccessJwt) 193 + err = comatproto.ServerRequestAccountDelete(context.Background(), session.Client) 194 + s.revokePdsSession(session) 270 195 if err != nil { 271 196 s.Logger.Error("requesting account deletion", "err", err) 272 197 s.Pages.Notice(w, "delete-error", "Failed to request account deletion. Try again later.") 273 198 return 274 199 } 275 - defer resp.Body.Close() 276 - 277 - if resp.StatusCode != http.StatusOK { 278 - respBody, _ := io.ReadAll(resp.Body) 279 - s.Logger.Error("requesting account deletion", "status", resp.StatusCode, "body", string(respBody)) 280 - s.Pages.Notice(w, "delete-error", "Failed to request account deletion. Try again later.") 281 - return 282 - } 283 200 284 201 s.Pages.DangerDeleteTokenStep(w) 285 202 } 286 203 287 204 func (s *Settings) deleteAccount(w http.ResponseWriter, r *http.Request) { 288 205 user := s.OAuth.GetMultiAccountUser(r) 289 - if !s.isTnglShUser(user.Pds()) { 206 + if !s.Config.Pds.IsTnglShUser(user.Pds()) { 290 207 s.Pages.Notice(w, "delete-error", "Only available for tngl.sh accounts.") 291 208 return 292 209 } ··· 306 223 return 307 224 } 308 225 309 - resp, err := s.pdsPost("com.atproto.server.deleteAccount", map[string]string{ 310 - "did": did, 311 - "password": password, 312 - "token": token, 313 - }, "") 226 + err := comatproto.ServerDeleteAccount(context.Background(), s.pdsClient(), &comatproto.ServerDeleteAccount_Input{ 227 + Did: did, 228 + Password: password, 229 + Token: token, 230 + }) 314 231 if err != nil { 315 232 s.Logger.Error("deleting account", "err", err) 316 233 s.Pages.Notice(w, "delete-error", "Failed to delete account. Try again later.") 317 234 return 318 235 } 319 - defer resp.Body.Close() 320 - 321 - if resp.StatusCode != http.StatusOK { 322 - respBody, _ := io.ReadAll(resp.Body) 323 - var errResp struct { 324 - Message string `json:"message"` 325 - } 326 - if json.Unmarshal(respBody, &errResp) == nil && errResp.Message != "" { 327 - s.Pages.Notice(w, "delete-error", errResp.Message) 328 - return 329 - } 330 - s.Logger.Error("deleting account", "status", resp.StatusCode) 331 - s.Pages.Notice(w, "delete-error", "Failed to delete account. No dice!") 332 - return 333 - } 334 236 335 237 if err := s.OAuth.DeleteSession(w, r); err != nil { 336 238 s.Logger.Error("clearing session after account deletion", "did", did, "err", err) ··· 361 263 362 264 func (s *Settings) reactivateAccount(w http.ResponseWriter, r *http.Request) { 363 265 user := s.OAuth.GetMultiAccountUser(r) 364 - if !s.isTnglShUser(user.Pds()) { 266 + if !s.Config.Pds.IsTnglShUser(user.Pds()) { 365 267 s.Pages.Notice(w, "reactivate-error", "Only available for tngl.sh accounts.") 366 268 return 367 269 } ··· 380 282 return 381 283 } 382 284 383 - resp, err := s.pdsPost("com.atproto.server.activateAccount", nil, session.AccessJwt) 384 - s.revokePdsSession(session.AccessJwt) 285 + err = comatproto.ServerActivateAccount(context.Background(), session.Client) 286 + s.revokePdsSession(session) 385 287 if err != nil { 386 288 s.Logger.Error("reactivating account", "err", err) 387 289 s.Pages.Notice(w, "reactivate-error", "Failed to reactivate account. Try again later.") 388 290 return 389 291 } 390 - defer resp.Body.Close() 391 - 392 - if resp.StatusCode != http.StatusOK { 393 - s.Logger.Error("reactivating account", "status", resp.StatusCode) 394 - s.Pages.Notice(w, "reactivate-error", "Failed to reactivate account. Try again later.") 395 - return 396 - } 397 292 398 293 s.Pages.HxRefresh(w) 399 294 }
+4 -8
appview/settings/settings.go
··· 251 251 log.Printf("failed to get users punchcard preferences: %s", err) 252 252 } 253 253 254 - isDeactivated := s.isTnglShUser(user.Pds()) && s.isAccountDeactivated(r.Context(), user.Did(), user.Pds()) 254 + isDeactivated := s.Config.Pds.IsTnglShUser(user.Pds()) && s.isAccountDeactivated(r.Context(), user.Did(), user.Pds()) 255 255 256 256 s.Pages.UserProfileSettings(w, pages.UserProfileSettingsParams{ 257 257 LoggedInUser: user, 258 258 PunchcardPreference: punchcardPreferences, 259 - IsTnglSh: s.isTnglShUser(user.Pds()), 259 + IsTnglSh: s.Config.Pds.IsTnglShUser(user.Pds()), 260 260 IsDeactivated: isDeactivated, 261 261 PdsDomain: s.pdsDomain(), 262 262 HandleOpen: r.URL.Query().Get("handle") == "1", ··· 715 715 } 716 716 } 717 717 718 - func (s *Settings) isTnglShUser(pdsHost string) bool { 719 - return s.Config.Pds.IsTnglShUser(pdsHost) 720 - } 721 - 722 718 func (s *Settings) pdsDomain() string { 723 719 parsed, err := url.Parse(s.Config.Pds.Host) 724 720 if err != nil { ··· 729 725 730 726 func (s *Settings) elevateForHandle(w http.ResponseWriter, r *http.Request) { 731 727 user := s.OAuth.GetMultiAccountUser(r) 732 - if !s.isTnglShUser(user.Pds()) { 728 + if !s.Config.Pds.IsTnglShUser(user.Pds()) { 733 729 http.Redirect(w, r, "/settings/profile", http.StatusSeeOther) 734 730 return 735 731 } ··· 757 753 758 754 func (s *Settings) updateHandle(w http.ResponseWriter, r *http.Request) { 759 755 user := s.OAuth.GetMultiAccountUser(r) 760 - if !s.isTnglShUser(user.Pds()) { 756 + if !s.Config.Pds.IsTnglShUser(user.Pds()) { 761 757 s.Pages.Notice(w, "handle-error", "Handle changes are only available for tngl.sh accounts.") 762 758 return 763 759 }
appview/state/login.go

This file has not been changed.

History

7 rounds 4 comments
sign up or login to add to the discussion
1 commit
expand
appview/settings: add account management UI for tngl.sh users
3/3 success
expand
expand 0 comments
pull request successfully merged
1 commit
expand
appview/settings: add account management UI for tngl.sh users
3/3 success
expand
expand 0 comments
1 commit
expand
appview/settings: add account management UI for tngl.sh users
3/3 success
expand
expand 1 comment
  • this can go in pages.go
1 commit
expand
appview/settings: add account management UI for tngl.sh users
3/3 success
expand
expand 0 comments
1 commit
expand
appview/settings: add account management UI for tngl.sh users
2/3 failed, 1/3 success
expand
expand 3 comments
  • here could you explain what an elevated auth flow is
  • here why not a template here? does this need to be in htmx.go?
  • here do we need this IsTnglSh bool?
  • here are there any indigo bits to achieve this?
  • here we have defined isTnglShUser once here, and another time here

do we need both?

1 commit
expand
appview/settings: add account management UI for tngl.sh users
3/3 failed
expand
expand 0 comments
1 commit
expand
appview/settings: add account management UI for tngl.sh users
3/3 success
expand
expand 0 comments