Add ALLOW_REGISTRATION flag and dynamic UI
This commit is contained in:
@@ -10,6 +10,9 @@ HOST=0.0.0.0
|
|||||||
JWT_SECRET=your-super-secure-jwt-secret-key-change-this-in-production
|
JWT_SECRET=your-super-secure-jwt-secret-key-change-this-in-production
|
||||||
SESSION_SECRET=your-session-secret-change-this-in-production
|
SESSION_SECRET=your-session-secret-change-this-in-production
|
||||||
|
|
||||||
|
# User Registration
|
||||||
|
ALLOW_REGISTRATION=true
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
DATABASE_PATH=/app/database/data/edh-stats.db
|
DATABASE_PATH=/app/database/data/edh-stats.db
|
||||||
DATABASE_BACKUP_PATH=/app/database/data/backups
|
DATABASE_BACKUP_PATH=/app/database/data/backups
|
||||||
@@ -26,4 +29,4 @@ RATE_LIMIT_MAX=100
|
|||||||
|
|
||||||
# Monitoring
|
# Monitoring
|
||||||
HEALTH_CHECK_ENABLED=true
|
HEALTH_CHECK_ENABLED=true
|
||||||
METRICS_ENABLED=false
|
METRICS_ENABLED=false
|
||||||
|
|||||||
@@ -37,4 +37,8 @@ export const securityConfig = {
|
|||||||
usernameMinLength: 3,
|
usernameMinLength: 3,
|
||||||
commanderNameMinLength: 2,
|
commanderNameMinLength: 2,
|
||||||
maxNotesLength: 1000
|
maxNotesLength: 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const registrationConfig = {
|
||||||
|
allowRegistration: process.env.ALLOW_REGISTRATION !== 'false'
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
// Authentication routes
|
// Authentication routes
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import User from '../models/User.js'
|
import User from '../models/User.js'
|
||||||
|
import { registrationConfig } from '../config/jwt.js'
|
||||||
|
|
||||||
// Validation schemas
|
// Validation schemas
|
||||||
const registerSchema = z.object({
|
const registerSchema = z.object({
|
||||||
username: z.string().min(3).max(50).regex(/^[a-zA-Z0-9_-]+$/, {
|
username: z
|
||||||
message: 'Username can only contain letters, numbers, underscores, and hyphens'
|
.string()
|
||||||
}),
|
.min(3)
|
||||||
|
.max(50)
|
||||||
|
.regex(/^[a-zA-Z0-9_-]+$/, {
|
||||||
|
message:
|
||||||
|
'Username can only contain letters, numbers, underscores, and hyphens'
|
||||||
|
}),
|
||||||
password: z.string().min(8).max(100),
|
password: z.string().min(8).max(100),
|
||||||
email: z.string().email().optional()
|
email: z.string().email().optional()
|
||||||
})
|
})
|
||||||
@@ -26,326 +32,382 @@ const updateProfileSchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export default async function authRoutes(fastify, options) {
|
export default async function authRoutes(fastify, options) {
|
||||||
|
// Public endpoint to check if registration is allowed
|
||||||
|
fastify.get('/config', async (request, reply) => {
|
||||||
|
return {
|
||||||
|
allowRegistration: registrationConfig.allowRegistration
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Register new user
|
// Register new user
|
||||||
fastify.post('/register', {
|
fastify.post(
|
||||||
config: { rateLimit: { max: 3, timeWindow: '15 minutes' } }
|
'/register',
|
||||||
}, async (request, reply) => {
|
{
|
||||||
try {
|
config: { rateLimit: { max: 3, timeWindow: '15 minutes' } }
|
||||||
// Validate input
|
},
|
||||||
const validatedData = registerSchema.parse(request.body)
|
async (request, reply) => {
|
||||||
|
try {
|
||||||
// Create user
|
// Check if registration is allowed
|
||||||
const user = await User.create(validatedData)
|
if (!registrationConfig.allowRegistration) {
|
||||||
|
reply.code(403).send({
|
||||||
// Generate JWT token
|
error: 'Registration Disabled',
|
||||||
const token = await reply.jwtSign({
|
message: 'User registration is currently disabled'
|
||||||
id: user.id,
|
})
|
||||||
username: user.username
|
return
|
||||||
}, {
|
}
|
||||||
expiresIn: '15m'
|
|
||||||
})
|
// Validate input
|
||||||
|
const validatedData = registerSchema.parse(request.body)
|
||||||
reply.code(201).send({
|
|
||||||
message: 'User registered successfully',
|
// Create user
|
||||||
user: {
|
const user = await User.create(validatedData)
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
// Generate JWT token
|
||||||
email: user.email,
|
const token = await reply.jwtSign(
|
||||||
created_at: user.created_at
|
{
|
||||||
},
|
id: user.id,
|
||||||
token
|
username: user.username
|
||||||
})
|
},
|
||||||
|
{
|
||||||
} catch (error) {
|
expiresIn: '15m'
|
||||||
if (error instanceof z.ZodError) {
|
}
|
||||||
reply.code(400).send({
|
)
|
||||||
error: 'Validation Error',
|
|
||||||
message: 'Invalid input data',
|
reply.code(201).send({
|
||||||
details: error.errors.map(e => e.message)
|
message: 'User registered successfully',
|
||||||
})
|
user: {
|
||||||
} else if (error.message.includes('already exists')) {
|
id: user.id,
|
||||||
reply.code(400).send({
|
username: user.username,
|
||||||
error: 'Registration Failed',
|
email: user.email,
|
||||||
message: error.message
|
created_at: user.created_at
|
||||||
})
|
},
|
||||||
} else {
|
token
|
||||||
fastify.log.error('Registration error:', error)
|
|
||||||
reply.code(500).send({
|
|
||||||
error: 'Internal Server Error',
|
|
||||||
message: 'Failed to register user'
|
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
reply.code(400).send({
|
||||||
|
error: 'Validation Error',
|
||||||
|
message: 'Invalid input data',
|
||||||
|
details: error.errors.map((e) => e.message)
|
||||||
|
})
|
||||||
|
} else if (error.message.includes('already exists')) {
|
||||||
|
reply.code(400).send({
|
||||||
|
error: 'Registration Failed',
|
||||||
|
message: error.message
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
fastify.log.error('Registration error:', error)
|
||||||
|
reply.code(500).send({
|
||||||
|
error: 'Internal Server Error',
|
||||||
|
message: 'Failed to register user'
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
// Login user
|
// Login user
|
||||||
fastify.post('/login', {
|
fastify.post(
|
||||||
config: { rateLimit: { max: 10, timeWindow: '15 minutes' } }
|
'/login',
|
||||||
}, async (request, reply) => {
|
{
|
||||||
try {
|
config: { rateLimit: { max: 10, timeWindow: '15 minutes' } }
|
||||||
const { username, password } = loginSchema.parse(request.body)
|
},
|
||||||
|
async (request, reply) => {
|
||||||
// Find user
|
try {
|
||||||
const user = await User.findByUsername(username)
|
const { username, password } = loginSchema.parse(request.body)
|
||||||
if (!user) {
|
|
||||||
reply.code(401).send({
|
// Find user
|
||||||
error: 'Authentication Failed',
|
const user = await User.findByUsername(username)
|
||||||
message: 'Invalid username or password'
|
if (!user) {
|
||||||
})
|
reply.code(401).send({
|
||||||
return
|
error: 'Authentication Failed',
|
||||||
}
|
message: 'Invalid username or password'
|
||||||
|
})
|
||||||
// Verify password
|
return
|
||||||
const isValidPassword = await User.verifyPassword(password, user.password_hash)
|
}
|
||||||
if (!isValidPassword) {
|
|
||||||
reply.code(401).send({
|
// Verify password
|
||||||
error: 'Authentication Failed',
|
const isValidPassword = await User.verifyPassword(
|
||||||
message: 'Invalid username or password'
|
password,
|
||||||
})
|
user.password_hash
|
||||||
return
|
)
|
||||||
}
|
if (!isValidPassword) {
|
||||||
|
reply.code(401).send({
|
||||||
// Generate JWT token
|
error: 'Authentication Failed',
|
||||||
const token = await reply.jwtSign({
|
message: 'Invalid username or password'
|
||||||
id: user.id,
|
})
|
||||||
username: user.username
|
return
|
||||||
}, {
|
}
|
||||||
expiresIn: '15m'
|
|
||||||
})
|
// Generate JWT token
|
||||||
|
const token = await reply.jwtSign(
|
||||||
reply.send({
|
{
|
||||||
message: 'Login successful',
|
id: user.id,
|
||||||
user: {
|
username: user.username
|
||||||
id: user.id,
|
},
|
||||||
username: user.username,
|
{
|
||||||
email: user.email
|
expiresIn: '15m'
|
||||||
},
|
}
|
||||||
token
|
)
|
||||||
})
|
|
||||||
|
reply.send({
|
||||||
} catch (error) {
|
message: 'Login successful',
|
||||||
if (error instanceof z.ZodError) {
|
user: {
|
||||||
reply.code(400).send({
|
id: user.id,
|
||||||
error: 'Validation Error',
|
username: user.username,
|
||||||
message: 'Invalid input data',
|
email: user.email
|
||||||
details: error.errors.map(e => e.message)
|
},
|
||||||
})
|
token
|
||||||
} else {
|
|
||||||
fastify.log.error('Login error:', error)
|
|
||||||
reply.code(500).send({
|
|
||||||
error: 'Internal Server Error',
|
|
||||||
message: 'Failed to authenticate user'
|
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
reply.code(400).send({
|
||||||
|
error: 'Validation Error',
|
||||||
|
message: 'Invalid input data',
|
||||||
|
details: error.errors.map((e) => e.message)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
fastify.log.error('Login error:', error)
|
||||||
|
reply.code(500).send({
|
||||||
|
error: 'Internal Server Error',
|
||||||
|
message: 'Failed to authenticate user'
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
// Refresh token
|
// Refresh token
|
||||||
fastify.post('/refresh', {
|
fastify.post(
|
||||||
config: {
|
'/refresh',
|
||||||
rateLimit: { max: 20, timeWindow: '15 minutes' }
|
{
|
||||||
}
|
config: {
|
||||||
}, async (request, reply) => {
|
rateLimit: { max: 20, timeWindow: '15 minutes' }
|
||||||
try {
|
}
|
||||||
await request.jwtVerify()
|
},
|
||||||
|
async (request, reply) => {
|
||||||
const user = await User.findById(request.user.id)
|
try {
|
||||||
if (!user) {
|
await request.jwtVerify()
|
||||||
|
|
||||||
|
const user = await User.findById(request.user.id)
|
||||||
|
if (!user) {
|
||||||
|
reply.code(401).send({
|
||||||
|
error: 'Authentication Failed',
|
||||||
|
message: 'User not found'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new token
|
||||||
|
const token = await reply.jwtSign(
|
||||||
|
{
|
||||||
|
id: user.id,
|
||||||
|
username: user.username
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expiresIn: '15m'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
reply.send({
|
||||||
|
message: 'Token refreshed successfully',
|
||||||
|
token
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
reply.code(401).send({
|
reply.code(401).send({
|
||||||
error: 'Authentication Failed',
|
error: 'Authentication Failed',
|
||||||
message: 'User not found'
|
message: 'Invalid or expired token'
|
||||||
})
|
})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate new token
|
|
||||||
const token = await reply.jwtSign({
|
|
||||||
id: user.id,
|
|
||||||
username: user.username
|
|
||||||
}, {
|
|
||||||
expiresIn: '15m'
|
|
||||||
})
|
|
||||||
|
|
||||||
reply.send({
|
|
||||||
message: 'Token refreshed successfully',
|
|
||||||
token
|
|
||||||
})
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
reply.code(401).send({
|
|
||||||
error: 'Authentication Failed',
|
|
||||||
message: 'Invalid or expired token'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
// Get current user profile
|
// Get current user profile
|
||||||
fastify.get('/me', {
|
fastify.get(
|
||||||
preHandler: [async (request, reply) => {
|
'/me',
|
||||||
try {
|
{
|
||||||
await request.jwtVerify()
|
preHandler: [
|
||||||
} catch (err) {
|
async (request, reply) => {
|
||||||
reply.code(401).send({
|
try {
|
||||||
error: 'Unauthorized',
|
await request.jwtVerify()
|
||||||
message: 'Invalid or expired token'
|
} catch (err) {
|
||||||
})
|
reply.code(401).send({
|
||||||
}
|
error: 'Unauthorized',
|
||||||
}]
|
message: 'Invalid or expired token'
|
||||||
}, async (request, reply) => {
|
})
|
||||||
try {
|
}
|
||||||
const user = await User.findById(request.user.id)
|
|
||||||
if (!user) {
|
|
||||||
reply.code(404).send({
|
|
||||||
error: 'Not Found',
|
|
||||||
message: 'User not found'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reply.send({
|
|
||||||
user: {
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
email: user.email,
|
|
||||||
created_at: user.created_at
|
|
||||||
}
|
}
|
||||||
})
|
]
|
||||||
|
},
|
||||||
} catch (error) {
|
async (request, reply) => {
|
||||||
fastify.log.error('Get profile error:', error)
|
try {
|
||||||
reply.code(500).send({
|
const user = await User.findById(request.user.id)
|
||||||
error: 'Internal Server Error',
|
if (!user) {
|
||||||
message: 'Failed to get user profile'
|
reply.code(404).send({
|
||||||
})
|
error: 'Not Found',
|
||||||
|
message: 'User not found'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.send({
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
created_at: user.created_at
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
fastify.log.error('Get profile error:', error)
|
||||||
|
reply.code(500).send({
|
||||||
|
error: 'Internal Server Error',
|
||||||
|
message: 'Failed to get user profile'
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
// Update user profile
|
// Update user profile
|
||||||
fastify.patch('/me', {
|
fastify.patch(
|
||||||
preHandler: [async (request, reply) => {
|
'/me',
|
||||||
try {
|
{
|
||||||
await request.jwtVerify()
|
preHandler: [
|
||||||
} catch (err) {
|
async (request, reply) => {
|
||||||
reply.code(401).send({
|
try {
|
||||||
error: 'Unauthorized',
|
await request.jwtVerify()
|
||||||
message: 'Invalid or expired token'
|
} catch (err) {
|
||||||
})
|
reply.code(401).send({
|
||||||
}
|
error: 'Unauthorized',
|
||||||
}]
|
message: 'Invalid or expired token'
|
||||||
}, async (request, reply) => {
|
})
|
||||||
try {
|
}
|
||||||
const validatedData = updateProfileSchema.parse(request.body)
|
|
||||||
|
|
||||||
const updated = await User.updateProfile(request.user.id, validatedData)
|
|
||||||
|
|
||||||
if (!updated) {
|
|
||||||
reply.code(400).send({
|
|
||||||
error: 'Update Failed',
|
|
||||||
message: 'No valid fields to update'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await User.findById(request.user.id)
|
|
||||||
|
|
||||||
reply.send({
|
|
||||||
message: 'Profile updated successfully',
|
|
||||||
user: {
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
email: user.email,
|
|
||||||
updated_at: user.updated_at
|
|
||||||
}
|
}
|
||||||
})
|
]
|
||||||
|
},
|
||||||
} catch (error) {
|
async (request, reply) => {
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
reply.code(400).send({
|
|
||||||
error: 'Validation Error',
|
|
||||||
message: 'Invalid input data',
|
|
||||||
details: error.errors.map(e => e.message)
|
|
||||||
})
|
|
||||||
} else if (error.message.includes('already exists')) {
|
|
||||||
reply.code(400).send({
|
|
||||||
error: 'Update Failed',
|
|
||||||
message: error.message
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
fastify.log.error('Update profile error:', error)
|
|
||||||
reply.code(500).send({
|
|
||||||
error: 'Internal Server Error',
|
|
||||||
message: 'Failed to update profile'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Change password
|
|
||||||
fastify.post('/change-password', {
|
|
||||||
preHandler: [async (request, reply) => {
|
|
||||||
try {
|
try {
|
||||||
await request.jwtVerify()
|
const validatedData = updateProfileSchema.parse(request.body)
|
||||||
} catch (err) {
|
|
||||||
reply.code(401).send({
|
const updated = await User.updateProfile(request.user.id, validatedData)
|
||||||
error: 'Unauthorized',
|
|
||||||
message: 'Invalid or expired token'
|
if (!updated) {
|
||||||
})
|
reply.code(400).send({
|
||||||
}
|
error: 'Update Failed',
|
||||||
}],
|
message: 'No valid fields to update'
|
||||||
config: { rateLimit: { max: 3, timeWindow: '1 hour' } }
|
})
|
||||||
}, async (request, reply) => {
|
return
|
||||||
try {
|
}
|
||||||
const { currentPassword, newPassword } = changePasswordSchema.parse(request.body)
|
|
||||||
|
const user = await User.findById(request.user.id)
|
||||||
// Verify current password
|
|
||||||
const user = await User.findByUsername(request.user.username)
|
reply.send({
|
||||||
if (!user) {
|
message: 'Profile updated successfully',
|
||||||
reply.code(404).send({
|
user: {
|
||||||
error: 'Not Found',
|
id: user.id,
|
||||||
message: 'User not found'
|
username: user.username,
|
||||||
})
|
email: user.email,
|
||||||
return
|
updated_at: user.updated_at
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValidPassword = await User.verifyPassword(currentPassword, user.password_hash)
|
|
||||||
if (!isValidPassword) {
|
|
||||||
reply.code(401).send({
|
|
||||||
error: 'Authentication Failed',
|
|
||||||
message: 'Current password is incorrect'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update password
|
|
||||||
const updated = await User.updatePassword(request.user.id, newPassword)
|
|
||||||
|
|
||||||
if (!updated) {
|
|
||||||
reply.code(500).send({
|
|
||||||
error: 'Internal Server Error',
|
|
||||||
message: 'Failed to update password'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reply.send({
|
|
||||||
message: 'Password changed successfully'
|
|
||||||
})
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
reply.code(400).send({
|
|
||||||
error: 'Validation Error',
|
|
||||||
message: 'Invalid input data',
|
|
||||||
details: error.errors.map(e => e.message)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
fastify.log.error('Change password error:', error)
|
|
||||||
reply.code(500).send({
|
|
||||||
error: 'Internal Server Error',
|
|
||||||
message: 'Failed to change password'
|
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
reply.code(400).send({
|
||||||
|
error: 'Validation Error',
|
||||||
|
message: 'Invalid input data',
|
||||||
|
details: error.errors.map((e) => e.message)
|
||||||
|
})
|
||||||
|
} else if (error.message.includes('already exists')) {
|
||||||
|
reply.code(400).send({
|
||||||
|
error: 'Update Failed',
|
||||||
|
message: error.message
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
fastify.log.error('Update profile error:', error)
|
||||||
|
reply.code(500).send({
|
||||||
|
error: 'Internal Server Error',
|
||||||
|
message: 'Failed to update profile'
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
}
|
|
||||||
|
// Change password
|
||||||
|
fastify.post(
|
||||||
|
'/change-password',
|
||||||
|
{
|
||||||
|
preHandler: [
|
||||||
|
async (request, reply) => {
|
||||||
|
try {
|
||||||
|
await request.jwtVerify()
|
||||||
|
} catch (err) {
|
||||||
|
reply.code(401).send({
|
||||||
|
error: 'Unauthorized',
|
||||||
|
message: 'Invalid or expired token'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
config: { rateLimit: { max: 3, timeWindow: '1 hour' } }
|
||||||
|
},
|
||||||
|
async (request, reply) => {
|
||||||
|
try {
|
||||||
|
const { currentPassword, newPassword } = changePasswordSchema.parse(
|
||||||
|
request.body
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify current password
|
||||||
|
const user = await User.findByUsername(request.user.username)
|
||||||
|
if (!user) {
|
||||||
|
reply.code(404).send({
|
||||||
|
error: 'Not Found',
|
||||||
|
message: 'User not found'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidPassword = await User.verifyPassword(
|
||||||
|
currentPassword,
|
||||||
|
user.password_hash
|
||||||
|
)
|
||||||
|
if (!isValidPassword) {
|
||||||
|
reply.code(401).send({
|
||||||
|
error: 'Authentication Failed',
|
||||||
|
message: 'Current password is incorrect'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update password
|
||||||
|
const updated = await User.updatePassword(request.user.id, newPassword)
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
reply.code(500).send({
|
||||||
|
error: 'Internal Server Error',
|
||||||
|
message: 'Failed to update password'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.send({
|
||||||
|
message: 'Password changed successfully'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
reply.code(400).send({
|
||||||
|
error: 'Validation Error',
|
||||||
|
message: 'Invalid input data',
|
||||||
|
details: error.errors.map((e) => e.message)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
fastify.log.error('Change password error:', error)
|
||||||
|
reply.code(500).send({
|
||||||
|
error: 'Internal Server Error',
|
||||||
|
message: 'Failed to change password'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,34 +1,82 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en" class="h-full bg-gray-50">
|
<html lang="en" class="h-full bg-gray-50">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>EDH Stats Tracker</title>
|
<title>EDH Stats Tracker</title>
|
||||||
<meta name="description" content="Track your Magic: The Gathering EDH/Commander games and statistics">
|
<meta
|
||||||
<link rel="stylesheet" href="/css/styles.css">
|
name="description"
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
content="Track your Magic: The Gathering EDH/Commander games and statistics"
|
||||||
</head>
|
/>
|
||||||
<body class="h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
<link rel="stylesheet" href="/css/styles.css" />
|
||||||
<div class="max-w-md w-full space-y-8 text-center">
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<h1 class="text-4xl font-bold font-mtg text-edh-primary mb-4">EDH Stats</h1>
|
</head>
|
||||||
<p class="text-xl text-gray-600 mb-8">Track your Commander games and statistics</p>
|
<body
|
||||||
|
class="h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"
|
||||||
<div class="space-y-4">
|
>
|
||||||
<a href="/login.html" class="btn btn-primary w-full">
|
<div class="max-w-md w-full space-y-8 text-center" x-data="indexApp()">
|
||||||
🎮 Login to Track Games
|
<h1 class="text-4xl font-bold font-mtg text-edh-primary mb-4">
|
||||||
</a>
|
EDH Stats
|
||||||
<a href="/register.html" class="btn btn-secondary w-full">
|
</h1>
|
||||||
📝 Create New Account
|
<p class="text-xl text-gray-600 mb-8">
|
||||||
</a>
|
Track your Commander games and statistics
|
||||||
</div>
|
</p>
|
||||||
|
|
||||||
<div class="text-sm text-gray-500">
|
<div class="space-y-4">
|
||||||
<p>Built with Fastify, SQLite, and Alpine.js</p>
|
<a href="/login.html" class="btn btn-primary w-full">
|
||||||
<p>Ready to track your Commander victories!</p>
|
🎮 Login to Track Games
|
||||||
</div>
|
</a>
|
||||||
|
<a
|
||||||
|
x-show="allowRegistration"
|
||||||
|
href="/register.html"
|
||||||
|
class="btn btn-secondary w-full"
|
||||||
|
>
|
||||||
|
📝 Create New Account
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm text-gray-500">
|
||||||
|
<p>Built with Fastify, SQLite, and Alpine.js</p>
|
||||||
|
<p>Ready to track your Commander victories!</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
<script
|
||||||
</body>
|
defer
|
||||||
</html>
|
src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"
|
||||||
|
></script>
|
||||||
|
<script>
|
||||||
|
function indexApp() {
|
||||||
|
return {
|
||||||
|
allowRegistration: true,
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.checkRegistrationConfig()
|
||||||
|
},
|
||||||
|
|
||||||
|
async checkRegistrationConfig() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/config')
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
this.allowRegistration = data.allowRegistration
|
||||||
|
} else {
|
||||||
|
// Default to true if endpoint fails
|
||||||
|
this.allowRegistration = true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check registration config:', error)
|
||||||
|
// Default to true if request fails
|
||||||
|
this.allowRegistration = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
Alpine.data('indexApp', indexApp)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -1,167 +1,314 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en" class="h-full bg-gray-50">
|
<html lang="en" class="h-full bg-gray-50">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Login - EDH Stats Tracker</title>
|
<title>Login - EDH Stats Tracker</title>
|
||||||
<meta name="description" content="Login to track your Magic: The Gathering EDH/Commander games">
|
<meta
|
||||||
<link rel="stylesheet" href="/css/styles.css">
|
name="description"
|
||||||
</head>
|
content="Login to track your Magic: The Gathering EDH/Commander games"
|
||||||
<body class="h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
/>
|
||||||
<div class="max-w-md w-full space-y-8" x-data="loginForm()">
|
<link rel="stylesheet" href="/css/styles.css" />
|
||||||
|
</head>
|
||||||
<!-- Header -->
|
<body
|
||||||
<div class="text-center">
|
class="h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"
|
||||||
<h1 class="text-4xl font-bold font-mtg text-edh-primary mb-2">EDH Stats</h1>
|
>
|
||||||
<h2 class="text-xl text-gray-600">Sign in to your account</h2>
|
<div class="max-w-md w-full space-y-8" x-data="loginWithRegistration()">
|
||||||
</div>
|
<!-- Header -->
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="text-4xl font-bold font-mtg text-edh-primary mb-2">
|
||||||
|
EDH Stats
|
||||||
|
</h1>
|
||||||
|
<h2 class="text-xl text-gray-600">Sign in to your account</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Login Form -->
|
<!-- Login Form -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<form class="space-y-6" @submit.prevent="handleLogin">
|
<form class="space-y-6" @submit.prevent="handleLogin">
|
||||||
<!-- Username Field -->
|
<!-- Username Field -->
|
||||||
<div>
|
<div>
|
||||||
<label for="username" class="form-label">Username</label>
|
<label for="username" class="form-label">Username</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input
|
<input
|
||||||
id="username"
|
id="username"
|
||||||
name="username"
|
name="username"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
x-model="formData.username"
|
x-model="formData.username"
|
||||||
@input="validateUsername()"
|
@input="validateUsername()"
|
||||||
:class="errors.username ? 'border-red-500 focus:ring-red-500' : ''"
|
:class="errors.username ? 'border-red-500 focus:ring-red-500' : ''"
|
||||||
class="form-input pl-10"
|
class="form-input pl-10"
|
||||||
placeholder="Enter your username"
|
placeholder="Enter your username"
|
||||||
>
|
/>
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div
|
||||||
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
>
|
||||||
</svg>
|
<svg
|
||||||
</div>
|
class="h-5 w-5 text-gray-400"
|
||||||
</div>
|
fill="none"
|
||||||
<p x-show="errors.username" x-text="errors.username" class="form-error"></p>
|
stroke="currentColor"
|
||||||
</div>
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
<!-- Password Field -->
|
<path
|
||||||
<div>
|
stroke-linecap="round"
|
||||||
<label for="password" class="form-label">Password</label>
|
stroke-linejoin="round"
|
||||||
<div class="relative">
|
stroke-width="2"
|
||||||
<input
|
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||||
id="password"
|
></path>
|
||||||
name="password"
|
</svg>
|
||||||
:type="showPassword ? 'text' : 'password'"
|
</div>
|
||||||
required
|
|
||||||
x-model="formData.password"
|
|
||||||
@input="validatePassword()"
|
|
||||||
:class="errors.password ? 'border-red-500 focus:ring-red-500' : ''"
|
|
||||||
class="form-input pl-10 pr-10"
|
|
||||||
placeholder="Enter your password"
|
|
||||||
>
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
@click="showPassword = !showPassword"
|
|
||||||
class="absolute inset-y-0 right-0 pr-3 flex items-center"
|
|
||||||
>
|
|
||||||
<svg x-show="!showPassword" class="h-5 w-5 text-gray-400 hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
|
||||||
</svg>
|
|
||||||
<svg x-show="showPassword" class="h-5 w-5 text-gray-400 hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p x-show="errors.password" x-text="errors.password" class="form-error"></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Remember Me -->
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<input
|
|
||||||
id="remember"
|
|
||||||
name="remember"
|
|
||||||
type="checkbox"
|
|
||||||
x-model="formData.remember"
|
|
||||||
class="h-4 w-4 text-edh-accent focus:ring-edh-accent border-gray-300 rounded"
|
|
||||||
>
|
|
||||||
<label for="remember" class="ml-2 block text-sm text-gray-900">
|
|
||||||
Remember me
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-sm">
|
|
||||||
<a href="#" class="font-medium text-edh-accent hover:text-edh-primary">
|
|
||||||
Forgot password?
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Error Message -->
|
|
||||||
<div x-show="serverError" x-transition class="rounded-md bg-red-50 p-4">
|
|
||||||
<div class="flex">
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3">
|
|
||||||
<p class="text-sm text-red-800" x-text="serverError"></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Submit Button -->
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
:disabled="loading"
|
|
||||||
class="btn btn-primary w-full flex justify-center items-center space-x-2"
|
|
||||||
:class="{ 'opacity-50 cursor-not-allowed': loading }"
|
|
||||||
>
|
|
||||||
<svg x-show="!loading" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path>
|
|
||||||
</svg>
|
|
||||||
<svg x-show="loading" class="animate-spin h-5 w-5" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
<span x-text="loading ? 'Signing in...' : 'Sign in'"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Register Link -->
|
|
||||||
<div class="text-center">
|
|
||||||
<p class="text-sm text-gray-600">
|
|
||||||
Don't have an account?
|
|
||||||
<a href="/register.html" class="font-medium text-edh-accent hover:text-edh-primary">
|
|
||||||
Sign up
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Test Credentials (for development) -->
|
|
||||||
<div class="card bg-blue-50 border-blue-200">
|
|
||||||
<h3 class="text-sm font-medium text-blue-800 mb-2">Test Credentials (Development)</h3>
|
|
||||||
<div class="text-xs text-blue-700 space-y-1">
|
|
||||||
<p><strong>Username:</strong> testuser</p>
|
|
||||||
<p><strong>Password:</strong> password123</p>
|
|
||||||
<p><strong>Username:</strong> magictg</p>
|
|
||||||
<p><strong>Password:</strong> password123</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p
|
||||||
|
x-show="errors.username"
|
||||||
|
x-text="errors.username"
|
||||||
|
class="form-error"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password Field -->
|
||||||
|
<div>
|
||||||
|
<label for="password" class="form-label">Password</label>
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
required
|
||||||
|
x-model="formData.password"
|
||||||
|
@input="validatePassword()"
|
||||||
|
:class="errors.password ? 'border-red-500 focus:ring-red-500' : ''"
|
||||||
|
class="form-input pl-10 pr-10"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="h-5 w-5 text-gray-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="showPassword = !showPassword"
|
||||||
|
class="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
x-show="!showPassword"
|
||||||
|
class="h-5 w-5 text-gray-400 hover:text-gray-600"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
x-show="showPassword"
|
||||||
|
class="h-5 w-5 text-gray-400 hover:text-gray-600"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
x-show="errors.password"
|
||||||
|
x-text="errors.password"
|
||||||
|
class="form-error"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Remember Me -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input
|
||||||
|
id="remember"
|
||||||
|
name="remember"
|
||||||
|
type="checkbox"
|
||||||
|
x-model="formData.remember"
|
||||||
|
class="h-4 w-4 text-edh-accent focus:ring-edh-accent border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
<label for="remember" class="ml-2 block text-sm text-gray-900">
|
||||||
|
Remember me
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="font-medium text-edh-accent hover:text-edh-primary"
|
||||||
|
>
|
||||||
|
Forgot password?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Message -->
|
||||||
|
<div
|
||||||
|
x-show="serverError"
|
||||||
|
x-transition
|
||||||
|
class="rounded-md bg-red-50 p-4"
|
||||||
|
>
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg
|
||||||
|
class="h-5 w-5 text-red-400"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<p class="text-sm text-red-800" x-text="serverError"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="loading"
|
||||||
|
class="btn btn-primary w-full flex justify-center items-center space-x-2"
|
||||||
|
:class="{ 'opacity-50 cursor-not-allowed': loading }"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
x-show="!loading"
|
||||||
|
class="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
x-show="loading"
|
||||||
|
class="animate-spin h-5 w-5"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
class="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
class="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<span x-text="loading ? 'Signing in...' : 'Sign in'"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Register Link -->
|
||||||
|
<div class="text-center" x-show="allowRegistration">
|
||||||
|
<p class="text-sm text-gray-600">
|
||||||
|
Don't have an account?
|
||||||
|
<a
|
||||||
|
href="/register.html"
|
||||||
|
class="font-medium text-edh-accent hover:text-edh-primary"
|
||||||
|
>
|
||||||
|
Sign up
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test Credentials (for development) -->
|
||||||
|
<div class="card bg-blue-50 border-blue-200">
|
||||||
|
<h3 class="text-sm font-medium text-blue-800 mb-2">
|
||||||
|
Test Credentials (Development)
|
||||||
|
</h3>
|
||||||
|
<div class="text-xs text-blue-700 space-y-1">
|
||||||
|
<p><strong>Username:</strong> testuser</p>
|
||||||
|
<p><strong>Password:</strong> password123</p>
|
||||||
|
<p><strong>Username:</strong> magictg</p>
|
||||||
|
<p><strong>Password:</strong> password123</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
<script
|
||||||
|
defer
|
||||||
|
src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"
|
||||||
|
></script>
|
||||||
<script src="/js/auth.js"></script>
|
<script src="/js/auth.js"></script>
|
||||||
</body>
|
<script>
|
||||||
</html>
|
function loginWithRegistration() {
|
||||||
|
return {
|
||||||
|
...loginForm(),
|
||||||
|
allowRegistration: true,
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
// Check registration config
|
||||||
|
await this.checkRegistrationConfig()
|
||||||
|
// Call parent init if it exists
|
||||||
|
if (typeof super.init === 'function') {
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async checkRegistrationConfig() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/config')
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
this.allowRegistration = data.allowRegistration
|
||||||
|
} else {
|
||||||
|
this.allowRegistration = true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check registration config:', error)
|
||||||
|
this.allowRegistration = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user