split oauth_tokens.claims column up; improved fonts; allow multiple sse connections per user; make navbar and use friendly

This commit is contained in:
2026-01-23 02:31:34 -07:00
parent 38aa3796b9
commit 3d70d08dac
16 changed files with 1703 additions and 69 deletions
@@ -0,0 +1,13 @@
BEGIN;
ALTER TABLE oauth_tokens
DROP COLUMN id_token_custom_claims_name,
DROP COLUMN id_token_custom_claims_picture,
DROP COLUMN id_token_custom_claims_nickname,
DROP COLUMN id_token_custom_claims_given_name,
DROP COLUMN id_token_custom_claims_updated_at,
DROP COLUMN id_token_custom_claims_family_name;
COMMIT;
@@ -0,0 +1,29 @@
BEGIN;
ALTER TABLE oauth_tokens
ADD COLUMN id_token_custom_claims_name TEXT,
ADD COLUMN id_token_custom_claims_picture TEXT,
ADD COLUMN id_token_custom_claims_nickname TEXT,
ADD COLUMN id_token_custom_claims_given_name TEXT,
ADD COLUMN id_token_custom_claims_updated_at TEXT,
ADD COLUMN id_token_custom_claims_family_name TEXT;
UPDATE oauth_tokens
SET id_token_custom_claims_name = claims ->> 'name',
id_token_custom_claims_picture = claims ->> 'picture',
id_token_custom_claims_nickname = claims ->> 'nickname',
id_token_custom_claims_given_name = claims ->> 'given_name',
id_token_custom_claims_updated_at = (claims ->> 'updated_at')::TIMESTAMPTZ,
id_token_custom_claims_family_name = claims ->> 'family_name';
ALTER TABLE oauth_tokens
ALTER COLUMN id_token_custom_claims_name SET NOT NULL,
ALTER COLUMN id_token_custom_claims_picture SET NOT NULL,
ALTER COLUMN id_token_custom_claims_nickname SET NOT NULL,
ALTER COLUMN id_token_custom_claims_given_name SET NOT NULL,
ALTER COLUMN id_token_custom_claims_updated_at SET NOT NULL,
ALTER COLUMN id_token_custom_claims_family_name SET NOT NULL;
COMMIT;
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 55 KiB

