feat: allow client-based web tokens

This commit is contained in:
DecDuck
2025-04-08 16:16:40 +10:00
parent 043ef6dcd2
commit 42349ad4e1
8 changed files with 59 additions and 7 deletions

View File

@ -42,7 +42,7 @@ export const $dropFetch: DropFetch = async (request, opts) => {
return object; return object;
} }
const headers = useRequestHeaders(["cookie"]); const headers = useRequestHeaders(["cookie", "authorization"]);
const data = await $fetch(request, { const data = await $fetch(request, {
...opts, ...opts,
headers: { ...opts?.headers, ...headers }, headers: { ...opts?.headers, ...headers },

View File

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "APITokenMode" ADD VALUE 'Client';

View File

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "APIToken" ADD COLUMN "clientId" TEXT;
-- AddForeignKey
ALTER TABLE "APIToken" ADD CONSTRAINT "APIToken_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "Client"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -27,6 +27,7 @@ model Invitation {
enum APITokenMode { enum APITokenMode {
User User
System System
Client
} }
model APIToken { model APIToken {
@ -38,6 +39,9 @@ model APIToken {
userId String? userId String?
user User? @relation(fields: [userId], references: [id]) user User? @relation(fields: [userId], references: [id])
clientId String?
client Client? @relation(fields: [clientId], references: [id], onDelete: Cascade)
acls String[] acls String[]
@@index([token]) @@index([token])

View File

@ -1,7 +1,7 @@
enum ClientCapabilities { enum ClientCapabilities {
PeerAPI @map("peerAPI") // other clients can use the HTTP API to P2P with this client PeerAPI @map("peerAPI") // other clients can use the HTTP API to P2P with this client
UserStatus @map("userStatus") // this client can report this user's status (playing, online, etc etc) UserStatus @map("userStatus") // this client can report this user's status (playing, online, etc etc)
CloudSaves @map("cloudSaves") // ability to save to save slots CloudSaves @map("cloudSaves") // ability to save to save slots
} }
// References a device // References a device
@ -19,6 +19,7 @@ model Client {
peerAPI ClientPeerAPIConfiguration? peerAPI ClientPeerAPIConfiguration?
lastAccessedSaves SaveSlot[] lastAccessedSaves SaveSlot[]
tokens APIToken[]
} }
model ClientPeerAPIConfiguration { model ClientPeerAPIConfiguration {

View File

@ -0,0 +1,31 @@
import { APITokenMode } from "@prisma/client";
import { DateTime } from "luxon";
import { UserACL } from "~/server/internal/acls";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import prisma from "~/server/internal/db/database";
export default defineClientEventHandler(
async (h3, { fetchUser, fetchClient, clientId }) => {
const user = await fetchUser();
const client = await fetchClient();
const acls: UserACL = [
"read",
"store:read",
"collections:read",
"object:read",
];
const token = await prisma.aPIToken.create({
data: {
name: `${client.name} Web Access Token ${DateTime.now().toISO()}`,
clientId,
userId: user.id,
mode: APITokenMode.Client,
acls,
},
});
return token.token;
}
);

View File

@ -2,7 +2,7 @@ import aclManager from "~/server/internal/acls";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["collections:new"]); const userId = await aclManager.getUserIdACL(h3, ["collections:read"]);
if (!userId) if (!userId)
throw createError({ throw createError({
statusCode: 403, statusCode: 403,

View File

@ -33,7 +33,7 @@ export const userACLs = [
] as const; ] as const;
const userACLPrefix = "user:"; const userACLPrefix = "user:";
type UserACL = Array<(typeof userACLs)[number]>; export type UserACL = Array<(typeof userACLs)[number]>;
export const systemACLs = [ export const systemACLs = [
"auth:read", "auth:read",
@ -69,7 +69,7 @@ export const systemACLs = [
] as const; ] as const;
const systemACLPrefix = "system:"; const systemACLPrefix = "system:";
type SystemACL = Array<(typeof systemACLs)[number]>; export type SystemACL = Array<(typeof systemACLs)[number]>;
class ACLManager { class ACLManager {
private getAuthorizationToken(request: MinimumRequestObject) { private getAuthorizationToken(request: MinimumRequestObject) {
@ -90,16 +90,25 @@ class ACLManager {
const authorizationToken = this.getAuthorizationToken(request); const authorizationToken = this.getAuthorizationToken(request);
if (!authorizationToken) return undefined; if (!authorizationToken) return undefined;
const token = await prisma.aPIToken.findUnique({ const token = await prisma.aPIToken.findUnique({
where: { token: authorizationToken }, where: {
token: authorizationToken,
mode: { in: [APITokenMode.User, APITokenMode.Client] },
},
}); });
if (!token) return undefined; if (!token) return undefined;
if (token.mode != APITokenMode.User || !token.userId) return undefined; // If it's a system token if (!token.userId)
throw new Error(
"No userId on user or client token - is something broken?"
);
for (const acl of acls) { for (const acl of acls) {
const tokenACLIndex = token.acls.findIndex((e) => e == acl); const tokenACLIndex = token.acls.findIndex((e) => e == acl);
if (tokenACLIndex != -1) return token.userId; if (tokenACLIndex != -1) return token.userId;
} }
console.log(token);
console.log(acls);
return undefined; return undefined;
} }