fix: cookie-auth-issues (#365)

* fix session clearing on error

* use singleton context to manage user state

* implement remember-me functionality

* fix errors

* fix more errors
This commit is contained in:
Hayden 2023-03-22 21:52:25 -08:00 committed by GitHub
parent ed1230e17d
commit faed343eda
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 175 additions and 89 deletions

View file

@ -12,7 +12,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: 1.19 go-version: "1.20"
- name: Install Task - name: Install Task
uses: arduino/setup-task@v1 uses: arduino/setup-task@v1

View file

@ -46,7 +46,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: 1.19 go-version: "1.20"
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:

View file

@ -24,7 +24,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: 1.19 go-version: "1.20"
- name: Set up QEMU - name: Set up QEMU
id: qemu id: qemu

View file

@ -25,7 +25,7 @@ func (a *app) SetupDemo() {
} }
// First check if we've already setup a demo user and skip if so // First check if we've already setup a demo user and skip if so
_, err := a.services.User.Login(context.Background(), registration.Email, registration.Password) _, err := a.services.User.Login(context.Background(), registration.Email, registration.Password, false)
if err == nil { if err == nil {
return return
} }
@ -36,7 +36,7 @@ func (a *app) SetupDemo() {
log.Fatal().Msg("Failed to setup demo") log.Fatal().Msg("Failed to setup demo")
} }
token, _ := a.services.User.Login(context.Background(), registration.Email, registration.Password) token, _ := a.services.User.Login(context.Background(), registration.Email, registration.Password, false)
self, _ := a.services.User.GetSelf(context.Background(), token.Raw) self, _ := a.services.User.GetSelf(context.Background(), token.Raw)
_, err = a.services.Items.CsvImport(context.Background(), self.GroupID, strings.NewReader(csvText)) _, err = a.services.Items.CsvImport(context.Background(), self.GroupID, strings.NewReader(csvText))

View file