+6 -1
View File
@@ -63,7 +63,12 @@ entity "**oauth_tokens**" {
*""id_token_issued_at"": //timestamp with time zone // *""id_token_issued_at"": //timestamp with time zone //
*""id_token_nonce"": //text // *""id_token_nonce"": //text //
*""id_token_access_token_hash"": //text // *""id_token_access_token_hash"": //text //
*""claims"": //jsonb // *""id_token_custom_claims_name"": //text //
*""id_token_custom_claims_picture"": //text //
*""id_token_custom_claims_nickname"": //text //
*""id_token_custom_claims_given_name"": //text //
*""id_token_custom_claims_family_name"": //text //
*""id_token_custom_claims_updated_at"": //timestamp with time zone //
} }
entity "**oauth_users**" { entity "**oauth_users**" {
+40 -13
View File
@@ -2,7 +2,6 @@ package authentication
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/url" "net/url"
@@ -131,8 +130,6 @@ func (a *Authenticator) Exchange(ctx context.Context, state, code string) (acces
return "", "", time.Time{}, err return "", "", time.Time{}, err
} }
claimsJSON, _ := json.Marshal(claims)
// store the token, and the potentially new user // store the token, and the potentially new user
if _, err := a.db.Exec( if _, err := a.db.Exec(
@@ -171,7 +168,12 @@ func (a *Authenticator) Exchange(ctx context.Context, state, code string) (acces
id_token_nonce, id_token_nonce,
id_token_access_token_hash, id_token_access_token_hash,
claims -- might want to open this up 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
) )
SELECT SELECT
@access_token, @access_token,
@@ -187,7 +189,12 @@ func (a *Authenticator) Exchange(ctx context.Context, state, code string) (acces
@id_token_nonce, @id_token_nonce,
@id_token_access_token_hash, @id_token_access_token_hash,
@claims @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
FROM FROM
the_user the_user
`, `,
@@ -205,7 +212,12 @@ func (a *Authenticator) Exchange(ctx context.Context, state, code string) (acces
"id_token_nonce": idToken.Nonce, "id_token_nonce": idToken.Nonce,
"id_token_access_token_hash": idToken.AccessTokenHash, "id_token_access_token_hash": idToken.AccessTokenHash,
"claims": json.RawMessage(claimsJSON), "id_token_custom_claims_family_name": claims.FamilyName,
"id_token_custom_claims_given_name": claims.GivenName,
"id_token_custom_claims_name": claims.Name,
"id_token_custom_claims_nickname": claims.Nickname,
"id_token_custom_claims_picture": claims.Picture,
"id_token_custom_claims_updated_at": claims.UpdatedAt,
}, },
); err != nil { ); err != nil {
return "", "", time.Time{}, fmt.Errorf("failed to perform query to save tokens: %w", err) return "", "", time.Time{}, fmt.Errorf("failed to perform query to save tokens: %w", err)
@@ -291,8 +303,6 @@ func (a *Authenticator) RefreshAccessToken(
return "", time.Time{}, err return "", time.Time{}, err
} }
claimsJSON, _ := json.Marshal(claims)
if _, err = a.db.Exec( if _, err = a.db.Exec(
ctx, ctx,
` `
@@ -318,7 +328,12 @@ func (a *Authenticator) RefreshAccessToken(
id_token_nonce, id_token_nonce,
id_token_access_token_hash, id_token_access_token_hash,
claims -- might want to open this up 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 ( VALUES (
@new_access_token, @new_access_token,
@@ -334,7 +349,12 @@ func (a *Authenticator) RefreshAccessToken(
@id_token_nonce, @id_token_nonce,
@id_token_access_token_hash, @id_token_access_token_hash,
@claims @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
) )
RETURNING RETURNING
access_token AS new_access_token access_token AS new_access_token
@@ -363,7 +383,12 @@ func (a *Authenticator) RefreshAccessToken(
"id_token_nonce": idToken.Nonce, "id_token_nonce": idToken.Nonce,
"id_token_access_token_hash": idToken.AccessTokenHash, "id_token_access_token_hash": idToken.AccessTokenHash,
"claims": json.RawMessage(claimsJSON), "id_token_custom_claims_family_name": claims.FamilyName,
"id_token_custom_claims_given_name": claims.GivenName,
"id_token_custom_claims_name": claims.Name,
"id_token_custom_claims_nickname": claims.Nickname,
"id_token_custom_claims_picture": claims.Picture,
"id_token_custom_claims_updated_at": claims.UpdatedAt,
}, },
); err != nil { ); err != nil {
return "", time.Time{}, fmt.Errorf("failed to save new access token and delete old access token: %w", err) return "", time.Time{}, fmt.Errorf("failed to save new access token and delete old access token: %w", err)
@@ -372,12 +397,14 @@ func (a *Authenticator) RefreshAccessToken(
return tkn.AccessToken, tkn.Expiry.UTC(), nil return tkn.AccessToken, tkn.Expiry.UTC(), nil
} }
func (a *Authenticator) verifyIDTokenAndClaimsFromToken(ctx context.Context, tkn *oauth2.Token) (idToken *oidc.IDToken, claims map[string]any, err error) { func (a *Authenticator) verifyIDTokenAndClaimsFromToken(ctx context.Context, tkn *oauth2.Token) (idToken *oidc.IDToken, claims *AccessTokenClaims, err error) {
if idToken, err = a.VerifyIDToken(ctx, tkn); err != nil { if idToken, err = a.VerifyIDToken(ctx, tkn); err != nil {
return nil, nil, fmt.Errorf("failed to verify id Token: %w", err) return nil, nil, fmt.Errorf("failed to verify id Token: %w", err)
} }
if err := idToken.Claims(&claims); err != nil { claims = new(AccessTokenClaims)
if err := idToken.Claims(claims); err != nil {
return nil, nil, fmt.Errorf("failed to obtain id token claims: %w", err) return nil, nil, fmt.Errorf("failed to obtain id token claims: %w", err)
} }
+21 -7
View File
@@ -3,7 +3,6 @@ package authentication
import ( import (
"context" "context"
"crypto/rand" "crypto/rand"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"ruben/inventory2/internal/consts" "ruben/inventory2/internal/consts"
@@ -102,7 +101,14 @@ func (a *Authenticator) GetAccessTokenClaimsAndExpiration(ctx context.Context, a
` `
SELECT SELECT
expiry, expiry,
claims
id_token_custom_claims_name,
id_token_custom_claims_picture,
id_token_custom_claims_nickname,
id_token_custom_claims_given_name,
id_token_custom_claims_family_name,
id_token_custom_claims_updated_at
FROM FROM
oauth_tokens oauth_tokens
WHERE WHERE
@@ -117,8 +123,13 @@ func (a *Authenticator) GetAccessTokenClaimsAndExpiration(ctx context.Context, a
} }
type Row struct { type Row struct {
Expiry time.Time Expiry time.Time
Claims json.RawMessage Id_token_custom_claims_name string
Id_token_custom_claims_picture string
Id_token_custom_claims_nickname string
Id_token_custom_claims_given_name string
Id_token_custom_claims_family_name string
Id_token_custom_claims_updated_at time.Time
} }
r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row]) r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row])
@@ -129,9 +140,12 @@ func (a *Authenticator) GetAccessTokenClaimsAndExpiration(ctx context.Context, a
return AccessTokenClaims{}, time.Time{}, fmt.Errorf("failed to scan row: %w", err) return AccessTokenClaims{}, time.Time{}, fmt.Errorf("failed to scan row: %w", err)
} }
if err := json.Unmarshal(r.Claims, &claims); err != nil { claims.Name = r.Id_token_custom_claims_name
return AccessTokenClaims{}, time.Time{}, fmt.Errorf("failed to scan claims json: %w", err) claims.Picture = r.Id_token_custom_claims_picture
} claims.Nickname = r.Id_token_custom_claims_nickname
claims.GivenName = r.Id_token_custom_claims_given_name
claims.FamilyName = r.Id_token_custom_claims_family_name
claims.UpdatedAt = r.Id_token_custom_claims_updated_at
return claims, r.Expiry, nil return claims, r.Expiry, nil
} }
+30 -9
View File
@@ -11,12 +11,16 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
const (
maxNumOpenConnectionsPerUser = 3
)
type ( type (
sseRouter struct { sseRouter struct {
log *logging.Logger log *logging.Logger
sse *sse.Queue sse *sse.Queue
users map[string]context.CancelFunc users map[string][maxNumOpenConnectionsPerUser]context.CancelFunc
lock sync.Mutex lock sync.Mutex
} }
) )
@@ -30,7 +34,7 @@ func Routes(
s := &sseRouter{ s := &sseRouter{
log: logger, log: logger,
sse: sq, sse: sq,
users: make(map[string]context.CancelFunc), users: make(map[string][maxNumOpenConnectionsPerUser]context.CancelFunc),
} }
r.GET("/", auth.AuthenticateAndAddIdentityGin(), response.Handler(s.serveEvents)) r.GET("/", auth.AuthenticateAndAddIdentityGin(), response.Handler(s.serveEvents))
@@ -46,10 +50,11 @@ func (r *sseRouter) serveEvents(c *gin.Context) (response.Response, error) {
"accountID", acctID, "accountID", acctID,
"userID", userID, "userID", userID,
"email", email, "email", email,
"userAgent", c.Request.UserAgent(),
) )
log.Info("user connected to sse queue") log.Info("user connected to sse queue")
ctx := r.closeExistingConnectionsForUserAndStoreCancelFuncForUser(c, userID) ctx := r.closeOutstandingConnectionsForUserAndStoreCancelFuncForUser(c, userID)
w := c.Writer w := c.Writer
@@ -80,18 +85,34 @@ func (r *sseRouter) serveEvents(c *gin.Context) (response.Response, error) {
return response.Status(200), nil return response.Status(200), nil
} }
func (r *sseRouter) closeExistingConnectionsForUserAndStoreCancelFuncForUser(ctx context.Context, userID string) context.Context { func (r *sseRouter) closeOutstandingConnectionsForUserAndStoreCancelFuncForUser(ctx context.Context, userID string) context.Context {
r.lock.Lock() r.lock.Lock()
defer r.lock.Unlock() defer r.lock.Unlock()
ctx, cancel := context.WithCancel(ctx)
closeConnFuncs := r.users[userID]
defer func() {
r.users[userID] = closeConnFuncs
}()
// close existing connection // close existing connection
if closeConn, ok := r.users[userID]; ok {
closeConn() for i := range maxNumOpenConnectionsPerUser {
delete(r.users, userID) if fn := closeConnFuncs[i]; fn == nil {
// not hit limit on connections.
// save the cancellation func and done
closeConnFuncs[i] = cancel
return ctx
}
} }
// store reference to cancel func // close the oldest connection, shift all cancellation funcs down, and push the new one in
ctx, r.users[userID] = context.WithCancel(ctx) closeConnFuncs[0]()
for i := range maxNumOpenConnectionsPerUser - 1 {
closeConnFuncs[i] = closeConnFuncs[i+1]
}
closeConnFuncs[maxNumOpenConnectionsPerUser-1] = cancel
return ctx return ctx
} }
File diff suppressed because it is too large Load Diff
+62 -14
View File
@@ -16,8 +16,7 @@
--text-xl: 1.25rem; --text-xl: 1.25rem;
--text-xl--line-height: calc(1.75 / 1.25); --text-xl--line-height: calc(1.75 / 1.25);
--text-3xl: 1.875rem; --text-3xl: 1.875rem;
--text-5xl: 3rem; --text-6xl: 3.75rem;
--text-8xl: 6rem;
--font-weight-semibold: 600; --font-weight-semibold: 600;
--font-weight-bold: 700; --font-weight-bold: 700;
--radius-lg: var(--radius); --radius-lg: var(--radius);
@@ -187,6 +186,30 @@
.static { .static {
position: static; position: static;
} }
.col-2 {
grid-column: 2;
}
.col-3 {
grid-column: 3;
}
.container {
width: 100%;
@media (width >= 40rem) {
max-width: 40rem;
}
@media (width >= 48rem) {
max-width: 48rem;
}
@media (width >= 64rem) {
max-width: 64rem;
}
@media (width >= 80rem) {
max-width: 80rem;
}
@media (width >= 96rem) {
max-width: 96rem;
}
}
.m-\[1em\] { .m-\[1em\] {
margin: 1em; margin: 1em;
} }
@@ -220,12 +243,18 @@
.flex { .flex {
display: flex; display: flex;
} }
.grid {
display: grid;
}
.hidden { .hidden {
display: none; display: none;
} }
.table { .table {
display: table; display: table;
} }
.max-h-full {
max-height: 100%;
}
.min-h-full { .min-h-full {
min-height: 100%; min-height: 100%;
} }
@@ -244,6 +273,12 @@
.grow { .grow {
flex-grow: 1; flex-grow: 1;
} }
.grow-1 {
flex-grow: 1;
}
.basis-\[fit-content\] {
flex-basis: fit-content;
}
.basis-full { .basis-full {
flex-basis: 100%; flex-basis: 100%;
} }
@@ -253,6 +288,9 @@
.resize { .resize {
resize: both; resize: both;
} }
.grid-cols-\[6rem_10fr_6rem\] {
grid-template-columns: 6rem 10fr 6rem;
}
.flex-col { .flex-col {
flex-direction: column; flex-direction: column;
} }
@@ -271,8 +309,8 @@
.overflow-x-auto { .overflow-x-auto {
overflow-x: auto; overflow-x: auto;
} }
.overflow-x-scroll { .overflow-x-hidden {
overflow-x: scroll; overflow-x: hidden;
} }
.overflow-y-hidden { .overflow-y-hidden {
overflow-y: hidden; overflow-y: hidden;
@@ -302,9 +340,6 @@
.p-\[1em\] { .p-\[1em\] {
padding: 1em; padding: 1em;
} }
.p-\[2em\] {
padding: 2em;
}
.pt-\[1em\] { .pt-\[1em\] {
padding-top: 1em; padding-top: 1em;
} }
@@ -314,6 +349,9 @@
.text-center { .text-center {
text-align: center; text-align: center;
} }
.font-display {
font-family: var(--display-family);
}
.text-lg { .text-lg {
font-size: var(--text-lg); font-size: var(--text-lg);
line-height: var(--tw-leading, var(--text-lg--line-height)); line-height: var(--tw-leading, var(--text-lg--line-height));
@@ -380,6 +418,16 @@
text-decoration-line: none; text-decoration-line: none;
} }
} }
.min-\[400px\]\:px-\[calc\(10vw-0\.5em\)\] {
@media (width >= 400px) {
padding-inline: calc(10vw - 0.5em);
}
}
.min-\[400px\]\:py-\[calc\(8vw-0\.5em\)\] {
@media (width >= 400px) {
padding-block: calc(8vw - 0.5em);
}
}
} }
@layer base { @layer base {
select { select {
@@ -560,23 +608,23 @@
font-weight: var(--display-weight); font-weight: var(--display-weight);
} }
h1 { h1 {
font-size: var(--text-8xl); font-size: var(--text-6xl);
} }
h2 { h2 {
font-size: var(--text-5xl);
}
h3 {
font-size: var(--text-3xl); font-size: var(--text-3xl);
} }
h4 { h3 {
font-size: var(--text-xl); font-size: var(--text-xl);
} }
h5 { h4 {
font-size: var(--text-lg); font-size: var(--text-lg);
} }
h6 { h5 {
font-size: var(--text-md); font-size: var(--text-md);
} }
h6 {
font-size: var(--text-sm);
}
ul, ol { ul, ol {
list-style: none; list-style: none;
} }
+7 -7
View File
@@ -272,23 +272,23 @@
} }
h1 { h1 {
font-size: var(--text-8xl); font-size: var(--text-6xl);
} }
h2 { h2 {
font-size: var(--text-5xl);
}
h3 {
font-size: var(--text-3xl); font-size: var(--text-3xl);
} }
h4 { h3 {
font-size: var(--text-xl); font-size: var(--text-xl);
} }
h5 { h4 {
font-size: var(--text-lg); font-size: var(--text-lg);
} }
h6 { h5 {
font-size: var(--text-md); font-size: var(--text-md);
} }
h6 {
font-size: var(--text-sm);
}
ul, ol { ul, ol {
list-style: none; list-style: none;
@@ -5,6 +5,7 @@
"HXPost" (printf "/api/accounts/%d/inventory/sync-groups/draft/listings" $acctID) "HXPost" (printf "/api/accounts/%d/inventory/sync-groups/draft/listings" $acctID)
"HXTarget" "#accounts-acct-id-inventory-sync-groups-draft-table" "HXTarget" "#accounts-acct-id-inventory-sync-groups-draft-table"
"HXSwap" "outerHTML" "HXSwap" "outerHTML"
"HXSelect" "#accounts-acct-id-inventory-sync-groups-draft-table"
"ID" "sync-groups-draft-table-add-listing-button" "ID" "sync-groups-draft-table-add-listing-button"
"Class" "mt-[1em] mb-[1em]" "Class" "mt-[1em] mb-[1em]"
"Text" "Add Listing" "Text" "Add Listing"
@@ -8,7 +8,9 @@
id="accounts-acct-id-inventory-sync-groups-draft-table" id="accounts-acct-id-inventory-sync-groups-draft-table"
class="max-w-full overflow-x-auto" class="max-w-full overflow-x-auto"
hx-get="{{ printf "/ui/accounts/%d/inventory/sync-groups/draft/table" $acctID }}" hx-get="{{ printf "/ui/accounts/%d/inventory/sync-groups/draft/table" $acctID }}"
hx-trigger="sse:{{ printf "accounts_%d_inventory_sync-groups_draft" $acctID }}" hx-trigger="sse:{{ printf "accounts_%d_inventory_sync-groups_draft" $acctID }},sse:{{ printf "accounts_%d_inventory_sync-groups_draft_listings" $acctID }}"
hx-select="#accounts-acct-id-inventory-sync-groups-draft-table"
hx-swap="outerHTML"
> >
<thead> <thead>
<tr> <tr>
@@ -30,6 +32,7 @@
</tr> </tr>
</thead> </thead>
<tbody <tbody
hx-swap="morph"
_=" _="
init init
set element allRowsFilledOut to false set element allRowsFilledOut to false
+4 -1
View File
@@ -1,4 +1,4 @@
{{/* .HXGet | .HXPost | .HXDelete, .HXTrigger, .HXTarget, .HXSwap, .HXVals, ._, .Text, .ID, .Class, .Disabled */}} {{/* .HXGet | .HXPost | .HXDelete, .HXTrigger, .HXTarget, .HXSelect, .HXSwap, .HXVals, ._, .Text, .ID, .Class, .Disabled */}}
<button <button
@@ -32,6 +32,9 @@
{{- if .HXTarget }} {{- if .HXTarget }}
hx-target="{{.HXTarget}}" hx-target="{{.HXTarget}}"
{{- end }} {{- end }}
{{- if .HXSelect }}
hx-select="{{.HXSelect}}"
{{- end }}
{{- if .HXSwap }} {{- if .HXSwap }}
hx-swap="{{.HXSwap}}" hx-swap="{{.HXSwap}}"
{{- end }} {{- end }}
+66 -13
View File
@@ -13,19 +13,27 @@
<script src="/scripts/_hyperscript.min.js.gz"></script> <script src="/scripts/_hyperscript.min.js.gz"></script>
<script src="/scripts/htmx-ext-sse.js"></script> <script src="/scripts/htmx-ext-sse.js"></script>
<script src="/scripts/htmx-ext-path-params.js"></script> <script src="/scripts/htmx-ext-path-params.js"></script>
<script src="/scripts/idiomorph-ext.js"></script>
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
<link rel="manifest" href="/favicon/site.webmanifest"> <link rel="manifest" href="/favicon/site.webmanifest">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Alexandria:wght@100..900&family=Geist:wght@100..900&family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap" rel="stylesheet">
{{/* This can be set by the template in ./pages being rendered */}} {{/* This can be set by the template in ./pages being rendered */}}
{{- block "head" . }}{{ end }} {{- block "head" . }}{{ end }}
</head> </head>
<body <body
class="basis-full grow bg-background flex flex-col items-stretch" class="basis-full grow bg-background flex flex-col items-stretch"
hx-ext="path-params,sse" hx-ext="morph,path-params,sse"
{{/* get requests to just replace the body - until we start replacing the head, this will be good */}}
hx-boost="true"
{{/* subscribe to sse (if logged in) */}} {{/* subscribe to sse (if logged in) */}}
{{- if and .Identity .Identity.AccessToken }} {{- if and .Identity .Identity.AccessToken }}
@@ -34,12 +42,35 @@
{{- end }} {{- end }}
> >
<header> <header>
<nav class="flex flex-col items-center text-xl overflow-x-scroll overflow-y-hidden "> <nav
class="
w-full
text-xl
overflow-x-hidden
overflow-y-hidden
flex
justify-center
"
>
{{- $path := or .Request.URL.Path "/" }} {{- $path := or .Request.URL.Path "/" }}
<ul class="flex text-nowrap overflow-x-scroll overflow-y-hidden"> <ul
hx-swap="morph:innerHTML"
class="
max-w-full
overflow-x-auto
overflow-y-hidden
text-nowrap
flex
basis-[fit-content]
"
style="
"
>
<li class="pt-[1em] pb-[1em]"> <li class="pt-[1em] pb-[1em]">
<a href="/ui" class="p-[1em] font-bold {{ if eq $path "/" }}selected{{ end }}"> <a href="/ui" class="p-[1em] font-display {{ if eq $path "/" }}selected{{ end }}">
Home Home
</a> </a>
</li> </li>
@@ -55,7 +86,7 @@
{{- if not $userID }} {{- if not $userID }}
<li class="pt-[1em] pb-[1em]"> <li class="pt-[1em] pb-[1em]">
<a href="/api/auth/login" class="p-[1em] font-bold {{ if eq $path "/auth/login" }}selected{{ end }}"> <a href="/api/auth/login" hx-boost="false" class="p-[1em] font-display {{ if eq $path "/auth/login" }}selected{{ end }}">
Log In / Sign Up Log In / Sign Up
</a> </a>
</li> </li>
@@ -63,25 +94,25 @@
{{- else if $acctID }} {{- else if $acctID }}
<li class="pt-[1em] pb-[1em]"> <li class="pt-[1em] pb-[1em]">
<a href="/ui/accounts/{{$acctID}}" class="p-[1em] font-bold {{ if eq $path (printf "/accounts/%d" $acctID) }}selected{{ end }}"> <a href="/ui/accounts/{{$acctID}}" class="p-[1em] font-display {{ if eq $path (printf "/accounts/%d" $acctID) }}selected{{ end }}">
Account Account
</a> </a>
</li> </li>
<li class="pt-[1em] pb-[1em]"> <li class="pt-[1em] pb-[1em]">
<a href="/ui/accounts/{{$acctID}}/inventory" class="p-[1em] font-bold {{ if eq $path (printf "/accounts/%d/inventory" $acctID) }}selected{{ end }}"> <a href="/ui/accounts/{{$acctID}}/inventory" class="p-[1em] font-display {{ if eq $path (printf "/accounts/%d/inventory" $acctID) }}selected{{ end }}">
Inventory Inventory
</a> </a>
</li> </li>
<li class="pt-[1em] pb-[1em]"> <li class="pt-[1em] pb-[1em]">
<a href="/ui/accounts/{{$acctID}}/reports" class="p-[1em] font-bold {{ if eq $path (printf "/accounts/%d/reports" $acctID) }}selected{{ end }}"> <a href="/ui/accounts/{{$acctID}}/reports" class="p-[1em] font-display {{ if eq $path (printf "/accounts/%d/reports" $acctID) }}selected{{ end }}">
Reports Reports
</a> </a>
</li> </li>
<li class="pt-[1em] pb-[1em]"> <li class="pt-[1em] pb-[1em]">
<a href="/api/auth/logout" class="p-[1em] font-bold"> <a href="/api/auth/logout" hx-boost="false" class="p-[1em] font-display">
Log Out Log Out
</a> </a>
</li> </li>
@@ -96,11 +127,33 @@
{{- block "body" . }}{{ end }} {{- block "body" . }}{{ end }}
</main> </main>
<footer class="flex flex-col items-center"> <footer class="grid grid-cols-[6rem_10fr_6rem] justify-center items-center">
<address class="flex items-center"> <address class="grow-1 col-2 flex justify-center">
<a href="mailto:contact-us@inventory-plus-plus.com" class="p-[1em] font-bold">Contact Us</a> <a href="mailto:contact-us@inventory-plus-plus.com" class="p-[1em] font-display text-center">Contact Us</a>
<a href="mailto:support@inventory-plus-plus.com" class="p-[1em] font-bold">Support</a> <a href="mailto:support@inventory-plus-plus.com" class="p-[1em] font-display text-center">Support</a>
</address> </address>
{{- if .Identity.Claims.Picture }}
<div
class="
col-3
max-w-full
max-h-full
flex
justify-center
items-center
"
>
<img
src="{{ .Identity.Claims.Picture }}"
style="
max-height: 3rem;
border-radius: 50%;
margin: 1rem 0;
"
/>
</div>
{{- end }}
</footer> </footer>
</body> </body>
</html> </html>
@@ -4,7 +4,10 @@
{{- $acctID := .Identity.Account }} {{- $acctID := .Identity.Account }}
<h1>Account: {{ $acctID.Email }}</h1> <section>
<h2>Account: {{ $acctID.Email }}</h2>
<h3>{{ .Identity.Claims.Name }}</h3>
</section>
{{- $etsyUser := .Etsy.GetUserPointerByAccountID $acctID.AccountID }} {{- $etsyUser := .Etsy.GetUserPointerByAccountID $acctID.AccountID }}
{{- if $etsyUser }} {{- if $etsyUser }}
+8 -1
View File
@@ -1,5 +1,12 @@
<section class="flex justify-center max-w-full mt-[3em] mb-[3em]"> <section class="flex justify-center max-w-full mt-[3em] mb-[3em]">
<h1 class="bg-card max-w-full p-[2em] border-[1px] border-sidebar-border rounded-lg text-nowrap"> <h1 class="
min-[400px]:px-[calc(10vw-0.5em)]
min-[400px]:py-[calc(8vw-0.5em)]
border-[1px] border-sidebar-border rounded-lg
bg-card
text-nowrap
text-center
">
Inventory++ Inventory++
</h1> </h1>