diff --git a/cli.ts b/cli.ts index 9610b6d6..878eefd1 100644 --- a/cli.ts +++ b/cli.ts @@ -10,11 +10,19 @@ import Table from "cli-table"; import extract from "extract-zip"; import { MediaBackend } from "media-manager"; import { lookup } from "mime-types"; -import { client } from "~database/datasource"; import { getUrl } from "~database/entities/Attachment"; -import { createNewLocalUser } from "~database/entities/User"; +import { + createNewLocalUser, + findFirstUser, + findManyUsers, + type User, +} from "~database/entities/User"; import { CliParameterType } from "~packages/cli-parser/cli-builder.type"; import { config } from "~packages/config-manager"; +import { db } from "~drizzle/db"; +import { emoji, openIdAccount, status, user } from "~drizzle/schema"; +import { type SQL, eq, inArray, isNotNull, isNull, like } from "drizzle-orm"; +import { findFirstStatuses, findManyStatuses } from "~database/entities/Status"; const args = process.argv; @@ -103,10 +111,9 @@ const cliBuilder = new CliBuilder([ } // Check if user already exists - const user = await client.user.findFirst({ - where: { - OR: [{ username }, { email }], - }, + const user = await findFirstUser({ + where: (user, { or, eq }) => + or(eq(user.username, username), eq(user.email, email)), }); if (user) { @@ -136,7 +143,7 @@ const cliBuilder = new CliBuilder([ console.log( `${chalk.green("✓")} Created user ${chalk.blue( - newUser.username, + newUser?.username, )}${admin ? chalk.green(" (admin)") : ""}`, ); @@ -189,13 +196,11 @@ const cliBuilder = new CliBuilder([ return 1; } - const user = await client.user.findFirst({ - where: { - username: username, - }, + const foundUser = await findFirstUser({ + where: (user, { eq }) => eq(user.username, username), }); - if (!user) { + if (!foundUser) { console.log(`${chalk.red("✗")} User not found`); return 1; } @@ -203,7 +208,7 @@ const cliBuilder = new CliBuilder([ if (!args.noconfirm) { process.stdout.write( `Are you sure you want to delete user ${chalk.blue( - user.username, + foundUser.username, )}?\n${chalk.red( chalk.bold( "This is a destructive action and cannot be undone!", @@ -220,14 +225,12 @@ const cliBuilder = new CliBuilder([ } } - await client.user.delete({ - where: { - id: user.id, - }, - }); + await db.delete(user).where(eq(user.id, foundUser.id)); console.log( - `${chalk.green("✓")} Deleted user ${chalk.blue(user.username)}`, + `${chalk.green("✓")} Deleted user ${chalk.blue( + foundUser.username, + )}`, ); return 0; @@ -308,21 +311,25 @@ const cliBuilder = new CliBuilder([ console.log(`${chalk.red("✗")} Invalid format`); return 1; } - const users = filterObjects( - await client.user.findMany({ - where: { - isAdmin: admins || undefined, - }, - take: args.limit ?? 200, - include: { - instance: - fields.length === 0 - ? true - : fields.includes("instance"), - }, - }), - fields, - ); + + // @ts-ignore + let users: (User & { + instance?: { + baseUrl: string; + }; + })[] = await findManyUsers({ + where: (user, { eq }) => + admins ? eq(user.isAdmin, true) : undefined, + limit: args.limit ?? 200, + }); + + // If instance is not in fields, remove them + if (fields.length > 0 && !fields.includes("instance")) { + users = users.map((user) => ({ + ...user, + instance: undefined, + })); + } if (args.redact) { for (const user of users) { @@ -377,9 +384,10 @@ const cliBuilder = new CliBuilder([ isAdmin: () => chalk.red(user.isAdmin ? "Yes" : "No"), instance: () => chalk.blue( - user.instance ? user.instance.base_url : "Local", + user.instance ? user.instance.baseUrl : "Local", ), - createdAt: () => chalk.blue(user.createdAt?.toISOString()), + createdAt: () => + chalk.blue(new Date(user.createdAt).toISOString()), id: () => chalk.blue(user.id), }; @@ -497,25 +505,13 @@ const cliBuilder = new CliBuilder([ return 1; } - const queries: Prisma.UserWhereInput[] = []; - - for (const field of fields) { - queries.push({ - [field]: { - contains: query, - mode: caseSensitive ? "default" : "insensitive", - }, - }); - } - - const users = await client.user.findMany({ - where: { - OR: queries, - }, - include: { - instance: true, - }, - take: Number(limit), + const users = await findManyUsers({ + where: (user, { or, eq }) => + or( + // @ts-expect-error + ...fields.map((field) => eq(user[field], query)), + ), + limit: Number(limit), }); if (redact) { @@ -560,7 +556,7 @@ const cliBuilder = new CliBuilder([ chalk.blue(user.displayName), chalk.red(user.isAdmin ? "Yes" : "No"), chalk.blue( - user.instanceId ? user.instance?.base_url : "Local", + user.instanceId ? user.instance?.baseUrl : "Local", ), ]); } @@ -635,13 +631,8 @@ const cliBuilder = new CliBuilder([ return 1; } - const user = await client.user.findFirst({ - where: { - username: username, - }, - include: { - linkedOpenIdAccounts: true, - }, + const user = await findFirstUser({ + where: (user, { eq }) => eq(user.username, username), }); if (!user) { @@ -649,9 +640,15 @@ const cliBuilder = new CliBuilder([ return 1; } - if ( - user.linkedOpenIdAccounts.find((a) => a.issuerId === issuerId) - ) { + const linkedOpenIdAccounts = await db.query.openIdAccount.findMany({ + where: (account, { eq, and }) => + and( + eq(account.userId, user.id), + eq(account.issuerId, issuerId), + ), + }); + + if (linkedOpenIdAccounts.find((a) => a.issuerId === issuerId)) { console.log( `${chalk.red("✗")} User ${chalk.blue( user.username, @@ -661,18 +658,10 @@ const cliBuilder = new CliBuilder([ } // Connect the OpenID account - await client.user.update({ - where: { - id: user.id, - }, - data: { - linkedOpenIdAccounts: { - create: { - issuerId: issuerId, - serverId: serverId, - }, - }, - }, + await db.insert(openIdAccount).values({ + issuerId: issuerId, + serverId: serverId, + userId: user.id, }); console.log( @@ -723,13 +712,8 @@ const cliBuilder = new CliBuilder([ return 1; } - const account = await client.openIdAccount.findFirst({ - where: { - serverId: id, - }, - include: { - User: true, - }, + const account = await db.query.openIdAccount.findFirst({ + where: (account, { eq }) => eq(account.serverId, id), }); if (!account) { @@ -737,17 +721,28 @@ const cliBuilder = new CliBuilder([ return 1; } - await client.openIdAccount.delete({ - where: { - id: account.id, - }, + if (!account.userId) { + console.log( + `${chalk.red("✗")} Account ${chalk.blue( + account.serverId, + )} is not connected to any user`, + ); + return 1; + } + + const user = await findFirstUser({ + where: (user, { eq }) => eq(user.id, account.userId ?? ""), }); + await db + .delete(openIdAccount) + .where(eq(openIdAccount.id, account.id)); + console.log( `${chalk.green( "✓", )} Disconnected OpenID account from user ${chalk.blue( - account.User?.username, + user?.username, )}`, ); @@ -800,10 +795,8 @@ const cliBuilder = new CliBuilder([ return 1; } - const note = await client.status.findFirst({ - where: { - id: id, - }, + const note = await findFirstStatuses({ + where: (status, { eq }) => eq(status.id, id), }); if (!note) { @@ -831,11 +824,7 @@ const cliBuilder = new CliBuilder([ } } - await client.status.delete({ - where: { - id: note.id, - }, - }); + await db.delete(status).where(eq(status.id, note.id)); console.log( `${chalk.green("✓")} Deleted note ${chalk.blue(note.id)}`, @@ -971,30 +960,30 @@ const cliBuilder = new CliBuilder([ }); } - let instanceIdQuery: Prisma.StatusWhereInput["instanceId"]; + let instanceQuery: SQL | undefined = isNull( + status.instanceId, + ); if (local && remote) { - instanceIdQuery = undefined; + instanceQuery = undefined; } else if (local) { - instanceIdQuery = null; + instanceQuery = isNull(status.instanceId); } else if (remote) { - instanceIdQuery = { - not: null, - }; - } else { - instanceIdQuery = undefined; + instanceQuery = isNotNull(status.instanceId); } - const notes = await client.status.findMany({ - where: { - OR: queries, - instanceId: instanceIdQuery, - }, - include: { - author: true, - instance: true, - }, - take: Number(limit), + const notes = await findManyStatuses({ + where: (status, { or, and }) => + and( + or( + ...fields.map((field) => + // @ts-expect-error + like(status[field], `%${query}%`), + ), + ), + instanceQuery, + ), + limit: Number(limit), }); if (redact) { @@ -1038,9 +1027,11 @@ const cliBuilder = new CliBuilder([ chalk.green(note.content), chalk.blue(note.author.username), chalk.red( - note.instanceId ? note.instance?.base_url : "Yes", + note.author.instanceId + ? note.author.instance?.baseUrl + : "Yes", ), - chalk.blue(note.createdAt.toISOString()), + chalk.blue(new Date(note.createdAt).toISOString()), ]); } @@ -1197,11 +1188,12 @@ const cliBuilder = new CliBuilder([ } // Check if emoji already exists - const existingEmoji = await client.emoji.findFirst({ - where: { - shortcode: shortcode, - instanceId: null, - }, + const existingEmoji = await db.query.emoji.findFirst({ + where: (emoji, { and, eq, isNull }) => + and( + eq(emoji.shortcode, shortcode), + isNull(emoji.instanceId), + ), }); if (existingEmoji) { @@ -1262,19 +1254,21 @@ const cliBuilder = new CliBuilder([ // Add the emoji const content_type = lookup(newUrl) || "application/octet-stream"; - const emoji = await client.emoji.create({ - data: { - shortcode: shortcode, - url: newUrl, - visible_in_picker: true, - content_type: content_type, - instanceId: null, - }, - }); + const newEmoji = ( + await db + .insert(emoji) + .values({ + shortcode: shortcode, + url: newUrl, + visibleInPicker: true, + contentType: content_type, + }) + .returning() + )[0]; console.log( `${chalk.green("✓")} Created emoji ${chalk.blue( - emoji.shortcode, + newEmoji.shortcode, )}`, ); @@ -1303,7 +1297,7 @@ const cliBuilder = new CliBuilder([ name: "shortcode", type: CliParameterType.STRING, description: - "Shortcode of the emoji to delete (can add up to two wildcards *)", + "Shortcode of the emoji to delete (wildcards supported)", needsValue: true, positioned: true, }, @@ -1339,35 +1333,12 @@ const cliBuilder = new CliBuilder([ return 1; } - // Validate up to one wildcard - if (shortcode.split("*").length > 3) { - console.log( - `${chalk.red( - "✗", - )} Invalid shortcode (can only have up to two wildcards)`, - ); - return 1; - } - - const hasWildcard = shortcode.includes("*"); - const hasTwoWildcards = shortcode.split("*").length === 3; - - const emojis = await client.emoji.findMany({ - where: { - shortcode: { - startsWith: hasWildcard - ? shortcode.split("*")[0] - : undefined, - endsWith: hasWildcard - ? shortcode.split("*").at(-1) - : undefined, - contains: hasTwoWildcards - ? shortcode.split("*")[1] - : undefined, - equals: hasWildcard ? undefined : shortcode, - }, - instanceId: null, - }, + const emojis = await db.query.emoji.findMany({ + where: (emoji, { and, isNull, like }) => + and( + like(emoji.shortcode, shortcode.replace(/\*/g, "%")), + isNull(emoji.instanceId), + ), }); if (emojis.length === 0) { @@ -1406,13 +1377,12 @@ const cliBuilder = new CliBuilder([ } } - await client.emoji.deleteMany({ - where: { - id: { - in: emojis.map((e) => e.id), - }, - }, - }); + await db.delete(emoji).where( + inArray( + emoji.id, + emojis.map((e) => e.id), + ), + ); console.log( `${chalk.green( @@ -1466,11 +1436,9 @@ const cliBuilder = new CliBuilder([ return 0; } - const emojis = await client.emoji.findMany({ - where: { - instanceId: null, - }, - take: Number(limit), + const emojis = await db.query.emoji.findMany({ + where: (emoji, { isNull }) => isNull(emoji.instanceId), + limit: Number(limit), }); if (format === "json") { @@ -1746,11 +1714,12 @@ const cliBuilder = new CliBuilder([ ).toString(); // Check if emoji already exists - const existingEmoji = await client.emoji.findFirst({ - where: { - shortcode: shortcode, - instanceId: null, - }, + const existingEmoji = await db.query.emoji.findFirst({ + where: (emoji, { and, eq, isNull }) => + and( + eq(emoji.shortcode, shortcode), + isNull(emoji.instanceId), + ), }); if (existingEmoji) { diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 76a68579..1d0cf319 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -132,6 +132,11 @@ export const application = pgTable( }, ); +export const applicationRelations = relations(application, ({ many }) => ({ + tokens: many(token), + loginFlows: many(openIdLoginFlow), +})); + export const token = pgTable("Token", { id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), tokenType: text("token_type").notNull(), @@ -337,6 +342,16 @@ export const openIdLoginFlow = pgTable("OpenIdLoginFlow", { issuerId: text("issuerId").notNull(), }); +export const openIdLoginFlowRelations = relations( + openIdLoginFlow, + ({ one }) => ({ + application: one(application, { + fields: [openIdLoginFlow.applicationId], + references: [application.id], + }), + }), +); + export const flag = pgTable("Flag", { id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), flagType: text("flagType").default("other").notNull(), diff --git a/server/api/api/v1/statuses/[id]/source.ts b/server/api/api/v1/statuses/[id]/source.ts index 418be9ca..cf45318d 100644 --- a/server/api/api/v1/statuses/[id]/source.ts +++ b/server/api/api/v1/statuses/[id]/source.ts @@ -1,8 +1,6 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse } from "@response"; -import { client } from "~database/datasource"; -import { isViewableByUser } from "~database/entities/Status"; -import { statusAndUserRelations } from "~database/entities/relations"; +import { findFirstStatuses, isViewableByUser } from "~database/entities/Status"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -26,9 +24,8 @@ export default apiRoute(async (req, matchedRoute, extraData) => { if (!user) return errorResponse("Unauthorized", 401); - const status = await client.status.findUnique({ - where: { id }, - include: statusAndUserRelations, + const status = await findFirstStatuses({ + where: (status, { eq }) => eq(status.id, id), }); // Check if user is authorized to view this status (if it's private) diff --git a/server/api/api/v1/statuses/[id]/unfavourite.ts b/server/api/api/v1/statuses/[id]/unfavourite.ts index 5705da7a..572f3999 100644 --- a/server/api/api/v1/statuses/[id]/unfavourite.ts +++ b/server/api/api/v1/statuses/[id]/unfavourite.ts @@ -1,13 +1,11 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; -import { client } from "~database/datasource"; import { deleteLike } from "~database/entities/Like"; import { findFirstStatuses, isViewableByUser, statusToAPI, } from "~database/entities/Status"; -import { statusAndUserRelations } from "~database/entities/relations"; import type { APIStatus } from "~types/entities/status"; export const meta = applyConfig({ diff --git a/server/api/api/v1/statuses/[id]/unpin.ts b/server/api/api/v1/statuses/[id]/unpin.ts index 074cfac1..1590bf74 100644 --- a/server/api/api/v1/statuses/[id]/unpin.ts +++ b/server/api/api/v1/statuses/[id]/unpin.ts @@ -1,8 +1,9 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; -import { client } from "~database/datasource"; -import { statusToAPI } from "~database/entities/Status"; -import { statusAndUserRelations } from "~database/entities/relations"; +import { and, eq } from "drizzle-orm"; +import { findFirstStatuses, statusToAPI } from "~database/entities/Status"; +import { db } from "~drizzle/db"; +import { statusToUser } from "~drizzle/schema"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -26,9 +27,8 @@ export default apiRoute(async (req, matchedRoute, extraData) => { if (!user) return errorResponse("Unauthorized", 401); - let status = await client.status.findUnique({ - where: { id }, - include: statusAndUserRelations, + const status = await findFirstStatuses({ + where: (status, { eq }) => eq(status.id, id), }); // Check if status exists @@ -37,21 +37,9 @@ export default apiRoute(async (req, matchedRoute, extraData) => { // Check if status is user's if (status.authorId !== user.id) return errorResponse("Unauthorized", 401); - await client.user.update({ - where: { id: user.id }, - data: { - pinnedNotes: { - disconnect: { - id: status.id, - }, - }, - }, - }); - - status = await client.status.findUnique({ - where: { id }, - include: statusAndUserRelations, - }); + await db + .delete(statusToUser) + .where(and(eq(statusToUser.a, status.id), eq(statusToUser.b, user.id))); if (!status) return errorResponse("Record not found", 404); diff --git a/server/api/api/v2/search/index.ts b/server/api/api/v2/search/index.ts index 00b6291a..6271d0c3 100644 --- a/server/api/api/v2/search/index.ts +++ b/server/api/api/v2/search/index.ts @@ -1,17 +1,16 @@ import { apiRoute, applyConfig } from "@api"; import { MeiliIndexType, meilisearch } from "@meilisearch"; import { errorResponse, jsonResponse } from "@response"; -import { client } from "~database/datasource"; -import { statusToAPI } from "~database/entities/Status"; +import { and, eq, sql } from "drizzle-orm"; +import { findManyStatuses, statusToAPI } from "~database/entities/Status"; import { - resolveUser, + findFirstUser, + findManyUsers, resolveWebFinger, userToAPI, } from "~database/entities/User"; -import { - statusAndUserRelations, - userRelations, -} from "~database/entities/relations"; +import { db } from "~drizzle/db"; +import { instance, user } from "~drizzle/schema"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -26,9 +25,6 @@ export const meta = applyConfig({ }, }); -/** - * Upload new media - */ export default apiRoute<{ q?: string; type?: string; @@ -40,7 +36,7 @@ export default apiRoute<{ limit?: number; offset?: number; }>(async (req, matchedRoute, extraData) => { - const { user } = extraData.auth; + const { user: self } = extraData.auth; const { q, @@ -60,7 +56,7 @@ export default apiRoute<{ return errorResponse("Meilisearch is not enabled", 501); } - if (!user && (resolve || offset)) { + if (!self && (resolve || offset)) { return errorResponse( "Cannot use resolve or offset without being authenticated", 401, @@ -87,15 +83,26 @@ export default apiRoute<{ const [username, domain] = accountMatches[0].split("@"); - const account = await client.user.findFirst({ - where: { - username, - instance: { - base_url: domain, - }, - }, - include: userRelations, - }); + const accountId = ( + await db + .select({ + id: user.id, + }) + .from(user) + .leftJoin(instance, eq(user.instanceId, instance.id)) + .where( + and( + eq(user.username, username), + eq(instance.baseUrl, domain), + ), + ) + )[0]?.id; + + const account = accountId + ? await findFirstUser({ + where: (user, { eq }) => eq(user.id, accountId), + }) + : null; if (account) { return jsonResponse({ @@ -146,43 +153,41 @@ export default apiRoute<{ ).hits; } - const accounts = await client.user.findMany({ - where: { - id: { - in: accountResults.map((hit) => hit.id), - }, - relationshipSubjects: { - some: { - subjectId: user?.id, - following: following ? true : undefined, - }, - }, - }, - orderBy: { - createdAt: "desc", - }, - include: userRelations, + const accounts = await findManyUsers({ + where: (user, { and, eq, inArray }) => + and( + inArray( + user.id, + accountResults.map((hit) => hit.id), + ), + self + ? sql`EXISTS (SELECT 1 FROM Relationships WHERE Relationships.subjectId = ${ + self?.id + } AND Relationships.following = ${ + following ? true : false + } AND Relationships.objectId = ${user.id})` + : undefined, + ), + orderBy: (user, { desc }) => desc(user.createdAt), }); - const statuses = await client.status.findMany({ - where: { - id: { - in: statusResults.map((hit) => hit.id), - }, - author: { - relationshipSubjects: { - some: { - subjectId: user?.id, - following: following ? true : undefined, - }, - }, - }, - authorId: account_id ? account_id : undefined, - }, - orderBy: { - createdAt: "desc", - }, - include: statusAndUserRelations, + const statuses = await findManyStatuses({ + where: (status, { and, eq, inArray }) => + and( + inArray( + status.id, + statusResults.map((hit) => hit.id), + ), + account_id ? eq(status.authorId, account_id) : undefined, + self + ? sql`EXISTS (SELECT 1 FROM Relationships WHERE Relationships.subjectId = ${ + self?.id + } AND Relationships.following = ${ + following ? true : false + } AND Relationships.objectId = ${status.authorId})` + : undefined, + ), + orderBy: (status, { desc }) => desc(status.createdAt), }); return jsonResponse({ diff --git a/server/api/oauth/authorize-external/index.ts b/server/api/oauth/authorize-external/index.ts index e392cd39..89dc0435 100644 --- a/server/api/oauth/authorize-external/index.ts +++ b/server/api/oauth/authorize-external/index.ts @@ -6,7 +6,8 @@ import { generateRandomCodeVerifier, processDiscoveryResponse, } from "oauth4webapi"; -import { client } from "~database/datasource"; +import { db } from "~drizzle/db"; +import { openIdLoginFlow } from "~drizzle/schema"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -60,20 +61,26 @@ export default apiRoute(async (req, matchedRoute, extraData) => { const codeVerifier = generateRandomCodeVerifier(); - // Store into database - - const newFlow = await client.openIdLoginFlow.create({ - data: { - codeVerifier, - application: { - connect: { - client_id: clientId, - }, - }, - issuerId, - }, + const application = await db.query.application.findFirst({ + where: (application, { eq }) => eq(application.clientId, clientId), }); + if (!application) { + return redirectToLogin("Invalid client_id"); + } + + // Store into database + const newFlow = ( + await db + .insert(openIdLoginFlow) + .values({ + codeVerifier, + applicationId: application.id, + issuerId, + }) + .returning() + )[0]; + const codeChallenge = await calculatePKCECodeChallenge(codeVerifier); return Response.redirect( diff --git a/server/api/oauth/callback/[issuer]/index.ts b/server/api/oauth/callback/[issuer]/index.ts index 7c590896..26eccf5f 100644 --- a/server/api/oauth/callback/[issuer]/index.ts +++ b/server/api/oauth/callback/[issuer]/index.ts @@ -13,8 +13,10 @@ import { userInfoRequest, validateAuthResponse, } from "oauth4webapi"; -import { client } from "~database/datasource"; import { TokenType } from "~database/entities/Token"; +import { db } from "~drizzle/db"; +import { token } from "~drizzle/schema"; +import { findFirstUser } from "~database/entities/User"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -46,11 +48,10 @@ export default apiRoute(async (req, matchedRoute, extraData) => { // Remove state query parameter from URL currentUrl.searchParams.delete("state"); const issuerParam = matchedRoute.params.issuer; - const flow = await client.openIdLoginFlow.findFirst({ - where: { - id: matchedRoute.query.flow, - }, - include: { + + const flow = await db.query.openIdLoginFlow.findFirst({ + where: (flow, { eq }) => eq(flow.id, matchedRoute.query.flow), + with: { application: true, }, }); @@ -142,15 +143,19 @@ export default apiRoute(async (req, matchedRoute, extraData) => { ), ); - const user = await client.user.findFirst({ - where: { - linkedOpenIdAccounts: { - some: { - serverId: sub, - issuerId: issuer.id, - }, - }, - }, + const userId = ( + await db.query.openIdAccount.findFirst({ + where: (account, { eq, and }) => + and(eq(account.serverId, sub), eq(account.issuerId, issuer.id)), + }) + )?.userId; + + if (!userId) { + return redirectToLogin("No user found with that account"); + } + + const user = await findFirstUser({ + where: (user, { eq }) => eq(user.id, userId), }); if (!user) { @@ -161,31 +166,21 @@ export default apiRoute(async (req, matchedRoute, extraData) => { const code = randomBytes(32).toString("hex"); - await client.application.update({ - where: { id: flow.application.id }, - data: { - tokens: { - create: { - access_token: randomBytes(64).toString("base64url"), - code: code, - scope: flow.application.scopes, - token_type: TokenType.BEARER, - user: { - connect: { - id: user.id, - }, - }, - }, - }, - }, + await db.insert(token).values({ + accessToken: randomBytes(64).toString("base64url"), + code: code, + scope: flow.application.scopes, + tokenType: TokenType.BEARER, + userId: user.id, + applicationId: flow.application.id, }); // Redirect back to application return Response.redirect( `/oauth/redirect?${new URLSearchParams({ - redirect_uri: flow.application.redirect_uris, + redirect_uri: flow.application.redirectUris, code, - client_id: flow.application.client_id, + client_id: flow.application.clientId, application: flow.application.name, website: flow.application.website ?? "", scope: flow.application.scopes, diff --git a/server/api/oauth/token/index.ts b/server/api/oauth/token/index.ts index a1e4851d..6618c283 100644 --- a/server/api/oauth/token/index.ts +++ b/server/api/oauth/token/index.ts @@ -1,6 +1,6 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; -import { client } from "~database/datasource"; +import { db } from "~drizzle/db"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -34,30 +34,41 @@ export default apiRoute<{ 400, ); + if (!code || !redirect_uri || !client_id || !client_secret || !scope) + return errorResponse( + "Missing required parameters code, redirect_uri, client_id, client_secret, scope", + 400, + ); + // Get associated token - const token = await client.token.findFirst({ - where: { - code, - application: { - client_id, - secret: client_secret, - redirect_uris: redirect_uri, - scopes: scope?.replaceAll("+", " "), - }, - scope: scope?.replaceAll("+", " "), - }, - include: { - application: true, - }, + const application = await db.query.application.findFirst({ + where: (application, { eq, and }) => + and( + eq(application.clientId, client_id), + eq(application.secret, client_secret), + eq(application.redirectUris, redirect_uri), + eq(application.scopes, scope?.replaceAll("+", " ")), + ), + }); + + if (!application) + return errorResponse( + "Invalid client credentials (missing applicaiton)", + 401, + ); + + const token = await db.query.token.findFirst({ + where: (token, { eq }) => + eq(token.code, code) && eq(token.applicationId, application.id), }); if (!token) return errorResponse("Invalid access token or client credentials", 401); return jsonResponse({ - access_token: token.access_token, - token_type: token.token_type, + access_token: token.accessToken, + token_type: token.tokenType, scope: token.scope, - created_at: Number(token.created_at), + created_at: new Date(token.createdAt).getTime(), }); }); diff --git a/server/api/objects/note/[uuid]/index.ts b/server/api/objects/note/[uuid]/index.ts index 505159bb..4d322eec 100644 --- a/server/api/objects/note/[uuid]/index.ts +++ b/server/api/objects/note/[uuid]/index.ts @@ -1,10 +1,6 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; -import type * as Lysand from "lysand-types"; -import { client } from "~database/datasource"; -import { statusToLysand } from "~database/entities/Status"; -import { userToLysand } from "~database/entities/User"; -import { statusAndUserRelations } from "~database/entities/relations"; +import { findFirstStatuses, statusToLysand } from "~database/entities/Status"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -21,11 +17,8 @@ export const meta = applyConfig({ export default apiRoute(async (req, matchedRoute, extraData) => { const uuid = matchedRoute.params.uuid; - const status = await client.status.findUnique({ - where: { - id: uuid, - }, - include: statusAndUserRelations, + const status = await findFirstStatuses({ + where: (status, { eq }) => eq(status.id, uuid), }); if (!status) { diff --git a/server/api/users/[uuid]/inbox/index.ts b/server/api/users/[uuid]/inbox/index.ts index 0a971439..24a20d83 100644 --- a/server/api/users/[uuid]/inbox/index.ts +++ b/server/api/users/[uuid]/inbox/index.ts @@ -1,17 +1,16 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, response } from "@response"; +import { eq } from "drizzle-orm"; import type * as Lysand from "lysand-types"; -import { client } from "~database/datasource"; -import { objectToInboxRequest } from "~database/entities/Federation"; -import { createNewStatus, resolveStatus } from "~database/entities/Status"; +import { resolveStatus } from "~database/entities/Status"; import { - followAcceptToLysand, + findFirstUser, getRelationshipToOtherUser, resolveUser, sendFollowAccept, } from "~database/entities/User"; -import { userRelations } from "~database/entities/relations"; -import type { APIStatus } from "~types/entities/status"; +import { db } from "~drizzle/db"; +import { notification, relationship } from "~drizzle/schema"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -28,19 +27,14 @@ export const meta = applyConfig({ export default apiRoute(async (req, matchedRoute, extraData) => { const uuid = matchedRoute.params.uuid; - const user = await client.user.findUnique({ - where: { - id: uuid, - }, - include: userRelations, + const user = await findFirstUser({ + where: (user, { eq }) => eq(user.id, uuid), }); if (!user) { return errorResponse("User not found", 404); } - const config = await extraData.configManager.getConfig(); - // Process incoming request const body = extraData.parsedRequest as Lysand.Entity; @@ -119,8 +113,6 @@ export default apiRoute(async (req, matchedRoute, extraData) => { } } - console.log(body); - // Add sent data to database switch (body.type) { case "Note": { @@ -154,33 +146,31 @@ export default apiRoute(async (req, matchedRoute, extraData) => { return errorResponse("Author not found", 400); } - const relationship = await getRelationshipToOtherUser( + const foundRelationship = await getRelationshipToOtherUser( account, user, ); // Check if already following - if (relationship.following) { + if (foundRelationship.following) { return response("Already following", 200); } - await client.relationship.update({ - where: { id: relationship.id }, - data: { + await db + .update(relationship) + .set({ following: !user.isLocked, requested: user.isLocked, showingReblogs: true, notifying: true, languages: [], - }, - }); + }) + .where(eq(relationship.id, foundRelationship.id)); - await client.notification.create({ - data: { - accountId: account.id, - type: user.isLocked ? "follow_request" : "follow", - notifiedId: user.id, - }, + await db.insert(notification).values({ + accountId: account.id, + type: user.isLocked ? "follow_request" : "follow", + notifiedId: user.id, }); if (!user.isLocked) { @@ -203,24 +193,24 @@ export default apiRoute(async (req, matchedRoute, extraData) => { console.log(account); - const relationship = await getRelationshipToOtherUser( + const foundRelationship = await getRelationshipToOtherUser( user, account, ); - console.log(relationship); + console.log(foundRelationship); - if (!relationship.requested) { + if (!foundRelationship.requested) { return response("There is no follow request to accept", 200); } - await client.relationship.update({ - where: { id: relationship.id }, - data: { + await db + .update(relationship) + .set({ following: true, requested: false, - }, - }); + }) + .where(eq(relationship.id, foundRelationship.id)); return response("Follow request accepted", 200); } @@ -233,22 +223,22 @@ export default apiRoute(async (req, matchedRoute, extraData) => { return errorResponse("Author not found", 400); } - const relationship = await getRelationshipToOtherUser( + const foundRelationship = await getRelationshipToOtherUser( user, account, ); - if (!relationship.requested) { + if (!foundRelationship.requested) { return response("There is no follow request to reject", 200); } - await client.relationship.update({ - where: { id: relationship.id }, - data: { + await db + .update(relationship) + .set({ requested: false, following: false, - }, - }); + }) + .where(eq(relationship.id, foundRelationship.id)); return response("Follow request rejected", 200); } diff --git a/server/api/users/[uuid]/index.ts b/server/api/users/[uuid]/index.ts index 91898470..34cd81e7 100644 --- a/server/api/users/[uuid]/index.ts +++ b/server/api/users/[uuid]/index.ts @@ -1,8 +1,6 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; -import { client } from "~database/datasource"; -import { userToLysand } from "~database/entities/User"; -import { userRelations } from "~database/entities/relations"; +import { findFirstUser, userToLysand } from "~database/entities/User"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -19,11 +17,8 @@ export const meta = applyConfig({ export default apiRoute(async (req, matchedRoute) => { const uuid = matchedRoute.params.uuid; - const user = await client.user.findUnique({ - where: { - id: uuid, - }, - include: userRelations, + const user = await findFirstUser({ + where: (user, { eq }) => eq(user.id, uuid), }); if (!user) { diff --git a/server/api/users/[uuid]/outbox/index.ts b/server/api/users/[uuid]/outbox/index.ts index 0e8092f8..5d2c97c7 100644 --- a/server/api/users/[uuid]/outbox/index.ts +++ b/server/api/users/[uuid]/outbox/index.ts @@ -1,8 +1,9 @@ import { apiRoute, applyConfig } from "@api"; import { jsonResponse } from "@response"; -import { client } from "~database/datasource"; -import { statusToLysand } from "~database/entities/Status"; -import { statusAndUserRelations } from "~database/entities/relations"; +import { and, count, eq, inArray } from "drizzle-orm"; +import { findManyStatuses, statusToLysand } from "~database/entities/Status"; +import { db } from "~drizzle/db"; +import { status } from "~drizzle/schema"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -16,35 +17,34 @@ export const meta = applyConfig({ route: "/users/:uuid/outbox", }); -/** - * ActivityPub user outbox endpoint - */ export default apiRoute(async (req, matchedRoute, extraData) => { const uuid = matchedRoute.params.uuid; const pageNumber = Number(matchedRoute.query.page) || 1; const config = await extraData.configManager.getConfig(); const host = new URL(config.http.base_url).hostname; - const statuses = await client.status.findMany({ - where: { - authorId: uuid, - visibility: { - in: ["public", "unlisted"], - }, - }, - take: 20, - skip: 20 * (pageNumber - 1), - include: statusAndUserRelations, + const statuses = await findManyStatuses({ + where: (status, { eq, and, inArray }) => + and( + eq(status.authorId, uuid), + inArray(status.visibility, ["public", "unlisted"]), + ), + offset: 20 * (pageNumber - 1), + limit: 20, + orderBy: (status, { desc }) => desc(status.createdAt), }); - const totalStatuses = await client.status.count({ - where: { - authorId: uuid, - visibility: { - in: ["public", "unlisted"], - }, - }, - }); + const totalStatuses = await db + .select({ + count: count(), + }) + .from(status) + .where( + and( + eq(status.authorId, uuid), + inArray(status.visibility, ["public", "unlisted"]), + ), + ); return jsonResponse({ first: `${host}/users/${uuid}/outbox?page=1`, diff --git a/server/api/well-known/webfinger/index.ts b/server/api/well-known/webfinger/index.ts index 3aa005c3..fb59e785 100644 --- a/server/api/well-known/webfinger/index.ts +++ b/server/api/well-known/webfinger/index.ts @@ -1,6 +1,6 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; -import { client } from "~database/datasource"; +import { findFirstUser } from "~database/entities/User"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -45,11 +45,9 @@ export default apiRoute<{ /[0-9A-F]{8}-[0-9A-F]{4}-[7][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}/i, ); - const user = await client.user.findUnique({ - where: { - id: isUuid ? requestedUser.split("@")[0] : undefined, - username: isUuid ? undefined : requestedUser.split("@")[0], - }, + const user = await findFirstUser({ + where: (user, { eq }) => + eq(isUuid ? user.id : user.username, requestedUser.split("@")[0]), }); if (!user) {