mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-19 09:01:22 +02:00
feat(auth): add password strength indicator and magic links
Password strength (zxcvbn-ts): - PasswordStrength component with 4-segment color bar and German feedback - Lazy-loaded with 150ms debounce to avoid SSR/bundle issues - Integrated into RegisterPage and ChangePassword components Magic Links (passwordless email): - Better Auth magicLink plugin (10-minute expiry) - sendMagicLinkEmail() in email service (German template) - Passthrough route for /magic-link/* endpoints - sendMagicLink() in shared-auth client - "Login-Link per E-Mail senden" button on all 20 login pages - All 21 auth stores have sendMagicLink() method Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
86d1da3587
commit
cc50c0c2ab
49 changed files with 430 additions and 1 deletions
|
|
@ -145,6 +145,12 @@ export const authStore = {
|
|||
return result;
|
||||
},
|
||||
|
||||
async sendMagicLink(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return { success: false, error: 'Auth not available on server' };
|
||||
return authService.sendMagicLink(email);
|
||||
},
|
||||
|
||||
isPasskeyAvailable(): boolean {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return false;
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@
|
|||
onSignInWithPasskey={() => authStore.signInWithPasskey()}
|
||||
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
|
||||
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
|
||||
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
|
||||
{goto}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
|
|
|
|||
|
|
@ -145,6 +145,12 @@ export const authStore = {
|
|||
return result;
|
||||
},
|
||||
|
||||
async sendMagicLink(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return { success: false, error: 'Auth not available on server' };
|
||||
return authService.sendMagicLink(email);
|
||||
},
|
||||
|
||||
isPasskeyAvailable(): boolean {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return false;
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@
|
|||
onSignInWithPasskey={() => authStore.signInWithPasskey()}
|
||||
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
|
||||
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
|
||||
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
|
||||
{goto}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
|
|
|
|||
|
|
@ -127,6 +127,12 @@ export const authStore = {
|
|||
return result;
|
||||
},
|
||||
|
||||
async sendMagicLink(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return { success: false, error: 'Auth not available on server' };
|
||||
return authService.sendMagicLink(email);
|
||||
},
|
||||
|
||||
isPasskeyAvailable(): boolean {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return false;
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
onSignInWithPasskey={() => authStore.signInWithPasskey()}
|
||||
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
|
||||
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
|
||||
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
|
||||
{goto}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
|
|
|
|||
|
|
@ -145,6 +145,12 @@ export const authStore = {
|
|||
return result;
|
||||
},
|
||||
|
||||
async sendMagicLink(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return { success: false, error: 'Auth not available on server' };
|
||||
return authService.sendMagicLink(email);
|
||||
},
|
||||
|
||||
isPasskeyAvailable(): boolean {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return false;
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@
|
|||
onSignInWithPasskey={() => authStore.signInWithPasskey()}
|
||||
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
|
||||
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
|
||||
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
|
||||
{goto}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
|
|
|
|||
|
|
@ -145,6 +145,12 @@ export const authStore = {
|
|||
return result;
|
||||
},
|
||||
|
||||
async sendMagicLink(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return { success: false, error: 'Auth not available on server' };
|
||||
return authService.sendMagicLink(email);
|
||||
},
|
||||
|
||||
isPasskeyAvailable(): boolean {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return false;
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
onSignInWithPasskey={() => authStore.signInWithPasskey()}
|
||||
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
|
||||
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
|
||||
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
|
||||
{goto}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
|
|
|
|||
|
|
@ -128,6 +128,12 @@ export const authStore = {
|
|||
return result;
|
||||
},
|
||||
|
||||
async sendMagicLink(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return { success: false, error: 'Auth not available on server' };
|
||||
return authService.sendMagicLink(email);
|
||||
},
|
||||
|
||||
isPasskeyAvailable(): boolean {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return false;
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@
|
|||
onSignInWithPasskey={() => authStore.signInWithPasskey()}
|
||||
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
|
||||
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
|
||||
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
|
||||
{goto}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
|
|
|
|||
|
|
@ -101,7 +101,12 @@ export const authStore = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
async sendMagicLink(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return { success: false, error: 'Auth not available on server' };
|
||||
return authService.sendMagicLink(email);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if passkeys are available in this browser
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
onSignInWithPasskey={() => authStore.signInWithPasskey()}
|
||||
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
|
||||
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
|
||||
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
|
||||
{goto}
|
||||
successRedirect="/dashboard"
|
||||
registerPath="/register"
|
||||
|
|
|
|||
|
|
@ -111,6 +111,10 @@ export const authStore = {
|
|||
return result;
|
||||
},
|
||||
|
||||
async sendMagicLink(email: string) {
|
||||
return authService.sendMagicLink(email);
|
||||
},
|
||||
|
||||
isPasskeyAvailable(): boolean {
|
||||
return authService.isPasskeyAvailable();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
onSignInWithPasskey={() => authStore.signInWithPasskey()}
|
||||
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
|
||||
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
|
||||
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
|
||||
{goto}
|
||||
successRedirect="/decks"
|
||||
registerPath="/register"
|
||||
|
|
|
|||
|
|
@ -129,6 +129,12 @@ export const authStore = {
|
|||
return result;
|
||||
},
|
||||
|
||||
async sendMagicLink(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return { success: false, error: 'Auth not available on server' };
|
||||
return authService.sendMagicLink(email);
|
||||
},
|
||||
|
||||
isPasskeyAvailable(): boolean {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return false;
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@
|
|||
onSignInWithPasskey={() => authStore.signInWithPasskey()}
|
||||
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
|
||||
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
|
||||
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
|
||||
{goto}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
|
|
|
|||
|
|
@ -143,6 +143,12 @@ export const authStore = {
|
|||
return result;
|
||||
},
|
||||
|
||||
async sendMagicLink(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return { success: false, error: 'Auth not available on server' };
|
||||
return authService.sendMagicLink(email);
|
||||
},
|
||||
|
||||
isPasskeyAvailable(): boolean {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return false;
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@
|
|||
onSignInWithPasskey={() => authStore.signInWithPasskey()}
|
||||
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
|
||||
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
|
||||
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
|
||||
{goto}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
|
|
|
|||
|
|
@ -131,6 +131,12 @@ export const authStore = {
|
|||
return result;
|
||||
},
|
||||
|
||||
async sendMagicLink(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return { success: false, error: 'Auth not available on server' };
|
||||
return authService.sendMagicLink(email);
|
||||
},
|
||||
|
||||
isPasskeyAvailable(): boolean {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return false;
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@
|
|||
onSignInWithPasskey={() => authStore.signInWithPasskey()}
|
||||
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
|
||||
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
|
||||
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
|
||||
{goto}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
|
|
|
|||
|
|
@ -143,6 +143,12 @@ export const authStore = {
|
|||
return result;
|
||||
},
|
||||
|
||||
async sendMagicLink(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return { success: false, error: 'Auth not available on server' };
|
||||
return authService.sendMagicLink(email);
|
||||
},
|
||||
|
||||
isPasskeyAvailable(): boolean {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return false;
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
onSignInWithPasskey={() => authStore.signInWithPasskey()}
|
||||
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
|
||||
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
|
||||
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
|
||||
{goto}
|
||||
successRedirect="/app/gallery"
|
||||
registerPath="/auth/signup"
|
||||
|
|
|
|||
|
|
@ -137,6 +137,12 @@ export const authStore = {
|
|||
return result;
|
||||
},
|
||||
|
||||
async sendMagicLink(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return { success: false, error: 'Auth not available on server' };
|
||||
return authService.sendMagicLink(email);
|
||||
},
|
||||
|
||||
isPasskeyAvailable(): boolean {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return false;
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@
|
|||
onSignInWithPasskey={() => authStore.signInWithPasskey()}
|
||||
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
|
||||
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
|
||||
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
|
||||
{goto}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
|
|
|
|||
|
|
@ -106,6 +106,12 @@ export const authStore = {
|
|||
return result;
|
||||
},
|
||||
|
||||
async sendMagicLink(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return { success: false, error: 'Auth not available on server' };
|
||||
return authService.sendMagicLink(email);
|
||||
},
|
||||
|
||||
isPasskeyAvailable(): boolean {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return false;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
onSignInWithPasskey={() => authStore.signInWithPasskey()}
|
||||
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
|
||||
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
|
||||
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
|
||||
{goto}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
|
|
|
|||
|
|
@ -142,6 +142,12 @@ export const auth = {
|
|||
return result;
|
||||
},
|
||||
|
||||
async sendMagicLink(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return { success: false, error: 'Auth not available on server' };
|
||||
return authService.sendMagicLink(email);
|
||||
},
|
||||
|
||||
isPasskeyAvailable(): boolean {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return false;
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
onSignInWithPasskey={() => auth.signInWithPasskey()}
|
||||
onVerifyTwoFactor={(code, trust) => auth.verifyTwoFactor(code, trust)}
|
||||
onVerifyBackupCode={(code) => auth.verifyBackupCode(code)}
|
||||
onSendMagicLink={(email) => auth.sendMagicLink(email)}
|
||||
{goto}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
|
|
|
|||
|
|
@ -135,6 +135,12 @@ export const authStore = {
|
|||
return result;
|
||||
},
|
||||
|
||||
async sendMagicLink(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return { success: false, error: 'Auth not available on server' };
|
||||
return authService.sendMagicLink(email);
|
||||
},
|
||||
|
||||
isPasskeyAvailable(): boolean {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return false;
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@
|
|||
onSignInWithPasskey={() => authStore.signInWithPasskey()}
|
||||
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
|
||||
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
|
||||
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
|
||||
{goto}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
|
|
|
|||
|
|
@ -139,6 +139,12 @@ export const authStore = {
|
|||
return result;
|
||||
},
|
||||
|
||||
async sendMagicLink(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return { success: false, error: 'Auth not available on server' };
|
||||
return authService.sendMagicLink(email);
|
||||
},
|
||||
|
||||
isPasskeyAvailable(): boolean {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return false;
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@
|
|||
onSignInWithPasskey={() => authStore.signInWithPasskey()}
|
||||
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
|
||||
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
|
||||
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
|
||||
{goto}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
|
|
|
|||
|
|
@ -141,6 +141,12 @@ export const authStore = {
|
|||
return result;
|
||||
},
|
||||
|
||||
async sendMagicLink(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return { success: false, error: 'Auth not available on server' };
|
||||
return authService.sendMagicLink(email);
|
||||
},
|
||||
|
||||
isPasskeyAvailable(): boolean {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return false;
|
||||
|
|
|
|||
|
|
@ -156,6 +156,12 @@ export const authStore = {
|
|||
return result;
|
||||
},
|
||||
|
||||
async sendMagicLink(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return { success: false, error: 'Auth not available on server' };
|
||||
return authService.sendMagicLink(email);
|
||||
},
|
||||
|
||||
isPasskeyAvailable(): boolean {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return false;
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@
|
|||
onSignInWithPasskey={() => authStore.signInWithPasskey()}
|
||||
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
|
||||
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
|
||||
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
|
||||
{goto}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
|
|
|
|||
|
|
@ -145,6 +145,12 @@ export const authStore = {
|
|||
return result;
|
||||
},
|
||||
|
||||
async sendMagicLink(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return { success: false, error: 'Auth not available on server' };
|
||||
return authService.sendMagicLink(email);
|
||||
},
|
||||
|
||||
isPasskeyAvailable(): boolean {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return false;
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
onSignInWithPasskey={() => authStore.signInWithPasskey()}
|
||||
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
|
||||
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
|
||||
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
|
||||
{goto}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue