fix(auth): send login token in POST body

This commit is contained in:
Orvanix
2026-03-12 17:15:07 +00:00
parent b20673e7ea
commit 81712ad3ce
4 changed files with 20 additions and 15 deletions
+2 -1
View File
@@ -253,7 +253,8 @@ oidc.admin_claim_value = 'readmeabook-admin'
- Login token stored as SHA-256 hash in `User.loginTokenHash` - Login token stored as SHA-256 hash in `User.loginTokenHash`
- Admin generates/revokes via user permissions modal - Admin generates/revokes via user permissions modal
- User login with token `/auth/token/login?token=rmab_...` - User navigates to `/auth/token/login?token=rmab_...` → page POSTs token to API in request body
- API: `POST /api/auth/token/login` with `{ token }` in JSON body
- Invalid token redirects to `/login` - Invalid token redirects to `/login`
## Security ## Security
+2 -2
View File
@@ -11,9 +11,9 @@ import crypto from 'crypto';
const logger = RMABLogger.create('API.Auth.TokenLogin'); const logger = RMABLogger.create('API.Auth.TokenLogin');
export async function GET(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const token = request.nextUrl.searchParams.get('token'); const { token } = await request.json();
if (!token) { if (!token) {
return NextResponse.json({ error: 'Missing token parameter' }, { status: 400 }); return NextResponse.json({ error: 'Missing token parameter' }, { status: 400 });
+5 -1
View File
@@ -22,7 +22,11 @@ function TokenLoginContent() {
return; return;
} }
fetch(`/api/auth/token/login?token=${encodeURIComponent(token)}`) fetch('/api/auth/token/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
})
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
if (data.error) { if (data.error) {
+10 -10
View File
@@ -19,7 +19,7 @@ vi.mock('@/lib/utils/jwt', () => ({
generateRefreshToken: generateRefreshTokenMock, generateRefreshToken: generateRefreshTokenMock,
})); }));
describe('GET /api/auth/token/login', () => { describe('POST /api/auth/token/login', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
generateAccessTokenMock.mockReturnValue('access-token'); generateAccessTokenMock.mockReturnValue('access-token');
@@ -37,9 +37,9 @@ describe('GET /api/auth/token/login', () => {
}); });
prismaMock.user.update.mockResolvedValueOnce({}); prismaMock.user.update.mockResolvedValueOnce({});
const { GET } = await import('@/app/api/auth/token/login/route'); const { POST } = await import('@/app/api/auth/token/login/route');
const request = { nextUrl: { searchParams: new URLSearchParams('token=rmab_valid_token') } }; const request = { json: vi.fn().mockResolvedValue({ token: 'rmab_valid_token' }) };
const response = await GET(request as any); const response = await POST(request as any);
const payload = await response.json(); const payload = await response.json();
expect(response.status).toBe(200); expect(response.status).toBe(200);
@@ -50,9 +50,9 @@ describe('GET /api/auth/token/login', () => {
}); });
it('returns 400 when token parameter is missing', async () => { it('returns 400 when token parameter is missing', async () => {
const { GET } = await import('@/app/api/auth/token/login/route'); const { POST } = await import('@/app/api/auth/token/login/route');
const request = { nextUrl: { searchParams: new URLSearchParams() } }; const request = { json: vi.fn().mockResolvedValue({}) };
const response = await GET(request as any); const response = await POST(request as any);
const payload = await response.json(); const payload = await response.json();
expect(response.status).toBe(400); expect(response.status).toBe(400);
@@ -62,9 +62,9 @@ describe('GET /api/auth/token/login', () => {
it('returns 401 when token is invalid or user not found', async () => { it('returns 401 when token is invalid or user not found', async () => {
prismaMock.user.findFirst.mockResolvedValueOnce(null); prismaMock.user.findFirst.mockResolvedValueOnce(null);
const { GET } = await import('@/app/api/auth/token/login/route'); const { POST } = await import('@/app/api/auth/token/login/route');
const request = { nextUrl: { searchParams: new URLSearchParams('token=rmab_invalid') } }; const request = { json: vi.fn().mockResolvedValue({ token: 'rmab_invalid' }) };
const response = await GET(request as any); const response = await POST(request as any);
const payload = await response.json(); const payload = await response.json();
expect(response.status).toBe(401); expect(response.status).toBe(401);