Fix the homepage CSP inline-style violations by removing React style attributes from the homepage app and moving them into CSS modules.
Build & Deploy / Build & Push Docker Image (push) Successful in 2m55s
Test / Type Check (all packages) (push) Successful in 59s
Build & Deploy / Deploy to VPS (push) Successful in 4s
Test / API Unit Tests (push) Successful in 51s
Test / Homepage Unit Tests (push) Successful in 43s
Test / Storefront Unit Tests (push) Successful in 42s
Test / Admin Unit Tests (push) Successful in 42s
Test / Dashboard Unit Tests (push) Successful in 44s
Test / API Integration Tests (push) Successful in 1m0s
Build & Deploy / Build & Push Docker Image (push) Successful in 2m55s
Test / Type Check (all packages) (push) Successful in 59s
Build & Deploy / Deploy to VPS (push) Successful in 4s
Test / API Unit Tests (push) Successful in 51s
Test / Homepage Unit Tests (push) Successful in 43s
Test / Storefront Unit Tests (push) Successful in 42s
Test / Admin Unit Tests (push) Successful in 42s
Test / Dashboard Unit Tests (push) Successful in 44s
Test / API Integration Tests (push) Successful in 1m0s
This commit is contained in:
@@ -27,6 +27,12 @@
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.brandLogo {
|
||||
flex-shrink: 0;
|
||||
border-radius: 0.5rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.mark {
|
||||
display: inline-grid;
|
||||
min-inline-size: var(--size-touch-min);
|
||||
|
||||
@@ -61,7 +61,7 @@ export function SiteFooter({ locale, messages }: SiteFooterProps) {
|
||||
width={40}
|
||||
height={40}
|
||||
unoptimized
|
||||
style={{ borderRadius: '0.5rem', objectFit: 'contain', flexShrink: 0 }}
|
||||
className={styles.brandLogo}
|
||||
/>
|
||||
<span>RentalDriveGo</span>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.formStack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.successIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
inline-size: 3.5rem;
|
||||
block-size: 3.5rem;
|
||||
margin: 0 auto 1rem;
|
||||
border-radius: 50%;
|
||||
background: var(--color-success-bg, #ecfdf5);
|
||||
}
|
||||
|
||||
.successIconSvg {
|
||||
inline-size: 1.75rem;
|
||||
block-size: 1.75rem;
|
||||
color: var(--color-success-text, #059669);
|
||||
}
|
||||
|
||||
.statusTitle {
|
||||
color: var(--color-heading);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.statusBody {
|
||||
margin-block-start: 0.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
border: 1px solid var(--color-error-border, #fecaca);
|
||||
border-radius: 1.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--color-error-bg, #fef2f2);
|
||||
color: var(--color-error-text, #b91c1c);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.spacedError {
|
||||
margin-block-end: 1.25rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-block-start: 1.5rem;
|
||||
border-block-start: 1px solid var(--color-border);
|
||||
padding-block-start: 1.5rem;
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footerLink {
|
||||
color: var(--color-heading);
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
|
||||
.mutedLink {
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: 0.875rem;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
|
||||
.passwordField {
|
||||
position: relative;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.passwordInput {
|
||||
inline-size: 100%;
|
||||
min-block-size: 2.875rem;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: var(--space-3) 2.5rem var(--space-3) var(--space-4);
|
||||
background: var(--surface-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.passwordToggle {
|
||||
position: absolute;
|
||||
inset-block: 0;
|
||||
inset-inline-end: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.forgotLinkWrap {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.infoBox {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 1.5rem;
|
||||
padding: 1rem;
|
||||
background: var(--color-surface-secondary);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.totpInput {
|
||||
font-size: 1.25rem;
|
||||
letter-spacing: 0.45em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.plainButton {
|
||||
inline-size: 100%;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: var(--color-text-tertiary);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.noToken {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.actionWrap {
|
||||
margin-block-start: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
.main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-block-size: calc(100vh - 80px);
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
inline-size: 100%;
|
||||
max-inline-size: 28rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-block-end: 2rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 1.5rem;
|
||||
padding: 0.375rem;
|
||||
background: var(--color-surface);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-block-start: 0.5rem;
|
||||
color: var(--color-heading);
|
||||
font-size: 1.75rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-block-start: 0.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 2rem;
|
||||
padding: 2rem;
|
||||
background: var(--color-surface);
|
||||
box-shadow: 0 30px 80px rgba(28, 25, 23, 0.06);
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import { localizedPath, type Locale } from '@/lib/localization/config'
|
||||
import Image from 'next/image'
|
||||
import type { ReactNode } from 'react'
|
||||
import styles from './AuthShell.module.css'
|
||||
|
||||
interface AuthShellProps {
|
||||
locale: Locale
|
||||
@@ -16,17 +17,10 @@ export function AuthShell({ locale, title, subtitle, children }: AuthShellProps)
|
||||
<main
|
||||
id="main-content"
|
||||
tabIndex={-1}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '2rem 1rem',
|
||||
minHeight: 'calc(100vh - 80px)',
|
||||
}}
|
||||
className={styles.main}
|
||||
>
|
||||
<div style={{ width: '100%', maxWidth: '28rem' }}>
|
||||
<div style={{ marginBottom: '2rem', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<a href={localizedPath('home', locale)}>
|
||||
<Image
|
||||
src="/rentaldrivego.png"
|
||||
@@ -35,49 +29,20 @@ export function AuthShell({ locale, title, subtitle, children }: AuthShellProps)
|
||||
height={96}
|
||||
priority
|
||||
unoptimized
|
||||
style={{
|
||||
display: 'block',
|
||||
borderRadius: '1.5rem',
|
||||
border: '1px solid var(--color-border)',
|
||||
background: 'var(--color-surface)',
|
||||
padding: '0.375rem',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.08)',
|
||||
}}
|
||||
className={styles.logo}
|
||||
/>
|
||||
</a>
|
||||
<h1
|
||||
style={{
|
||||
marginTop: '0.5rem',
|
||||
fontSize: '1.75rem',
|
||||
fontWeight: 800,
|
||||
letterSpacing: '-0.03em',
|
||||
color: 'var(--color-heading)',
|
||||
}}
|
||||
>
|
||||
<h1 className={styles.title}>
|
||||
{title}
|
||||
</h1>
|
||||
{subtitle ? (
|
||||
<p
|
||||
style={{
|
||||
marginTop: '0.5rem',
|
||||
fontSize: '0.875rem',
|
||||
color: 'var(--color-text-secondary)',
|
||||
}}
|
||||
>
|
||||
<p className={styles.subtitle}>
|
||||
{subtitle}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<section
|
||||
style={{
|
||||
borderRadius: '2rem',
|
||||
border: '1px solid var(--color-border)',
|
||||
background: 'var(--color-surface)',
|
||||
padding: '2rem',
|
||||
boxShadow: '0 30px 80px rgba(28,25,23,0.06)',
|
||||
}}
|
||||
>
|
||||
<section className={styles.panel}>
|
||||
{children}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AuthShell } from '@/components/auth/AuthShell'
|
||||
import { API_BASE } from '@/lib/api'
|
||||
import { localizedPath, type Locale } from '@/lib/localization/config'
|
||||
import { useState } from 'react'
|
||||
import styles from './AuthForms.module.css'
|
||||
|
||||
interface Dict {
|
||||
title: string
|
||||
@@ -98,21 +99,10 @@ export function ForgotPasswordForm({ locale }: { locale: Locale }) {
|
||||
return (
|
||||
<AuthShell locale={locale} title={dict.title} subtitle={dict.subtitle}>
|
||||
{sent ? (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
margin: '0 auto 1rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '3.5rem',
|
||||
height: '3.5rem',
|
||||
borderRadius: '50%',
|
||||
background: 'var(--color-success-bg, #ecfdf5)',
|
||||
}}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<div className={styles.successIcon}>
|
||||
<svg
|
||||
style={{ width: '1.75rem', height: '1.75rem', color: 'var(--color-success-text, #059669)' }}
|
||||
className={styles.successIconSvg}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -121,26 +111,17 @@ export function ForgotPasswordForm({ locale }: { locale: Locale }) {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-heading)' }}>
|
||||
<h2 className={styles.statusTitle}>
|
||||
{dict.successTitle}
|
||||
</h2>
|
||||
<p style={{ marginTop: '0.5rem', fontSize: '0.875rem', color: 'var(--color-text-secondary)' }}>
|
||||
<p className={styles.statusBody}>
|
||||
{dict.successBody}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
||||
<form onSubmit={handleSubmit} className={styles.formStack}>
|
||||
{error ? (
|
||||
<div
|
||||
style={{
|
||||
borderRadius: '1.5rem',
|
||||
border: '1px solid var(--color-error-border, #fecaca)',
|
||||
background: 'var(--color-error-bg, #fef2f2)',
|
||||
padding: '0.75rem 1rem',
|
||||
fontSize: '0.875rem',
|
||||
color: 'var(--color-error-text, #b91c1c)',
|
||||
}}
|
||||
>
|
||||
<div className={styles.error}>
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -163,24 +144,8 @@ export function ForgotPasswordForm({ locale }: { locale: Locale }) {
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: '1.5rem',
|
||||
borderTop: '1px solid var(--color-border)',
|
||||
paddingTop: '1.5rem',
|
||||
textAlign: 'center',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href={signInHref}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
color: 'var(--color-heading)',
|
||||
textDecoration: 'underline',
|
||||
textUnderlineOffset: '4px',
|
||||
}}
|
||||
>
|
||||
<div className={styles.footer}>
|
||||
<a href={signInHref} className={styles.footerLink}>
|
||||
{dict.backToLogin}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { API_BASE } from '@/lib/api';
|
||||
import { localizedPath, type Locale } from '@/lib/localization/config';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { Suspense, useState } from 'react';
|
||||
import styles from './AuthForms.module.css';
|
||||
|
||||
interface Dict {
|
||||
title: string;
|
||||
@@ -144,16 +145,10 @@ function ResetPasswordContent({ locale }: { locale: Locale }) {
|
||||
if (!token) {
|
||||
return (
|
||||
<AuthShell locale={locale} title={dict.title}>
|
||||
<p
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
fontSize: '0.875rem',
|
||||
color: 'var(--color-text-secondary)',
|
||||
}}
|
||||
>
|
||||
<p className={styles.noToken}>
|
||||
{dict.noToken}
|
||||
</p>
|
||||
<div style={{ marginTop: '1rem', textAlign: 'center' }}>
|
||||
<div className={styles.actionWrap}>
|
||||
<a href={forgotHref}>
|
||||
<Button intent="primary">{dict.requestNew}</Button>
|
||||
</a>
|
||||
@@ -165,25 +160,10 @@ function ResetPasswordContent({ locale }: { locale: Locale }) {
|
||||
if (done) {
|
||||
return (
|
||||
<AuthShell locale={locale} title={dict.title}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
margin: '0 auto 1rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '3.5rem',
|
||||
height: '3.5rem',
|
||||
borderRadius: '50%',
|
||||
background: 'var(--color-success-bg, #ecfdf5)',
|
||||
}}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<div className={styles.successIcon}>
|
||||
<svg
|
||||
style={{
|
||||
width: '1.75rem',
|
||||
height: '1.75rem',
|
||||
color: 'var(--color-success-text, #059669)',
|
||||
}}
|
||||
className={styles.successIconSvg}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -192,19 +172,13 @@ function ResetPasswordContent({ locale }: { locale: Locale }) {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-heading)' }}>
|
||||
<h2 className={styles.statusTitle}>
|
||||
{dict.successTitle}
|
||||
</h2>
|
||||
<p
|
||||
style={{
|
||||
marginTop: '0.5rem',
|
||||
fontSize: '0.875rem',
|
||||
color: 'var(--color-text-secondary)',
|
||||
}}
|
||||
>
|
||||
<p className={styles.statusBody}>
|
||||
{dict.successBody}
|
||||
</p>
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<div className={styles.actionWrap}>
|
||||
<a href={signInHref}>
|
||||
<Button intent="primary" fullWidth>
|
||||
{dict.signIn}
|
||||
@@ -220,25 +194,16 @@ function ResetPasswordContent({ locale }: { locale: Locale }) {
|
||||
<AuthShell locale={locale} title={dict.title} subtitle={dict.subtitle}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}
|
||||
className={styles.formStack}
|
||||
>
|
||||
{error ? (
|
||||
<div
|
||||
style={{
|
||||
borderRadius: '1.5rem',
|
||||
border: '1px solid var(--color-error-border, #fecaca)',
|
||||
background: 'var(--color-error-bg, #fef2f2)',
|
||||
padding: '0.75rem 1rem',
|
||||
fontSize: '0.875rem',
|
||||
color: 'var(--color-error-text, #b91c1c)',
|
||||
}}
|
||||
>
|
||||
<div className={styles.error}>
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<FormField id="reset-password" label={dict.newPassword} required>
|
||||
<div style={{ position: 'relative', width: '100%' }}>
|
||||
<div className={styles.passwordField}>
|
||||
<input
|
||||
id="reset-password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
@@ -248,34 +213,13 @@ function ResetPasswordContent({ locale }: { locale: Locale }) {
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
autoComplete="new-password"
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: '2.875rem',
|
||||
padding: 'var(--space-3) 2.5rem var(--space-3) var(--space-4)',
|
||||
border: '1px solid var(--border-strong)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
background: 'var(--surface-primary)',
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
className={styles.passwordInput}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword((v) => !v)}
|
||||
tabIndex={-1}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: '0.5rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
color: 'var(--text-tertiary)',
|
||||
padding: 0,
|
||||
}}
|
||||
className={styles.passwordToggle}
|
||||
>
|
||||
{showPassword ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
@@ -292,7 +236,7 @@ function ResetPasswordContent({ locale }: { locale: Locale }) {
|
||||
</FormField>
|
||||
|
||||
<FormField id="reset-confirm" label={dict.confirmPassword} required>
|
||||
<div style={{ position: 'relative', width: '100%' }}>
|
||||
<div className={styles.passwordField}>
|
||||
<input
|
||||
id="reset-confirm"
|
||||
type={showConfirm ? 'text' : 'password'}
|
||||
@@ -302,34 +246,13 @@ function ResetPasswordContent({ locale }: { locale: Locale }) {
|
||||
onChange={(e) => setConfirm(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
autoComplete="new-password"
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: '2.875rem',
|
||||
padding: 'var(--space-3) 2.5rem var(--space-3) var(--space-4)',
|
||||
border: '1px solid var(--border-strong)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
background: 'var(--surface-primary)',
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
className={styles.passwordInput}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowConfirm((v) => !v)}
|
||||
tabIndex={-1}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: '0.5rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
color: 'var(--text-tertiary)',
|
||||
padding: 0,
|
||||
}}
|
||||
className={styles.passwordToggle}
|
||||
>
|
||||
{showConfirm ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
@@ -350,24 +273,8 @@ function ResetPasswordContent({ locale }: { locale: Locale }) {
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: '1.5rem',
|
||||
borderTop: '1px solid var(--color-border)',
|
||||
paddingTop: '1.5rem',
|
||||
textAlign: 'center',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href={signInHref}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
color: 'var(--color-heading)',
|
||||
textDecoration: 'underline',
|
||||
textUnderlineOffset: '4px',
|
||||
}}
|
||||
>
|
||||
<div className={styles.footer}>
|
||||
<a href={signInHref} className={styles.footerLink}>
|
||||
{dict.backToLogin}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -5,9 +5,11 @@ import { TextInput } from '@/components/forms/TextInput';
|
||||
import { FormField } from '@/components/forms/FormField';
|
||||
import { AuthShell } from '@/components/auth/AuthShell';
|
||||
import { API_BASE, EMPLOYEE_PROFILE_KEY } from '@/lib/api';
|
||||
import { classNames } from '@/lib/components/classNames';
|
||||
import { localizedPath, type Locale } from '@/lib/localization/config';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import styles from './AuthForms.module.css';
|
||||
|
||||
interface Dict {
|
||||
title: string;
|
||||
@@ -240,15 +242,7 @@ export function SignInForm({ locale }: { locale: Locale }) {
|
||||
<AuthShell locale={locale} title={dict.title} subtitle={dict.subtitle}>
|
||||
{error ? (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '1.25rem',
|
||||
borderRadius: '1.5rem',
|
||||
border: '1px solid var(--color-error-border, #fecaca)',
|
||||
background: 'var(--color-error-bg, #fef2f2)',
|
||||
padding: '0.75rem 1rem',
|
||||
fontSize: '0.875rem',
|
||||
color: 'var(--color-error-text, #b91c1c)',
|
||||
}}
|
||||
className={classNames(styles.error, styles.spacedError)}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
@@ -257,7 +251,7 @@ export function SignInForm({ locale }: { locale: Locale }) {
|
||||
{step === 'credentials' ? (
|
||||
<form
|
||||
onSubmit={handleCredentials}
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}
|
||||
className={styles.formStack}
|
||||
>
|
||||
<FormField id="signin-email" label={dict.email} required>
|
||||
<TextInput
|
||||
@@ -272,7 +266,7 @@ export function SignInForm({ locale }: { locale: Locale }) {
|
||||
</FormField>
|
||||
|
||||
<FormField id="signin-password" label={dict.password} required>
|
||||
<div style={{ position: 'relative', width: '100%' }}>
|
||||
<div className={styles.passwordField}>
|
||||
<input
|
||||
id="signin-password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
@@ -281,34 +275,13 @@ export function SignInForm({ locale }: { locale: Locale }) {
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
autoComplete="current-password"
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: '2.875rem',
|
||||
padding: 'var(--space-3) 2.5rem var(--space-3) var(--space-4)',
|
||||
border: '1px solid var(--border-strong)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
background: 'var(--surface-primary)',
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
className={styles.passwordInput}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword((v) => !v)}
|
||||
tabIndex={-1}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: '0.5rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
color: 'var(--text-tertiary)',
|
||||
padding: 0,
|
||||
}}
|
||||
className={styles.passwordToggle}
|
||||
>
|
||||
{showPassword ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
@@ -328,15 +301,10 @@ export function SignInForm({ locale }: { locale: Locale }) {
|
||||
{loading ? dict.signingIn : dict.signIn}
|
||||
</Button>
|
||||
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div className={styles.forgotLinkWrap}>
|
||||
<a
|
||||
href={hrefBase}
|
||||
style={{
|
||||
fontSize: '0.875rem',
|
||||
color: 'var(--color-text-tertiary)',
|
||||
textDecoration: 'underline',
|
||||
textUnderlineOffset: '4px',
|
||||
}}
|
||||
className={styles.mutedLink}
|
||||
>
|
||||
{dict.forgotPassword}
|
||||
</a>
|
||||
@@ -345,19 +313,9 @@ export function SignInForm({ locale }: { locale: Locale }) {
|
||||
) : (
|
||||
<form
|
||||
onSubmit={handleTotp}
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
borderRadius: '1.5rem',
|
||||
border: '1px solid var(--color-border)',
|
||||
background: 'var(--color-surface-secondary)',
|
||||
padding: '1rem',
|
||||
textAlign: 'center',
|
||||
fontSize: '0.875rem',
|
||||
color: 'var(--color-text-secondary)',
|
||||
}}
|
||||
className={styles.formStack}
|
||||
>
|
||||
<div className={styles.infoBox}>
|
||||
{dict.enterCode}
|
||||
</div>
|
||||
|
||||
@@ -373,7 +331,7 @@ export function SignInForm({ locale }: { locale: Locale }) {
|
||||
setTotpCode(e.target.value.replace(/[^0-9A-Za-z-]/g, '').toUpperCase())
|
||||
}
|
||||
placeholder={dict.totpPlaceholder}
|
||||
style={{ textAlign: 'center', fontSize: '1.25rem', letterSpacing: '0.45em' }}
|
||||
className={styles.totpInput}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
@@ -388,14 +346,7 @@ export function SignInForm({ locale }: { locale: Locale }) {
|
||||
setTotpCode('');
|
||||
setError(null);
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
fontSize: '0.875rem',
|
||||
color: 'var(--color-text-tertiary)',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
className={styles.plainButton}
|
||||
>
|
||||
{dict.back}
|
||||
</button>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.skeleton span {
|
||||
inline-size: 100%;
|
||||
block-size: 0.9rem;
|
||||
border-radius: var(--radius-pill);
|
||||
background: linear-gradient(
|
||||
@@ -22,6 +23,9 @@
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s linear infinite;
|
||||
}
|
||||
.skeleton span:last-child {
|
||||
inline-size: 68%;
|
||||
}
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
|
||||
@@ -31,7 +31,7 @@ export function Skeleton({
|
||||
{...props}
|
||||
>
|
||||
{Array.from({ length: lines }, (_, index) => (
|
||||
<span key={index} style={{ inlineSize: index === lines - 1 ? '68%' : '100%' }} />
|
||||
<span key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -101,7 +101,7 @@ validate_prod_env_file() {
|
||||
redis_password="$(read_env_value REDIS_PASSWORD)"
|
||||
jwt_secret="$(read_env_value JWT_SECRET)"
|
||||
|
||||
if [[ "${acme_email}" != "${DEFAULT_ACME_EMAIL}" ]]; then
|
||||
if [[ -z "${acme_email}" || "${acme_email}" != *@* || "${acme_email}" == *example.com* ]]; then
|
||||
echo "Invalid ACME_EMAIL in ${ENV_FILE}. Set ACME_EMAIL=${DEFAULT_ACME_EMAIL} for Let's Encrypt." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user