go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/apputil/auth.go (about) 1 /* 2 3 Copyright (c) 2023 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package apputil 9 10 import ( 11 "context" 12 "net/http" 13 "net/url" 14 "time" 15 16 "go.charczuk.com/sdk/logutil" 17 "go.charczuk.com/sdk/oauth" 18 "go.charczuk.com/sdk/uuid" 19 "go.charczuk.com/sdk/web" 20 ) 21 22 // Auth is the auth controller. 23 type Auth struct { 24 BaseController 25 Config Config 26 AuthedRedirectPath string 27 CreateUserListener func(context.Context, *User) error 28 FetchSessionListener func(context.Context, *web.Session) error 29 OAuth *oauth.Manager 30 DB *ModelManager 31 } 32 33 var ( 34 _ web.FetchSessionHandler = (*Auth)(nil) 35 _ web.PersistSessionHandler = (*Auth)(nil) 36 _ web.RemoveSessionHandler = (*Auth)(nil) 37 _ web.LoginRedirectHandler = (*Auth)(nil) 38 ) 39 40 // Register adds the controller routes to the application. 41 func (a Auth) Register(app *web.App) { 42 app.AuthPersister = a 43 44 app.Get("/login", web.SessionAware(a.login)) 45 app.Get("/logout", web.SessionAwareStable(a.logout)) 46 app.Get("/oauth/google", web.SessionAware(a.oauthGoogle)) 47 } 48 49 // GET /login 50 func (a Auth) login(r web.Context) web.Result { 51 if r.Session() != nil { 52 return a.authedRedirect() 53 } 54 oauthURL, err := a.OAuth.OAuthURL(r.Request(), oauth.OptStateRedirectURI(r.Request().RequestURI)) 55 if err != nil { 56 return r.Views().InternalError(err) 57 } 58 return web.RedirectWithMethod("GET", oauthURL) 59 } 60 61 // GET /oauth/google 62 func (a Auth) oauthGoogle(ctx web.Context) web.Result { 63 if ctx.Session() != nil { 64 return a.authedRedirect() 65 } 66 result, err := a.OAuth.Finish(ctx.Request()) 67 if err != nil { 68 logutil.Error(logutil.GetLogger(ctx), err) 69 return ctx.App().Views.NotAuthorized() 70 } 71 72 user, existingUserFound, err := a.DB.GetUserByEmail(ctx, result.Profile.Email) 73 if err != nil { 74 return ctx.App().Views.InternalError(err) 75 } 76 ApplyOAuthProfileToUser(&user, result.Profile) 77 if !existingUserFound { 78 user.ID = uuid.V4() 79 user.CreatedUTC = time.Now().UTC() 80 } 81 user.LastLoginUTC = time.Now().UTC() 82 user.LastSeenUTC = time.Now().UTC() 83 84 if err = a.DB.Invoke(ctx).Upsert(&user); err != nil { 85 return ctx.Views().InternalError(err) 86 } 87 88 if !existingUserFound && a.CreateUserListener != nil { 89 if err = a.CreateUserListener(ctx, &user); err != nil { 90 return ctx.Views().InternalError(err) 91 } 92 } 93 94 _, err = ctx.App().Login(user.ID.String(), ctx) 95 if err != nil { 96 return ctx.Views().InternalError(err) 97 } 98 if len(result.State.RedirectURI) > 0 { 99 return web.RedirectWithMethodf(http.MethodGet, result.State.RedirectURI) 100 } 101 return a.authedRedirect() 102 } 103 104 // logout logs the user out. 105 func (a Auth) logout(ctx web.Context) web.Result { 106 if ctx.Session() == nil { 107 return ctx.App().Views.NotAuthorized() 108 } 109 if err := ctx.App().Logout(ctx); err != nil { 110 return ctx.Views().InternalError(err) 111 } 112 return web.RedirectWithMethod("GET", "/") 113 } 114 115 // 116 // helpers 117 // 118 119 func (a Auth) authedRedirect() web.Result { 120 redirectTargetPath := "/" 121 if a.AuthedRedirectPath != "" { 122 redirectTargetPath = a.AuthedRedirectPath 123 } 124 return web.RedirectWithMethod(http.MethodGet, redirectTargetPath) 125 } 126 127 // SessionTimeout implements session timeout provider. 128 func (a Auth) SessionTimeout(_ *web.Session) time.Time { 129 return time.Now().UTC().AddDate(0, 0, 14) 130 } 131 132 // FetchSession implements web.FetchSessionHandler. 133 func (a Auth) FetchSession(ctx context.Context, sessionID string) (*web.Session, error) { 134 var dbSession Session 135 _, err := a.DB.Invoke(ctx).Get(&dbSession, sessionID) 136 if err != nil { 137 return nil, err 138 } 139 if dbSession.IsZero() { 140 return nil, nil 141 } 142 143 var session web.Session 144 dbSession.ApplyTo(&session) 145 var user User 146 if _, err = a.DB.Invoke(ctx).Get(&user, dbSession.UserID); err != nil { 147 return nil, err 148 } 149 session.State = SessionState{ 150 User: &user, 151 } 152 if a.FetchSessionListener != nil { 153 if err = a.FetchSessionListener(ctx, &session); err != nil { 154 return nil, err 155 } 156 } 157 return &session, nil 158 } 159 160 // PersistSession implements web.PersistSessionHandler. 161 func (a Auth) PersistSession(ctx context.Context, session *web.Session) error { 162 dbSession := &Session{ 163 SessionID: session.SessionID, 164 UserID: uuid.MustParse(session.UserID), 165 BaseURL: session.BaseURL, 166 CreatedUTC: session.CreatedUTC, 167 ExpiresUTC: session.ExpiresUTC, 168 LastSeenUTC: time.Now().UTC(), 169 UserAgent: session.UserAgent, 170 RemoteAddr: session.RemoteAddr, 171 Locale: session.Locale, 172 } 173 return a.DB.Invoke(ctx).Upsert(dbSession) 174 } 175 176 // RemoveSession implements web.RemoveSessionHandler. 177 func (a Auth) RemoveSession(ctx context.Context, sessionID string) error { 178 var session Session 179 _, err := a.DB.Invoke(ctx).Get(&session, sessionID) 180 if err != nil { 181 return err 182 } 183 _, err = a.DB.Invoke(ctx).Delete(session) 184 return err 185 } 186 187 // LoginRedirect implements web.LoginRedirectHandler. 188 func (a Auth) LoginRedirect(ctx web.Context) *url.URL { 189 from := ctx.Request().URL.Path 190 oauthURL, err := a.OAuth.OAuthURL(ctx.Request(), oauth.OptStateRedirectURI(from)) 191 if err != nil { 192 return &url.URL{RawPath: "/login?error=invalid_oauth_url"} 193 } 194 parsed, err := url.Parse(oauthURL) 195 if err != nil { 196 return &url.URL{RawPath: "/login?error=invalid_oauth_url"} 197 } 198 return parsed 199 }