diff --git a/apps/ops-web/src/App.vue b/apps/ops-web/src/App.vue
index 97bc225..f7f8d8e 100644
--- a/apps/ops-web/src/App.vue
+++ b/apps/ops-web/src/App.vue
@@ -1,5 +1,5 @@
-
+
@@ -13,6 +13,15 @@ import { useAuthStore } from "@/stores/auth";
const auth = useAuthStore();
+const themeConfig = {
+ token: {
+ colorPrimary: "#4f46e5",
+ colorLink: "#4f46e5",
+ colorInfo: "#4f46e5",
+ borderRadius: 6,
+ },
+};
+
onMounted(() => {
if (!auth.initialized) auth.hydrate();
});
diff --git a/apps/ops-web/src/layouts/AppShell.vue b/apps/ops-web/src/layouts/AppShell.vue
index 8f1d5f6..d546101 100644
--- a/apps/ops-web/src/layouts/AppShell.vue
+++ b/apps/ops-web/src/layouts/AppShell.vue
@@ -138,14 +138,14 @@ onMounted(() => {
.ops-brand {
height: 40px;
- background: #f0f8ff;
- border: 1px solid #bae0ff;
+ background: #eef2ff;
+ border: 1px solid #c7d2fe;
margin: 16px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
- color: #1677ff;
+ color: #4f46e5;
font-weight: 800;
font-size: 16px;
letter-spacing: 0.05em;
@@ -195,7 +195,7 @@ onMounted(() => {
}
.user-avatar {
- background-color: #1677ff;
+ background-color: #4f46e5;
color: #fff;
width: 28px;
height: 28px;
diff --git a/apps/ops-web/src/lib/http.ts b/apps/ops-web/src/lib/http.ts
index 4f5a96d..1419e9e 100644
--- a/apps/ops-web/src/lib/http.ts
+++ b/apps/ops-web/src/lib/http.ts
@@ -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}`;
diff --git a/apps/ops-web/src/lib/storage.ts b/apps/ops-web/src/lib/storage.ts
index 6b6f4b8..66c0baf 100644
--- a/apps/ops-web/src/lib/storage.ts
+++ b/apps/ops-web/src/lib/storage.ts
@@ -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;
+ 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;
+ 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
}
diff --git a/apps/ops-web/src/stores/auth.ts b/apps/ops-web/src/stores/auth.ts
index a2d8752..9a929af 100644
--- a/apps/ops-web/src/stores/auth.ts
+++ b/apps/ops-web/src/stores/auth.ts
@@ -5,7 +5,9 @@ import { http } from "@/lib/http";
import {
type OperatorView,
clearStoredSession,
+ readLoginPreference,
readStoredSession,
+ writeLoginPreference,
writeStoredSession,
} from "@/lib/storage";
@@ -20,25 +22,32 @@ export const useAuthStore = defineStore("ops-auth", () => {
const expiresAt = ref(null);
const operator = ref(null);
const initialized = ref(false);
+ const persistent = ref(false);
function hydrate(): void {
const stored = readStoredSession();
- accessToken.value = stored.accessToken;
- expiresAt.value = stored.expiresAt;
- operator.value = stored.operator;
+ accessToken.value = stored.session.accessToken;
+ expiresAt.value = stored.session.expiresAt;
+ operator.value = stored.session.operator;
+ persistent.value = stored.persistent;
initialized.value = true;
}
- async function login(username: string, password: string): Promise {
+ async function login(username: string, password: string, remember: boolean): Promise {
const result = await http.post("/auth/login", { username, password });
accessToken.value = result.access_token;
expiresAt.value = result.expires_at;
operator.value = result.operator;
- writeStoredSession({
- accessToken: result.access_token,
- expiresAt: result.expires_at,
- operator: result.operator,
- });
+ persistent.value = remember;
+ writeStoredSession(
+ {
+ accessToken: result.access_token,
+ expiresAt: result.expires_at,
+ operator: result.operator,
+ },
+ remember,
+ );
+ writeLoginPreference({ remember, lastUsername: username });
}
async function refreshSelf(): Promise {
@@ -53,11 +62,14 @@ export const useAuthStore = defineStore("ops-auth", () => {
role: me.role,
status: me.status,
};
- writeStoredSession({
- accessToken: accessToken.value,
- expiresAt: expiresAt.value,
- operator: operator.value,
- });
+ writeStoredSession(
+ {
+ accessToken: accessToken.value,
+ expiresAt: expiresAt.value,
+ operator: operator.value,
+ },
+ persistent.value,
+ );
} catch {
logout();
}
@@ -67,6 +79,7 @@ export const useAuthStore = defineStore("ops-auth", () => {
accessToken.value = null;
expiresAt.value = null;
operator.value = null;
+ persistent.value = false;
clearStoredSession();
}
@@ -78,9 +91,11 @@ export const useAuthStore = defineStore("ops-auth", () => {
operator,
initialized,
isAuthenticated,
+ persistent,
hydrate,
login,
refreshSelf,
logout,
+ readLoginPreference,
};
});
diff --git a/apps/ops-web/src/views/LoginView.vue b/apps/ops-web/src/views/LoginView.vue
index 5d0f491..53c79c9 100644
--- a/apps/ops-web/src/views/LoginView.vue
+++ b/apps/ops-web/src/views/LoginView.vue
@@ -72,11 +72,12 @@