feat: email and password login
This commit is contained in:
@@ -231,7 +231,7 @@ export async function mockApi(path, options = {}, context = {}) {
|
||||
ok: true,
|
||||
favicon_url: "/webapp-favicon/1111111111111111/icon-180.png",
|
||||
variants: {
|
||||
"32": "/webapp-favicon/1111111111111111/icon-32.png",
|
||||
32: "/webapp-favicon/1111111111111111/icon-32.png",
|
||||
apple_touch: "/webapp-favicon/1111111111111111/apple-touch-icon.png",
|
||||
},
|
||||
};
|
||||
@@ -254,7 +254,8 @@ export async function mockApi(path, options = {}, context = {}) {
|
||||
DEV_MOCK.config.faviconUrl = updates.WEBAPP_FAVICON_URL || "";
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(updates, "WEBAPP_LOGO_FAVICON_URL")) {
|
||||
DEV_MOCK.config.faviconUrl = updates.WEBAPP_LOGO_FAVICON_URL || DEV_MOCK.config.faviconUrl || "";
|
||||
DEV_MOCK.config.faviconUrl =
|
||||
updates.WEBAPP_LOGO_FAVICON_URL || DEV_MOCK.config.faviconUrl || "";
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(updates, "WEBAPP_FAVICON_USE_CUSTOM")) {
|
||||
DEV_MOCK.config.faviconUseCustom = Boolean(updates.WEBAPP_FAVICON_USE_CUSTOM);
|
||||
@@ -337,6 +338,9 @@ export async function mockApi(path, options = {}, context = {}) {
|
||||
if (path === "/auth/email/verify" || path === "/auth/email/magic") {
|
||||
return { ok: true, csrf_token: "local-preview-csrf" };
|
||||
}
|
||||
if (path === "/auth/email/password") {
|
||||
return { ok: false, error: "password_login_failed", fallback: "email_code" };
|
||||
}
|
||||
if (path === "/auth/token") {
|
||||
return { ok: true, csrf_token: "local-preview-csrf" };
|
||||
}
|
||||
@@ -403,6 +407,19 @@ export async function mockApi(path, options = {}, context = {}) {
|
||||
if (path === "/account/email/verify" && String(options.method || "").toUpperCase() === "POST") {
|
||||
return { ok: true, csrf_token: "local-preview-csrf" };
|
||||
}
|
||||
if (
|
||||
path === "/account/password/request" &&
|
||||
String(options.method || "").toUpperCase() === "POST"
|
||||
) {
|
||||
return { ok: true };
|
||||
}
|
||||
if (
|
||||
path === "/account/password/confirm" &&
|
||||
String(options.method || "").toUpperCase() === "POST"
|
||||
) {
|
||||
DEV_MOCK.data.user.password_auth_enabled = true;
|
||||
return { ok: true, password_auth_enabled: true };
|
||||
}
|
||||
if (path === "/account/telegram/link" && String(options.method || "").toUpperCase() === "POST") {
|
||||
return { ok: true, csrf_token: "local-preview-csrf" };
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ export const DEV_MOCK = {
|
||||
username: "username",
|
||||
email: "user@example.com",
|
||||
email_verified: true,
|
||||
password_auth_enabled: false,
|
||||
telegram_id: 100200300,
|
||||
telegram_linked: true,
|
||||
telegram_photo_url: "",
|
||||
|
||||
@@ -29,10 +29,20 @@ export function createAccountStore({
|
||||
linkEmailIsError: false,
|
||||
linkEmailFieldError: "",
|
||||
linkEmailResendCooldown: 0,
|
||||
setPasswordOpen: false,
|
||||
setPasswordBusy: false,
|
||||
setPasswordPending: false,
|
||||
setPasswordValue: "",
|
||||
setPasswordConfirm: "",
|
||||
setPasswordCode: "",
|
||||
setPasswordStatus: "",
|
||||
setPasswordIsError: false,
|
||||
setPasswordResendCooldown: 0,
|
||||
languageBusy: false,
|
||||
});
|
||||
|
||||
let linkEmailResendTimer = null;
|
||||
let setPasswordResendTimer = null;
|
||||
|
||||
function setLinkEmailStatus(message, isError = false) {
|
||||
state.update((s) => ({ ...s, linkEmailStatus: message, linkEmailIsError: isError }));
|
||||
@@ -45,6 +55,13 @@ export function createAccountStore({
|
||||
}
|
||||
}
|
||||
|
||||
function clearPasswordCooldownTimer() {
|
||||
if (setPasswordResendTimer) {
|
||||
window.clearInterval(setPasswordResendTimer);
|
||||
setPasswordResendTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startCooldownTimer(seconds = 60) {
|
||||
clearCooldownTimer();
|
||||
state.update((s) => ({ ...s, linkEmailResendCooldown: Math.max(0, Number(seconds || 60)) }));
|
||||
@@ -59,6 +76,20 @@ export function createAccountStore({
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function startPasswordCooldownTimer(seconds = 60) {
|
||||
clearPasswordCooldownTimer();
|
||||
state.update((s) => ({ ...s, setPasswordResendCooldown: Math.max(0, Number(seconds || 60)) }));
|
||||
setPasswordResendTimer = window.setInterval(() => {
|
||||
const s = get(state);
|
||||
if (s.setPasswordResendCooldown <= 1) {
|
||||
state.update((s) => ({ ...s, setPasswordResendCooldown: 0 }));
|
||||
clearPasswordCooldownTimer();
|
||||
return;
|
||||
}
|
||||
state.update((s) => ({ ...s, setPasswordResendCooldown: s.setPasswordResendCooldown - 1 }));
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function openLinkEmailDialog(email) {
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
@@ -90,6 +121,65 @@ export function createAccountStore({
|
||||
clearCooldownTimer();
|
||||
}
|
||||
|
||||
function setPasswordStatus(message, isError = false) {
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
setPasswordStatus: message,
|
||||
setPasswordIsError: isError,
|
||||
}));
|
||||
}
|
||||
|
||||
function openSetPasswordDialog() {
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
setPasswordOpen: true,
|
||||
setPasswordBusy: false,
|
||||
setPasswordPending: false,
|
||||
setPasswordValue: "",
|
||||
setPasswordConfirm: "",
|
||||
setPasswordCode: "",
|
||||
setPasswordStatus: "",
|
||||
setPasswordIsError: false,
|
||||
setPasswordResendCooldown: 0,
|
||||
}));
|
||||
clearPasswordCooldownTimer();
|
||||
}
|
||||
|
||||
function closeSetPasswordDialog() {
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
setPasswordOpen: false,
|
||||
setPasswordBusy: false,
|
||||
setPasswordPending: false,
|
||||
setPasswordValue: "",
|
||||
setPasswordConfirm: "",
|
||||
setPasswordCode: "",
|
||||
setPasswordStatus: "",
|
||||
setPasswordIsError: false,
|
||||
setPasswordResendCooldown: 0,
|
||||
}));
|
||||
clearPasswordCooldownTimer();
|
||||
}
|
||||
|
||||
function validatePasswordDraft() {
|
||||
const s = get(state);
|
||||
const password = String(s.setPasswordValue || "");
|
||||
const passwordConfirm = String(s.setPasswordConfirm || "");
|
||||
if (password.length < 8) {
|
||||
setPasswordStatus(t("wa_password_too_short"), true);
|
||||
return false;
|
||||
}
|
||||
if (password.length > 128) {
|
||||
setPasswordStatus(t("wa_password_too_long"), true);
|
||||
return false;
|
||||
}
|
||||
if (password !== passwordConfirm) {
|
||||
setPasswordStatus(t("wa_password_mismatch"), true);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function requestLinkEmailCode() {
|
||||
const s = get(state);
|
||||
if (s.linkEmailPending && s.linkEmailResendCooldown > 0) return;
|
||||
@@ -150,6 +240,66 @@ export function createAccountStore({
|
||||
}
|
||||
}
|
||||
|
||||
async function requestSetPasswordCode() {
|
||||
const s = get(state);
|
||||
if (s.setPasswordPending && s.setPasswordResendCooldown > 0) return;
|
||||
if (!validatePasswordDraft()) return;
|
||||
state.update((s) => ({ ...s, setPasswordBusy: true }));
|
||||
setPasswordStatus(t("wa_auth_sending_code"));
|
||||
try {
|
||||
const response = await api("/account/password/request", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
if (!response?.ok) throw response;
|
||||
state.update((s) => ({ ...s, setPasswordPending: true, setPasswordCode: "" }));
|
||||
setPasswordStatus("");
|
||||
startPasswordCooldownTimer(60);
|
||||
} catch (error) {
|
||||
setPasswordStatus(emailError(error, t("wa_password_code_send_failed"), t), true);
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, setPasswordBusy: false }));
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmSetPassword() {
|
||||
const s = get(state);
|
||||
if (!validatePasswordDraft()) return;
|
||||
const code = String(s.setPasswordCode || "")
|
||||
.replace(/\D/g, "")
|
||||
.slice(0, 6);
|
||||
if (code.length !== 6) {
|
||||
setPasswordStatus(t("wa_auth_enter_code_6digits"), true);
|
||||
return;
|
||||
}
|
||||
state.update((s) => ({ ...s, setPasswordBusy: true }));
|
||||
setPasswordStatus(t("wa_auth_checking_code"));
|
||||
try {
|
||||
const response = await api("/account/password/confirm", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
password: s.setPasswordValue,
|
||||
password_confirm: s.setPasswordConfirm,
|
||||
code,
|
||||
}),
|
||||
});
|
||||
if (!response?.ok) throw response;
|
||||
await loadData();
|
||||
closeSetPasswordDialog();
|
||||
showToast(t("wa_password_set_success"));
|
||||
} catch (error) {
|
||||
const fallback =
|
||||
error?.error === "password_mismatch"
|
||||
? t("wa_password_mismatch")
|
||||
: error?.error === "password_too_short"
|
||||
? t("wa_password_too_short")
|
||||
: t("wa_password_set_failed");
|
||||
setPasswordStatus(emailError(error, fallback, t), true);
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, setPasswordBusy: false }));
|
||||
}
|
||||
}
|
||||
|
||||
async function linkTelegramAccountWithPayload(payload) {
|
||||
state.update((s) => ({ ...s, linkTelegramBusy: true }));
|
||||
try {
|
||||
@@ -228,11 +378,16 @@ export function createAccountStore({
|
||||
update: state.update,
|
||||
openLinkEmailDialog,
|
||||
closeLinkEmailDialog,
|
||||
openSetPasswordDialog,
|
||||
closeSetPasswordDialog,
|
||||
requestLinkEmailCode,
|
||||
verifyLinkEmailCode,
|
||||
requestSetPasswordCode,
|
||||
confirmSetPassword,
|
||||
linkTelegramAccount,
|
||||
updateAccountLanguage,
|
||||
logout,
|
||||
clearLinkEmailResendTimer: clearCooldownTimer,
|
||||
clearSetPasswordResendTimer: clearPasswordCooldownTimer,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,8 +25,11 @@ export function createAuthStore({
|
||||
loginEmailTooltipOpen: false,
|
||||
authResendCooldown: 0,
|
||||
email: "",
|
||||
emailPassword: "",
|
||||
pendingEmail: "",
|
||||
emailCode: "",
|
||||
passwordLoginMode: false,
|
||||
passwordLoginFallback: false,
|
||||
});
|
||||
|
||||
let authResendTimer = null;
|
||||
@@ -169,6 +172,7 @@ export function createAuthStore({
|
||||
loginEmailFieldError: "",
|
||||
loginEmailTooltipOpen: false,
|
||||
authBusy: true,
|
||||
passwordLoginFallback: false,
|
||||
}));
|
||||
setAuthStatus(t("wa_auth_sending_code"));
|
||||
try {
|
||||
@@ -188,6 +192,53 @@ export function createAuthStore({
|
||||
}
|
||||
}
|
||||
|
||||
async function loginWithEmailPassword() {
|
||||
const s = get(state);
|
||||
const normalized = s.email.trim().toLowerCase();
|
||||
const password = String(s.emailPassword || "");
|
||||
if (!normalized || !normalized.includes("@")) {
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
loginEmailFieldError: t("wa_auth_invalid_email"),
|
||||
loginEmailTooltipOpen: true,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (!password) {
|
||||
setAuthStatus(t("wa_auth_password_required"), true);
|
||||
return;
|
||||
}
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
loginEmailFieldError: "",
|
||||
loginEmailTooltipOpen: false,
|
||||
authBusy: true,
|
||||
passwordLoginFallback: false,
|
||||
}));
|
||||
setAuthStatus(t("wa_auth_checking_password"));
|
||||
try {
|
||||
const response = await publicApi("/auth/email/password", {
|
||||
email: normalized,
|
||||
password,
|
||||
});
|
||||
if (!response.ok || !response.csrf_token) throw response;
|
||||
setToken("", response.csrf_token);
|
||||
await loadData();
|
||||
setAuthStatus("");
|
||||
} catch (error) {
|
||||
if (error?.error === "rate_limited") {
|
||||
setAuthStatus(emailError(error, t("wa_auth_password_login_failed"), t), true);
|
||||
} else if (error?.error === "banned") {
|
||||
setAuthStatus(t("wa_auth_access_denied"), true);
|
||||
} else {
|
||||
state.update((s) => ({ ...s, passwordLoginFallback: true }));
|
||||
setAuthStatus(t("wa_auth_password_login_failed"), true);
|
||||
}
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, authBusy: false }));
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyEmailCode() {
|
||||
const s = get(state);
|
||||
const code = s.emailCode.replace(/\\D/g, "").slice(0, 6);
|
||||
@@ -278,6 +329,7 @@ export function createAuthStore({
|
||||
finalizeMagicLogin,
|
||||
finalizeTelegramAuth,
|
||||
requestEmailCode,
|
||||
loginWithEmailPassword,
|
||||
verifyEmailCode,
|
||||
openTelegramLogin,
|
||||
clearCooldownTimer,
|
||||
|
||||
Reference in New Issue
Block a user