add invite to space feature

This commit is contained in:
juancwu 2026-01-14 21:11:16 +00:00
commit 4d6e6799a0
10 changed files with 407 additions and 8 deletions

View file

@ -15,11 +15,12 @@ import (
)
type authHandler struct {
authService *service.AuthService
authService *service.AuthService
inviteService *service.InviteService
}
func NewAuthHandler(authService *service.AuthService) *authHandler {
return &authHandler{authService: authService}
func NewAuthHandler(authService *service.AuthService, inviteService *service.InviteService) *authHandler {
return &authHandler{authService: authService, inviteService: inviteService}
}
func (h *authHandler) AuthPage(w http.ResponseWriter, r *http.Request) {
@ -88,6 +89,28 @@ func (h *authHandler) VerifyMagicLink(w http.ResponseWriter, r *http.Request) {
h.authService.SetJWTCookie(w, jwtToken, time.Now().Add(7*24*time.Hour))
// Check for pending invite
inviteCookie, err := r.Cookie("pending_invite")
if err == nil && inviteCookie.Value != "" {
spaceID, err := h.inviteService.AcceptInvite(inviteCookie.Value, user.ID)
if err != nil {
slog.Error("failed to process pending invite", "error", err, "token", inviteCookie.Value)
// Don't fail the login, just maybe notify user?
} else {
slog.Info("accepted pending invite", "user_id", user.ID, "space_id", spaceID)
// Clear cookie
http.SetCookie(w, &http.Cookie{
Name: "pending_invite",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
})
// If we want to redirect to the space immediately, we can.
// But check onboarding first.
}
}
needsOnboarding, err := h.authService.NeedsOnboarding(user.ID)
if err != nil {
slog.Warn("failed to check onboarding status", "error", err, "user_id", user.ID)

View file

@ -20,14 +20,16 @@ type SpaceHandler struct {
tagService *service.TagService
listService *service.ShoppingListService
expenseService *service.ExpenseService
inviteService *service.InviteService
}
func NewSpaceHandler(ss *service.SpaceService, ts *service.TagService, sls *service.ShoppingListService, es *service.ExpenseService) *SpaceHandler {
func NewSpaceHandler(ss *service.SpaceService, ts *service.TagService, sls *service.ShoppingListService, es *service.ExpenseService, is *service.InviteService) *SpaceHandler {
return &SpaceHandler{
spaceService: ss,
tagService: ts,
listService: sls,
expenseService: es,
inviteService: is,
}
}
@ -353,3 +355,57 @@ func (h *SpaceHandler) CreateExpense(w http.ResponseWriter, r *http.Request) {
ui.Render(w, r, pages.ExpenseCreatedResponse(newExpense, balance))
}
func (h *SpaceHandler) CreateInvite(w http.ResponseWriter, r *http.Request) {
spaceID := r.PathValue("spaceID")
user := ctxkeys.User(r.Context())
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
email := r.FormValue("email")
if email == "" {
http.Error(w, "Email is required", http.StatusBadRequest)
return
}
_, err := h.inviteService.CreateInvite(spaceID, user.ID, email)
if err != nil {
slog.Error("failed to create invite", "error", err, "space_id", spaceID)
http.Error(w, "Failed to create invite", http.StatusInternalServerError)
return
}
// TODO: Return a nice UI response (toast or list update)
w.Write([]byte("Invitation sent!"))
}
func (h *SpaceHandler) JoinSpace(w http.ResponseWriter, r *http.Request) {
token := r.PathValue("token")
user := ctxkeys.User(r.Context())
if user != nil {
spaceID, err := h.inviteService.AcceptInvite(token, user.ID)
if err != nil {
slog.Error("failed to accept invite", "error", err, "token", token)
http.Error(w, "Failed to join space: "+err.Error(), http.StatusBadRequest)
return
}
http.Redirect(w, r, "/app/spaces/"+spaceID, http.StatusSeeOther)
return
}
// Not logged in: set cookie and redirect to auth
http.SetCookie(w, &http.Cookie{
Name: "pending_invite",
Value: token,
Path: "/",
Expires: time.Now().Add(1 * time.Hour),
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
http.Redirect(w, r, "/auth?invite=true", http.StatusTemporaryRedirect)
}