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 +2 -2
- package/bin/falak-app-duo +12 -8
- package/package.json +1 -1
- package/templates/default/index.html +16 -15
- package/templates/default/package.json +1 -0
- package/templates/default/src/assets/main.css +2 -2
- package/templates/default/src/i18n/index.js +11 -7
- package/templates/default/src/main.js +26 -14
- package/templates/default/src/services/api.js +5 -3
- package/templates/default/src/services/demoApi.js +19 -19
- package/templates/default/src/storage.js +86 -0
- package/templates/default/src/stores/auth.js +4 -3
- package/templates/default/src/theme.js +10 -6
- package/templates/default/src/utils/crypto.js +3 -2
- package/templates/default/src/views/Login.vue +6 -5
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
|
|
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
|
-
|
|
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(
|
|
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("
|
|
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
|
|
249
|
-
lines.push("
|
|
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.
|
|
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
|
-
//
|
|
9
|
-
//
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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>
|
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
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
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
app.use(PrimeVue, {
|
|
26
|
+
theme: {
|
|
27
|
+
preset: Aura,
|
|
28
|
+
options: {
|
|
29
|
+
cssLayer: false,
|
|
30
|
+
},
|
|
21
31
|
},
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
app.use(
|
|
25
|
-
app.use(
|
|
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
|
|
25
|
-
//
|
|
26
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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 =
|
|
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 &&
|
|
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 =
|
|
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
|
-
|
|
31
|
+
storage.set(STORAGE_KEY, encrypt(token));
|
|
31
32
|
},
|
|
32
33
|
clearToken() {
|
|
33
34
|
this.userToken = null;
|
|
34
35
|
this.user = null;
|
|
35
|
-
|
|
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).
|
|
3
|
-
// before any stored choice we follow the OS preference.
|
|
4
|
-
//
|
|
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 =
|
|
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
|
-
|
|
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
|
|
5
|
-
// (e.g.
|
|
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 =
|
|
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)
|
|
50
|
-
else
|
|
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)
|
|
59
|
-
else
|
|
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");
|