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
|
@ -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()"
|
||||
>
|
||||
|
|
|
@ -34,6 +34,7 @@ export function useUserApi(): UserClient {
|
|||
requests.addResponseInterceptor(logger);
|
||||
requests.addResponseInterceptor(r => {
|
||||
if (r.status === 401) {
|
||||
console.error("unauthorized request, invalidating session");
|
||||
authCtx.invalidateSession();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -4,7 +4,6 @@ import { UserOut } from "~~/lib/api/types/data-contracts";
|
|||
import { UserClient } from "~~/lib/api/user";
|
||||
|
||||
export interface IAuthContext {
|
||||
self?: UserOut;
|
||||
get token(): string | null;
|
||||
get expiresAt(): string | null;
|
||||
get attachmentToken(): string | null;
|
||||
|
@ -37,10 +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>;
|
||||
|
@ -58,14 +64,22 @@ class AuthContext implements IAuthContext {
|
|||
return this._attachmentToken.value;
|
||||
}
|
||||
|
||||
constructor(
|
||||
token: CookieRef<string | null>,
|
||||
expiresAt: CookieRef<string | null>,
|
||||
attachmentToken: CookieRef<string | null>
|
||||
) {
|
||||
this._token = token;
|
||||
this._expiresAt = expiresAt;
|
||||
this._attachmentToken = attachmentToken;
|
||||
private constructor(token: string, expiresAt: string, attachmentToken: string) {
|
||||
this._token = useCookie(token);
|
||||
this._expiresAt = useCookie(expiresAt);
|
||||
this._attachmentToken = useCookie(attachmentToken);
|
||||
}
|
||||
|
||||
static get instance() {
|
||||
if (!this._instance) {
|
||||
this._instance = new AuthContext(
|
||||
AuthContext.cookieTokenKey,
|
||||
AuthContext.cookieExpiresAtKey,
|
||||
AuthContext.cookieAttachmentTokenKey
|
||||
);
|
||||
}
|
||||
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
isExpired() {
|
||||
|
@ -81,29 +95,28 @@ class AuthContext implements IAuthContext {
|
|||
}
|
||||
|
||||
isAuthorized() {
|
||||
return this._token.value !== null && !this.isExpired();
|
||||
return !!this._token.value && !this.isExpired();
|
||||
}
|
||||
|
||||
invalidateSession() {
|
||||
this.user = undefined;
|
||||
|
||||
// Delete the cookies
|
||||
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;
|
||||
|
@ -121,9 +134,5 @@ class AuthContext implements IAuthContext {
|
|||
}
|
||||
|
||||
export function useAuthContext(): IAuthContext {
|
||||
const tokenCookie = useCookie("hb.auth.token");
|
||||
const expiresAtCookie = useCookie("hb.auth.expires_at");
|
||||
const attachmentTokenCookie = useCookie("hb.auth.attachment_token");
|
||||
|
||||
return new AuthContext(tokenCookie, expiresAtCookie, attachmentTokenCookie);
|
||||
return AuthContext.instance;
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@
|
|||
import { useLabelStore } from "~~/stores/labels";
|
||||
import { useLocationStore } from "~~/stores/locations";
|
||||
|
||||
const username = computed(() => authCtx.self?.name || "User");
|
||||
const username = computed(() => authCtx.user?.name || "User");
|
||||
|
||||
// Preload currency format
|
||||
useFormatCurrency();
|
||||
|
@ -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 = false) {
|
||||
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;
|
||||
|
|
|
@ -39,19 +39,4 @@ export class UserClient extends BaseAPI {
|
|||
|
||||
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 api = useUserApi();
|
||||
|
||||
if (!ctx.isAuthorized()) {
|
||||
return navigateTo("/");
|
||||
}
|
||||
|
||||
if (!ctx.user) {
|
||||
const { data, error } = await api.user.self();
|
||||
if (error) {
|
||||
navigateTo("/");
|
||||
return navigateTo("/");
|
||||
}
|
||||
|
||||
ctx.user = data.item;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -82,14 +82,15 @@
|
|||
const auth = useAuthContext();
|
||||
|
||||
const details = computed(() => {
|
||||
console.log(auth.user);
|
||||
return [
|
||||
{
|
||||
name: "Name",
|
||||
text: auth.self?.name || "Unknown",
|
||||
text: auth.user?.name || "Unknown",
|
||||
},
|
||||
{
|
||||
name: "Email",
|
||||
text: auth.self?.email || "Unknown",
|
||||
text: auth.user?.email || "Unknown",
|
||||
},
|
||||
] as Detail[];
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue