Skip to content

Commit 12ce2e9

Browse files
Added Introduction Modal (#106)
* Added Modal with new handler/service * PR Suggestions * Transition to Tanstack forms * fix(onboard): fix styling and update route --------- Co-authored-by: Alexander Wang <alexander.yisu.wang@outlook.com> Co-authored-by: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com>
1 parent 7a52a9f commit 12ce2e9

13 files changed

Lines changed: 476 additions & 66 deletions

File tree

apps/api/cmd/api/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,14 @@ func main() {
6666

6767
// Injections into services
6868
authService := services.NewAuthService(userRepo, accountRepo, sessionRepo, txm, client, logger, &cfg.Auth)
69+
userService := services.NewUserService(userRepo, logger)
6970
eventInterestService := services.NewEventInterestService(eventInterestRepo, logger)
7071
eventService := services.NewEventService(eventRepo, userRepo, logger)
7172
emailService := services.NewEmailService(taskQueueClient, logger)
7273
applicationService := services.NewApplicationService(applicationRepo, eventService, txm, r2Client, &cfg.CoreBuckets, logger)
7374

7475
// Injections into handlers
75-
apiHandlers := handlers.NewHandlers(authService, eventInterestService, eventService, emailService, applicationService, cfg, logger)
76+
apiHandlers := handlers.NewHandlers(authService, userService, eventInterestService, eventService, emailService, applicationService, cfg, logger)
7677

7778
api := api.NewAPI(&logger, apiHandlers, mw)
7879

apps/api/internal/api/api.go

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func (api *API) setupRoutes(mw *mw.Middleware) {
4949
api.Router.Use(middleware.RealIP)
5050
api.Router.Use(cors.Handler(cors.Options{
5151
AllowedOrigins: AllowedOrigins,
52-
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
52+
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
5353
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
5454
ExposedHeaders: []string{"Link"},
5555
AllowCredentials: true,
@@ -60,39 +60,60 @@ func (api *API) setupRoutes(mw *mw.Middleware) {
6060
api.Router.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
6161
api.Logger.Trace().Str("method", r.Method).Str("path", r.URL.Path).Msg("Received ping.")
6262
w.Header().Set("Content-Type", "text/plain")
63-
w.Header().Set("Content-Length", "6") // "pong!\n" is 6 bytes
63+
w.Header().Set("Content-Length", "6")
6464
if _, err := w.Write([]byte("pong!\n")); err != nil {
6565
log.Err(err)
6666
}
6767
})
6868

69-
// Auth routes
69+
// --- Auth routes ---
7070
api.Router.Route("/auth", func(r chi.Router) {
7171
r.Get("/callback", api.Handlers.Auth.OAuthCallback)
7272

73+
// Protected auth routes
7374
r.Group(func(r chi.Router) {
7475
r.Use(mw.Auth.RequireAuth)
7576
r.Get("/me", api.Handlers.Auth.GetMe)
7677
r.Post("/logout", api.Handlers.Auth.Logout)
7778
})
7879
})
7980

80-
// Event routes
81+
// --- User routes ---
82+
api.Router.Route("/users", func(r chi.Router) {
83+
r.Use(mw.Auth.RequireAuth)
84+
r.Get("/me", api.Handlers.User.GetProfile)
85+
r.Patch("/me/onboarding", api.Handlers.User.CompleteOnboarding)
86+
})
87+
88+
// --- Event routes ---
8189
api.Router.Route("/events", func(r chi.Router) {
90+
91+
// Superuser-only
8292
r.With(mw.Auth.RequireAuth, ensureSuperuser).Post("/", api.Handlers.Event.CreateEvent)
93+
94+
// Authenticated
8395
r.With(mw.Auth.RequireAuth).Get("/", api.Handlers.Event.GetEvents)
96+
97+
// Event-specific routes
8498
r.Route("/{eventId}", func(r chi.Router) {
99+
r.Use(mw.Auth.RequireAuth)
85100

86-
r.With(mw.Auth.RequireAuth, ensureEventAdmin).Patch("/", api.Handlers.Event.UpdateEventById)
87-
r.With(mw.Auth.RequireAuth, ensureSuperuser).Delete("/", api.Handlers.Event.DeleteEventById)
88-
r.With(mw.Auth.RequireAuth, ensureEventAdmin).Get("/staff", api.Handlers.Event.GetEventStaffUsers)
89-
r.With(mw.Auth.RequireAuth, ensureEventAdmin).Post("/roles", api.Handlers.Event.AssignEventRole)
90-
r.With(mw.Auth.RequireAuth).Get("/role", api.Handlers.Event.GetEventRole)
101+
// General access
91102
r.Get("/", api.Handlers.Event.GetEventByID)
92-
r.With(httprate.LimitByIP(5, time.Minute)).Post("/interest", api.Handlers.EventInterest.AddEmailToEvent)
103+
r.Post("/interest", api.Handlers.EventInterest.AddEmailToEvent)
104+
105+
// Admin-only
106+
r.With(ensureEventAdmin).Patch("/", api.Handlers.Event.UpdateEventById)
107+
r.With(ensureEventAdmin).Get("/staff", api.Handlers.Event.GetEventStaffUsers)
108+
r.With(ensureEventAdmin).Post("/roles", api.Handlers.Event.AssignEventRole)
109+
r.With(ensureEventAdmin).Get("/role", api.Handlers.Event.GetEventRole)
110+
111+
// Superuser-only
112+
r.With(ensureSuperuser).Delete("/", api.Handlers.Event.DeleteEventById)
113+
114+
// Application routes
93115
r.Route("/application", func(r chi.Router) {
94116
r.Use(mw.Auth.RequireAuth)
95-
96117
r.Get("/", api.Handlers.Application.GetApplicationByUserAndEventID)
97118
r.Post("/submit", api.Handlers.Application.SubmitApplication)
98119
r.Post("/save", api.Handlers.Application.SaveApplication)

apps/api/internal/api/handlers/handlers.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
type Handlers struct {
1010
Auth *AuthHandler
11+
User *UserHandler
1112
EventInterest *EventInterestHandler
1213
Event *EventHandler
1314
Email *EmailHandler
@@ -16,6 +17,7 @@ type Handlers struct {
1617

1718
func NewHandlers(
1819
authService *services.AuthService,
20+
userService *services.UserService,
1921
eventInterestService *services.EventInterestService,
2022
eventService *services.EventService,
2123
emailService *services.EmailService,
@@ -25,6 +27,7 @@ func NewHandlers(
2527
) *Handlers {
2628
return &Handlers{
2729
Auth: NewAuthHandler(authService, cfg, logger),
30+
User: NewUserHandler(userService, logger),
2831
EventInterest: NewEventInterestHandler(eventInterestService, cfg, logger),
2932
Event: NewEventHandler(eventService, cfg, logger),
3033
Email: NewEmailHandler(emailService, logger),
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package handlers
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
7+
"github.com/rs/zerolog"
8+
res "github.com/swamphacks/core/apps/api/internal/api/response"
9+
"github.com/swamphacks/core/apps/api/internal/ctxutils"
10+
"github.com/swamphacks/core/apps/api/internal/email"
11+
"github.com/swamphacks/core/apps/api/internal/services"
12+
)
13+
14+
type UserHandler struct {
15+
userService *services.UserService
16+
logger zerolog.Logger
17+
}
18+
19+
func NewUserHandler(userService *services.UserService, logger zerolog.Logger) *UserHandler {
20+
return &UserHandler{
21+
userService: userService,
22+
logger: logger.With().Str("handler", "UserHandler").Str("component", "user").Logger(),
23+
}
24+
}
25+
26+
func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
27+
userId := ctxutils.GetUserIdFromCtx(r.Context())
28+
if userId == nil {
29+
res.SendError(w, http.StatusUnauthorized, res.NewError("unauthorized", "User not authenticated"))
30+
return
31+
}
32+
33+
user, err := h.userService.GetUser(r.Context(), *userId)
34+
if err != nil {
35+
h.logger.Err(err).Msg("failed to get user profile")
36+
if err == services.ErrUserNotFound {
37+
res.SendError(w, http.StatusNotFound, res.NewError("user_not_found", "User profile not found"))
38+
} else {
39+
res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went seriously wrong."))
40+
}
41+
return
42+
}
43+
44+
res.Send(w, http.StatusOK, user)
45+
}
46+
47+
type CompleteOnboardingRequest struct {
48+
Name string `json:"name"`
49+
PreferredEmail string `json:"preferred_email"`
50+
}
51+
52+
func (h *UserHandler) CompleteOnboarding(w http.ResponseWriter, r *http.Request) {
53+
userId := ctxutils.GetUserIdFromCtx(r.Context())
54+
if userId == nil {
55+
res.SendError(w, http.StatusUnauthorized, res.NewError("unauthorized", "User not authenticated"))
56+
return
57+
}
58+
59+
var req CompleteOnboardingRequest
60+
decoder := json.NewDecoder(r.Body)
61+
decoder.DisallowUnknownFields() // Prevents requests with extraneous fields
62+
if err := decoder.Decode(&req); err != nil {
63+
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Invalid request body"))
64+
return
65+
}
66+
67+
// Validate required fields
68+
if req.Name == "" || req.PreferredEmail == "" {
69+
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Name and preferred email are required"))
70+
return
71+
}
72+
73+
// Validate email format
74+
if !email.IsValidEmail(req.PreferredEmail) {
75+
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_email", "Invalid email format"))
76+
return
77+
}
78+
79+
err := h.userService.CompleteOnboarding(r.Context(), *userId, req.Name, req.PreferredEmail)
80+
if err != nil {
81+
h.logger.Err(err).Msg("failed to complete onboarding")
82+
if err == services.ErrUserNotFound {
83+
res.SendError(w, http.StatusNotFound, res.NewError("user_not_found", "User not found"))
84+
} else {
85+
res.SendError(w, http.StatusInternalServerError, res.NewError("update_failed", "Failed to complete onboarding"))
86+
}
87+
return
88+
}
89+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- +goose Up
2+
-- +goose StatementBegin
3+
ALTER TABLE auth.users
4+
ADD COLUMN preferred_email TEXT;
5+
-- +goose StatementEnd
6+
7+
-- +goose Down
8+
-- +goose StatementBegin
9+
ALTER TABLE auth.users
10+
DROP COLUMN preferred_email;
11+
-- +goose StatementEnd

apps/api/internal/db/queries/users.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ SET
2222
name = CASE WHEN @name_do_update::boolean THEN @name ELSE name END,
2323
email = CASE WHEN @email_do_update::boolean THEN @email ELSE email END,
2424
email_verified = CASE WHEN @email_verified_do_update::boolean THEN @email_verified ELSE email_verified END,
25+
preferred_email = CASE WHEN @preferred_email_do_update::boolean THEN @preferred_email ELSE preferred_email END,
2526
onboarded = CASE WHEN @onboarded_do_update::boolean THEN @onboarded ELSE onboarded END,
2627
image = CASE WHEN @image_do_update::boolean THEN @image ELSE image END,
2728
updated_at = NOW()

apps/api/internal/db/repository/users.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,13 @@ func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*sqlc.Au
6464

6565
return &user, nil
6666
}
67+
68+
func (r *UserRepository) UpdateUser(ctx context.Context, params sqlc.UpdateUserParams) error {
69+
err := r.db.Query.UpdateUser(ctx, params)
70+
if err != nil {
71+
if err == pgx.ErrNoRows {
72+
return ErrUserNotFound
73+
}
74+
}
75+
return err
76+
}

apps/api/internal/db/sqlc/event_roles.sql.go

Lines changed: 13 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/api/internal/db/sqlc/models.go

Lines changed: 10 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)