feat(auth): add API token authentication for OpenSheets Companion

- Implement JWT-based authentication system for device access
  - Access tokens (7 day expiry) and refresh tokens (90 day expiry)
  - HMAC-SHA256 signing with timing-safe comparison
  - Token hashing with SHA-256 for secure storage

- Add device authentication endpoints:
  - POST /api/auth/device/token - Login with email/password, get tokens
  - POST /api/auth/device/refresh - Refresh access token
  - POST /api/auth/device/verify - Verify token validity
  - GET /api/auth/device/tokens - List user's API tokens
  - DELETE /api/auth/device/tokens/[id] - Revoke specific token

- Track token usage (last used timestamp and IP)
This commit is contained in:
Felipe Coutinho
2026-01-23 12:11:19 +00:00
parent 29a457ad36
commit 2532f2d6ad
6 changed files with 584 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
/**
* GET /api/auth/device/tokens
*
* Lista todos os tokens de API do usuário.
* Requer sessão web autenticada.
*/
import { auth } from "@/lib/auth/config";
import { db } from "@/lib/db";
import { apiTokens } from "@/db/schema";
import { eq, desc } from "drizzle-orm";
import { headers } from "next/headers";
import { NextResponse } from "next/server";
export async function GET() {
try {
// Verificar autenticação via sessão web
const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user) {
return NextResponse.json(
{ error: "Não autenticado" },
{ status: 401 }
);
}
// Buscar tokens ativos do usuário
const tokens = await db
.select({
id: apiTokens.id,
name: apiTokens.name,
tokenPrefix: apiTokens.tokenPrefix,
lastUsedAt: apiTokens.lastUsedAt,
lastUsedIp: apiTokens.lastUsedIp,
expiresAt: apiTokens.expiresAt,
createdAt: apiTokens.createdAt,
})
.from(apiTokens)
.where(eq(apiTokens.userId, session.user.id))
.orderBy(desc(apiTokens.createdAt));
// Separar tokens ativos e revogados
const activeTokens = tokens.filter((t) => !t.expiresAt || new Date(t.expiresAt) > new Date());
return NextResponse.json({ tokens: activeTokens });
} catch (error) {
console.error("[API] Error listing device tokens:", error);
return NextResponse.json(
{ error: "Erro ao listar tokens" },
{ status: 500 }
);
}
}