mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
feat(auth):add login via token in frontend
This commit is contained in:
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Component: Token Login Route
|
||||||
|
* Documentation: documentation/backend/services/auth.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { prisma } from '@/lib/db';
|
||||||
|
import { generateAccessToken, generateRefreshToken } from '@/lib/utils/jwt';
|
||||||
|
import { RMABLogger } from '@/lib/utils/logger';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
const logger = RMABLogger.create('API.Auth.TokenLogin');
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const token = request.nextUrl.searchParams.get('token');
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.json({ error: 'Missing token parameter' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
|
||||||
|
|
||||||
|
const user = await prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
loginTokenHash: tokenHash,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
plexId: true,
|
||||||
|
plexUsername: true,
|
||||||
|
plexEmail: true,
|
||||||
|
avatarUrl: true,
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
logger.warn('Token login failed - not found or user deleted');
|
||||||
|
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: { lastLoginAt: new Date() },
|
||||||
|
});
|
||||||
|
|
||||||
|
const accessToken = generateAccessToken({
|
||||||
|
sub: user.id,
|
||||||
|
plexId: user.plexId,
|
||||||
|
username: user.plexUsername,
|
||||||
|
role: user.role,
|
||||||
|
});
|
||||||
|
|
||||||
|
const refreshToken = generateRefreshToken(user.id);
|
||||||
|
|
||||||
|
logger.info('Token login successful', { username: user.plexUsername });
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
username: user.plexUsername,
|
||||||
|
email: user.plexEmail,
|
||||||
|
avatarUrl: user.avatarUrl,
|
||||||
|
role: user.role,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Token login error', {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
return NextResponse.json({ error: 'Authentication failed' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* Component: Token Login Page
|
||||||
|
* Documentation: documentation/backend/services/auth.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Suspense, useEffect } from 'react';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
|
|
||||||
|
function TokenLoginContent() {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const { setAuthData } = useAuth();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = searchParams.get('token');
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
router.replace('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/api/auth/token/login?token=${encodeURIComponent(token)}`)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.error) {
|
||||||
|
router.replace('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('accessToken', data.accessToken);
|
||||||
|
localStorage.setItem('refreshToken', data.refreshToken);
|
||||||
|
localStorage.setItem('user', JSON.stringify(data.user));
|
||||||
|
|
||||||
|
setAuthData(data.user, data.accessToken);
|
||||||
|
router.push('/');
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
router.replace('/login');
|
||||||
|
});
|
||||||
|
}, [searchParams, router, setAuthData]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TokenLoginPage() {
|
||||||
|
return (
|
||||||
|
<Suspense>
|
||||||
|
<TokenLoginContent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user