Claude-assisted improvements (untested)

- dev auth flow (side-step OAuth)
- db event processing integration tests
- dev scripts (eg Makefile)
- db / test db migration setup scripts.
This commit is contained in:
2026-08-20 00:35:22 -06:00
parent 50adc8e2de
commit 6a473b2ea0
35 changed files with 1242 additions and 144 deletions
+11 -24
View File
@@ -17,26 +17,11 @@ import (
"ruben/inventory2/logging"
)
// TODO: move these to a config?
const (
// The URL of our Auth0 Tenant Domain.
// If you're using a Custom Domain, be sure to set this to that value instead.
AUTH0_DOMAIN = "dev-uq3gqy5bdnwxmr6d.us.auth0.com"
// Our Auth0 application"s Client ID.
AUTH0_CLIENT_ID = "JEjrXTQ9fxlTLgp9RgTIACpUk8a2lqNT"
// Our Auth0 application"s Client Secret.
AUTH0_CLIENT_SECRET = "83U-iWdVaNnwk9XDzteo_2VMyOq_l1siKYqg1_2E7jCzgL8MnkaxlysPMcPMGlxA"
// The Callback URL of our application.
AUTH0_CALLBACK_URL = "https://inventory-plus-plus.com/api/auth/login/callback"
)
type (
// Authenticator is used to authenticate our users.
Authenticator struct {
log *logging.Logger
log *logging.Logger
domain string
*oidc.Provider
oauth2.Config
db *pgxpool.Pool
@@ -65,10 +50,11 @@ func New(
ctx context.Context,
db *pgxpool.Pool,
logger *logging.Logger,
domain, clientID, clientSecret, callbackURL string,
) (*Authenticator, error) {
provider, err := oidc.NewProvider(
ctx,
"https://"+AUTH0_DOMAIN+"/",
"https://"+domain+"/",
)
if err != nil {
return nil, err
@@ -76,11 +62,12 @@ func New(
return &Authenticator{
log: logger,
domain: domain,
Provider: provider,
Config: oauth2.Config{
ClientID: AUTH0_CLIENT_ID,
ClientSecret: AUTH0_CLIENT_SECRET,
RedirectURL: AUTH0_CALLBACK_URL,
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: callbackURL,
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile"},
},
@@ -244,7 +231,7 @@ func (a *Authenticator) VerifyIDToken(ctx context.Context, token *oauth2.Token)
func (a *Authenticator) GetLogoutURL(requestHost string) *url.URL {
return &url.URL{
Scheme: "https",
Host: AUTH0_DOMAIN,
Host: a.domain,
Path: "/v2/logout",
RawQuery: url.Values{
"returnTo": {
@@ -253,7 +240,7 @@ func (a *Authenticator) GetLogoutURL(requestHost string) *url.URL {
Host: requestHost,
}).String(),
},
"client_id": {AUTH0_CLIENT_ID},
"client_id": {a.Config.ClientID},
}.Encode(),
}
}
@@ -267,7 +254,7 @@ func (a *Authenticator) RefreshAccessToken(
err error,
) {
refreshToken, tokenType, err := a.getRefreshTokenForAccessToken(ctx, accessToken)
refreshToken, tokenType, err := a.getRefreshTokenForAccessToken(ctx, oldAccessToken)
if err != nil {
return "", time.Time{}, fmt.Errorf("failed to load refresh token: %w", err)
}
+97
View File
@@ -0,0 +1,97 @@
package authentication
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
// DevLogin mints a local session for userID without going through the real
// Auth0 OAuth flow. It writes a real oauth_users/oauth_tokens row, so every
// other code path (identity lookup, account linking/creation, cookie
// handling) treats it exactly like a normal login.
//
// Only ever call this from a route gated on an explicit dev-mode flag -
// never wire it up unconditionally, since it lets the caller authenticate
// as any user_id with no credentials.
func (a *Authenticator) DevLogin(ctx context.Context, userID, name string) (accessToken string, expiration time.Time, err error) {
tokenBytes := make([]byte, 32)
if _, err := rand.Read(tokenBytes); err != nil {
return "", time.Time{}, fmt.Errorf("failed to generate access token: %w", err)
}
accessToken = "dev_" + hex.EncodeToString(tokenBytes)
// Set far in the future so the near-expiry refresh path in
// server/auth never tries to refresh a token that has no real
// Auth0-side refresh token behind it.
expiration = time.Now().Add(365 * 24 * time.Hour)
if _, err = a.db.Exec(
ctx,
`
WITH ensured_user AS (
INSERT INTO oauth_users (user_id)
VALUES (@user_id)
ON CONFLICT DO NOTHING
)
INSERT INTO oauth_tokens (
access_token,
token_type,
refresh_token,
expiry,
id_token_issuer,
id_token_audience,
id_token_subject,
id_token_expiry,
id_token_issued_at,
id_token_nonce,
id_token_access_token_hash,
id_token_custom_claims_family_name,
id_token_custom_claims_given_name,
id_token_custom_claims_name,
id_token_custom_claims_nickname,
id_token_custom_claims_picture,
id_token_custom_claims_updated_at
)
VALUES (
@access_token,
'dev',
'',
@expiry,
'dev-auth-bypass',
@audience,
@user_id,
@expiry,
NOW(),
'',
'',
@name,
@name,
@name,
@name,
'',
NOW()
)
`,
pgx.NamedArgs{
"access_token": accessToken,
"expiry": expiration,
"user_id": userID,
"name": name,
"audience": pgtype.FlatArray[string]{"dev"},
},
); err != nil {
return "", time.Time{}, fmt.Errorf("failed to save dev session: %w", err)
}
return accessToken, expiration, nil
}
+95
View File
@@ -0,0 +1,95 @@
package authentication
import (
"context"
"errors"
"testing"
"time"
"ruben/inventory2/consts"
"ruben/inventory2/internal/testdb"
)
func TestDevLogin(t *testing.T) {
pool := testdb.Pool(t)
auth := &Authenticator{db: pool}
ctx := context.Background()
userID := testdb.NewUserID(t)
t.Cleanup(func() {
pool.Exec(context.Background(), "DELETE FROM oauth_tokens WHERE id_token_subject = $1", userID)
pool.Exec(context.Background(), "DELETE FROM oauth_users WHERE user_id = $1", userID)
})
accessToken, expiration, err := auth.DevLogin(ctx, userID, "Test User")
if err != nil {
t.Fatalf("DevLogin() error = %v", err)
}
if accessToken == "" {
t.Fatal("DevLogin() returned an empty access token")
}
if !expiration.After(time.Now().Add(30 * 24 * time.Hour)) {
t.Errorf("DevLogin() expiration = %v, want something far enough out to avoid the near-expiry refresh path", expiration)
}
claims, err := auth.GetAccessTokenClaimsAndExpiration(ctx, accessToken)
if err != nil {
t.Fatalf("GetAccessTokenClaimsAndExpiration() error = %v", err)
}
if claims.Name != "Test User" {
t.Errorf("claims.Name = %q, want %q", claims.Name, "Test User")
}
// Postgres timestamptz has microsecond precision, so the round-tripped
// value loses the sub-microsecond portion of Go's nanosecond clock.
if diff := claims.Expiration.Sub(expiration); diff > time.Millisecond || diff < -time.Millisecond {
t.Errorf("claims.Expiration = %v, want ~%v (diff %v)", claims.Expiration, expiration, diff)
}
_, tokenType, err := auth.getRefreshTokenForAccessToken(ctx, accessToken)
if err != nil {
t.Fatalf("getRefreshTokenForAccessToken() error = %v", err)
}
if tokenType != "dev" {
t.Errorf("tokenType = %q, want %q", tokenType, "dev")
}
}
// DevLogin should be safe to call more than once for the same user_id -
// e.g. testing multiple times as the same dev identity - since oauth_users
// is keyed on user_id but each call mints its own oauth_tokens row.
func TestDevLogin_SameUserIDTwice(t *testing.T) {
pool := testdb.Pool(t)
auth := &Authenticator{db: pool}
ctx := context.Background()
userID := testdb.NewUserID(t)
t.Cleanup(func() {
pool.Exec(context.Background(), "DELETE FROM oauth_tokens WHERE id_token_subject = $1", userID)
pool.Exec(context.Background(), "DELETE FROM oauth_users WHERE user_id = $1", userID)
})
token1, _, err := auth.DevLogin(ctx, userID, "Test User")
if err != nil {
t.Fatalf("first DevLogin() error = %v", err)
}
token2, _, err := auth.DevLogin(ctx, userID, "Test User")
if err != nil {
t.Fatalf("second DevLogin() error = %v", err)
}
if token1 == token2 {
t.Fatalf("DevLogin() returned the same access token twice: %q", token1)
}
}
func TestGetAccessTokenClaimsAndExpiration_UnknownToken(t *testing.T) {
pool := testdb.Pool(t)
auth := &Authenticator{db: pool}
ctx := context.Background()
_, err := auth.GetAccessTokenClaimsAndExpiration(ctx, "no-such-token-"+testdb.NewUserID(t))
if !errors.Is(err, consts.ErrNotFound) {
t.Fatalf("GetAccessTokenClaimsAndExpiration() error = %v, want %v", err, consts.ErrNotFound)
}
}