diff --git a/database_migrations/000011_link_users_and_accounts.down.sql b/database_migrations/000011_link_users_and_accounts.down.sql new file mode 100644 index 0000000..71eccda --- /dev/null +++ b/database_migrations/000011_link_users_and_accounts.down.sql @@ -0,0 +1 @@ +DROP TABLE oauth_users; diff --git a/database_migrations/000011_link_users_and_accounts.up.sql b/database_migrations/000011_link_users_and_accounts.up.sql new file mode 100644 index 0000000..d0fb19c --- /dev/null +++ b/database_migrations/000011_link_users_and_accounts.up.sql @@ -0,0 +1,25 @@ +-- save the oauth user_id to keep track of all known users, and to link to accounts. + +CREATE TABLE oauth_users ( + user_id TEXT NOT NULL, + + PRIMARY KEY (user_id) +); + +INSERT INTO oauth_users ( + user_id +) +SELECT + DISTINCT id_token_subject +FROM + oauth_tokens; + +ALTER TABLE oauth_tokens + ADD CONSTRAINT fk_oauth_tokens_oauth_users + FOREIGN KEY (id_token_subject) + REFERENCES oauth_users (user_id); + +ALTER TABLE accounts + ADD COLUMN user_id TEXT + UNIQUE + REFERENCES oauth_users (user_id) NOT NULL; diff --git a/diagrams/database_schema.svg b/diagrams/database_schema.svg index 2a148f2..02a62f1 100644 --- a/diagrams/database_schema.svg +++ b/diagrams/database_schema.svg @@ -1 +1 @@ -accountsaccount_id:serial [PK]email:textverified:booleanetsy_access_tokensuser_id:integer [PK][FK]access_token:textrefresh_token:textaccess_token_expiration:timestamp with time zonerefresh_token_expiration:timestamp with time zoneetsy_store_eventsstore_id:text [PK][FK]event_timestamp:timestamp with time zone [PK][FK]event_id:text [PK][FK]platform:text [FK]etsy_usersuser_id:integer [PK]account_id:integer [FK]shop_id:integerraw_store_eventsplatform:text [PK]store_id:text [PK]event_timestamp:timestamp with time zone [PK]event_id:text [PK]raw_payload:jsonbschema_migrationsversion:bigint [PK]dirty:booleantiktok_store_eventsstore_id:text [PK][FK]event_timestamp:timestamp with time zone [PK][FK]event_id:text [PK][FK]platform:text [FK]wix_store_eventsstore_id:text [PK][FK]event_timestamp:timestamp with time zone [PK][FK]event_id:text [PK][FK]platform:text [FK] \ No newline at end of file +accountsaccount_id:serial [PK]email:textverified:booleanuser_id:text [FK]etsy_access_tokensuser_id:integer [PK][FK]access_token:textrefresh_token:textaccess_token_expiration:timestamp with time zonerefresh_token_expiration:timestamp with time zoneetsy_oauth_requestsstate:bytea [PK]code_verifier:byteaexpiration:timestamp with time zoneaccount_id:integer [FK]etsy_store_eventsstore_id:text [PK][FK]event_timestamp:timestamp with time zone [PK][FK]event_id:text [PK][FK]platform:text [FK]etsy_usersuser_id:integer [PK]account_id:integer [FK]shop_id:integeroauth_login_statesstate:bytea [PK]expiration:timestamp with time zoneoauth_tokensaccess_token:text [PK]token_type:textrefresh_token:textexpiry:timestamp with time zoneid_token_issuer:textid_token_audience:text[]id_token_subject:text [FK]id_token_expiry:timestamp with time zoneid_token_issued_at:timestamp with time zoneid_token_nonce:textid_token_access_token_hash:textclaims:jsonboauth_usersuser_id:text [PK]raw_store_eventsplatform:text [PK]store_id:text [PK]event_timestamp:timestamp with time zone [PK]event_id:text [PK]raw_payload:jsonbschema_migrationsversion:bigint [PK]dirty:booleantiktok_store_eventsstore_id:text [PK][FK]event_timestamp:timestamp with time zone [PK][FK]event_id:text [PK][FK]platform:text [FK]wix_store_eventsstore_id:text [PK][FK]event_timestamp:timestamp with time zone [PK][FK]event_id:text [PK][FK]platform:text [FK] \ No newline at end of file diff --git a/diagrams/database_schema.uml b/diagrams/database_schema.uml index 4837f53..e171f87 100644 --- a/diagrams/database_schema.uml +++ b/diagrams/database_schema.uml @@ -7,6 +7,7 @@ entity "**accounts**" { -- *""email"": //text // *""verified"": //boolean // + *""user_id"": //text [FK]// } entity "**etsy_access_tokens**" { @@ -18,6 +19,14 @@ entity "**etsy_access_tokens**" { *""refresh_token_expiration"": //timestamp with time zone // } +entity "**etsy_oauth_requests**" { + + ""state"": //bytea [PK]// + -- + *""code_verifier"": //bytea // + *""expiration"": //timestamp with time zone // + *""account_id"": //integer [FK]// +} + entity "**etsy_store_events**" { + ""store_id"": //text [PK][FK]// + ""event_timestamp"": //timestamp with time zone [PK][FK]// @@ -33,6 +42,33 @@ entity "**etsy_users**" { *""shop_id"": //integer // } +entity "**oauth_login_states**" { + + ""state"": //bytea [PK]// + -- + *""expiration"": //timestamp with time zone // +} + +entity "**oauth_tokens**" { + + ""access_token"": //text [PK]// + -- + *""token_type"": //text // + *""refresh_token"": //text // + *""expiry"": //timestamp with time zone // + *""id_token_issuer"": //text // + *""id_token_audience"": //text[] // + *""id_token_subject"": //text [FK]// + *""id_token_expiry"": //timestamp with time zone // + *""id_token_issued_at"": //timestamp with time zone // + *""id_token_nonce"": //text // + *""id_token_access_token_hash"": //text // + *""claims"": //jsonb // +} + +entity "**oauth_users**" { + + ""user_id"": //text [PK]// + -- +} + entity "**raw_store_events**" { + ""platform"": //text [PK]// + ""store_id"": //text [PK]// @@ -64,8 +100,12 @@ entity "**wix_store_events**" { *""platform"": //text [FK]// } +"**accounts**" }-- "**oauth_users**" + "**etsy_access_tokens**" ||-|| "**etsy_users**" +"**etsy_oauth_requests**" }-- "**accounts**" + "**etsy_store_events**" }-- "**raw_store_events**" "**etsy_store_events**" }-- "**raw_store_events**" @@ -76,6 +116,8 @@ entity "**wix_store_events**" { "**etsy_users**" }-- "**accounts**" +"**oauth_tokens**" }-- "**oauth_users**" + "**tiktok_store_events**" }-- "**raw_store_events**" "**tiktok_store_events**" }-- "**raw_store_events**" diff --git a/internal/domains/accounts/accounts.go b/internal/domains/accounts/accounts.go index 3785200..fe743ea 100644 --- a/internal/domains/accounts/accounts.go +++ b/internal/domains/accounts/accounts.go @@ -21,8 +21,9 @@ type ( } Account struct { - ID int64 - Email string + UserID string + ID int64 + Email string } ) @@ -39,12 +40,24 @@ func (db *Store) WithContext(ctx context.Context) *StoreWithContext { } } -func (db *Store) CreateAccount(ctx context.Context, email string) (Account, error) { +func (db *Store) CreateAccount(ctx context.Context, userID, email string) (Account, error) { rows, err := db.db.Query( ctx, - "INSERT INTO accounts (email) VALUES (@email) RETURNING account_id", + ` + INSERT INTO accounts ( + user_id, + email + ) + VALUES ( + @user_id, + @email + ) + RETURNING + account_id + `, pgx.NamedArgs{ - "email": email, + "user_id": userID, + "email": email, }, ) if err != nil { @@ -53,12 +66,16 @@ func (db *Store) CreateAccount(ctx context.Context, email string) (Account, erro acctID, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[int64]) if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return Account{}, consts.ErrNotFound + } return Account{}, fmt.Errorf("failed to scan row: %w", err) } return Account{ - ID: acctID, - Email: email, + UserID: userID, + ID: acctID, + Email: email, }, nil } @@ -89,6 +106,44 @@ func (db *Store) GetAccount(ctx context.Context, id int64) (Account, error) { }, nil } +func (db *Store) GetAccountByUserID(ctx context.Context, userID string) (Account, error) { + rows, err := db.db.Query( + ctx, + `SELECT + email, account_id + FROM + accounts + WHERE + user_id = @user_id`, + pgx.NamedArgs{ + "user_id": userID, + }, + ) + if err != nil { + return Account{}, fmt.Errorf("failed to perform query: %w", err) + } + + type Row struct { + Email string + Account_ID int64 + } + + r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return Account{}, consts.ErrNotFound + } + + return Account{}, fmt.Errorf("failed to scan row: %w", err) + } + + return Account{ + UserID: userID, + ID: r.Account_ID, + Email: r.Email, + }, nil +} + func (db *Store) GetAccountByEmail(ctx context.Context, email string) (Account, error) { rows, err := db.db.Query( ctx, @@ -116,10 +171,21 @@ func (db *Store) GetAccountByEmail(ctx context.Context, email string) (Account, }, nil } -func (db *StoreWithContext) CreateAccount(email string) (Account, error) { - return db.db.CreateAccount(db.ctx, email) +func (db *StoreWithContext) CreateAccount(userID, email string) (Account, error) { + return db.db.CreateAccount(db.ctx, userID, email) } func (db *StoreWithContext) GetAccount(id int64) (Account, error) { return db.db.GetAccount(db.ctx, id) } + +func (db *StoreWithContext) GetAccountPointerByUserID(userID string) (*Account, error) { + acct, err := db.db.GetAccountByUserID(db.ctx, userID) + if err == nil { + return &acct, nil + } + if errors.Is(err, consts.ErrNotFound) { + return nil, nil + } + return nil, err +} diff --git a/internal/domains/authentication/auth.go b/internal/domains/authentication/auth.go index fcdf499..146f411 100644 --- a/internal/domains/authentication/auth.go +++ b/internal/domains/authentication/auth.go @@ -2,7 +2,6 @@ package authentication import ( "context" - "crypto/rand" "encoding/json" "errors" "fmt" @@ -82,90 +81,7 @@ func New(ctx context.Context, db *pgxpool.Pool) (*Authenticator, error) { }, nil } -// VerifyIDToken verifies that an *oauth2.Token is a valid *oidc.IDToken. -func (a *Authenticator) VerifyIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) { - rawIDToken, ok := token.Extra("id_token").(string) - if !ok { - return nil, errors.New("no id_token field in oauth2 token") - } - - oidcConfig := &oidc.Config{ - ClientID: a.ClientID, - } - - return a.Verifier(oidcConfig).Verify(ctx, rawIDToken) -} - -// NewState creates a new state for logging in, saving it in the database. -func (a *Authenticator) NewState(ctx context.Context) ([32]byte, error) { - state, err := generateRandomState() - if err != nil { - return state, fmt.Errorf("failed to generate random state: %w", err) - } - - if _, err = a.db.Exec( - ctx, - "INSERT INTO oauth_login_states (state) VALUES (@state)", - pgx.NamedArgs{ - "state": state[:], - }, - ); err != nil { - return state, fmt.Errorf("failed to execute query: %w", err) - } - - return state, nil -} - -func generateRandomState() ([32]byte, error) { - var b [32]byte - _, err := rand.Read(b[:]) - return b, err -} - -// GetStateExpiration get's the oauth state's expiration -// func (a *Authenticator) GetStateExpiration(ctx context.Context, state [32]byte) (time.Time, error) { -func (a *Authenticator) GetStateExpiration(ctx context.Context, state string) (time.Time, error) { - rows, err := a.db.Query( - ctx, - `SELECT expiration from oauth_login_states WHERE state = ('\x' || @state)::BYTEA`, - pgx.NamedArgs{ - //"state": state[:], - "state": state, - }, - ) - if err != nil { - return time.Time{}, fmt.Errorf("failed to perform query: %w", err) - } - - exp, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[time.Time]) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return time.Time{}, consts.ErrNotFound - } - - return time.Time{}, fmt.Errorf("failed to scan row: %w", err) - } - - return exp, nil -} - -func (a *Authenticator) GetLogoutURL(requestHost string) *url.URL { - return &url.URL{ - Scheme: "https", - Host: AUTH0_DOMAIN, - Path: "/v2/logout", - RawQuery: url.Values{ - "returnTo": { - (&url.URL{ - Scheme: "https", - Host: requestHost, - }).String(), - }, - "client_id": {AUTH0_CLIENT_ID}, - }.Encode(), - } -} - +// Exchange exchanges an auth code for an access token. func (a *Authenticator) Exchange(ctx context.Context, state, code string) (accessToken string, expiration time.Time, err error) { // validate state @@ -193,8 +109,6 @@ func (a *Authenticator) Exchange(ctx context.Context, state, code string) (acces fmt.Println("ID TOKEN:", idToken) - // claims []byte - var claims map[string]any if err := idToken.Claims(&claims); err != nil { return "", time.Time{}, fmt.Errorf("Failed to obtain id token claims: %w", err) @@ -204,9 +118,30 @@ func (a *Authenticator) Exchange(ctx context.Context, state, code string) (acces claimsJSON, _ := json.Marshal(claims) + // store the token, and the potentially new user + if _, err := a.db.Exec( ctx, ` + WITH new_user AS ( + INSERT INTO oauth_users ( + user_id + ) + VALUES ( + @id_token_subject + ) + ON CONFLICT DO NOTHING + RETURNING + user_id + ), the_user AS ( + SELECT + COALESCE(user_id, user_id_2) as user_id + FROM + new_user + RIGHT JOIN + (SELECT @id_token_subject as user_id_2) + ON TRUE + ) INSERT INTO oauth_tokens ( access_token, token_type, @@ -221,9 +156,9 @@ func (a *Authenticator) Exchange(ctx context.Context, state, code string) (acces id_token_nonce, id_token_access_token_hash, - claims + claims -- might want to open this up ) - VALUES ( + SELECT @access_token, @token_type, @refresh_token, @@ -231,14 +166,15 @@ func (a *Authenticator) Exchange(ctx context.Context, state, code string) (acces @id_token_issuer, @id_token_audience, - @id_token_subject, + user_id, @id_token_expiry, @id_token_issued_at, @id_token_nonce, @id_token_access_token_hash, @claims - ) + FROM + the_user `, pgx.NamedArgs{ "access_token": token.AccessToken, @@ -263,51 +199,33 @@ func (a *Authenticator) Exchange(ctx context.Context, state, code string) (acces return token.AccessToken, token.Expiry.UTC(), nil } -// TODO: need to automatically clean up expired tokens -func (a *Authenticator) DeleteOAuthTokens(ctx context.Context, accessToken string) error { - _, err := a.db.Exec(ctx, "DELETE FROM oauth_tokens WHERE access_token = @access_token", pgx.NamedArgs{"access_token": accessToken}) - if err != nil { - return fmt.Errorf("failed to perform query: %w", err) +// VerifyIDToken verifies that an *oauth2.Token is a valid *oidc.IDToken. +func (a *Authenticator) VerifyIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) { + rawIDToken, ok := token.Extra("id_token").(string) + if !ok { + return nil, errors.New("no id_token field in oauth2 token") } - return nil + + oidcConfig := &oidc.Config{ + ClientID: a.ClientID, + } + + return a.Verifier(oidcConfig).Verify(ctx, rawIDToken) } -func (a *Authenticator) GetAccessTokenClaimsAndExpiration(ctx context.Context, accessToken string) (claims AccessTokenClaims, expiration time.Time, err error) { - rows, err := a.db.Query( - ctx, - ` - SELECT - expiry, - claims - FROM - oauth_tokens - WHERE - access_token = @access_token - `, - pgx.NamedArgs{ - "access_token": accessToken, - }, - ) - if err != nil { - return AccessTokenClaims{}, time.Time{}, fmt.Errorf("failed to perform query: %w", err) +func (a *Authenticator) GetLogoutURL(requestHost string) *url.URL { + return &url.URL{ + Scheme: "https", + Host: AUTH0_DOMAIN, + Path: "/v2/logout", + RawQuery: url.Values{ + "returnTo": { + (&url.URL{ + Scheme: "https", + Host: requestHost, + }).String(), + }, + "client_id": {AUTH0_CLIENT_ID}, + }.Encode(), } - - type Row struct { - Expiry time.Time - Claims json.RawMessage - } - - r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row]) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return AccessTokenClaims{}, time.Time{}, consts.ErrNotFound - } - return AccessTokenClaims{}, time.Time{}, fmt.Errorf("failed to scan row: %w", err) - } - - if err := json.Unmarshal(r.Claims, &claims); err != nil { - return AccessTokenClaims{}, time.Time{}, fmt.Errorf("failed to scan claims json: %w", err) - } - - return claims, r.Expiry, nil } diff --git a/internal/domains/authentication/store.go b/internal/domains/authentication/store.go new file mode 100644 index 0000000..20e7897 --- /dev/null +++ b/internal/domains/authentication/store.go @@ -0,0 +1,115 @@ +package authentication + +import ( + "context" + "crypto/rand" + "encoding/json" + "errors" + "fmt" + "ruben/inventory2/internal/consts" + "time" + + "github.com/jackc/pgx/v5" +) + +// NewState creates a new state for logging in, saving it in the database. +func (a *Authenticator) NewState(ctx context.Context) ([32]byte, error) { + state, err := generateRandomState() + if err != nil { + return state, fmt.Errorf("failed to generate random state: %w", err) + } + + if _, err = a.db.Exec( + ctx, + "INSERT INTO oauth_login_states (state) VALUES (@state)", + pgx.NamedArgs{ + "state": state[:], + }, + ); err != nil { + return state, fmt.Errorf("failed to execute query: %w", err) + } + + return state, nil +} + +func generateRandomState() ([32]byte, error) { + var b [32]byte + _, err := rand.Read(b[:]) + return b, err +} + +// GetStateExpiration get's the oauth state's expiration +// func (a *Authenticator) GetStateExpiration(ctx context.Context, state [32]byte) (time.Time, error) { +func (a *Authenticator) GetStateExpiration(ctx context.Context, state string) (time.Time, error) { + rows, err := a.db.Query( + ctx, + `SELECT expiration from oauth_login_states WHERE state = ('\x' || @state)::BYTEA`, + pgx.NamedArgs{ + //"state": state[:], + "state": state, + }, + ) + if err != nil { + return time.Time{}, fmt.Errorf("failed to perform query: %w", err) + } + + exp, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[time.Time]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return time.Time{}, consts.ErrNotFound + } + + return time.Time{}, fmt.Errorf("failed to scan row: %w", err) + } + + return exp, nil +} + +// TODO: need to automatically clean up expired tokens +func (a *Authenticator) DeleteOAuthTokens(ctx context.Context, accessToken string) error { + _, err := a.db.Exec(ctx, "DELETE FROM oauth_tokens WHERE access_token = @access_token", pgx.NamedArgs{"access_token": accessToken}) + if err != nil { + return fmt.Errorf("failed to perform query: %w", err) + } + return nil +} + +func (a *Authenticator) GetAccessTokenClaimsAndExpiration(ctx context.Context, accessToken string) (claims AccessTokenClaims, expiration time.Time, err error) { + rows, err := a.db.Query( + ctx, + ` + SELECT + expiry, + claims + FROM + oauth_tokens + WHERE + access_token = @access_token + `, + pgx.NamedArgs{ + "access_token": accessToken, + }, + ) + if err != nil { + return AccessTokenClaims{}, time.Time{}, fmt.Errorf("failed to perform query: %w", err) + } + + type Row struct { + Expiry time.Time + Claims json.RawMessage + } + + r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return AccessTokenClaims{}, time.Time{}, consts.ErrNotFound + } + return AccessTokenClaims{}, time.Time{}, fmt.Errorf("failed to scan row: %w", err) + } + + if err := json.Unmarshal(r.Claims, &claims); err != nil { + return AccessTokenClaims{}, time.Time{}, fmt.Errorf("failed to scan claims json: %w", err) + } + + return claims, r.Expiry, nil +} diff --git a/internal/site/accounts.go b/internal/site/accounts.go new file mode 100644 index 0000000..ac6385a --- /dev/null +++ b/internal/site/accounts.go @@ -0,0 +1,27 @@ +package site + +import ( + "fmt" + "net/http" +) + +// POST /accounts +func (s *Server) createAccount(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + email := r.FormValue("email") + if email == "" { + http.Error(w, "no email provided", http.StatusBadRequest) + return + } + + userID := getAccessTokenClaims(ctx).Subject + + acct, err := s.accts.CreateAccount(ctx, userID, email) + if err != nil { + http.Error(w, fmt.Sprintf("failed to create account: %v", err), http.StatusInternalServerError) + return + } + + //http.Redirect(w, r, fmt.Sprintf("/site/accounts/%d", acct.ID), http.StatusSeeOther) + http.Redirect(w, r, fmt.Sprintf("/accounts/%d", acct.ID), http.StatusSeeOther) +} diff --git a/internal/site/auth.go b/internal/site/auth.go index d4549ce..4a3fb31 100644 --- a/internal/site/auth.go +++ b/internal/site/auth.go @@ -13,7 +13,7 @@ import ( // just keep this around long enough for testing auth middleware.. func (s *Server) testAuthEndpoint(w http.ResponseWriter, r *http.Request) { - fmt.Println("SUCCESS:", getCustomClaims(r.Context())) + fmt.Println("SUCCESS:", getAccessTokenClaims(r.Context())) http.Redirect(w, r, "/", http.StatusTemporaryRedirect) } @@ -48,12 +48,36 @@ func (s *Server) authenticate(h http.Handler) http.Handler { return } - h.ServeHTTP(w, r.WithContext(context.WithValue(ctx, customClaimsKey{}, claims))) + h.ServeHTTP(w, r.WithContext(setAccessTokenClaims(ctx, claims))) }) } +func (s *Server) getAccessTokenClaims(r *http.Request) (authentication.AccessTokenClaims, bool) { + ck, err := r.Cookie("access_token") + if err != nil { + return authentication.AccessTokenClaims{}, false + } + + ctx := r.Context() + + claims, expiration, err := s.auth.GetAccessTokenClaimsAndExpiration(ctx, ck.Value) + if err != nil { + return authentication.AccessTokenClaims{}, false + } + if expiration.Before(time.Now()) { + return authentication.AccessTokenClaims{}, false + } + + return claims, true +} + +// stores custom claims in request context +func setAccessTokenClaims(ctx context.Context, claims authentication.AccessTokenClaims) context.Context { + return context.WithValue(ctx, customClaimsKey{}, claims) +} + // get custom claims from request context -func getCustomClaims(ctx context.Context) authentication.AccessTokenClaims { +func getAccessTokenClaims(ctx context.Context) authentication.AccessTokenClaims { c, _ := ctx.Value(customClaimsKey{}).(authentication.AccessTokenClaims) return c } diff --git a/internal/site/login.go b/internal/site/login.go index 8651eb9..daa1e52 100644 --- a/internal/site/login.go +++ b/internal/site/login.go @@ -48,20 +48,19 @@ func (s *Server) loginCallback(w http.ResponseWriter, r *http.Request) { accessToken, expiration, err := s.auth.Exchange(ctx, q.Get("state"), q.Get("code")) if err != nil { - http.Error(w, "Failed to exchange an authorization code for a token", http.StatusUnauthorized) + http.Error(w, fmt.Sprintf("Failed to exchange an authorization code for a token: %v", err), http.StatusUnauthorized) return } // set access_token cookie and redirect to a reasonable place w.Header().Set("Set-Cookie", (&http.Cookie{ - Name: "access_token", - Value: accessToken, - Path: "/", - Expires: expiration, - MaxAge: 0, // using Expiration instead - Secure: true, - SameSite: http.SameSiteStrictMode, + Name: "access_token", + Value: accessToken, + Path: "/", + Expires: expiration, + MaxAge: 0, // using Expiration instead + Secure: true, }).String()) http.Redirect(w, r, "/", http.StatusTemporaryRedirect) diff --git a/internal/site/site.go b/internal/site/site.go index 3a689ae..fd35052 100644 --- a/internal/site/site.go +++ b/internal/site/site.go @@ -93,15 +93,12 @@ func NewServer( s.mux.HandleFunc("GET /login", s.loginPage) s.mux.HandleFunc("GET /login/callback", s.loginCallback) s.mux.HandleFunc("GET /logout", s.logoutPage) + s.mux.Handle("POST /accounts", s.authenticate(http.HandlerFunc(s.createAccount))) // TODO: eliminate once no longer used. s.mux.HandleFunc("POST /login", s.login) - // TODO: when a user is created, we should make an account for them that is associated with their openid subject. - // - then this can go away - s.mux.HandleFunc("POST /accounts", s.createAccount) - - // TODO: test the new auth middleware + // TODO: get rid of this, once we're confident this isn't needed... s.mux.Handle("GET /test-auth", s.authenticate(http.HandlerFunc(s.testAuthEndpoint))) // webpage content @@ -116,6 +113,7 @@ func NewServer( }))) s.mux.Handle("GET /styles/", http.StripPrefix("/styles", http.FileServer(http.Dir(contentDir+"/styles")))) + // TODO: put auth on individual templates, somehow... s.mux.HandleFunc("GET /", s.serveTemplates) return s @@ -125,22 +123,3 @@ func NewServer( func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.mux.ServeHTTP(w, r) } - -// POST /accounts -func (s *Server) createAccount(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - email := r.FormValue("email") - if email == "" { - http.Error(w, "no email provided", http.StatusBadRequest) - return - } - - acct, err := s.accts.CreateAccount(ctx, email) - if err != nil { - http.Error(w, fmt.Sprintf("failed to create account: %v", err), http.StatusInternalServerError) - return - } - - //http.Redirect(w, r, fmt.Sprintf("/site/accounts/%d", acct.ID), http.StatusSeeOther) - http.Redirect(w, r, fmt.Sprintf("/accounts/%d", acct.ID), http.StatusSeeOther) -} diff --git a/internal/site/templates.go b/internal/site/templates.go index e886d26..10ca0cd 100644 --- a/internal/site/templates.go +++ b/internal/site/templates.go @@ -7,12 +7,25 @@ import ( "path" "path/filepath" "strings" + + "ruben/inventory2/internal/domains/accounts" ) // GET / func (s *Server) serveTemplates(w http.ResponseWriter, r *http.Request) { ctx := r.Context() name, pathParams := getPageTemplateNameForURL(r.URL) + + var ( + acct accounts.Account + userID string + ) + claims, ok := s.getAccessTokenClaims(r) + if ok { + userID = claims.Subject + acct, _ = s.accts.GetAccountByUserID(ctx, userID) + } + b, err := s.templater.ExecutePage( name, "Request", @@ -28,6 +41,14 @@ func (s *Server) serveTemplates(w http.ResponseWriter, r *http.Request) { s.accts.WithContext(ctx), "Etsy", s.etsy.WithContext(ctx), + + // claims + "Claims", + claims, + "UserID", + userID, + "Account", + acct, ) if err != nil { // TODO: handle 'not found' as a 404? diff --git a/internal/site/templates/component_bodies/nav_bar.html.tmpl b/internal/site/templates/component_bodies/nav_bar.html.tmpl index 4b460d0..bcbf639 100644 --- a/internal/site/templates/component_bodies/nav_bar.html.tmpl +++ b/internal/site/templates/component_bodies/nav_bar.html.tmpl @@ -1,3 +1,5 @@ +{{/* "dot" . */}} + diff --git a/internal/site/templates/layout.html.tmpl b/internal/site/templates/layout.html.tmpl index 48181f4..a15bafa 100644 --- a/internal/site/templates/layout.html.tmpl +++ b/internal/site/templates/layout.html.tmpl @@ -1,7 +1,9 @@ - WIP + + {{ block "title" . }} Inventory++ {{ end }} + diff --git a/internal/site/templates/page_bodies/account-creation.html.tmpl b/internal/site/templates/page_bodies/account-creation.html.tmpl new file mode 100644 index 0000000..6e32005 --- /dev/null +++ b/internal/site/templates/page_bodies/account-creation.html.tmpl @@ -0,0 +1,16 @@ +{{- define "title" }} Inventory++ Create an Account {{ end }} + +{{- componentBody "nav_bar" "dot" . }} + +{{/* TODO: will need to verify the email address */}} + +
+
+ + + +
+
diff --git a/internal/site/templates/page_bodies/accounts/{acctID}/index.html.tmpl b/internal/site/templates/page_bodies/accounts/{acctID}/index.html.tmpl index 65f5c05..66b3462 100644 --- a/internal/site/templates/page_bodies/accounts/{acctID}/index.html.tmpl +++ b/internal/site/templates/page_bodies/accounts/{acctID}/index.html.tmpl @@ -1,19 +1,19 @@ -{{ componentBody "nav_bar" }} +{{- define "title" }} Inventory++ Account {{ end }} + +{{- componentBody "nav_bar" "dot" . }} -{{- $acctID := parseInt64 .PathParams.acctID }} -{{- $acct := (.Accounts.GetAccount $acctID) }} +

Account: {{ .Account.Email }}

-

Account: {{ $acct.Email }} (id: {{ $acctID }})

- -{{- $etsyUser := .Etsy.GetUserPointerByAccountID $acctID }} +{{- $etsyUser := .Etsy.GetUserPointerByAccountID .Account.ID }} {{- if $etsyUser }}

Etsy User: {{ $etsyUser.UserID }}; Shop ID: {{ $etsyUser.ShopID }}

{{- else }}

- + {{/* TODO: create this link dynamically, not EVERYTIME THE PAGE IS LOADED */}} + Link Your Etsy Store!

{{- end }} -

View Reports

+

View Reports

diff --git a/internal/site/templates/page_bodies/accounts/{acctID}/inventory.html.tmpl b/internal/site/templates/page_bodies/accounts/{acctID}/inventory.html.tmpl new file mode 100644 index 0000000..91b87f0 --- /dev/null +++ b/internal/site/templates/page_bodies/accounts/{acctID}/inventory.html.tmpl @@ -0,0 +1,6 @@ +{{- define "title" }} Inventory++ {{ end }} + +{{ componentBody "nav_bar" "dot" . }} + + +

Inventory management page: WIP

diff --git a/internal/site/templates/page_bodies/accounts/{acctID}/reports.html.tmpl b/internal/site/templates/page_bodies/accounts/{acctID}/reports.html.tmpl index 570787e..6e2068d 100644 --- a/internal/site/templates/page_bodies/accounts/{acctID}/reports.html.tmpl +++ b/internal/site/templates/page_bodies/accounts/{acctID}/reports.html.tmpl @@ -1,4 +1,6 @@ -{{ componentBody "nav_bar" }} +{{- define "title" }} Inventory++ Reports {{ end }} + +{{ componentBody "nav_bar" "dot" . }} {{ $storeID := .Request.URL.Query.Get "store-id" }} diff --git a/internal/site/templates/page_bodies/index.html.tmpl b/internal/site/templates/page_bodies/index.html.tmpl index 4f9510d..f15ec4c 100644 --- a/internal/site/templates/page_bodies/index.html.tmpl +++ b/internal/site/templates/page_bodies/index.html.tmpl @@ -1,7 +1,10 @@ -{{ componentBody "nav_bar" }} +{{ componentBody "nav_bar" "dot" . }}

Home

-

Sign Up!

+{{- if and .UserID (not .Account.ID) }} +

New Account

+{{- end }} -

Test Auth

+ +Test Auth diff --git a/internal/site/templates/page_bodies/login.html.tmpl b/internal/site/templates/page_bodies/login.html.tmpl index 50865b3..d52ee76 100644 --- a/internal/site/templates/page_bodies/login.html.tmpl +++ b/internal/site/templates/page_bodies/login.html.tmpl @@ -1,4 +1,6 @@ -{{ componentBody "nav_bar" }} +{{/* TODO: delete this page, when certain it's not wanted anymore */}} + +{{ componentBody "nav_bar" "dot" . }}

Log In

diff --git a/internal/site/templates/page_bodies/sign-up.html.tmpl b/internal/site/templates/page_bodies/sign-up.html.tmpl index 632a03c..6302b33 100644 --- a/internal/site/templates/page_bodies/sign-up.html.tmpl +++ b/internal/site/templates/page_bodies/sign-up.html.tmpl @@ -1,4 +1,6 @@ -{{ componentBody "nav_bar" }} +{{/* TODO: not used - read for deletion */}} + +{{ componentBody "nav_bar" "dot" . }}

Sign Up