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  }