Convert remaining routes to Drizzle

This commit is contained in:
Jesse Wierzbinski 2024-04-13 14:07:05 -10:00
parent 05e45ff5aa
commit 90d522eaa3
No known key found for this signature in database
14 changed files with 383 additions and 422 deletions

323
cli.ts
View file

@ -10,11 +10,19 @@ import Table from "cli-table";
import extract from "extract-zip"; import extract from "extract-zip";
import { MediaBackend } from "media-manager"; import { MediaBackend } from "media-manager";
import { lookup } from "mime-types"; import { lookup } from "mime-types";
import { client } from "~database/datasource";
import { getUrl } from "~database/entities/Attachment"; 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 { CliParameterType } from "~packages/cli-parser/cli-builder.type";
import { config } from "~packages/config-manager"; 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; const args = process.argv;
@ -103,10 +111,9 @@ const cliBuilder = new CliBuilder([
} }
// Check if user already exists // Check if user already exists
const user = await client.user.findFirst({ const user = await findFirstUser({
where: { where: (user, { or, eq }) =>
OR: [{ username }, { email }], or(eq(user.username, username), eq(user.email, email)),
},
}); });
if (user) { if (user) {
@ -136,7 +143,7 @@ const cliBuilder = new CliBuilder([
console.log( console.log(
`${chalk.green("✓")} Created user ${chalk.blue( `${chalk.green("✓")} Created user ${chalk.blue(
newUser.username, newUser?.username,
)}${admin ? chalk.green(" (admin)") : ""}`, )}${admin ? chalk.green(" (admin)") : ""}`,
); );
@ -189,13 +196,11 @@ const cliBuilder = new CliBuilder([
return 1; return 1;
} }
const user = await client.user.findFirst({ const foundUser = await findFirstUser({
where: { where: (user, { eq }) => eq(user.username, username),
username: username,
},
}); });
if (!user) { if (!foundUser) {
console.log(`${chalk.red("✗")} User not found`); console.log(`${chalk.red("✗")} User not found`);
return 1; return 1;
} }
@ -203,7 +208,7 @@ const cliBuilder = new CliBuilder([
if (!args.noconfirm) { if (!args.noconfirm) {
process.stdout.write( process.stdout.write(
`Are you sure you want to delete user ${chalk.blue( `Are you sure you want to delete user ${chalk.blue(
user.username, foundUser.username,
)}?\n${chalk.red( )}?\n${chalk.red(
chalk.bold( chalk.bold(
"This is a destructive action and cannot be undone!", "This is a destructive action and cannot be undone!",
@ -220,14 +225,12 @@ const cliBuilder = new CliBuilder([
} }
} }
await client.user.delete({ await db.delete(user).where(eq(user.id, foundUser.id));
where: {
id: user.id,
},
});
console.log( console.log(
`${chalk.green("✓")} Deleted user ${chalk.blue(user.username)}`, `${chalk.green("✓")} Deleted user ${chalk.blue(
foundUser.username,
)}`,
); );
return 0; return 0;
@ -308,21 +311,25 @@ const cliBuilder = new CliBuilder([
console.log(`${chalk.red("✗")} Invalid format`); console.log(`${chalk.red("✗")} Invalid format`);
return 1; return 1;
} }
const users = filterObjects(
await client.user.findMany({ // @ts-ignore
where: { let users: (User & {
isAdmin: admins || undefined, instance?: {
}, baseUrl: string;
take: args.limit ?? 200, };
include: { })[] = await findManyUsers({
instance: where: (user, { eq }) =>
fields.length === 0 admins ? eq(user.isAdmin, true) : undefined,
? true limit: args.limit ?? 200,
: fields.includes("instance"), });
},
}), // If instance is not in fields, remove them
fields, if (fields.length > 0 && !fields.includes("instance")) {
); users = users.map((user) => ({
...user,
instance: undefined,
}));
}
if (args.redact) { if (args.redact) {
for (const user of users) { for (const user of users) {
@ -377,9 +384,10 @@ const cliBuilder = new CliBuilder([
isAdmin: () => chalk.red(user.isAdmin ? "Yes" : "No"), isAdmin: () => chalk.red(user.isAdmin ? "Yes" : "No"),
instance: () => instance: () =>
chalk.blue( 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), id: () => chalk.blue(user.id),
}; };
@ -497,25 +505,13 @@ const cliBuilder = new CliBuilder([
return 1; return 1;
} }
const queries: Prisma.UserWhereInput[] = []; const users = await findManyUsers({
where: (user, { or, eq }) =>
for (const field of fields) { or(
queries.push({ // @ts-expect-error
[field]: { ...fields.map((field) => eq(user[field], query)),
contains: query, ),
mode: caseSensitive ? "default" : "insensitive", limit: Number(limit),
},
});
}
const users = await client.user.findMany({
where: {
OR: queries,
},
include: {
instance: true,
},
take: Number(limit),
}); });
if (redact) { if (redact) {
@ -560,7 +556,7 @@ const cliBuilder = new CliBuilder([
chalk.blue(user.displayName), chalk.blue(user.displayName),
chalk.red(user.isAdmin ? "Yes" : "No"), chalk.red(user.isAdmin ? "Yes" : "No"),
chalk.blue( 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; return 1;
} }
const user = await client.user.findFirst({ const user = await findFirstUser({
where: { where: (user, { eq }) => eq(user.username, username),
username: username,
},
include: {
linkedOpenIdAccounts: true,
},
}); });
if (!user) { if (!user) {
@ -649,9 +640,15 @@ const cliBuilder = new CliBuilder([
return 1; return 1;
} }
if ( const linkedOpenIdAccounts = await db.query.openIdAccount.findMany({
user.linkedOpenIdAccounts.find((a) => a.issuerId === issuerId) where: (account, { eq, and }) =>
) { and(
eq(account.userId, user.id),
eq(account.issuerId, issuerId),
),
});
if (linkedOpenIdAccounts.find((a) => a.issuerId === issuerId)) {
console.log( console.log(
`${chalk.red("✗")} User ${chalk.blue( `${chalk.red("✗")} User ${chalk.blue(
user.username, user.username,
@ -661,18 +658,10 @@ const cliBuilder = new CliBuilder([
} }
// Connect the OpenID account // Connect the OpenID account
await client.user.update({ await db.insert(openIdAccount).values({
where: {
id: user.id,
},
data: {
linkedOpenIdAccounts: {
create: {
issuerId: issuerId, issuerId: issuerId,
serverId: serverId, serverId: serverId,
}, userId: user.id,
},
},
}); });
console.log( console.log(
@ -723,13 +712,8 @@ const cliBuilder = new CliBuilder([
return 1; return 1;
} }
const account = await client.openIdAccount.findFirst({ const account = await db.query.openIdAccount.findFirst({
where: { where: (account, { eq }) => eq(account.serverId, id),
serverId: id,
},
include: {
User: true,
},
}); });
if (!account) { if (!account) {
@ -737,17 +721,28 @@ const cliBuilder = new CliBuilder([
return 1; return 1;
} }
await client.openIdAccount.delete({ if (!account.userId) {
where: { console.log(
id: account.id, `${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( console.log(
`${chalk.green( `${chalk.green(
"✓", "✓",
)} Disconnected OpenID account from user ${chalk.blue( )} Disconnected OpenID account from user ${chalk.blue(
account.User?.username, user?.username,
)}`, )}`,
); );
@ -800,10 +795,8 @@ const cliBuilder = new CliBuilder([
return 1; return 1;
} }
const note = await client.status.findFirst({ const note = await findFirstStatuses({
where: { where: (status, { eq }) => eq(status.id, id),
id: id,
},
}); });
if (!note) { if (!note) {
@ -831,11 +824,7 @@ const cliBuilder = new CliBuilder([
} }
} }
await client.status.delete({ await db.delete(status).where(eq(status.id, note.id));
where: {
id: note.id,
},
});
console.log( console.log(
`${chalk.green("✓")} Deleted note ${chalk.blue(note.id)}`, `${chalk.green("✓")} Deleted note ${chalk.blue(note.id)}`,
@ -971,30 +960,30 @@ const cliBuilder = new CliBuilder([
}); });
} }
let instanceIdQuery: Prisma.StatusWhereInput["instanceId"]; let instanceQuery: SQL<unknown> | undefined = isNull(
status.instanceId,
);
if (local && remote) { if (local && remote) {
instanceIdQuery = undefined; instanceQuery = undefined;
} else if (local) { } else if (local) {
instanceIdQuery = null; instanceQuery = isNull(status.instanceId);
} else if (remote) { } else if (remote) {
instanceIdQuery = { instanceQuery = isNotNull(status.instanceId);
not: null,
};
} else {
instanceIdQuery = undefined;
} }
const notes = await client.status.findMany({ const notes = await findManyStatuses({
where: { where: (status, { or, and }) =>
OR: queries, and(
instanceId: instanceIdQuery, or(
}, ...fields.map((field) =>
include: { // @ts-expect-error
author: true, like(status[field], `%${query}%`),
instance: true, ),
}, ),
take: Number(limit), instanceQuery,
),
limit: Number(limit),
}); });
if (redact) { if (redact) {
@ -1038,9 +1027,11 @@ const cliBuilder = new CliBuilder([
chalk.green(note.content), chalk.green(note.content),
chalk.blue(note.author.username), chalk.blue(note.author.username),
chalk.red( 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 // Check if emoji already exists
const existingEmoji = await client.emoji.findFirst({ const existingEmoji = await db.query.emoji.findFirst({
where: { where: (emoji, { and, eq, isNull }) =>
shortcode: shortcode, and(
instanceId: null, eq(emoji.shortcode, shortcode),
}, isNull(emoji.instanceId),
),
}); });
if (existingEmoji) { if (existingEmoji) {
@ -1262,19 +1254,21 @@ const cliBuilder = new CliBuilder([
// Add the emoji // Add the emoji
const content_type = lookup(newUrl) || "application/octet-stream"; const content_type = lookup(newUrl) || "application/octet-stream";
const emoji = await client.emoji.create({ const newEmoji = (
data: { await db
.insert(emoji)
.values({
shortcode: shortcode, shortcode: shortcode,
url: newUrl, url: newUrl,
visible_in_picker: true, visibleInPicker: true,
content_type: content_type, contentType: content_type,
instanceId: null, })
}, .returning()
}); )[0];
console.log( console.log(
`${chalk.green("✓")} Created emoji ${chalk.blue( `${chalk.green("✓")} Created emoji ${chalk.blue(
emoji.shortcode, newEmoji.shortcode,
)}`, )}`,
); );
@ -1303,7 +1297,7 @@ const cliBuilder = new CliBuilder([
name: "shortcode", name: "shortcode",
type: CliParameterType.STRING, type: CliParameterType.STRING,
description: description:
"Shortcode of the emoji to delete (can add up to two wildcards *)", "Shortcode of the emoji to delete (wildcards supported)",
needsValue: true, needsValue: true,
positioned: true, positioned: true,
}, },
@ -1339,35 +1333,12 @@ const cliBuilder = new CliBuilder([
return 1; return 1;
} }
// Validate up to one wildcard const emojis = await db.query.emoji.findMany({
if (shortcode.split("*").length > 3) { where: (emoji, { and, isNull, like }) =>
console.log( and(
`${chalk.red( like(emoji.shortcode, shortcode.replace(/\*/g, "%")),
"✗", isNull(emoji.instanceId),
)} 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,
},
}); });
if (emojis.length === 0) { if (emojis.length === 0) {
@ -1406,13 +1377,12 @@ const cliBuilder = new CliBuilder([
} }
} }
await client.emoji.deleteMany({ await db.delete(emoji).where(
where: { inArray(
id: { emoji.id,
in: emojis.map((e) => e.id), emojis.map((e) => e.id),
}, ),
}, );
});
console.log( console.log(
`${chalk.green( `${chalk.green(
@ -1466,11 +1436,9 @@ const cliBuilder = new CliBuilder([
return 0; return 0;
} }
const emojis = await client.emoji.findMany({ const emojis = await db.query.emoji.findMany({
where: { where: (emoji, { isNull }) => isNull(emoji.instanceId),
instanceId: null, limit: Number(limit),
},
take: Number(limit),
}); });
if (format === "json") { if (format === "json") {
@ -1746,11 +1714,12 @@ const cliBuilder = new CliBuilder([
).toString(); ).toString();
// Check if emoji already exists // Check if emoji already exists
const existingEmoji = await client.emoji.findFirst({ const existingEmoji = await db.query.emoji.findFirst({
where: { where: (emoji, { and, eq, isNull }) =>
shortcode: shortcode, and(
instanceId: null, eq(emoji.shortcode, shortcode),
}, isNull(emoji.instanceId),
),
}); });
if (existingEmoji) { if (existingEmoji) {

View file

@ -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", { export const token = pgTable("Token", {
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
tokenType: text("token_type").notNull(), tokenType: text("token_type").notNull(),
@ -337,6 +342,16 @@ export const openIdLoginFlow = pgTable("OpenIdLoginFlow", {
issuerId: text("issuerId").notNull(), issuerId: text("issuerId").notNull(),
}); });
export const openIdLoginFlowRelations = relations(
openIdLoginFlow,
({ one }) => ({
application: one(application, {
fields: [openIdLoginFlow.applicationId],
references: [application.id],
}),
}),
);
export const flag = pgTable("Flag", { export const flag = pgTable("Flag", {
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
flagType: text("flagType").default("other").notNull(), flagType: text("flagType").default("other").notNull(),

View file

@ -1,8 +1,6 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse } from "@response"; import { errorResponse } from "@response";
import { client } from "~database/datasource"; import { findFirstStatuses, isViewableByUser } from "~database/entities/Status";
import { isViewableByUser } from "~database/entities/Status";
import { statusAndUserRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -26,9 +24,8 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
const status = await client.status.findUnique({ const status = await findFirstStatuses({
where: { id }, where: (status, { eq }) => eq(status.id, id),
include: statusAndUserRelations,
}); });
// Check if user is authorized to view this status (if it's private) // Check if user is authorized to view this status (if it's private)

View file

@ -1,13 +1,11 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource";
import { deleteLike } from "~database/entities/Like"; import { deleteLike } from "~database/entities/Like";
import { import {
findFirstStatuses, findFirstStatuses,
isViewableByUser, isViewableByUser,
statusToAPI, statusToAPI,
} from "~database/entities/Status"; } from "~database/entities/Status";
import { statusAndUserRelations } from "~database/entities/relations";
import type { APIStatus } from "~types/entities/status"; import type { APIStatus } from "~types/entities/status";
export const meta = applyConfig({ export const meta = applyConfig({

View file

@ -1,8 +1,9 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { and, eq } from "drizzle-orm";
import { statusToAPI } from "~database/entities/Status"; import { findFirstStatuses, statusToAPI } from "~database/entities/Status";
import { statusAndUserRelations } from "~database/entities/relations"; import { db } from "~drizzle/db";
import { statusToUser } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -26,9 +27,8 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
let status = await client.status.findUnique({ const status = await findFirstStatuses({
where: { id }, where: (status, { eq }) => eq(status.id, id),
include: statusAndUserRelations,
}); });
// Check if status exists // Check if status exists
@ -37,21 +37,9 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
// Check if status is user's // Check if status is user's
if (status.authorId !== user.id) return errorResponse("Unauthorized", 401); if (status.authorId !== user.id) return errorResponse("Unauthorized", 401);
await client.user.update({ await db
where: { id: user.id }, .delete(statusToUser)
data: { .where(and(eq(statusToUser.a, status.id), eq(statusToUser.b, user.id)));
pinnedNotes: {
disconnect: {
id: status.id,
},
},
},
});
status = await client.status.findUnique({
where: { id },
include: statusAndUserRelations,
});
if (!status) return errorResponse("Record not found", 404); if (!status) return errorResponse("Record not found", 404);

View file

@ -1,17 +1,16 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { MeiliIndexType, meilisearch } from "@meilisearch"; import { MeiliIndexType, meilisearch } from "@meilisearch";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { and, eq, sql } from "drizzle-orm";
import { statusToAPI } from "~database/entities/Status"; import { findManyStatuses, statusToAPI } from "~database/entities/Status";
import { import {
resolveUser, findFirstUser,
findManyUsers,
resolveWebFinger, resolveWebFinger,
userToAPI, userToAPI,
} from "~database/entities/User"; } from "~database/entities/User";
import { import { db } from "~drizzle/db";
statusAndUserRelations, import { instance, user } from "~drizzle/schema";
userRelations,
} from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -26,9 +25,6 @@ export const meta = applyConfig({
}, },
}); });
/**
* Upload new media
*/
export default apiRoute<{ export default apiRoute<{
q?: string; q?: string;
type?: string; type?: string;
@ -40,7 +36,7 @@ export default apiRoute<{
limit?: number; limit?: number;
offset?: number; offset?: number;
}>(async (req, matchedRoute, extraData) => { }>(async (req, matchedRoute, extraData) => {
const { user } = extraData.auth; const { user: self } = extraData.auth;
const { const {
q, q,
@ -60,7 +56,7 @@ export default apiRoute<{
return errorResponse("Meilisearch is not enabled", 501); return errorResponse("Meilisearch is not enabled", 501);
} }
if (!user && (resolve || offset)) { if (!self && (resolve || offset)) {
return errorResponse( return errorResponse(
"Cannot use resolve or offset without being authenticated", "Cannot use resolve or offset without being authenticated",
401, 401,
@ -87,15 +83,26 @@ export default apiRoute<{
const [username, domain] = accountMatches[0].split("@"); const [username, domain] = accountMatches[0].split("@");
const account = await client.user.findFirst({ const accountId = (
where: { await db
username, .select({
instance: { id: user.id,
base_url: domain, })
}, .from(user)
}, .leftJoin(instance, eq(user.instanceId, instance.id))
include: userRelations, .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) { if (account) {
return jsonResponse({ return jsonResponse({
@ -146,43 +153,41 @@ export default apiRoute<{
).hits; ).hits;
} }
const accounts = await client.user.findMany({ const accounts = await findManyUsers({
where: { where: (user, { and, eq, inArray }) =>
id: { and(
in: accountResults.map((hit) => hit.id), inArray(
}, user.id,
relationshipSubjects: { accountResults.map((hit) => hit.id),
some: { ),
subjectId: user?.id, self
following: following ? true : undefined, ? sql`EXISTS (SELECT 1 FROM Relationships WHERE Relationships.subjectId = ${
}, self?.id
}, } AND Relationships.following = ${
}, following ? true : false
orderBy: { } AND Relationships.objectId = ${user.id})`
createdAt: "desc", : undefined,
}, ),
include: userRelations, orderBy: (user, { desc }) => desc(user.createdAt),
}); });
const statuses = await client.status.findMany({ const statuses = await findManyStatuses({
where: { where: (status, { and, eq, inArray }) =>
id: { and(
in: statusResults.map((hit) => hit.id), inArray(
}, status.id,
author: { statusResults.map((hit) => hit.id),
relationshipSubjects: { ),
some: { account_id ? eq(status.authorId, account_id) : undefined,
subjectId: user?.id, self
following: following ? true : undefined, ? sql`EXISTS (SELECT 1 FROM Relationships WHERE Relationships.subjectId = ${
}, self?.id
}, } AND Relationships.following = ${
}, following ? true : false
authorId: account_id ? account_id : undefined, } AND Relationships.objectId = ${status.authorId})`
}, : undefined,
orderBy: { ),
createdAt: "desc", orderBy: (status, { desc }) => desc(status.createdAt),
},
include: statusAndUserRelations,
}); });
return jsonResponse({ return jsonResponse({

View file

@ -6,7 +6,8 @@ import {
generateRandomCodeVerifier, generateRandomCodeVerifier,
processDiscoveryResponse, processDiscoveryResponse,
} from "oauth4webapi"; } from "oauth4webapi";
import { client } from "~database/datasource"; import { db } from "~drizzle/db";
import { openIdLoginFlow } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -60,20 +61,26 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
const codeVerifier = generateRandomCodeVerifier(); const codeVerifier = generateRandomCodeVerifier();
// Store into database const application = await db.query.application.findFirst({
where: (application, { eq }) => eq(application.clientId, clientId),
const newFlow = await client.openIdLoginFlow.create({
data: {
codeVerifier,
application: {
connect: {
client_id: clientId,
},
},
issuerId,
},
}); });
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); const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
return Response.redirect( return Response.redirect(

View file

@ -13,8 +13,10 @@ import {
userInfoRequest, userInfoRequest,
validateAuthResponse, validateAuthResponse,
} from "oauth4webapi"; } from "oauth4webapi";
import { client } from "~database/datasource";
import { TokenType } from "~database/entities/Token"; 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({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -46,11 +48,10 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
// Remove state query parameter from URL // Remove state query parameter from URL
currentUrl.searchParams.delete("state"); currentUrl.searchParams.delete("state");
const issuerParam = matchedRoute.params.issuer; const issuerParam = matchedRoute.params.issuer;
const flow = await client.openIdLoginFlow.findFirst({
where: { const flow = await db.query.openIdLoginFlow.findFirst({
id: matchedRoute.query.flow, where: (flow, { eq }) => eq(flow.id, matchedRoute.query.flow),
}, with: {
include: {
application: true, application: true,
}, },
}); });
@ -142,15 +143,19 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
), ),
); );
const user = await client.user.findFirst({ const userId = (
where: { await db.query.openIdAccount.findFirst({
linkedOpenIdAccounts: { where: (account, { eq, and }) =>
some: { and(eq(account.serverId, sub), eq(account.issuerId, issuer.id)),
serverId: sub, })
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) { if (!user) {
@ -161,31 +166,21 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
const code = randomBytes(32).toString("hex"); const code = randomBytes(32).toString("hex");
await client.application.update({ await db.insert(token).values({
where: { id: flow.application.id }, accessToken: randomBytes(64).toString("base64url"),
data: {
tokens: {
create: {
access_token: randomBytes(64).toString("base64url"),
code: code, code: code,
scope: flow.application.scopes, scope: flow.application.scopes,
token_type: TokenType.BEARER, tokenType: TokenType.BEARER,
user: { userId: user.id,
connect: { applicationId: flow.application.id,
id: user.id,
},
},
},
},
},
}); });
// Redirect back to application // Redirect back to application
return Response.redirect( return Response.redirect(
`/oauth/redirect?${new URLSearchParams({ `/oauth/redirect?${new URLSearchParams({
redirect_uri: flow.application.redirect_uris, redirect_uri: flow.application.redirectUris,
code, code,
client_id: flow.application.client_id, client_id: flow.application.clientId,
application: flow.application.name, application: flow.application.name,
website: flow.application.website ?? "", website: flow.application.website ?? "",
scope: flow.application.scopes, scope: flow.application.scopes,

View file

@ -1,6 +1,6 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { db } from "~drizzle/db";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -34,30 +34,41 @@ export default apiRoute<{
400, 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 // Get associated token
const token = await client.token.findFirst({ const application = await db.query.application.findFirst({
where: { where: (application, { eq, and }) =>
code, and(
application: { eq(application.clientId, client_id),
client_id, eq(application.secret, client_secret),
secret: client_secret, eq(application.redirectUris, redirect_uri),
redirect_uris: redirect_uri, eq(application.scopes, scope?.replaceAll("+", " ")),
scopes: scope?.replaceAll("+", " "), ),
}, });
scope: scope?.replaceAll("+", " "),
}, if (!application)
include: { return errorResponse(
application: true, "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) if (!token)
return errorResponse("Invalid access token or client credentials", 401); return errorResponse("Invalid access token or client credentials", 401);
return jsonResponse({ return jsonResponse({
access_token: token.access_token, access_token: token.accessToken,
token_type: token.token_type, token_type: token.tokenType,
scope: token.scope, scope: token.scope,
created_at: Number(token.created_at), created_at: new Date(token.createdAt).getTime(),
}); });
}); });

View file

@ -1,10 +1,6 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import type * as Lysand from "lysand-types"; import { findFirstStatuses, statusToLysand } from "~database/entities/Status";
import { client } from "~database/datasource";
import { statusToLysand } from "~database/entities/Status";
import { userToLysand } from "~database/entities/User";
import { statusAndUserRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -21,11 +17,8 @@ export const meta = applyConfig({
export default apiRoute(async (req, matchedRoute, extraData) => { export default apiRoute(async (req, matchedRoute, extraData) => {
const uuid = matchedRoute.params.uuid; const uuid = matchedRoute.params.uuid;
const status = await client.status.findUnique({ const status = await findFirstStatuses({
where: { where: (status, { eq }) => eq(status.id, uuid),
id: uuid,
},
include: statusAndUserRelations,
}); });
if (!status) { if (!status) {

View file

@ -1,17 +1,16 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, response } from "@response"; import { errorResponse, response } from "@response";
import { eq } from "drizzle-orm";
import type * as Lysand from "lysand-types"; import type * as Lysand from "lysand-types";
import { client } from "~database/datasource"; import { resolveStatus } from "~database/entities/Status";
import { objectToInboxRequest } from "~database/entities/Federation";
import { createNewStatus, resolveStatus } from "~database/entities/Status";
import { import {
followAcceptToLysand, findFirstUser,
getRelationshipToOtherUser, getRelationshipToOtherUser,
resolveUser, resolveUser,
sendFollowAccept, sendFollowAccept,
} from "~database/entities/User"; } from "~database/entities/User";
import { userRelations } from "~database/entities/relations"; import { db } from "~drizzle/db";
import type { APIStatus } from "~types/entities/status"; import { notification, relationship } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -28,19 +27,14 @@ export const meta = applyConfig({
export default apiRoute(async (req, matchedRoute, extraData) => { export default apiRoute(async (req, matchedRoute, extraData) => {
const uuid = matchedRoute.params.uuid; const uuid = matchedRoute.params.uuid;
const user = await client.user.findUnique({ const user = await findFirstUser({
where: { where: (user, { eq }) => eq(user.id, uuid),
id: uuid,
},
include: userRelations,
}); });
if (!user) { if (!user) {
return errorResponse("User not found", 404); return errorResponse("User not found", 404);
} }
const config = await extraData.configManager.getConfig();
// Process incoming request // Process incoming request
const body = extraData.parsedRequest as Lysand.Entity; 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 // Add sent data to database
switch (body.type) { switch (body.type) {
case "Note": { case "Note": {
@ -154,33 +146,31 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
return errorResponse("Author not found", 400); return errorResponse("Author not found", 400);
} }
const relationship = await getRelationshipToOtherUser( const foundRelationship = await getRelationshipToOtherUser(
account, account,
user, user,
); );
// Check if already following // Check if already following
if (relationship.following) { if (foundRelationship.following) {
return response("Already following", 200); return response("Already following", 200);
} }
await client.relationship.update({ await db
where: { id: relationship.id }, .update(relationship)
data: { .set({
following: !user.isLocked, following: !user.isLocked,
requested: user.isLocked, requested: user.isLocked,
showingReblogs: true, showingReblogs: true,
notifying: true, notifying: true,
languages: [], languages: [],
}, })
}); .where(eq(relationship.id, foundRelationship.id));
await client.notification.create({ await db.insert(notification).values({
data: {
accountId: account.id, accountId: account.id,
type: user.isLocked ? "follow_request" : "follow", type: user.isLocked ? "follow_request" : "follow",
notifiedId: user.id, notifiedId: user.id,
},
}); });
if (!user.isLocked) { if (!user.isLocked) {
@ -203,24 +193,24 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
console.log(account); console.log(account);
const relationship = await getRelationshipToOtherUser( const foundRelationship = await getRelationshipToOtherUser(
user, user,
account, account,
); );
console.log(relationship); console.log(foundRelationship);
if (!relationship.requested) { if (!foundRelationship.requested) {
return response("There is no follow request to accept", 200); return response("There is no follow request to accept", 200);
} }
await client.relationship.update({ await db
where: { id: relationship.id }, .update(relationship)
data: { .set({
following: true, following: true,
requested: false, requested: false,
}, })
}); .where(eq(relationship.id, foundRelationship.id));
return response("Follow request accepted", 200); return response("Follow request accepted", 200);
} }
@ -233,22 +223,22 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
return errorResponse("Author not found", 400); return errorResponse("Author not found", 400);
} }
const relationship = await getRelationshipToOtherUser( const foundRelationship = await getRelationshipToOtherUser(
user, user,
account, account,
); );
if (!relationship.requested) { if (!foundRelationship.requested) {
return response("There is no follow request to reject", 200); return response("There is no follow request to reject", 200);
} }
await client.relationship.update({ await db
where: { id: relationship.id }, .update(relationship)
data: { .set({
requested: false, requested: false,
following: false, following: false,
}, })
}); .where(eq(relationship.id, foundRelationship.id));
return response("Follow request rejected", 200); return response("Follow request rejected", 200);
} }

View file

@ -1,8 +1,6 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { findFirstUser, userToLysand } from "~database/entities/User";
import { userToLysand } from "~database/entities/User";
import { userRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -19,11 +17,8 @@ export const meta = applyConfig({
export default apiRoute(async (req, matchedRoute) => { export default apiRoute(async (req, matchedRoute) => {
const uuid = matchedRoute.params.uuid; const uuid = matchedRoute.params.uuid;
const user = await client.user.findUnique({ const user = await findFirstUser({
where: { where: (user, { eq }) => eq(user.id, uuid),
id: uuid,
},
include: userRelations,
}); });
if (!user) { if (!user) {

View file

@ -1,8 +1,9 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { jsonResponse } from "@response"; import { jsonResponse } from "@response";
import { client } from "~database/datasource"; import { and, count, eq, inArray } from "drizzle-orm";
import { statusToLysand } from "~database/entities/Status"; import { findManyStatuses, statusToLysand } from "~database/entities/Status";
import { statusAndUserRelations } from "~database/entities/relations"; import { db } from "~drizzle/db";
import { status } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -16,35 +17,34 @@ export const meta = applyConfig({
route: "/users/:uuid/outbox", route: "/users/:uuid/outbox",
}); });
/**
* ActivityPub user outbox endpoint
*/
export default apiRoute(async (req, matchedRoute, extraData) => { export default apiRoute(async (req, matchedRoute, extraData) => {
const uuid = matchedRoute.params.uuid; const uuid = matchedRoute.params.uuid;
const pageNumber = Number(matchedRoute.query.page) || 1; const pageNumber = Number(matchedRoute.query.page) || 1;
const config = await extraData.configManager.getConfig(); const config = await extraData.configManager.getConfig();
const host = new URL(config.http.base_url).hostname; const host = new URL(config.http.base_url).hostname;
const statuses = await client.status.findMany({ const statuses = await findManyStatuses({
where: { where: (status, { eq, and, inArray }) =>
authorId: uuid, and(
visibility: { eq(status.authorId, uuid),
in: ["public", "unlisted"], inArray(status.visibility, ["public", "unlisted"]),
}, ),
}, offset: 20 * (pageNumber - 1),
take: 20, limit: 20,
skip: 20 * (pageNumber - 1), orderBy: (status, { desc }) => desc(status.createdAt),
include: statusAndUserRelations,
}); });
const totalStatuses = await client.status.count({ const totalStatuses = await db
where: { .select({
authorId: uuid, count: count(),
visibility: { })
in: ["public", "unlisted"], .from(status)
}, .where(
}, and(
}); eq(status.authorId, uuid),
inArray(status.visibility, ["public", "unlisted"]),
),
);
return jsonResponse({ return jsonResponse({
first: `${host}/users/${uuid}/outbox?page=1`, first: `${host}/users/${uuid}/outbox?page=1`,

View file

@ -1,6 +1,6 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { findFirstUser } from "~database/entities/User";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], 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, /[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({ const user = await findFirstUser({
where: { where: (user, { eq }) =>
id: isUuid ? requestedUser.split("@")[0] : undefined, eq(isUuid ? user.id : user.username, requestedUser.split("@")[0]),
username: isUuid ? undefined : requestedUser.split("@")[0],
},
}); });
if (!user) { if (!user) {