falak-app-duo 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,7 @@ integrations on the frontend side: **Ably** (realtime) and **Brevo**
13
13
  | Ably (realtime) | **New** — via Laravel Echo, authorized through the same API-key + user-token headers as the rest of the app |
14
14
  | Brevo (email/SMS) | Kept — calls go through Laravel, key never ships to the browser |
15
15
  | Laravel API auth | `X-API-Key` (app) + `Authorization: Bearer <token>` (user) |
16
- | AES-256 | Kept — encrypts the cached user token in localStorage |
16
+ | AES-256 | Kept — encrypts the cached user token in IndexedDB |
17
17
  | Arabic/RTL | Kept — baked in via vue-i18n + postcss-rtlcss (opt-out: `--no-rtl` or the RTL prompt) |
18
18
 
19
19
  ## Install & run
@@ -43,7 +43,7 @@ Every request from `src/services/api.js` carries two headers:
43
43
  - `X-API-Key` — identifies this client app, set once per environment in `.env`.
44
44
  - `Authorization: Bearer <user token>` — identifies the logged-in user, issued
45
45
  by your Laravel `/auth/login` endpoint and stored AES-256 encrypted in
46
- `localStorage` (see `src/utils/crypto.js` and `src/stores/auth.js`).
46
+ IndexedDB (see `src/utils/crypto.js`, `src/storage.js` and `src/stores/auth.js`).
47
47
 
48
48
  ## Ably (realtime)
49
49
 
