feat(ops-web/auth): add remember-me with session/local storage split
Persistent logins land in localStorage, ephemeral ones in sessionStorage, and we recall the last username + remember preference on the login form. Also propagate the indigo brand color through the antd ConfigProvider theme token so the side nav and avatar match the redesigned login.
This commit is contained in:
@@ -44,7 +44,7 @@ const client: AxiosInstance = axios.create({
|
||||
});
|
||||
|
||||
client.interceptors.request.use((config) => {
|
||||
const session = readStoredSession();
|
||||
const { session } = readStoredSession();
|
||||
if (session.accessToken) {
|
||||
config.headers = config.headers ?? {};
|
||||
config.headers["Authorization"] = `Bearer ${session.accessToken}`;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const STORAGE_KEY = "geo.ops-web.session";
|
||||
const SESSION_KEY = "geo.ops-web.session";
|
||||
const PREFERENCE_KEY = "geo.ops-web.login-preference";
|
||||
|
||||
export interface OperatorView {
|
||||
id: number;
|
||||
@@ -15,22 +16,36 @@ export interface OpsSession {
|
||||
operator: OperatorView | null;
|
||||
}
|
||||
|
||||
export interface LoginPreference {
|
||||
remember: boolean;
|
||||
lastUsername: string;
|
||||
}
|
||||
|
||||
export interface StoredSession {
|
||||
session: OpsSession;
|
||||
persistent: boolean;
|
||||
}
|
||||
|
||||
function emptySession(): OpsSession {
|
||||
return { accessToken: null, expiresAt: null, operator: null };
|
||||
}
|
||||
|
||||
function hasStorage(): boolean {
|
||||
return typeof window !== "undefined" && "localStorage" in window;
|
||||
function emptyPreference(): LoginPreference {
|
||||
return { remember: false, lastUsername: "" };
|
||||
}
|
||||
|
||||
export function readStoredSession(): OpsSession {
|
||||
if (!hasStorage()) return emptySession();
|
||||
function hasWindow(): boolean {
|
||||
return typeof window !== "undefined";
|
||||
}
|
||||
|
||||
function readFrom(storage: Storage): OpsSession | null {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return emptySession();
|
||||
const raw = storage.getItem(SESSION_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw) as Partial<OpsSession>;
|
||||
if (typeof parsed.accessToken !== "string" || !parsed.accessToken) return null;
|
||||
return {
|
||||
accessToken: typeof parsed.accessToken === "string" ? parsed.accessToken : null,
|
||||
accessToken: parsed.accessToken,
|
||||
expiresAt: typeof parsed.expiresAt === "number" ? parsed.expiresAt : null,
|
||||
operator:
|
||||
parsed.operator && typeof parsed.operator === "object"
|
||||
@@ -38,23 +53,68 @@ export function readStoredSession(): OpsSession {
|
||||
: null,
|
||||
};
|
||||
} catch {
|
||||
return emptySession();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function writeStoredSession(session: OpsSession): void {
|
||||
if (!hasStorage()) return;
|
||||
export function readStoredSession(): StoredSession {
|
||||
if (!hasWindow()) return { session: emptySession(), persistent: false };
|
||||
const persistent = readFrom(window.localStorage);
|
||||
if (persistent) return { session: persistent, persistent: true };
|
||||
const ephemeral = readFrom(window.sessionStorage);
|
||||
if (ephemeral) return { session: ephemeral, persistent: false };
|
||||
return { session: emptySession(), persistent: false };
|
||||
}
|
||||
|
||||
export function writeStoredSession(session: OpsSession, persistent: boolean): void {
|
||||
if (!hasWindow()) return;
|
||||
const target = persistent ? window.localStorage : window.sessionStorage;
|
||||
const other = persistent ? window.sessionStorage : window.localStorage;
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(session));
|
||||
target.setItem(SESSION_KEY, JSON.stringify(session));
|
||||
} catch {
|
||||
// ignore quota / serialization errors
|
||||
}
|
||||
}
|
||||
|
||||
export function clearStoredSession(): void {
|
||||
if (!hasStorage()) return;
|
||||
try {
|
||||
window.localStorage.removeItem(STORAGE_KEY);
|
||||
other.removeItem(SESSION_KEY);
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
export function clearStoredSession(): void {
|
||||
if (!hasWindow()) return;
|
||||
try {
|
||||
window.localStorage.removeItem(SESSION_KEY);
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
try {
|
||||
window.sessionStorage.removeItem(SESSION_KEY);
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
export function readLoginPreference(): LoginPreference {
|
||||
if (!hasWindow()) return emptyPreference();
|
||||
try {
|
||||
const raw = window.localStorage.getItem(PREFERENCE_KEY);
|
||||
if (!raw) return emptyPreference();
|
||||
const parsed = JSON.parse(raw) as Partial<LoginPreference>;
|
||||
return {
|
||||
remember: parsed.remember === true,
|
||||
lastUsername: typeof parsed.lastUsername === "string" ? parsed.lastUsername : "",
|
||||
};
|
||||
} catch {
|
||||
return emptyPreference();
|
||||
}
|
||||
}
|
||||
|
||||
export function writeLoginPreference(pref: LoginPreference): void {
|
||||
if (!hasWindow()) return;
|
||||
try {
|
||||
window.localStorage.setItem(PREFERENCE_KEY, JSON.stringify(pref));
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user