@ -23,6 +23,7 @@ type (
LoginForm struct { LoginForm struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
StayLoggedIn bool `json:"stayLoggedIn"`
} }
) )
@ -34,6 +35,7 @@ type (
// @Accept application/json // @Accept application/json
// @Param username formData string false "string" example(admin@admin.com) // @Param username formData string false "string" example(admin@admin.com)
// @Param password formData string false "string" example(admin) // @Param password formData string false "string" example(admin)
// @Param payload body LoginForm true "Login Data"
// @Produce json // @Produce json
// @Success 200 {object} TokenResponse // @Success 200 {object} TokenResponse
// @Router /v1/users/login [POST] // @Router /v1/users/login [POST]
@ -50,6 +52,7 @@ func (ctrl *V1Controller) HandleAuthLogin() errchain.HandlerFunc {
loginForm.Username = r.PostFormValue("username") loginForm.Username = r.PostFormValue("username")
loginForm.Password = r.PostFormValue("password") loginForm.Password = r.PostFormValue("password")
loginForm.StayLoggedIn = r.PostFormValue("stayLoggedIn") == "true"
case "application/json": case "application/json":
err := server.Decode(r, loginForm) err := server.Decode(r, loginForm)
if err != nil { if err != nil {
@ -73,7 +76,7 @@ func (ctrl *V1Controller) HandleAuthLogin() errchain.HandlerFunc {
) )
} }
newToken, err := ctrl.svc.User.Login(r.Context(), strings.ToLower(loginForm.Username), loginForm.Password) newToken, err := ctrl.svc.User.Login(r.Context(), strings.ToLower(loginForm.Username), loginForm.Password, loginForm.StayLoggedIn)
if err != nil { if err != nil {
return validate.NewRequestError(errors.New("authentication failed"), http.StatusInternalServerError) return validate.NewRequestError(errors.New("authentication failed"), http.StatusInternalServerError)
} }

View file

@ -1575,6 +1575,15 @@ const docTemplate = `{
"description": "string", "description": "string",
"name": "password", "name": "password",
"in": "formData" "in": "formData"
},
{
"description": "Login Data",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.LoginForm"
}
} }
], ],
"responses": { "responses": {
@ -2761,6 +2770,20 @@ const docTemplate = `{
} }
} }
}, },
"v1.LoginForm": {
"type": "object",
"properties": {
"password": {
"type": "string"
},
"stayLoggedIn": {
"type": "boolean"
},
"username": {
"type": "string"
}
}
},
"v1.TokenResponse": { "v1.TokenResponse": {
"type": "object", "type": "object",
"properties": { "properties": {

View file

@ -1567,6 +1567,15 @@
"description": "string", "description": "string",
"name": "password", "name": "password",
"in": "formData" "in": "formData"
},
{
"description": "Login Data",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.LoginForm"
}
} }
], ],
"responses": { "responses": {
@ -2753,6 +2762,20 @@
} }
} }
}, },
"v1.LoginForm": {
"type": "object",
"properties": {
"password": {
"type": "string"
},
"stayLoggedIn": {
"type": "boolean"
},
"username": {
"type": "string"
}
}
},
"v1.TokenResponse": { "v1.TokenResponse": {
"type": "object", "type": "object",
"properties": { "properties": {

View file

@ -676,6 +676,15 @@ definitions:
token: token:
type: string type: string
type: object type: object
v1.LoginForm:
properties:
password:
type: string
stayLoggedIn:
type: boolean
username:
type: string
type: object
v1.TokenResponse: v1.TokenResponse:
properties: properties:
attachmentToken: attachmentToken:
@ -1642,6 +1651,12 @@ paths:
in: formData in: formData
name: password name: password
type: string type: string
- description: Login Data
in: body
name: payload
required: true
schema:
$ref: '#/definitions/v1.LoginForm'
produces: produces:
- application/json - application/json
responses: responses:

View file

@ -503,7 +503,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@ -529,7 +528,6 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
@ -602,10 +600,8 @@ github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrf
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

View file

@ -3,10 +3,8 @@ package services
import ( import (
"context" "context"
"log" "log"
"math/rand"
"os" "os"
"testing" "testing"
"time"
"github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
@ -49,8 +47,6 @@ func bootstrap() {
} }
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
rand.Seed(int64(time.Now().Unix()))
client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil { if err != nil {
log.Fatalf("failed opening connection to sqlite: %v", err) log.Fatalf("failed opening connection to sqlite: %v", err)

View file

@ -140,12 +140,18 @@ func (svc *UserService) UpdateSelf(ctx context.Context, ID uuid.UUID, data repo.
// ============================================================================ // ============================================================================
// User Authentication // User Authentication
func (svc *UserService) createSessionToken(ctx context.Context, userId uuid.UUID) (UserAuthTokenDetail, error) { func (svc *UserService) createSessionToken(ctx context.Context, userId uuid.UUID, extendedSession bool) (UserAuthTokenDetail, error) {
attachmentToken := hasher.GenerateToken() attachmentToken := hasher.GenerateToken()
expiresAt := time.Now().Add(oneWeek)
if extendedSession {
expiresAt = time.Now().Add(oneWeek * 4)
}
attachmentData := repo.UserAuthTokenCreate{ attachmentData := repo.UserAuthTokenCreate{
UserID: userId, UserID: userId,
TokenHash: attachmentToken.Hash, TokenHash: attachmentToken.Hash,
ExpiresAt: time.Now().Add(oneWeek), ExpiresAt: expiresAt,
} }
_, err := svc.repos.AuthTokens.CreateToken(ctx, attachmentData, authroles.RoleAttachments) _, err := svc.repos.AuthTokens.CreateToken(ctx, attachmentData, authroles.RoleAttachments)
@ -157,7 +163,7 @@ func (svc *UserService) createSessionToken(ctx context.Context, userId uuid.UUID
data := repo.UserAuthTokenCreate{ data := repo.UserAuthTokenCreate{
UserID: userId, UserID: userId,
TokenHash: userToken.Hash, TokenHash: userToken.Hash,
ExpiresAt: time.Now().Add(oneWeek), ExpiresAt: expiresAt,
} }
created, err := svc.repos.AuthTokens.CreateToken(ctx, data, authroles.RoleUser) created, err := svc.repos.AuthTokens.CreateToken(ctx, data, authroles.RoleUser)
@ -172,7 +178,7 @@ func (svc *UserService) createSessionToken(ctx context.Context, userId uuid.UUID
}, nil }, nil
} }
func (svc *UserService) Login(ctx context.Context, username, password string) (UserAuthTokenDetail, error) { func (svc *UserService) Login(ctx context.Context, username, password string, extendedSession bool) (UserAuthTokenDetail, error) {
usr, err := svc.repos.Users.GetOneEmail(ctx, username) usr, err := svc.repos.Users.GetOneEmail(ctx, username)
if err != nil { if err != nil {
// SECURITY: Perform hash to ensure response times are the same // SECURITY: Perform hash to ensure response times are the same
@ -184,7 +190,7 @@ func (svc *UserService) Login(ctx context.Context, username, password string) (U
return UserAuthTokenDetail{}, ErrorInvalidLogin return UserAuthTokenDetail{}, ErrorInvalidLogin
} }
return svc.createSessionToken(ctx, usr.ID) return svc.createSessionToken(ctx, usr.ID, extendedSession)
} }
func (svc *UserService) Logout(ctx context.Context, token string) error { func (svc *UserService) Logout(ctx context.Context, token string) error {
@ -201,7 +207,7 @@ func (svc *UserService) RenewToken(ctx context.Context, token string) (UserAuthT
return UserAuthTokenDetail{}, ErrorInvalidToken return UserAuthTokenDetail{}, ErrorInvalidToken
} }
return svc.createSessionToken(ctx, dbToken.ID) return svc.createSessionToken(ctx, dbToken.ID, false)
} }
// DeleteSelf deletes the user that is currently logged based of the provided UUID // DeleteSelf deletes the user that is currently logged based of the provided UUID

View file

@ -3,10 +3,8 @@ package repo
import ( import (
"context" "context"
"log" "log"
"math/rand"
"os" "os"
"testing" "testing"
"time"
"github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent"
"github.com/hay-kot/homebox/backend/pkgs/faker" "github.com/hay-kot/homebox/backend/pkgs/faker"
@ -40,8 +38,6 @@ func bootstrap() {
} }
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
rand.Seed(int64(time.Now().Unix()))
client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil { if err != nil {
log.Fatalf("failed opening connection to sqlite: %v", err) log.Fatalf("failed opening connection to sqlite: %v", err)

View file

@ -10,7 +10,6 @@ var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
type Faker struct{} type Faker struct{}
func NewFaker() *Faker { func NewFaker() *Faker {
rand.Seed(time.Now().UnixNano())
return &Faker{} return &Faker{}
} }

View file

@ -1567,6 +1567,15 @@
"description": "string", "description": "string",
"name": "password", "name": "password",
"in": "formData" "in": "formData"
},
{
"description": "Login Data",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.LoginForm"
}
} }
], ],
"responses": { "responses": {
@ -2753,6 +2762,20 @@
} }
} }
}, },
"v1.LoginForm": {
"type": "object",
"properties": {
"password": {
"type": "string"
},
"stayLoggedIn": {
"type": "boolean"
},
"username": {
"type": "string"
}
}
},
"v1.TokenResponse": { "v1.TokenResponse": {
"type": "object", "type": "object",
"properties": { "properties": {

View file

@ -1,9 +1,9 @@
<template> <template>
<div class="flex"> <div class="relative">
<FormTextField v-model="value" placeholder="Password" label="Password" :type="inputType"> </FormTextField> <FormTextField v-model="value" placeholder="Password" label="Password" :type="inputType"> </FormTextField>
<button <button
type="button" type="button"
class="inline-flex p-1 ml-1 justify-center mt-auto mb-3 tooltip" class="inline-flex p-1 ml-1 justify-center mt-auto mb-3 tooltip absolute top-11 right-3"
data-tip="Toggle Password Show" data-tip="Toggle Password Show"
@click="toggle()" @click="toggle()"
> >

View file

@ -34,6 +34,7 @@ export function useUserApi(): UserClient {
requests.addResponseInterceptor(logger); requests.addResponseInterceptor(logger);
requests.addResponseInterceptor(r => { requests.addResponseInterceptor(r => {
if (r.status === 401) { if (r.status === 401) {
console.error("unauthorized request, invalidating session");
authCtx.invalidateSession(); authCtx.invalidateSession();
} }
}); });

View file

@ -4,7 +4,6 @@ import { UserOut } from "~~/lib/api/types/data-contracts";
import { UserClient } from "~~/lib/api/user"; import { UserClient } from "~~/lib/api/user";
export interface IAuthContext { export interface IAuthContext {
self?: UserOut;
get token(): string | null; get token(): string | null;
get expiresAt(): string | null; get expiresAt(): string | null;
get attachmentToken(): string | null; get attachmentToken(): string | null;
@ -37,10 +36,17 @@ export interface IAuthContext {
/** /**
* Logs in the user and sets the authorization context via cookies * Logs in the user and sets the authorization context via cookies
*/ */
login(api: PublicApi, email: string, password: string): ReturnType<PublicApi["login"]>; login(api: PublicApi, email: string, password: string, stayLoggedIn: boolean): ReturnType<PublicApi["login"]>;
} }
class AuthContext implements IAuthContext { class AuthContext implements IAuthContext {
// eslint-disable-next-line no-use-before-define
private static _instance?: AuthContext;
private static readonly cookieTokenKey = "hb.auth.token";
private static readonly cookieExpiresAtKey = "hb.auth.expires_at";
private static readonly cookieAttachmentTokenKey = "hb.auth.attachment_token";
user?: UserOut; user?: UserOut;
private _token: CookieRef<string | null>; private _token: CookieRef<string | null>;
private _expiresAt: CookieRef<string | null>; private _expiresAt: CookieRef<string | null>;
@ -58,14 +64,22 @@ class AuthContext implements IAuthContext {
return this._attachmentToken.value; return this._attachmentToken.value;
} }
constructor( private constructor(token: string, expiresAt: string, attachmentToken: string) {
token: CookieRef<string | null>, this._token = useCookie(token);
expiresAt: CookieRef<string | null>, this._expiresAt = useCookie(expiresAt);
attachmentToken: CookieRef<string | null> this._attachmentToken = useCookie(attachmentToken);
) { }
this._token = token;
this._expiresAt = expiresAt; static get instance() {
this._attachmentToken = attachmentToken; if (!this._instance) {
this._instance = new AuthContext(
AuthContext.cookieTokenKey,
AuthContext.cookieExpiresAtKey,
AuthContext.cookieAttachmentTokenKey
);
}
return this._instance;
} }
isExpired() { isExpired() {
@ -81,29 +95,28 @@ class AuthContext implements IAuthContext {
} }
isAuthorized() { isAuthorized() {
return this._token.value !== null && !this.isExpired(); return !!this._token.value && !this.isExpired();
} }
invalidateSession() { invalidateSession() {
this.user = undefined; this.user = undefined;
// Delete the cookies
this._token.value = null; this._token.value = null;
this._expiresAt.value = null; this._expiresAt.value = null;
this._attachmentToken.value = null; this._attachmentToken.value = null;
navigateTo("/");
console.log("Session invalidated");
} }
async login(api: PublicApi, email: string, password: string) { async login(api: PublicApi, email: string, password: string, stayLoggedIn: boolean) {
const r = await api.login(email, password); const r = await api.login(email, password, stayLoggedIn);
if (!r.error) { if (!r.error) {
this._token.value = r.data.token; this._token.value = r.data.token;
this._expiresAt.value = r.data.expiresAt as string; this._expiresAt.value = r.data.expiresAt as string;
this._attachmentToken.value = r.data.attachmentToken; this._attachmentToken.value = r.data.attachmentToken;
console.log({
token: this._token.value,
expiresAt: this._expiresAt.value,
attachmentToken: this._attachmentToken.value,
});
} }
return r; return r;
@ -121,9 +134,5 @@ class AuthContext implements IAuthContext {
} }
export function useAuthContext(): IAuthContext { export function useAuthContext(): IAuthContext {
const tokenCookie = useCookie("hb.auth.token"); return AuthContext.instance;
const expiresAtCookie = useCookie("hb.auth.expires_at");
const attachmentTokenCookie = useCookie("hb.auth.attachment_token");
return new AuthContext(tokenCookie, expiresAtCookie, attachmentTokenCookie);
} }

View file

@ -94,7 +94,7 @@
import { useLabelStore } from "~~/stores/labels"; import { useLabelStore } from "~~/stores/labels";
import { useLocationStore } from "~~/stores/locations"; import { useLocationStore } from "~~/stores/locations";
const username = computed(() => authCtx.self?.name || "User"); const username = computed(() => authCtx.user?.name || "User");
// Preload currency format // Preload currency format
useFormatCurrency(); useFormatCurrency();
@ -226,11 +226,6 @@
const api = useUserApi(); const api = useUserApi();
async function logout() { async function logout() {
const { error } = await authCtx.logout(api); await authCtx.logout(api);
if (error) {
return;
}
navigateTo("/");
} }
</script> </script>

View file

@ -1,10 +1,5 @@
import { BaseAPI, route } from "./base"; import { BaseAPI, route } from "./base";
import { ApiSummary, TokenResponse, UserRegistration } from "./types/data-contracts"; import { ApiSummary, LoginForm, TokenResponse, UserRegistration } from "./types/data-contracts";
export type LoginPayload = {
username: string;
password: string;
};
export type StatusResult = { export type StatusResult = {
health: boolean; health: boolean;
@ -18,12 +13,13 @@ export class PublicApi extends BaseAPI {
return this.http.get<ApiSummary>({ url: route("/status") }); return this.http.get<ApiSummary>({ url: route("/status") });
} }
public login(username: string, password: string) { public login(username: string, password: string, stayLoggedIn = false) {
return this.http.post<LoginPayload, TokenResponse>({ return this.http.post<LoginForm, TokenResponse>({
url: route("/users/login"), url: route("/users/login"),
body: { body: {
username, username,
password, password,
stayLoggedIn,
}, },
}); });
} }

View file

@ -412,6 +412,12 @@ export interface ItemAttachmentToken {
token: string; token: string;
} }
export interface LoginForm {
password: string;
stayLoggedIn: boolean;
username: string;
}
export interface TokenResponse { export interface TokenResponse {
attachmentToken: string; attachmentToken: string;
expiresAt: Date | string; expiresAt: Date | string;

View file

@ -39,19 +39,4 @@ export class UserClient extends BaseAPI {
Object.freeze(this); Object.freeze(this);
} }
/** @deprecated use this.user.self() */
public self() {
return this.user.self();
}
/** @deprecated use this.user.logout() */
public logout() {
return this.user.logout();
}
/** @deprecated use this.user.delete() */
public deleteAccount() {
return this.user.delete();
}
} }

View file

@ -2,10 +2,14 @@ export default defineNuxtRouteMiddleware(async () => {
const ctx = useAuthContext(); const ctx = useAuthContext();
const api = useUserApi(); const api = useUserApi();
if (!ctx.isAuthorized()) {
return navigateTo("/");
}
if (!ctx.user) { if (!ctx.user) {
const { data, error } = await api.user.self(); const { data, error } = await api.user.self();
if (error) { if (error) {
navigateTo("/"); return navigateTo("/");
} }
ctx.user = data.item; ctx.user = data.item;

View file

@ -40,6 +40,7 @@
const email = ref(""); const email = ref("");
const password = ref(""); const password = ref("");
const canRegister = ref(false); const canRegister = ref(false);
const remember = ref(false);
const groupToken = computed<string>({ const groupToken = computed<string>({
get() { get() {
@ -91,7 +92,7 @@
async function login() { async function login() {
loading.value = true; loading.value = true;
const { error } = await ctx.login(api, email.value, loginPassword.value); const { error } = await ctx.login(api, email.value, loginPassword.value, remember.value);
if (error) { if (error) {
toast.error("Invalid email or password"); toast.error("Invalid email or password");
@ -196,8 +197,16 @@
</template> </template>
<FormTextField v-model="email" label="Email" /> <FormTextField v-model="email" label="Email" />
<FormPassword v-model="loginPassword" label="Password" /> <FormPassword v-model="loginPassword" label="Password" />
<div class="card-actions justify-end mt-2"> <div class="max-w-[140px]">
<button type="submit" class="btn btn-primary" :class="loading ? 'loading' : ''" :disabled="loading"> <FormCheckbox v-model="remember" label="Remember Me" />
</div>
<div class="card-actions justify-end">
<button
type="submit"
class="btn btn-primary btn-block"
:class="loading ? 'loading' : ''"
:disabled="loading"
>
Login Login
</button> </button>
</div> </div>

View file

@ -82,14 +82,15 @@
const auth = useAuthContext(); const auth = useAuthContext();
const details = computed(() => { const details = computed(() => {
console.log(auth.user);
return [ return [
{ {
name: "Name", name: "Name",
text: auth.self?.name || "Unknown", text: auth.user?.name || "Unknown",
}, },
{ {
name: "Email", name: "Email",
text: auth.self?.email || "Unknown", text: auth.user?.email || "Unknown",
}, },
] as Detail[]; ] as Detail[];
}); });