chore: harden webapp auth and update deps

This commit is contained in:
3252a8
2026-05-13 15:05:30 +03:00
parent 2b7d1f0be8
commit 2169fd8b50
24 changed files with 576 additions and 754 deletions
+2 -5
View File
@@ -53,9 +53,7 @@
CSRF_COOKIE_NAME,
isManuallyLoggedOut as readManualLogoutFlag,
markManualLogout as markManualLogoutInStorage,
persistToken,
readCookie,
readStoredToken,
} from "./lib/webapp/session.js";
import { createTelegramSdk } from "./lib/webapp/telegramSdk.js";
import { mockApi as runMockApi } from "./lib/webapp/mockApi.js";
@@ -105,7 +103,7 @@
let languageClickGuardArmTimer = null;
let emailAvatarUrl = "";
let avatarHashToken = "";
let token = MOCK ? "local-preview" : readStoredToken();
let token = MOCK ? "local-preview" : "";
let csrfToken = MOCK ? "" : readCookie(CSRF_COOKIE_NAME) || "";
let scrollLockApplied = false;
let tg = null;
@@ -132,7 +130,6 @@
const apiClient = createApiClient({
apiBase: CFG.apiBase,
csrfCookieName: CSRF_COOKIE_NAME,
getToken: () => token,
getCsrfToken: () => csrfToken,
onUnauthorized: () => {
clearToken();
@@ -621,7 +618,7 @@
clearManualLogoutFlag();
token = nextToken || "";
csrfToken = nextCsrf || readCookie(CSRF_COOKIE_NAME) || "";
if (token && !MOCK) persistToken(token);
if (!MOCK) clearStoredToken();
}
function clearToken() {
@@ -65,6 +65,12 @@
revealedSecrets = next;
}
function secretPlaceholder(field) {
if (settingsDirty[field.key]?.deleted) return field.placeholder || "••••••••";
if (field.has_value) return at("settings_secret_configured", {}, "Secret is set");
return field.placeholder || at("settings_secret_empty", {}, "Not set");
}
function groupSectionFields(section) {
const groups = new Map();
for (const field of section.fields || []) {
@@ -124,7 +130,9 @@
<div class="admin-setting-meta">
<strong>
{fieldLabelText(field)}
<AdminBadge variant="warning">{at("settings_badge_secret", {}, "Secret")}</AdminBadge>
{#if field.secret}
<AdminBadge variant="warning">{at("settings_badge_secret", {}, "Secret")}</AdminBadge>
{/if}
{#if isOverridden(field)}
<AdminBadge variant="success">{at("settings_badge_override", {}, "Override")}</AdminBadge>
{/if}
@@ -189,7 +197,7 @@
<input
class="input"
type={revealed ? "text" : "password"}
placeholder={field.placeholder || "••••••••"}
placeholder={secretPlaceholder(field)}
autocomplete="off"
value={valueFor(field) ?? ""}
on:input={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
@@ -201,10 +201,10 @@ export async function mockApi(path, options = {}, context = {}) {
if (path === "/me") return clone(DEV_MOCK.data);
if (path === "/auth/email/request") return { ok: true };
if (path === "/auth/email/verify" || path === "/auth/email/magic") {
return { ok: true, token: "local-preview", csrf_token: "local-preview-csrf" };
return { ok: true, csrf_token: "local-preview-csrf" };
}
if (path === "/auth/token") {
return { ok: true, token: "local-preview", csrf_token: "local-preview-csrf" };
return { ok: true, csrf_token: "local-preview-csrf" };
}
if (path === "/promo/apply") return { ok: true, end_date_text: "31.05.2026" };
if (path === "/devices") return clone(DEV_MOCK.data.devices);
@@ -267,10 +267,10 @@ export async function mockApi(path, options = {}, context = {}) {
return { ok: true };
}
if (path === "/account/email/verify" && String(options.method || "").toUpperCase() === "POST") {
return { ok: true, token: "local-preview", csrf_token: "local-preview-csrf" };
return { ok: true, csrf_token: "local-preview-csrf" };
}
if (path === "/account/telegram/link" && String(options.method || "").toUpperCase() === "POST") {
return { ok: true, token: "local-preview", csrf_token: "local-preview-csrf" };
return { ok: true, csrf_token: "local-preview-csrf" };
}
if (path === "/payments" && String(options.method || "").toUpperCase() === "POST") {
return {
@@ -3,7 +3,6 @@ import { readCookie } from "./session.js";
export function createApiClient({
apiBase = "",
csrfCookieName = "rw_webapp_csrf",
getToken = () => "",
getCsrfToken = () => "",
onUnauthorized = () => {},
mockApi = null,
@@ -14,8 +13,6 @@ export function createApiClient({
const method = String(options.method || "GET").toUpperCase();
const headers = { ...(options.headers || {}) };
const token = getToken();
if (token) headers.Authorization = `Bearer ${token}`;
const csrf = getCsrfToken() || readCookie(csrfCookieName) || "";
if (csrf && ["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
@@ -23,7 +20,11 @@ export function createApiClient({
}
if (options.body && !headers["Content-Type"]) headers["Content-Type"] = "application/json";
const response = await fetch(`${apiBase}${path}`, { ...options, headers });
const response = await fetch(`${apiBase}${path}`, {
...options,
headers,
credentials: "same-origin",
});
const payload = await response.json().catch(() => ({}));
if (response.status === 401) onUnauthorized();
return payload;
@@ -38,6 +39,7 @@ export function createApiClient({
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
signal: options.signal,
credentials: "same-origin",
});
return response.json();
}
@@ -13,16 +13,6 @@ export function readCookie(name) {
return cookie ? decodeURIComponent(cookie.slice(prefix.length)) : "";
}
export function readStoredToken(storageKey = TOKEN_STORAGE_KEY) {
if (typeof localStorage === "undefined") return "";
return localStorage.getItem(storageKey) || "";
}
export function persistToken(token, storageKey = TOKEN_STORAGE_KEY) {
if (!token || typeof localStorage === "undefined") return;
localStorage.setItem(storageKey, token);
}
export function clearStoredToken(storageKey = TOKEN_STORAGE_KEY) {
if (typeof localStorage === "undefined") return;
localStorage.removeItem(storageKey);
@@ -139,7 +139,7 @@ export function createAccountStore({
body: JSON.stringify({ email: s.linkEmailPending, code }),
});
if (!response?.ok) throw response;
if (response?.token) setToken(response.token, response.csrf_token);
if (response?.csrf_token) setToken("", response.csrf_token);
await loadData();
closeLinkEmailDialog();
showToast(t("wa_settings_linked"));
@@ -158,7 +158,7 @@ export function createAccountStore({
body: JSON.stringify(payload),
});
if (!response?.ok) throw response;
if (response?.token) setToken(response.token, response.csrf_token);
if (response?.csrf_token) setToken("", response.csrf_token);
await loadData();
showToast(t("wa_settings_linked"));
} catch (error) {
@@ -96,8 +96,8 @@ export function createAuthStore({
const referralParam = readReferralParam(getTg());
if (referralParam) payload.referral_code = referralParam;
const response = await publicApi("/auth/email/magic", payload);
if (response.ok && response.token) {
setToken(response.token, response.csrf_token);
if (response.ok && response.csrf_token) {
setToken("", response.csrf_token);
clearAuthQuery();
await loadData();
return true;
@@ -126,8 +126,8 @@ export function createAuthStore({
const referralParam = readReferralParam(getTg());
if (referralParam) payload.referral_code = referralParam;
const response = await publicApi("/auth/token", payload, { signal: options.signal });
if (response.ok && response.token) {
setToken(response.token, response.csrf_token);
if (response.ok && response.csrf_token) {
setToken("", response.csrf_token);
clearAuthQuery();
setAuthStatus("");
await loadData();
@@ -202,8 +202,8 @@ export function createAuthStore({
const referralParam = readReferralParam(getTg());
if (referralParam) payload.referral_code = referralParam;
const response = await publicApi("/auth/email/verify", payload);
if (!response.ok || !response.token) throw response;
setToken(response.token, response.csrf_token);
if (!response.ok || !response.csrf_token) throw response;
setToken("", response.csrf_token);
await loadData();
setAuthStatus("");
} catch (error) {
@@ -34,7 +34,7 @@ export function createBillingStore({ billing, loadData, t, showToast, openExtern
defaultMethod = ""
) {
state.update((s) => {
let step = s.paymentStep;
let step;
let plan = s.selectedPlan;
let tariffKey = s.selectedTariffKey;