forked from wrenn/wrenn
v0.1.3 (#36)
## What's new Compliance, audit, and account lifecycle improvements — admin actions are now fully auditable, user data is properly anonymized on deletion, and OAuth signup flow gives users control over their profile. ### Audit - Added audit logging for all admin actions (user activate/deactivate, team BYOC toggle, team delete, template delete, build create/cancel) - Added admin audit page with infinite scroll and hierarchical filters - Fixed audit log team assignment — admin/host actions now correctly land under PlatformTeamID - Anonymize audit logs on user hard-delete (actor name, IDs, emails stripped) - Deduplicated audit logger internals (665 → 374 lines, no behavior change) ### Authentication - Separated GitHub OAuth login/signup flows — login no longer auto-creates accounts - Added name confirmation dialog for new GitHub signups ### Account Lifecycle - Email notification sent when account is permanently deleted after grace period - Audit log anonymization tied to user purge (per-user transactional) ### UX - Removed accent gradient bars from admin host dialogs (border + shadow only) - Frontend renders deleted users as styled badge in audit log view ### Others - Version bump - Bug fixes Reviewed-on: wrenn/wrenn#36
This commit is contained in:
@ -55,8 +55,14 @@ func (h *oauthHandler) Redirect(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
mac := computeHMAC(h.jwtSecret, state)
|
||||
cookieVal := state + ":" + mac
|
||||
// Persist intent (login|signup) in the state cookie so the callback can enforce it.
|
||||
intent := r.URL.Query().Get("intent")
|
||||
if intent != "signup" {
|
||||
intent = "login"
|
||||
}
|
||||
|
||||
mac := computeHMAC(h.jwtSecret, state+":"+intent)
|
||||
cookieVal := state + ":" + mac + ":" + intent
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "oauth_state",
|
||||
@ -105,13 +111,17 @@ func (h *oauthHandler) Callback(w http.ResponseWriter, r *http.Request) {
|
||||
Secure: isSecure(r),
|
||||
})
|
||||
|
||||
parts := strings.SplitN(stateCookie.Value, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
parts := strings.SplitN(stateCookie.Value, ":", 3)
|
||||
if len(parts) < 2 {
|
||||
redirectWithError(w, r, redirectBase, "invalid_state")
|
||||
return
|
||||
}
|
||||
nonce, expectedMAC := parts[0], parts[1]
|
||||
if !hmac.Equal([]byte(computeHMAC(h.jwtSecret, nonce)), []byte(expectedMAC)) {
|
||||
intent := "login"
|
||||
if len(parts) == 3 && parts[2] == "signup" {
|
||||
intent = "signup"
|
||||
}
|
||||
if !hmac.Equal([]byte(computeHMAC(h.jwtSecret, nonce+":"+intent)), []byte(expectedMAC)) {
|
||||
redirectWithError(w, r, redirectBase, "invalid_state")
|
||||
return
|
||||
}
|
||||
@ -249,6 +259,12 @@ func (h *oauthHandler) Callback(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Block auto-registration when intent is login-only.
|
||||
if intent == "login" {
|
||||
redirectWithError(w, r, redirectBase, "no_account")
|
||||
return
|
||||
}
|
||||
|
||||
// New OAuth identity — check for email collision.
|
||||
existingUser, err := h.db.GetUserByEmail(ctx, email)
|
||||
if err == nil {
|
||||
@ -365,6 +381,17 @@ func (h *oauthHandler) Callback(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Signal frontend that this is a new signup so it can show the name confirmation dialog.
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "wrenn_oauth_new_signup",
|
||||
Value: "1",
|
||||
Path: "/auth/",
|
||||
MaxAge: 60,
|
||||
HttpOnly: false,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Secure: isSecure(r),
|
||||
})
|
||||
|
||||
redirectWithToken(w, r, redirectBase, token, id.FormatUserID(userID), id.FormatTeamID(teamID), email, profile.Name)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user