Fix authentication state persistence and admin role display
- Implement complete authentication system with JWT token validation - Add auth provider with persistent login state across page refreshes - Create multilingual login/register forms with Material-UI components - Fix token validation using raw SQL queries to bypass Prisma sync issues - Add comprehensive error handling for expired/invalid tokens - Create profile and settings pages with full i18n support - Add proper user role management (admin/user) with database sync - Implement secure middleware with CSRF protection and auth checks - Add debug endpoints for troubleshooting authentication issues - Fix Zustand store persistence for authentication state 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
29
lib/auth/client.ts
Normal file
29
lib/auth/client.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export function isTokenExpired(token: string): boolean {
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split('.')[1])) as { exp?: number }
|
||||
if (!payload || !payload.exp) {
|
||||
console.log('Token has no expiration data')
|
||||
return true
|
||||
}
|
||||
|
||||
const currentTime = Math.floor(Date.now() / 1000)
|
||||
const isExpired = payload.exp < currentTime
|
||||
console.log(`Token expiration check: exp=${payload.exp}, now=${currentTime}, expired=${isExpired}`)
|
||||
return isExpired
|
||||
} catch (error) {
|
||||
console.log('Token validation error:', error)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export function clearExpiredToken(): void {
|
||||
const token = localStorage.getItem('authToken')
|
||||
if (token && isTokenExpired(token)) {
|
||||
console.log('Clearing expired token from localStorage')
|
||||
localStorage.removeItem('authToken')
|
||||
} else if (token) {
|
||||
console.log('Token exists and is valid')
|
||||
} else {
|
||||
console.log('No token in localStorage')
|
||||
}
|
||||
}
|
||||
@@ -23,22 +23,50 @@ export function generateToken(userId: string): string {
|
||||
|
||||
export async function verifyToken(token: string) {
|
||||
try {
|
||||
console.log('Server: Verifying token with JWT_SECRET exists:', !!process.env.JWT_SECRET)
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string }
|
||||
console.log('Server: Token verification successful, userId:', payload.userId)
|
||||
return payload
|
||||
} catch (error) {
|
||||
console.log('Server: Token verification failed:', error.message)
|
||||
throw new Error('Invalid token')
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserFromToken(token: string) {
|
||||
try {
|
||||
console.log('Server: Getting user from token...')
|
||||
const payload = await verifyToken(token)
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: payload.userId },
|
||||
select: { id: true, email: true, name: true, theme: true, fontSize: true }
|
||||
})
|
||||
console.log('Server: Token payload userId:', payload.userId)
|
||||
|
||||
// Use raw query to avoid Prisma client sync issues
|
||||
const users = await prisma.$queryRaw`
|
||||
SELECT id, email, name, role, theme, "fontSize", "createdAt", "updatedAt", "lastLoginAt"
|
||||
FROM "User"
|
||||
WHERE id = ${payload.userId}
|
||||
`
|
||||
const user = Array.isArray(users) && users.length > 0 ? users[0] : null
|
||||
console.log('Server: User query result:', !!user)
|
||||
if (user) {
|
||||
console.log('Server: User found - email:', user.email)
|
||||
} else {
|
||||
console.log('Server: No user found with id:', payload.userId)
|
||||
}
|
||||
return user
|
||||
} catch (error) {
|
||||
console.log('Server: getUserFromToken error:', error.message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function isTokenExpired(token: string): boolean {
|
||||
try {
|
||||
const payload = jwt.decode(token) as { exp?: number }
|
||||
if (!payload || !payload.exp) return true
|
||||
|
||||
const currentTime = Math.floor(Date.now() / 1000)
|
||||
return payload.exp < currentTime
|
||||
} catch (error) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,14 @@ export const useStore = create<AppState>()(
|
||||
}),
|
||||
{
|
||||
name: 'bible-chat-storage',
|
||||
partialize: (state) => ({
|
||||
user: state.user,
|
||||
theme: state.theme,
|
||||
fontSize: state.fontSize,
|
||||
currentBook: state.currentBook,
|
||||
currentChapter: state.currentChapter,
|
||||
bookmarks: state.bookmarks,
|
||||
}),
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -1,25 +1,71 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
// User validation schemas
|
||||
export const userRegistrationSchema = z.object({
|
||||
email: z.string()
|
||||
.email('Email invalid')
|
||||
.min(3, 'Email-ul trebuie să aibă cel puțin 3 caractere')
|
||||
.max(254, 'Email-ul trebuie să aibă maximum 254 caractere'),
|
||||
password: z.string()
|
||||
.min(8, 'Parola trebuie să aibă cel puțin 8 caractere')
|
||||
.max(128, 'Parola trebuie să aibă maximum 128 caractere')
|
||||
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, 'Parola trebuie să conțină cel puțin o literă mică, o literă mare și o cifră'),
|
||||
name: z.string()
|
||||
.min(2, 'Numele trebuie să aibă cel puțin 2 caractere')
|
||||
.max(100, 'Numele trebuie să aibă maximum 100 caractere')
|
||||
.optional()
|
||||
})
|
||||
// User validation schemas - localized
|
||||
export const createUserRegistrationSchema = (locale: string = 'ro') => {
|
||||
const messages = {
|
||||
ro: {
|
||||
invalidEmail: 'Email invalid',
|
||||
emailMinLength: 'Email-ul trebuie să aibă cel puțin 3 caractere',
|
||||
emailMaxLength: 'Email-ul trebuie să aibă maximum 254 caractere',
|
||||
passwordMinLength: 'Parola trebuie să aibă cel puțin 8 caractere',
|
||||
passwordMaxLength: 'Parola trebuie să aibă maximum 128 caractere',
|
||||
passwordComplexity: 'Parola trebuie să conțină cel puțin o literă mică, o literă mare și o cifră',
|
||||
nameMinLength: 'Numele trebuie să aibă cel puțin 2 caractere',
|
||||
nameMaxLength: 'Numele trebuie să aibă maximum 100 caractere'
|
||||
},
|
||||
en: {
|
||||
invalidEmail: 'Invalid email',
|
||||
emailMinLength: 'Email must be at least 3 characters',
|
||||
emailMaxLength: 'Email must be maximum 254 characters',
|
||||
passwordMinLength: 'Password must be at least 8 characters',
|
||||
passwordMaxLength: 'Password must be maximum 128 characters',
|
||||
passwordComplexity: 'Password must contain at least one lowercase letter, one uppercase letter and one digit',
|
||||
nameMinLength: 'Name must be at least 2 characters',
|
||||
nameMaxLength: 'Name must be maximum 100 characters'
|
||||
}
|
||||
}
|
||||
|
||||
export const userLoginSchema = z.object({
|
||||
email: z.string().email('Email invalid'),
|
||||
password: z.string().min(1, 'Parola este obligatorie')
|
||||
})
|
||||
const msg = messages[locale as keyof typeof messages] || messages.ro
|
||||
|
||||
return z.object({
|
||||
email: z.string()
|
||||
.email(msg.invalidEmail)
|
||||
.min(3, msg.emailMinLength)
|
||||
.max(254, msg.emailMaxLength),
|
||||
password: z.string()
|
||||
.min(8, msg.passwordMinLength)
|
||||
.max(128, msg.passwordMaxLength)
|
||||
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, msg.passwordComplexity),
|
||||
name: z.string()
|
||||
.min(2, msg.nameMinLength)
|
||||
.max(100, msg.nameMaxLength)
|
||||
.optional()
|
||||
})
|
||||
}
|
||||
|
||||
export const createUserLoginSchema = (locale: string = 'ro') => {
|
||||
const messages = {
|
||||
ro: {
|
||||
invalidEmail: 'Email invalid',
|
||||
passwordRequired: 'Parola este obligatorie'
|
||||
},
|
||||
en: {
|
||||
invalidEmail: 'Invalid email',
|
||||
passwordRequired: 'Password is required'
|
||||
}
|
||||
}
|
||||
|
||||
const msg = messages[locale as keyof typeof messages] || messages.ro
|
||||
|
||||
return z.object({
|
||||
email: z.string().email(msg.invalidEmail),
|
||||
password: z.string().min(1, msg.passwordRequired)
|
||||
})
|
||||
}
|
||||
|
||||
// Backward compatibility - default to Romanian
|
||||
export const userRegistrationSchema = createUserRegistrationSchema('ro')
|
||||
export const userLoginSchema = createUserLoginSchema('ro')
|
||||
|
||||
// Chat validation schemas
|
||||
export const chatMessageSchema = z.object({
|
||||
|
||||
@@ -36,7 +36,20 @@ async function resolveVectorTable(language: string): Promise<{ table: string; ex
|
||||
) AS exists`,
|
||||
[VECTOR_SCHEMA, `bv_${lang}_${ab}`]
|
||||
)
|
||||
return { table, exists: Boolean(check.rows?.[0]?.exists) }
|
||||
let exists = Boolean(check.rows?.[0]?.exists)
|
||||
if (!exists) {
|
||||
// Fallback: use any table for this language
|
||||
const anyTbl = await client.query(
|
||||
`SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema = $1 AND table_name LIKE $2
|
||||
ORDER BY table_name LIMIT 1`,
|
||||
[VECTOR_SCHEMA, `bv_${lang}_%`]
|
||||
)
|
||||
if (anyTbl.rows?.[0]?.table_name) {
|
||||
return { table: `${VECTOR_SCHEMA}."${anyTbl.rows[0].table_name}"`, exists: true }
|
||||
}
|
||||
}
|
||||
return { table, exists }
|
||||
} finally {
|
||||
client.release()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user