Merge pull request #32 from cloud-neutral-toolkit/feature/refine-registration-and-cors

Feature/refine registration and cors
This commit is contained in:
cloudneutral 2026-01-25 12:27:36 +08:00 committed by GitHub
commit 73005db01c
8 changed files with 485 additions and 648 deletions

BIN
frontend.log Normal file

Binary file not shown.

View File

@ -62,6 +62,20 @@ const nextConfig = {
return config;
},
async headers() {
return [
{
source: "/api/:path*",
headers: [
{ key: "Access-Control-Allow-Credentials", value: "true" },
{ key: "Access-Control-Allow-Origin", value: process.env.CORS_ALLOWED_ORIGINS || "https://console.svc.plus,http://localhost:3000" },
{ key: "Access-Control-Allow-Methods", value: "GET,POST,PUT,PATCH,DELETE,OPTIONS" },
{ key: "Access-Control-Allow-Headers", value: "Content-Type, Authorization, X-Requested-With, X-Account-Session" },
],
},
];
},
reactStrictMode: true,
typedRoutes: false,
turbopack: {

10
run-api-test.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash
# run-api-test.sh
# Placeholder for API tests - as requested by user pattern.
# Currently assumes API tests might be located similarly or future expansion.
# If no API tests defined yet, this is a stub.
echo "Running API tests..."
# Example: npm run test:api
# or vitest specific path
echo "No specific API test command configured yet. Please update this script."

8
run-ui-test.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash
# run-ui-test.sh
# Usage: ./run-ui-test.sh [test-file]
TEST_FILE=${1:-"tests/e2e/ui/registration/register.spec.ts"}
echo "Running UI tests: $TEST_FILE"
npx playwright test "$TEST_FILE"

File diff suppressed because it is too large Load Diff

View File

@ -222,6 +222,8 @@ type AuthRegisterTranslation = {
form: {
title: string
subtitle: string
name: string
namePlaceholder: string
email: string
emailPlaceholder: string
password: string
@ -723,6 +725,8 @@ export const translations: Record<'en' | 'zh', Translation> = {
form: {
title: 'Create your account',
subtitle: 'Submit your email and password, request the code, and enter it to activate your account.',
name: 'Username',
namePlaceholder: '4-16 chars, starts with letter',
email: 'Work email',
emailPlaceholder: 'name@example.com',
password: 'Password',
@ -1384,6 +1388,8 @@ export const translations: Record<'en' | 'zh', Translation> = {
form: {
title: '创建账号',
subtitle: '先提交邮箱和密码获取验证码,再输入邮箱收到的验证码完成注册。',
name: '用户名',
namePlaceholder: '4-16位字母或数字字母开头',
email: '邮箱',
emailPlaceholder: 'name@example.com',
password: '密码',

View File

@ -1,50 +0,0 @@
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const DEFAULT_ALLOWED_ORIGINS = 'https://console.svc.plus,http://localhost:3000'
function parseAllowedOrigins() {
const raw = process.env.CORS_ALLOWED_ORIGINS ?? DEFAULT_ALLOWED_ORIGINS
return raw
.split(',')
.map((value) => value.trim())
.filter(Boolean)
}
function resolveAllowedOrigin(origin: string | null, allowed: string[]) {
if (!origin) return null
if (allowed.includes('*')) return '*'
return allowed.includes(origin) ? origin : null
}
function applyCorsHeaders(response: NextResponse, origin: string | null) {
if (!origin) return
response.headers.set('Access-Control-Allow-Origin', origin)
response.headers.set('Access-Control-Allow-Credentials', 'true')
response.headers.set('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS')
response.headers.set(
'Access-Control-Allow-Headers',
'Content-Type, Authorization, X-Requested-With, X-Account-Session',
)
response.headers.set('Vary', 'Origin')
}
export function middleware(request: NextRequest) {
const allowedOrigins = parseAllowedOrigins()
const originHeader = request.headers.get('origin')
const allowedOrigin = resolveAllowedOrigin(originHeader, allowedOrigins)
if (request.method === 'OPTIONS') {
const response = new NextResponse(null, { status: 204 })
applyCorsHeaders(response, allowedOrigin)
return response
}
const response = NextResponse.next()
applyCorsHeaders(response, allowedOrigin)
return response
}
export const config = {
matcher: ['/api/:path*'],
}

View File

@ -0,0 +1,93 @@
import { test, expect } from '@playwright/test';
test.describe('Registration Flow', () => {
test.beforeEach(async ({ page }) => {
// Mock the send email API to always succeed
await page.route('/api/auth/register/send', async (route) => {
await route.fulfill({ status: 200, body: JSON.stringify({ success: true }) });
});
// Mock the verification/register API to always succeed
await page.route('/api/auth/register', async (route) => {
await route.fulfill({ status: 200, body: JSON.stringify({ success: true, redirectTo: '/' }) });
});
// Mock the login API that is called after registration
await page.route('/api/auth/login', async (route) => {
await route.fulfill({ status: 200, body: JSON.stringify({ success: true, redirectTo: '/' }) });
});
await page.goto('/register');
});
test('should validate username rules', async ({ page }) => {
const usernameInput = page.getByLabel(/Username|用户名/);
const emailInput = page.getByLabel(/Email|邮箱/);
const passwordInput = page.getByLabel(/^Password|密码$/);
const confirmPasswordInput = page.getByLabel(/Confirm Password|确认密码/);
const nextButton = page.getByRole('button', { name: /Next|下一步|获取验证码/ });
// Helper to fill form and check for error
const attemptRegistration = async (username: string) => {
await usernameInput.fill(username);
await nextButton.click();
};
// 1. Starts with digit
await attemptRegistration('1abc');
await expect(page.getByText(/Invalid Name|用户名格式不正确/)).toBeVisible();
// 2. Too short
await attemptRegistration('abc');
await expect(page.getByText(/Invalid Name|用户名格式不正确/)).toBeVisible();
// 3. Special characters
await attemptRegistration('user@name');
await expect(page.getByText(/Invalid Name|用户名格式不正确/)).toBeVisible();
// 4. Valid username - should NOT show error (but will fail on other fields if empty, so we just check user field validation logic primarily)
// To strictly check if it proceeds, we need to fill other fields
await usernameInput.fill('ValidUser1');
await emailInput.fill('test@example.com');
await passwordInput.fill('Password123!');
await confirmPasswordInput.fill('Password123!');
await page.getByRole('checkbox').check();
// Clear any previous errors
// Click Next
await nextButton.click();
// Expect transition to Step 2 (Verification code input should appear)
await expect(page.locator('input[inputmode="numeric"]')).toHaveCount(6);
});
test('should auto-submit after entering 6-digit code', async ({ page }) => {
// Fill Step 1
await page.getByLabel(/Username|用户名/).fill('AutoSubmitUser');
await page.getByLabel(/Email|邮箱/).fill('test@example.com');
await page.getByLabel(/^Password|密码$/).fill('Password123!');
await page.getByLabel(/Confirm Password|确认密码/).fill('Password123!');
await page.getByRole('checkbox').check();
await page.getByRole('button', { name: /Next|下一步|获取验证码/ }).click();
// Wait for Step 2
const codeInputs = page.locator('input[inputmode="numeric"]');
await expect(codeInputs).toHaveCount(6);
// Enter code digits 1-6 slowly
const code = '123456';
for (let i = 0; i < code.length; i++) {
await codeInputs.nth(i).type(code[i], { delay: 100 });
}
// Verify auto-submit triggered
// The button should show loading state or we should be redirected
// Since we mocked success, we expect redirect or success message
// Updating checking logic based on UI behavior
await expect(page.getByRole('button', { name: /Completing|完成中/ })).toBeVisible();
// Eventually should redirect or show success
// If redirection happens within the test context, page URL should change or success alert appears
// await expect(page).toHaveURL('/'); // Uncomment if verification of redirect is strictly needed and works with the mock
});
});