package/bin/falak-app-duo CHANGED
@@ -215,6 +215,7 @@ function buildI18nModule(locales, defaultLocale) {
215
215
  const ordered = ["ar", "en", "es", "de", "fr", "ru", "it"].filter((c) => locales.includes(c));
216
216
  const lines = [];
217
217
  lines.push('import { createI18n } from "vue-i18n";');
218
+ lines.push('import storage from "@/storage";');
218
219
  for (const code of ordered) {
219
220
  lines.push(`import ${code} from "./locales/${code}.json";`);
220
221
  }
@@ -226,27 +227,30 @@ function buildI18nModule(locales, defaultLocale) {
226
227
  lines.push("];");
227
228
  lines.push("");
228
229
  lines.push('const STORAGE_KEY = "falak_locale";');
229
- lines.push("const stored = localStorage.getItem(STORAGE_KEY);");
230
- lines.push(
231
- `const savedLocale = SUPPORTED_LOCALES.some((l) => l.code === stored) ? stored : "${defaultLocale}";`
232
- );
233
230
  lines.push("");
234
231
  lines.push("const i18n = createI18n({");
235
232
  lines.push(" legacy: false,");
236
- lines.push(" locale: savedLocale,");
233
+ lines.push(` locale: "${defaultLocale}",`);
237
234
  lines.push(' fallbackLocale: "en",');
238
235
  lines.push(` messages: { ${ordered.join(", ")} },`);
239
236
  lines.push("});");
240
237
  lines.push("");
241
238
  lines.push("export function setLocale(locale) {");
242
239
  lines.push(" i18n.global.locale.value = locale;");
243
- lines.push(" localStorage.setItem(STORAGE_KEY, locale);");
240
+ lines.push(" storage.set(STORAGE_KEY, locale);");
244
241
  lines.push(" document.documentElement.lang = locale;");
245
242
  lines.push(' document.documentElement.dir = locale === "ar" ? "rtl" : "ltr";');
246
243
  lines.push("}");
247
244
  lines.push("");
248
- lines.push("// Apply on load");
249
- lines.push("setLocale(savedLocale);");
245
+ lines.push("// Apply the persisted locale (falling back to the default) once storage is");
246
+ lines.push("// ready. main.js awaits this before mounting so the first paint is correct.");
247
+ lines.push("export async function initLocale() {");
248
+ lines.push(" const stored = await storage.get(STORAGE_KEY);");
249
+ lines.push(
250
+ ` const saved = SUPPORTED_LOCALES.some((l) => l.code === stored) ? stored : "${defaultLocale}";`
251
+ );
252
+ lines.push(" setLocale(saved);");
253
+ lines.push("}");
250
254
  lines.push("");
251
255
  lines.push("export default i18n;");
252
256
  lines.push("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "falak-app-duo",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Vue 3 boilerplate scaffolder — Ably (realtime), Brevo, AES-256, Arabic/RTL, and a Laravel API client (API key + user token auth). No Firebase, no Groq/Paymob on the frontend (Groq is backend-only; Paymob dropped).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,23 +4,24 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Falak App</title>
7
+ <style>
8
+ /* The theme is resolved from IndexedDB (async) before the app mounts;
9
+ keep the page hidden until then so the first paint is never the wrong
10
+ theme. main.js removes this class once the theme is applied. */
11
+ html.theme-loading {
12
+ visibility: hidden;
13
+ }
14
+ </style>
7
15
  <script>
8
- // FOUC guard: paint with the right theme before the app mounts.
9
- // Mirrors src/theme.js.getTheme() — keep in sync.
16
+ // Runs before first paint: hide the page until main.js has hydrated the
17
+ // IndexedDB store and applied the theme (see src/main.js).
10
18
  (function () {
11
- var s;
12
- try {
13
- s = localStorage.getItem("falak_theme");
14
- } catch (e) {}
15
- if (s === "light") {
16
- document.documentElement.classList.add("light");
17
- } else if (s !== "dark") {
18
- try {
19
- if (window.matchMedia("(prefers-color-scheme: light)").matches) {
20
- document.documentElement.classList.add("light");
21
- }
22
- } catch (e) {}
23
- }
19
+ var el = document.documentElement;
20
+ el.classList.add("theme-loading");
21
+ // Safety net: never leave the page hidden if the app fails to boot.
22
+ setTimeout(function () {
23
+ el.classList.remove("theme-loading");
24
+ }, 3000);
24
25
  })();
25
26
  </script>
26
27
  </head>
@@ -13,6 +13,7 @@
13
13
  "ably": "^2.5.0",
14
14
  "axios": "^1.7.7",
15
15
  "crypto-js": "^4.2.0",
16
+ "idb": "^8.0.0",
16
17
  "laravel-echo": "^1.16.1",
17
18
  "otplib": "^12.0.1",
18
19
  "pinia": "^2.2.2",
@@ -5,8 +5,8 @@
5
5
  /* ============================================================
6
6
  Theme tokens
7
7
  Dark is the default (no class on <html>); light is applied by
8
- toggling `.light` on <html> (see src/theme.js + the FOUC guard
9
- in index.html). Components use ONLY these semantic tokens — never
8
+ toggling `.light` on <html> (see src/theme.js + the `theme-loading`
9
+ guard in index.html). Components use ONLY these semantic tokens — never
10
10
  hardcoded dark-palette utilities — so a single var flip re-themes
11
11
  the whole app. Keep `html { }` (dark) and `html.light { }` in sync.
12
12
  ============================================================ */
@@ -1,4 +1,5 @@
1
1
  import { createI18n } from "vue-i18n";
2
+ import storage from "@/storage";
2
3
  import ar from "./locales/ar.json";
3
4
  import de from "./locales/de.json";
4
5
  import en from "./locales/en.json";
@@ -18,24 +19,27 @@ export const SUPPORTED_LOCALES = [
18
19
  ];
19
20
 
20
21
  const STORAGE_KEY = "falak_locale";
21
- const stored = localStorage.getItem(STORAGE_KEY);
22
- const savedLocale = SUPPORTED_LOCALES.some((l) => l.code === stored) ? stored : "ar";
23
22
 
24
23
  const i18n = createI18n({
25
24
  legacy: false,
26
- locale: savedLocale,
25
+ locale: "ar",
27
26
  fallbackLocale: "en",
28
27
  messages: { ar, de, en, es, fr, it, ru },
29
28
  });
30
29
 
31
30
  export function setLocale(locale) {
32
31
  i18n.global.locale.value = locale;
33
- localStorage.setItem(STORAGE_KEY, locale);
32
+ storage.set(STORAGE_KEY, locale);
34
33
  document.documentElement.lang = locale;
35
34
  document.documentElement.dir = locale === "ar" ? "rtl" : "ltr";
36
35
  }
37
36
 
38
- // Apply on load
39
- setLocale(savedLocale);
37
+ // Apply the persisted locale (falling back to the default) once storage is
38
+ // ready. main.js awaits this before mounting so the first paint is correct.
39
+ export async function initLocale() {
40
+ const stored = await storage.get(STORAGE_KEY);
41
+ const saved = SUPPORTED_LOCALES.some((l) => l.code === stored) ? stored : "ar";
42
+ setLocale(saved);
43
+ }
40
44
 
41
- export default i18n;
45
+ export default i18n;
@@ -5,24 +5,36 @@ import Aura from "@primeuix/themes/aura";
5
5
  import "primeicons/primeicons.css";
6
6
  import App from "./App.vue";
7
7
  import router from "./router";
8
- import i18n from "./i18n";
8
+ import i18n, { initLocale } from "./i18n";
9
9
  import { initTheme } from "./theme";
10
+ import { ready } from "./storage";
10
11
  import "./assets/main.css";
11
12
 
12
- initTheme();
13
+ // Hydrate the IndexedDB store before mounting: the theme, locale and auth
14
+ // token are read synchronously from its in-memory mirror, so this guarantees
15
+ // the first paint has the right theme/language. The `theme-loading` class
16
+ // (set in index.html) keeps the page hidden until now to avoid any flash.
17
+ async function bootstrap() {
18
+ await ready();
19
+ initTheme();
20
+ await initLocale();
21
+ document.documentElement.classList.remove("theme-loading");
13
22
 
14
- const app = createApp(App);
23
+ const app = createApp(App);
15
24
 
16
- app.use(PrimeVue, {
17
- theme: {
18
- preset: Aura,
19
- options: {
20
- cssLayer: false,
25
+ app.use(PrimeVue, {
26
+ theme: {
27
+ preset: Aura,
28
+ options: {
29
+ cssLayer: false,
30
+ },
21
31
  },
22
- },
23
- });
24
- app.use(createPinia());
25
- app.use(router);
26
- app.use(i18n);
32
+ });
33
+ app.use(createPinia());
34
+ app.use(router);
35
+ app.use(i18n);
27
36
 
28
- app.mount("#app");
37
+ app.mount("#app");
38
+ }
39
+
40
+ bootstrap();
@@ -1,5 +1,6 @@
1
1
  import axios from "axios";
2
2
  import { decrypt } from "@/utils/crypto";
3
+ import storage from "@/storage";
3
4
 
4
5
  /**
5
6
  * Central client for talking to the Laravel backend.
@@ -21,9 +22,10 @@ const api = axios.create({
21
22
  });
22
23
 
23
24
  api.interceptors.request.use((config) => {
24
- // Read the token straight from storage (not the Pinia store) to avoid a
25
- // circular import between this file and stores/auth.js.
26
- const raw = localStorage.getItem("falak_user_token");
25
+ // Read the token from the in-memory mirror of the IndexedDB store (hydrated
26
+ // at boot) rather than the Pinia store, to avoid a circular import between
27
+ // this file and stores/auth.js.
28
+ const raw = storage.getSync("falak_user_token");
27
29
  if (raw) {
28
30
  const token = decrypt(raw);
29
31
  if (token) config.headers.Authorization = `Bearer ${token}`;
@@ -3,7 +3,7 @@
3
3
  // When VITE_DATA_MODE=demo the whole app runs on the bundled demo data with no
4
4
  // Laravel backend or network required. This module mirrors the same routes the
5
5
  // real API exposes — auth/me, auth/login, profile/settings writes, entity
6
- // lists — and serves them from a localStorage-backed database seeded from
6
+ // lists — and serves them from an IndexedDB-backed database seeded from
7
7
  // src/data/demoAccounts.js and src/data/demo/*.js. Flip VITE_DATA_MODE=live and
8
8
  // the app talks to the real backend again (see services/api.js).
9
9
  //
@@ -11,6 +11,7 @@
11
11
  // file discovers them via import.meta.glob — only files that exist at build
12
12
  // time are bundled, and nothing breaks when ones are missing.
13
13
  import { decrypt } from "@/utils/crypto";
14
+ import storage from "@/storage";
14
15
 
15
16
  const DB_KEY = "falak_demo_db";
16
17
  const TOKEN_KEY = "falak_user_token";
@@ -105,16 +106,16 @@ function fingerprint() {
105
106
 
106
107
  let db = null;
107
108
 
108
- function loadDB() {
109
+ // Loaded lazily on the first request (always after storage.ready() has run at
110
+ // boot, so the mirror already holds any persisted DB). Falls back to a fresh
111
+ // seed when the stored fingerprint no longer matches the bundled data.
112
+ function ensureDB() {
113
+ if (db) return db;
109
114
  const fp = fingerprint();
110
- try {
111
- const raw = JSON.parse(localStorage.getItem(DB_KEY) || "null");
112
- if (raw && raw.fingerprint === fp) {
113
- db = raw;
114
- return db;
115
- }
116
- } catch {
117
- // Fall through to a fresh seed.
115
+ const raw = storage.getSync(DB_KEY, null);
116
+ if (raw && raw.fingerprint === fp) {
117
+ db = raw;
118
+ return db;
118
119
  }
119
120
  const collections = buildCollections();
120
121
  db = { fingerprint: fp, accounts: buildSeedAccounts(collections), collections };
@@ -123,15 +124,10 @@ function loadDB() {
123
124
  }
124
125
 
125
126
  function persist() {
126
- try {
127
- localStorage.setItem(DB_KEY, JSON.stringify(db));
128
- } catch {
129
- // Quota/storage unavailable — keep running in-memory only.
130
- }
127
+ // Mirror updates synchronously; IndexedDB write happens in the background.
128
+ storage.set(DB_KEY, db);
131
129
  }
132
130
 
133
- loadDB();
134
-
135
131
  function fail(message, status = 400) {
136
132
  const error = new Error(message);
137
133
  error.response = { status, data: { message } };
@@ -139,7 +135,7 @@ function fail(message, status = 400) {
139
135
  }
140
136
 
141
137
  function tokenFromStorage() {
142
- const raw = localStorage.getItem(TOKEN_KEY);
138
+ const raw = storage.getSync(TOKEN_KEY);
143
139
  if (!raw) return null;
144
140
  const token = decrypt(raw);
145
141
  const match = /^demo\.(\d+)$/.exec(token || "");
@@ -148,7 +144,7 @@ function tokenFromStorage() {
148
144
 
149
145
  function requireAccount() {
150
146
  const id = tokenFromStorage();
151
- const account = id && db.accounts.find((a) => String(a.id) === String(id));
147
+ const account = id && ensureDB().accounts.find((a) => String(a.id) === String(id));
152
148
  if (!account) fail("Unauthenticated", 401);
153
149
  return account;
154
150
  }
@@ -225,6 +221,7 @@ function overview() {
225
221
 
226
222
  const demoApi = {
227
223
  async get(url) {
224
+ ensureDB();
228
225
  const path = (url || "").split("?")[0];
229
226
  if (path === "/auth/me") return { data: publicUser(requireAccount()) };
230
227
  if (path === "/overview") return { data: overview() };
@@ -234,6 +231,7 @@ const demoApi = {
234
231
  },
235
232
 
236
233
  async post(url, body = {}) {
234
+ ensureDB();
237
235
  const path = (url || "").split("?")[0];
238
236
  switch (path) {
239
237
  case "/auth/login": {
@@ -331,6 +329,7 @@ const demoApi = {
331
329
  },
332
330
 
333
331
  async put(url, body = {}) {
332
+ ensureDB();
334
333
  const path = (url || "").split("?")[0];
335
334
  if (path === "/auth/me") {
336
335
  const account = requireAccount();
@@ -371,6 +370,7 @@ const demoApi = {
371
370
  },
372
371
 
373
372
  async delete(url) {
373
+ ensureDB();
374
374
  const entity = matchEntity((url || "").split("?")[0]);
375
375
  if (entity?.id !== null) {
376
376
  const rows = getEntityRows(entity.name);
@@ -0,0 +1,86 @@
1
+ // Single IndexedDB-backed key/value store for every piece of client-side
2
+ // persistence (auth token, locale, theme, remembered login, demo database).
3
+ // The app never uses localStorage/sessionStorage.
4
+ //
5
+ // IndexedDB is asynchronous, but a few call sites need a value synchronously
6
+ // (the axios Authorization header, the theme applied in App.vue). To support
7
+ // that we keep an in-memory mirror of every key and hydrate it once at boot
8
+ // via `ready()` — main.js awaits it before mounting. `getSync` then reads the
9
+ // mirror. Writes update the mirror immediately and persist to IndexedDB in the
10
+ // background, so the UI never blocks on disk.
11
+ import { openDB } from "idb";
12
+
13
+ const DB_NAME = "falak_app";
14
+ const STORE_NAME = "kv";
15
+ const DB_VERSION = 1;
16
+
17
+ let dbPromise = null;
18
+ function getDB() {
19
+ if (!dbPromise) {
20
+ dbPromise = openDB(DB_NAME, DB_VERSION, {
21
+ upgrade(db) {
22
+ if (!db.objectStoreNames.contains(STORE_NAME)) db.createObjectStore(STORE_NAME);
23
+ },
24
+ });
25
+ }
26
+ return dbPromise;
27
+ }
28
+
29
+ const mirror = new Map();
30
+ let readyPromise = null;
31
+
32
+ async function hydrate() {
33
+ try {
34
+ const db = await getDB();
35
+ const keys = await db.getAllKeys(STORE_NAME);
36
+ const values = await db.getAll(STORE_NAME);
37
+ keys.forEach((key, index) => {
38
+ // Don't clobber a value written while hydration was in flight.
39
+ if (!mirror.has(key)) mirror.set(key, values[index]);
40
+ });
41
+ } catch {
42
+ // IndexedDB unavailable (private mode / SSR / tests) — stay in-memory.
43
+ }
44
+ }
45
+
46
+ // Resolves once the mirror has been populated from IndexedDB.
47
+ export function ready() {
48
+ if (!readyPromise) readyPromise = hydrate();
49
+ return readyPromise;
50
+ }
51
+
52
+ // Synchronous read from the in-memory mirror (populated after ready()).
53
+ export function getSync(key, fallback = null) {
54
+ return mirror.has(key) ? mirror.get(key) : fallback;
55
+ }
56
+
57
+ // Asynchronous read — always reflects the persisted value.
58
+ export async function get(key, fallback = null) {
59
+ await ready();
60
+ return mirror.has(key) ? mirror.get(key) : fallback;
61
+ }
62
+
63
+ export async function set(key, value) {
64
+ mirror.set(key, value);
65
+ await ready();
66
+ try {
67
+ const db = await getDB();
68
+ await db.put(STORE_NAME, value, key);
69
+ } catch {
70
+ // Persist failed — the in-memory mirror still holds the value.
71
+ }
72
+ }
73
+
74
+ export async function remove(key) {
75
+ mirror.delete(key);
76
+ await ready();
77
+ try {
78
+ const db = await getDB();
79
+ await db.delete(STORE_NAME, key);
80
+ } catch {
81
+ // Nothing persisted to remove.
82
+ }
83
+ }
84
+
85
+ const storage = { ready, get, getSync, set, remove };
86
+ export default storage;
@@ -1,11 +1,12 @@
1
1
  import { defineStore } from "pinia";
2
2
  import { encrypt, decrypt } from "@/utils/crypto";
3
3
  import { client } from "@/services/dataClient";
4
+ import storage from "@/storage";
4
5
 
5
6
  const STORAGE_KEY = "falak_user_token";
6
7
 
7
8
  function loadToken() {
8
- const raw = localStorage.getItem(STORAGE_KEY);
9
+ const raw = storage.getSync(STORAGE_KEY);
9
10
  if (!raw) return null;
10
11
  try {
11
12
  return decrypt(raw) || null;
@@ -27,12 +28,12 @@ export const useAuthStore = defineStore("auth", {
27
28
  actions: {
28
29
  setToken(token) {
29
30
  this.userToken = token;
30
- localStorage.setItem(STORAGE_KEY, encrypt(token));
31
+ storage.set(STORAGE_KEY, encrypt(token));
31
32
  },
32
33
  clearToken() {
33
34
  this.userToken = null;
34
35
  this.user = null;
35
- localStorage.removeItem(STORAGE_KEY);
36
+ storage.remove(STORAGE_KEY);
36
37
  },
37
38
  // Two-header auth: X-API-Key identifies the *app*, Authorization Bearer
38
39
  // identifies the *user*. Both are required by the Laravel API middleware.
@@ -1,11 +1,15 @@
1
1
  // Light/dark theme. Dark is the default (no class); light adds `.light` on
2
- // <html> (tokens in assets/main.css flip). Choice persists in localStorage;
3
- // before any stored choice we follow the OS preference. The tiny duplicated
4
- // logic in index.html <head> runs first to avoid a flash of the wrong theme.
2
+ // <html> (tokens in assets/main.css flip). The choice persists in IndexedDB
3
+ // (src/storage.js); before any stored choice we follow the OS preference.
4
+ // main.js awaits storage.ready() before initTheme(), so getSync() sees the
5
+ // persisted value; index.html keeps the page hidden until then to avoid a flash
6
+ // of the wrong theme.
7
+ import storage from "@/storage";
8
+
5
9
  const KEY = "falak_theme";
6
10
 
7
11
  export function getTheme() {
8
- const stored = localStorage.getItem(KEY);
12
+ const stored = storage.getSync(KEY);
9
13
  if (stored === "light" || stored === "dark") return stored;
10
14
  if (window.matchMedia?.("(prefers-color-scheme: light)").matches) return "light";
11
15
  return "dark";
@@ -20,7 +24,7 @@ export function initTheme() {
20
24
  }
21
25
 
22
26
  export function setTheme(theme) {
23
- localStorage.setItem(KEY, theme);
27
+ storage.set(KEY, theme);
24
28
  applyTheme(theme);
25
29
  }
26
30
 
@@ -28,4 +32,4 @@ export function toggleTheme() {
28
32
  const next = getTheme() === "light" ? "dark" : "light";
29
33
  setTheme(next);
30
34
  return next;
31
- }
35
+ }
@@ -1,8 +1,9 @@
1
1
  import CryptoJS from "crypto-js";
2
2
 
3
3
  // AES-256-CBC, key/IV pulled from .env (generated fresh per-project by the CLI).
4
- // Use this for encrypting sensitive values you must keep in localStorage/sessionStorage
5
- // (e.g. a cached user token) — never for anything you'd rather the backend handle.
4
+ // Use this for encrypting sensitive values you must keep client-side
5
+ // (e.g. the cached user token in the IndexedDB store, src/storage.js) — never
6
+ // for anything you'd rather the backend handle.
6
7
  const KEY = CryptoJS.enc.Hex.parse(import.meta.env.VITE_AES_KEY || "");
7
8
  const IV = CryptoJS.enc.Hex.parse(import.meta.env.VITE_AES_IV || "");
8
9
 
@@ -7,6 +7,7 @@ import AuthLayout from "@/components/AuthLayout.vue";
7
7
  import SocialIcon from "@/components/SocialIcon.vue";
8
8
  import { useRipple } from "@/composables/useRipple";
9
9
  import { isDemoMode, client } from "@/services/dataClient";
10
+ import storage from "@/storage";
10
11
 
11
12
  const { t } = useI18n();
12
13
  const router = useRouter();
@@ -38,7 +39,7 @@ onMounted(async () => {
38
39
  }
39
40
  });
40
41
 
41
- const savedIdentifier = localStorage.getItem(REMEMBER_KEY) || "";
42
+ const savedIdentifier = storage.getSync(REMEMBER_KEY, "") || "";
42
43
  if (savedIdentifier) {
43
44
  identifier.value = savedIdentifier;
44
45
  remember.value = true;
@@ -46,8 +47,8 @@ if (savedIdentifier) {
46
47
  }
47
48
 
48
49
  watch(remember, (on) => {
49
- if (on) localStorage.setItem(REMEMBER_KEY, identifier.value);
50
- else localStorage.removeItem(REMEMBER_KEY);
50
+ if (on) storage.set(REMEMBER_KEY, identifier.value);
51
+ else storage.remove(REMEMBER_KEY);
51
52
  });
52
53
 
53
54
  async function submit() {
@@ -55,8 +56,8 @@ async function submit() {
55
56
  loading.value = true;
56
57
  try {
57
58
  await auth.login(identifier.value, password.value);
58
- if (remember.value) localStorage.setItem(REMEMBER_KEY, identifier.value);
59
- else localStorage.removeItem(REMEMBER_KEY);
59
+ if (remember.value) storage.set(REMEMBER_KEY, identifier.value);
60
+ else storage.remove(REMEMBER_KEY);
60
61
  router.push({ name: "home" });
61
62
  } catch (e) {
62
63
  error.value = t("auth.loginError");