11package handler
22
33import (
4+ "context"
45 "crypto/rand"
56 "encoding/hex"
67 "encoding/json"
@@ -9,9 +10,11 @@ import (
910
1011 "github.com/capyrpi/api/internal/database"
1112 "github.com/capyrpi/api/internal/middleware"
13+ "github.com/capyrpi/api/internal/oauth"
1214 "github.com/go-chi/chi/v5"
1315 "github.com/golang-jwt/jwt/v5"
1416 "github.com/google/uuid"
17+ "github.com/jackc/pgx/v5"
1518 "github.com/jackc/pgx/v5/pgtype"
1619 "golang.org/x/crypto/bcrypt"
1720)
@@ -58,8 +61,16 @@ type CreateBotTokenRequest struct {
5861// @Success 302
5962// @Router /auth/google [get]
6063func (h * Handler ) GoogleAuth (w http.ResponseWriter , r * http.Request ) {
61- // TODO: Implement Google OAuth redirect
62- h .respondError (w , http .StatusNotImplemented , "Google OAuth not yet implemented" )
64+ state , err := oauth .GenerateStateToken ()
65+ if err != nil {
66+ h .respondError (w , http .StatusInternalServerError , "Failed to generate state" )
67+ return
68+ }
69+
70+ // Set state cookie to verify callback
71+ h .setStateCookie (w , state )
72+
73+ http .Redirect (w , r , h .googleAuth .GetAuthURL (state ), http .StatusFound )
6374}
6475
6576// GoogleCallback handles Google OAuth callback
@@ -72,8 +83,39 @@ func (h *Handler) GoogleAuth(w http.ResponseWriter, r *http.Request) {
7283// @Failure 400 {object} ErrorResponse
7384// @Router /auth/google/callback [get]
7485func (h * Handler ) GoogleCallback (w http.ResponseWriter , r * http.Request ) {
75- // TODO: Implement Google OAuth callback
76- h .respondError (w , http .StatusNotImplemented , "Google OAuth not yet implemented" )
86+ // Verify state
87+ state := r .URL .Query ().Get ("state" )
88+ if ! h .verifyStateCookie (w , r , state ) {
89+ h .respondError (w , http .StatusBadRequest , "Invalid state parameter" )
90+ return
91+ }
92+
93+ code := r .URL .Query ().Get ("code" )
94+ if code == "" {
95+ h .respondError (w , http .StatusBadRequest , "Missing auth code" )
96+ return
97+ }
98+
99+ userInfo , err := h .googleAuth .ExchangeCode (r .Context (), code )
100+ if err != nil {
101+ h .respondError (w , http .StatusInternalServerError , "Failed to exchange code" )
102+ return
103+ }
104+
105+ user , err := h .upsertUser (r .Context (), userInfo .Email , userInfo .GivenName , userInfo .FamilyName )
106+ if err != nil {
107+ h .handleDBError (w , err )
108+ return
109+ }
110+
111+ token , err := h .generateJWT (user )
112+ if err != nil {
113+ h .respondError (w , http .StatusInternalServerError , "Failed to generate session" )
114+ return
115+ }
116+
117+ h .setAuthCookie (w , token )
118+ http .Redirect (w , r , h .config .OAuth .RedirectURL , http .StatusFound )
77119}
78120
79121// MicrosoftAuth initiates Microsoft OAuth flow
@@ -83,8 +125,14 @@ func (h *Handler) GoogleCallback(w http.ResponseWriter, r *http.Request) {
83125// @Success 302
84126// @Router /auth/microsoft [get]
85127func (h * Handler ) MicrosoftAuth (w http.ResponseWriter , r * http.Request ) {
86- // TODO: Implement Microsoft OAuth redirect
87- h .respondError (w , http .StatusNotImplemented , "Microsoft OAuth not yet implemented" )
128+ state , err := oauth .GenerateStateToken ()
129+ if err != nil {
130+ h .respondError (w , http .StatusInternalServerError , "Failed to generate state" )
131+ return
132+ }
133+
134+ h .setStateCookie (w , state )
135+ http .Redirect (w , r , h .microsoftAuth .GetAuthURL (state ), http .StatusFound )
88136}
89137
90138// MicrosoftCallback handles Microsoft OAuth callback
@@ -97,8 +145,44 @@ func (h *Handler) MicrosoftAuth(w http.ResponseWriter, r *http.Request) {
97145// @Failure 400 {object} ErrorResponse
98146// @Router /auth/microsoft/callback [get]
99147func (h * Handler ) MicrosoftCallback (w http.ResponseWriter , r * http.Request ) {
100- // TODO: Implement Microsoft OAuth callback
101- h .respondError (w , http .StatusNotImplemented , "Microsoft OAuth not yet implemented" )
148+ state := r .URL .Query ().Get ("state" )
149+ if ! h .verifyStateCookie (w , r , state ) {
150+ h .respondError (w , http .StatusBadRequest , "Invalid state parameter" )
151+ return
152+ }
153+
154+ code := r .URL .Query ().Get ("code" )
155+ if code == "" {
156+ h .respondError (w , http .StatusBadRequest , "Missing auth code" )
157+ return
158+ }
159+
160+ userInfo , err := h .microsoftAuth .ExchangeCode (r .Context (), code )
161+ if err != nil {
162+ h .respondError (w , http .StatusInternalServerError , "Failed to exchange code" )
163+ return
164+ }
165+
166+ // Use PrincipalName (email) or Mail
167+ email := userInfo .UserPrincipalName
168+ if email == "" {
169+ email = userInfo .Mail
170+ }
171+
172+ user , err := h .upsertUser (r .Context (), email , userInfo .GivenName , userInfo .Surname )
173+ if err != nil {
174+ h .handleDBError (w , err )
175+ return
176+ }
177+
178+ token , err := h .generateJWT (user )
179+ if err != nil {
180+ h .respondError (w , http .StatusInternalServerError , "Failed to generate session" )
181+ return
182+ }
183+
184+ h .setAuthCookie (w , token )
185+ http .Redirect (w , r , h .config .OAuth .RedirectURL , http .StatusFound )
102186}
103187
104188// ============================================================================
@@ -400,6 +484,61 @@ func (h *Handler) setAuthCookie(w http.ResponseWriter, token string) {
400484 })
401485}
402486
487+ func (h * Handler ) setStateCookie (w http.ResponseWriter , state string ) {
488+ http .SetCookie (w , & http.Cookie {
489+ Name : "oauth_state" ,
490+ Value : state ,
491+ Path : "/v1/auth" ,
492+ Domain : h .config .Cookie .Domain ,
493+ MaxAge : 300 , // 5 minutes
494+ Secure : h .config .Cookie .Secure ,
495+ HttpOnly : true ,
496+ SameSite : http .SameSiteLaxMode ,
497+ })
498+ }
499+
500+ func (h * Handler ) verifyStateCookie (w http.ResponseWriter , r * http.Request , state string ) bool {
501+ cookie , err := r .Cookie ("oauth_state" )
502+ if err != nil {
503+ return false
504+ }
505+ // Clear cookie
506+ http .SetCookie (w , & http.Cookie {
507+ Name : "oauth_state" ,
508+ Value : "" ,
509+ Path : "/v1/auth" ,
510+ Domain : h .config .Cookie .Domain ,
511+ MaxAge : - 1 ,
512+ Secure : h .config .Cookie .Secure ,
513+ HttpOnly : true ,
514+ SameSite : http .SameSiteLaxMode ,
515+ })
516+ return cookie .Value == state
517+ }
518+
519+ func (h * Handler ) upsertUser (ctx context.Context , email , firstName , lastName string ) (database.User , error ) {
520+ pgEmail := toPgTextFromString (email )
521+
522+ // Check if user exists
523+ user , err := h .queries .GetUserByEmail (ctx , pgEmail )
524+ if err == nil {
525+ return user , nil
526+ }
527+
528+ if err != pgx .ErrNoRows {
529+ return database.User {}, err
530+ }
531+
532+ // Create new user
533+ return h .queries .CreateUser (ctx , database.CreateUserParams {
534+ FirstName : firstName ,
535+ LastName : lastName ,
536+ PersonalEmail : pgEmail , // Default to personal email for oauth
537+ SchoolEmail : pgtype.Text {Valid : false },
538+ Role : database.NullUserRole {UserRole : database .UserRoleStudent , Valid : true }, // Default role
539+ })
540+ }
541+
403542func getEmail (user database.User ) string {
404543 if user .SchoolEmail .Valid {
405544 return user .SchoolEmail .String
0 commit comments