split oauth_tokens.claims column up; improved fonts; allow multiple sse connections per user; make navbar and use friendly
This commit is contained in:
@@ -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 |
@@ -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**" {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+1
@@ -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"
|
||||||
|
|||||||
+4
-1
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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 }}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user