Add all REST API endpoints for mobile app consumption: Routes: - GET /api/v1/routes — paginated list with cursor, Zod validation - GET /api/v1/routes/:id — full detail with GPX, versions - POST /api/v1/routes — create with Zod-validated body - PUT /api/v1/routes/:id — update, creates new version - DELETE /api/v1/routes/:id — returns 204 Activities: - GET /api/v1/activities — paginated list with cursor - GET /api/v1/activities/:id — full detail - POST /api/v1/activities — create with GPX stat extraction - DELETE /api/v1/activities/:id — returns 204 Supporting: - POST /api/v1/routes/compute — BRouter proxy - POST /api/v1/uploads — presigned upload URL generation Device management: - GET /api/v1/auth/devices — list connected devices with isCurrent - DELETE /api/v1/auth/devices/:id — revoke device token - Store device_name on token exchange Infrastructure: - requireApiUser() guard — returns 401 with structured error - apiError() helper for consistent error responses - All endpoints use Zod schemas from @trails-cool/api for validation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
49 lines
1.5 KiB
TypeScript
49 lines
1.5 KiB
TypeScript
import type { Route } from "./+types/api.v1.auth.devices";
|
|
import { requireApiUser } from "~/lib/api-guard.server";
|
|
import { eq, and, isNull } from "drizzle-orm";
|
|
import { getDb } from "~/lib/db";
|
|
import { oauthTokens } from "@trails-cool/db/schema/journal";
|
|
|
|
/** GET /api/v1/auth/devices — list connected devices */
|
|
export async function loader({ request }: Route.LoaderArgs) {
|
|
const user = await requireApiUser(request);
|
|
const db = getDb();
|
|
|
|
const tokens = await db
|
|
.select({
|
|
id: oauthTokens.id,
|
|
deviceName: oauthTokens.deviceName,
|
|
lastActiveAt: oauthTokens.lastActiveAt,
|
|
createdAt: oauthTokens.createdAt,
|
|
})
|
|
.from(oauthTokens)
|
|
.where(
|
|
and(
|
|
eq(oauthTokens.userId, user.id),
|
|
isNull(oauthTokens.revokedAt),
|
|
),
|
|
);
|
|
|
|
// Determine which token is the current one (from the bearer token)
|
|
const authHeader = request.headers.get("Authorization");
|
|
const currentToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
|
|
let currentTokenId: string | null = null;
|
|
if (currentToken) {
|
|
const [match] = await db
|
|
.select({ id: oauthTokens.id })
|
|
.from(oauthTokens)
|
|
.where(eq(oauthTokens.accessToken, currentToken));
|
|
currentTokenId = match?.id ?? null;
|
|
}
|
|
|
|
return Response.json({
|
|
devices: tokens.map((t) => ({
|
|
id: t.id,
|
|
deviceName: t.deviceName ?? "Unknown device",
|
|
lastActiveAt: t.lastActiveAt?.toISOString() ?? null,
|
|
createdAt: t.createdAt.toISOString(),
|
|
isCurrent: t.id === currentTokenId,
|
|
})),
|
|
});
|
|
}
|