mirror of
https://github.com/hay-kot/homebox.git
synced 2025-08-05 09:10:26 +00:00
implement remember-me functionality
This commit is contained in:
parent
a275090e03
commit
3ca4ad8836
13 changed files with 145 additions and 46 deletions
|
@ -25,7 +25,7 @@ func (a *app) SetupDemo() {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
return
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ func (a *app) SetupDemo() {
|
|||
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)
|
||||
|
||||
_, err = a.services.Items.CsvImport(context.Background(), self.GroupID, strings.NewReader(csvText))
|
||||
|
|
|
@ -21,8 +21,9 @@ type (
|
|||
}
|
||||
|
||||
LoginForm struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
StayLoggedIn bool `json:"stayLoggedIn"`
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -34,6 +35,7 @@ type (
|
|||
// @Accept application/json
|
||||
// @Param username formData string false "string" example(admin@admin.com)
|
||||
// @Param password formData string false "string" example(admin)
|
||||
// @Param payload body LoginForm true "Login Data"
|
||||
// @Produce json
|
||||
// @Success 200 {object} TokenResponse
|
||||
// @Router /v1/users/login [POST]
|
||||
|
@ -50,6 +52,7 @@ func (ctrl *V1Controller) HandleAuthLogin() errchain.HandlerFunc {
|
|||
|
||||
loginForm.Username = r.PostFormValue("username")
|
||||
loginForm.Password = r.PostFormValue("password")
|
||||
loginForm.StayLoggedIn = r.PostFormValue("stayLoggedIn") == "true"
|
||||
case "application/json":
|
||||
err := server.Decode(r, loginForm)
|
||||
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 {
|
||||
return validate.NewRequestError(errors.New("authentication failed"), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
|
@ -1575,6 +1575,15 @@ const docTemplate = `{
|
|||
"description": "string",
|
||||
"name": "password",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"description": "Login Data",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.LoginForm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -2761,6 +2770,20 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1.LoginForm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"stayLoggedIn": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.TokenResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -1567,6 +1567,15 @@
|
|||
"description": "string",
|
||||
"name": "password",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"description": "Login Data",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.LoginForm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -2753,6 +2762,20 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1.LoginForm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"stayLoggedIn": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.TokenResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -676,6 +676,15 @@ definitions:
|
|||
token:
|
||||
type: string
|
||||
type: object
|
||||
v1.LoginForm:
|
||||
properties:
|
||||
password:
|
||||
type: string
|
||||
stayLoggedIn:
|
||||
type: boolean
|
||||
username:
|
||||
type: string
|
||||
type: object
|
||||
v1.TokenResponse:
|
||||
properties:
|
||||
attachmentToken:
|
||||
|
@ -1642,6 +1651,12 @@ paths:
|
|||
in: formData
|
||||
name: password
|
||||
type: string
|
||||
- description: Login Data
|
||||
in: body
|
||||
name: payload
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/v1.LoginForm'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
|
|
@ -140,12 +140,18 @@ func (svc *UserService) UpdateSelf(ctx context.Context, ID uuid.UUID, data repo.
|
|||
// ============================================================================
|
||||
// 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()
|
||||
|
||||
expiresAt := time.Now().Add(oneWeek)
|
||||
if extendedSession {
|
||||
expiresAt = time.Now().Add(oneWeek * 4)
|
||||
}
|
||||
|
||||
attachmentData := repo.UserAuthTokenCreate{
|
||||
UserID: userId,
|
||||
TokenHash: attachmentToken.Hash,
|
||||
ExpiresAt: time.Now().Add(oneWeek),
|
||||
ExpiresAt: expiresAt,
|
||||
}
|
||||
|
||||
_, 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{
|
||||
UserID: userId,
|
||||
TokenHash: userToken.Hash,
|
||||
ExpiresAt: time.Now().Add(oneWeek),
|
||||
ExpiresAt: expiresAt,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
// 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 svc.createSessionToken(ctx, usr.ID)
|
||||
return svc.createSessionToken(ctx, usr.ID, extendedSession)
|
||||
}
|
||||
|
||||
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 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
|
||||
|
|
|
@ -1567,6 +1567,15 @@
|
|||
"description": "string",
|
||||
"name": "password",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"description": "Login Data",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.LoginForm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -2753,6 +2762,20 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1.LoginForm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"stayLoggedIn": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.TokenResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div class="flex">
|
||||
<div class="relative">
|
||||
<FormTextField v-model="value" placeholder="Password" label="Password" :type="inputType"> </FormTextField>
|
||||
<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"
|
||||
@click="toggle()"
|
||||
>
|
||||
|
|
|
@ -36,13 +36,17 @@ export interface IAuthContext {
|
|||
/**
|
||||
* 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 {
|
||||
// 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;
|
||||
private _token: CookieRef<string | null>;
|
||||
private _expiresAt: CookieRef<string | null>;
|
||||
|
@ -68,7 +72,11 @@ class AuthContext implements IAuthContext {
|
|||
|
||||
static get instance() {
|
||||
if (!this._instance) {
|
||||
this._instance = new AuthContext("hb.auth.token", "hb.auth.expires_at", "hb.auth.attachment_token");
|
||||
this._instance = new AuthContext(
|
||||
AuthContext.cookieTokenKey,
|
||||
AuthContext.cookieExpiresAtKey,
|
||||
AuthContext.cookieAttachmentTokenKey
|
||||
);
|
||||
}
|
||||
|
||||
return this._instance;
|
||||
|
@ -94,29 +102,21 @@ class AuthContext implements IAuthContext {
|
|||
this.user = undefined;
|
||||
|
||||
// Delete the cookies
|
||||
// @ts-expect-error
|
||||
this._token.value = undefined;
|
||||
// @ts-expect-error
|
||||
this._expiresAt.value = undefined;
|
||||
// @ts-expect-error
|
||||
this._attachmentToken.value = undefined;
|
||||
this._token.value = null;
|
||||
this._expiresAt.value = null;
|
||||
this._attachmentToken.value = null;
|
||||
|
||||
navigateTo("/");
|
||||
console.log("Session invalidated");
|
||||
}
|
||||
|
||||
async login(api: PublicApi, email: string, password: string) {
|
||||
const r = await api.login(email, password);
|
||||
async login(api: PublicApi, email: string, password: string, stayLoggedIn: boolean) {
|
||||
const r = await api.login(email, password, stayLoggedIn);
|
||||
|
||||
if (!r.error) {
|
||||
this._token.value = r.data.token;
|
||||
this._expiresAt.value = r.data.expiresAt as string;
|
||||
this._attachmentToken.value = r.data.attachmentToken;
|
||||
|
||||
console.log({
|
||||
token: this._token.value,
|
||||
expiresAt: this._expiresAt.value,
|
||||
attachmentToken: this._attachmentToken.value,
|
||||
});
|
||||
}
|
||||
|
||||
return r;
|
||||
|
|
|
@ -226,11 +226,6 @@
|
|||
const api = useUserApi();
|
||||
|
||||
async function logout() {
|
||||
const { error } = await authCtx.logout(api);
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigateTo("/");
|
||||
await authCtx.logout(api);
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
import { BaseAPI, route } from "./base";
|
||||
import { ApiSummary, TokenResponse, UserRegistration } from "./types/data-contracts";
|
||||
|
||||
export type LoginPayload = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
import { ApiSummary, LoginForm, TokenResponse, UserRegistration } from "./types/data-contracts";
|
||||
|
||||
export type StatusResult = {
|
||||
health: boolean;
|
||||
|
@ -18,12 +13,13 @@ export class PublicApi extends BaseAPI {
|
|||
return this.http.get<ApiSummary>({ url: route("/status") });
|
||||
}
|
||||
|
||||
public login(username: string, password: string) {
|
||||
return this.http.post<LoginPayload, TokenResponse>({
|
||||
public login(username: string, password: string, stayLoggedIn: boolean) {
|
||||
return this.http.post<LoginForm, TokenResponse>({
|
||||
url: route("/users/login"),
|
||||
body: {
|
||||
username,
|
||||
password,
|
||||
stayLoggedIn,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -412,6 +412,12 @@ export interface ItemAttachmentToken {
|
|||
token: string;
|
||||
}
|
||||
|
||||
export interface LoginForm {
|
||||
password: string;
|
||||
stayLoggedIn: boolean;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface TokenResponse {
|
||||
attachmentToken: string;
|
||||
expiresAt: Date | string;
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
const email = ref("");
|
||||
const password = ref("");
|
||||
const canRegister = ref(false);
|
||||
const remember = ref(false);
|
||||
|
||||
const groupToken = computed<string>({
|
||||
get() {
|
||||
|
@ -91,7 +92,7 @@
|
|||
|
||||
async function login() {
|
||||
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) {
|
||||
toast.error("Invalid email or password");
|
||||
|
@ -196,8 +197,16 @@
|
|||
</template>
|
||||
<FormTextField v-model="email" label="Email" />
|
||||
<FormPassword v-model="loginPassword" label="Password" />
|
||||
<div class="card-actions justify-end mt-2">
|
||||
<button type="submit" class="btn btn-primary" :class="loading ? 'loading' : ''" :disabled="loading">
|
||||
<div class="max-w-[140px]">
|
||||
<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
|
||||
</button>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue