forked from mirrors/homebox
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:
parent
ed1230e17d
commit
faed343eda
24 changed files with 175 additions and 89 deletions
2
.github/workflows/partial-backend.yaml
vendored
2
.github/workflows/partial-backend.yaml
vendored
|
@ -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
|
||||||
|
|
2
.github/workflows/partial-frontend.yaml
vendored
2
.github/workflows/partial-frontend.yaml
vendored
|
@ -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:
|
||||||
|
|
2
.github/workflows/partial-publish.yaml
vendored
2
.github/workflows/partial-publish.yaml
vendored
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -21,8 +21,9 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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=
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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()"
|
||||||
>
|
>
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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[];
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue