feat: add actual user for trpc wss-dev-server (#261)
* feat: add actual user for trpc wss-dev-server #233 * chore: address pull request feedback * fix: deepsource issue
This commit is contained in:
@@ -29,10 +29,10 @@ describe("AppController", () => {
|
|||||||
.mockReturnValueOnce(Promise.resolve("ABC"));
|
.mockReturnValueOnce(Promise.resolve("ABC"));
|
||||||
|
|
||||||
// act
|
// act
|
||||||
const a = await appController.getHello();
|
const app = await appController.getHello();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(a).toBe("ABC");
|
expect(app).toBe("ABC");
|
||||||
expect(appService.getHello).toHaveBeenCalledTimes(1);
|
expect(appService.getHello).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,14 +12,16 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm tsx ./src/wssDevServer.ts",
|
"dev": "pnpm with-env tsx ./src/wssDevServer.ts",
|
||||||
"clean": "rm -rf .turbo node_modules",
|
"clean": "rm -rf .turbo node_modules",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
|
"with-env": "dotenv -e ../../.env --"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/auth": "workspace:^0.1.0",
|
"@homarr/auth": "workspace:^0.1.0",
|
||||||
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^",
|
"@homarr/log": "workspace:^",
|
||||||
|
|||||||
@@ -155,9 +155,9 @@ export const boardRouter = createTRPCRouter({
|
|||||||
save: publicProcedure
|
save: publicProcedure
|
||||||
.input(validation.board.save)
|
.input(validation.board.save)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await ctx.db.transaction(async (tx) => {
|
await ctx.db.transaction(async (transaction) => {
|
||||||
const dbBoard = await getFullBoardWithWhere(
|
const dbBoard = await getFullBoardWithWhere(
|
||||||
tx,
|
transaction,
|
||||||
eq(boards.id, input.id),
|
eq(boards.id, input.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (addedSections.length > 0) {
|
if (addedSections.length > 0) {
|
||||||
await tx.insert(sections).values(
|
await transaction.insert(sections).values(
|
||||||
addedSections.map((section) => ({
|
addedSections.map((section) => ({
|
||||||
id: section.id,
|
id: section.id,
|
||||||
kind: section.kind,
|
kind: section.kind,
|
||||||
@@ -188,7 +188,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
const addedItems = filterAddedItems(inputItems, dbItems);
|
const addedItems = filterAddedItems(inputItems, dbItems);
|
||||||
|
|
||||||
if (addedItems.length > 0) {
|
if (addedItems.length > 0) {
|
||||||
await tx.insert(items).values(
|
await transaction.insert(items).values(
|
||||||
addedItems.map((item) => ({
|
addedItems.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
kind: item.kind,
|
kind: item.kind,
|
||||||
@@ -226,7 +226,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (addedIntegrationRelations.length > 0) {
|
if (addedIntegrationRelations.length > 0) {
|
||||||
await tx.insert(integrationItems).values(
|
await transaction.insert(integrationItems).values(
|
||||||
addedIntegrationRelations.map((relation) => ({
|
addedIntegrationRelations.map((relation) => ({
|
||||||
itemId: relation.itemId,
|
itemId: relation.itemId,
|
||||||
integrationId: relation.integrationId,
|
integrationId: relation.integrationId,
|
||||||
@@ -237,7 +237,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
const updatedItems = filterUpdatedItems(inputItems, dbItems);
|
const updatedItems = filterUpdatedItems(inputItems, dbItems);
|
||||||
|
|
||||||
for (const item of updatedItems) {
|
for (const item of updatedItems) {
|
||||||
await tx
|
await transaction
|
||||||
.update(items)
|
.update(items)
|
||||||
.set({
|
.set({
|
||||||
kind: item.kind,
|
kind: item.kind,
|
||||||
@@ -260,7 +260,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
const prev = dbBoard.sections.find(
|
const prev = dbBoard.sections.find(
|
||||||
(dbSection) => dbSection.id === section.id,
|
(dbSection) => dbSection.id === section.id,
|
||||||
);
|
);
|
||||||
await tx
|
await transaction
|
||||||
.update(sections)
|
.update(sections)
|
||||||
.set({
|
.set({
|
||||||
position: section.position,
|
position: section.position,
|
||||||
@@ -282,7 +282,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const relation of removedIntegrationRelations) {
|
for (const relation of removedIntegrationRelations) {
|
||||||
await tx
|
await transaction
|
||||||
.delete(integrationItems)
|
.delete(integrationItems)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
@@ -296,7 +296,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const itemIds = removedItems.map((item) => item.id);
|
const itemIds = removedItems.map((item) => item.id);
|
||||||
if (itemIds.length > 0) {
|
if (itemIds.length > 0) {
|
||||||
await tx.delete(items).where(inArray(items.id, itemIds));
|
await transaction.delete(items).where(inArray(items.id, itemIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
const removedSections = filterRemovedItems(
|
const removedSections = filterRemovedItems(
|
||||||
@@ -306,7 +306,9 @@ export const boardRouter = createTRPCRouter({
|
|||||||
const sectionIds = removedSections.map((section) => section.id);
|
const sectionIds = removedSections.map((section) => section.id);
|
||||||
|
|
||||||
if (sectionIds.length > 0) {
|
if (sectionIds.length > 0) {
|
||||||
await tx.delete(sections).where(inArray(sections.id, sectionIds));
|
await transaction
|
||||||
|
.delete(sections)
|
||||||
|
.where(inArray(sections.id, sectionIds));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
@@ -340,14 +342,14 @@ export const boardRouter = createTRPCRouter({
|
|||||||
savePermissions: publicProcedure
|
savePermissions: publicProcedure
|
||||||
.input(validation.board.savePermissions)
|
.input(validation.board.savePermissions)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await ctx.db.transaction(async (tx) => {
|
await ctx.db.transaction(async (transaction) => {
|
||||||
await tx
|
await transaction
|
||||||
.delete(boardPermissions)
|
.delete(boardPermissions)
|
||||||
.where(eq(boardPermissions.boardId, input.id));
|
.where(eq(boardPermissions.boardId, input.id));
|
||||||
if (input.permissions.length === 0) {
|
if (input.permissions.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await tx.insert(boardPermissions).values(
|
await transaction.insert(boardPermissions).values(
|
||||||
input.permissions.map((permission) => ({
|
input.permissions.map((permission) => ({
|
||||||
userId: permission.user.id,
|
userId: permission.user.id,
|
||||||
permission: permission.permission,
|
permission: permission.permission,
|
||||||
|
|||||||
@@ -245,19 +245,27 @@ const key = Buffer.from(
|
|||||||
|
|
||||||
//Encrypting text
|
//Encrypting text
|
||||||
export function encryptSecret(text: string): `${string}.${string}` {
|
export function encryptSecret(text: string): `${string}.${string}` {
|
||||||
const iv = crypto.randomBytes(16);
|
const initializationVector = crypto.randomBytes(16);
|
||||||
const cipher = crypto.createCipheriv(algorithm, Buffer.from(key), iv);
|
const cipher = crypto.createCipheriv(
|
||||||
|
algorithm,
|
||||||
|
Buffer.from(key),
|
||||||
|
initializationVector,
|
||||||
|
);
|
||||||
let encrypted = cipher.update(text);
|
let encrypted = cipher.update(text);
|
||||||
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||||
return `${encrypted.toString("hex")}.${iv.toString("hex")}`;
|
return `${encrypted.toString("hex")}.${initializationVector.toString("hex")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypting text
|
// Decrypting text
|
||||||
function decryptSecret(value: `${string}.${string}`) {
|
function decryptSecret(value: `${string}.${string}`) {
|
||||||
const [data, dataIv] = value.split(".") as [string, string];
|
const [data, dataIv] = value.split(".") as [string, string];
|
||||||
const iv = Buffer.from(dataIv, "hex");
|
const initializationVector = Buffer.from(dataIv, "hex");
|
||||||
const encryptedText = Buffer.from(data, "hex");
|
const encryptedText = Buffer.from(data, "hex");
|
||||||
const decipher = crypto.createDecipheriv(algorithm, Buffer.from(key), iv);
|
const decipher = crypto.createDecipheriv(
|
||||||
|
algorithm,
|
||||||
|
Buffer.from(key),
|
||||||
|
initializationVector,
|
||||||
|
);
|
||||||
let decrypted = decipher.update(encryptedText);
|
let decrypted = decipher.update(encryptedText);
|
||||||
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
||||||
return decrypted.toString();
|
return decrypted.toString();
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { initTRPC, TRPCError } from "@trpc/server";
|
|||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
import type { Session } from "@homarr/auth";
|
import type { Session } from "@homarr/auth";
|
||||||
import { auth } from "@homarr/auth";
|
|
||||||
import { db } from "@homarr/db";
|
import { db } from "@homarr/db";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
import { ZodError } from "@homarr/validation";
|
import { ZodError } from "@homarr/validation";
|
||||||
@@ -27,11 +26,11 @@ import { ZodError } from "@homarr/validation";
|
|||||||
*
|
*
|
||||||
* @see https://trpc.io/docs/server/context
|
* @see https://trpc.io/docs/server/context
|
||||||
*/
|
*/
|
||||||
export const createTRPCContext = async (opts: {
|
export const createTRPCContext = (opts: {
|
||||||
headers: Headers;
|
headers: Headers;
|
||||||
session: Session | null;
|
session: Session | null;
|
||||||
}) => {
|
}) => {
|
||||||
const session = opts.session ?? (await auth());
|
const session = opts.session;
|
||||||
const source = opts.headers.get("x-trpc-source") ?? "unknown";
|
const source = opts.headers.get("x-trpc-source") ?? "unknown";
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { applyWSSHandler } from "@trpc/server/adapters/ws";
|
import { applyWSSHandler } from "@trpc/server/adapters/ws";
|
||||||
import { WebSocketServer } from "ws";
|
import { WebSocketServer } from "ws";
|
||||||
|
|
||||||
|
import { getSessionFromToken, sessionTokenCookieName } from "@homarr/auth";
|
||||||
|
import { parseCookies } from "@homarr/common";
|
||||||
|
import { db } from "@homarr/db";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
import { appRouter } from "./root";
|
import { appRouter } from "./root";
|
||||||
@@ -12,33 +15,41 @@ const wss = new WebSocketServer({
|
|||||||
const handler = applyWSSHandler({
|
const handler = applyWSSHandler({
|
||||||
wss,
|
wss,
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
createContext: ({ req }) => {
|
createContext: async ({ req }) => {
|
||||||
return createTRPCContext({
|
try {
|
||||||
headers: {
|
const headers = Object.entries(req.headers).map(
|
||||||
...req.headers,
|
([key, value]) =>
|
||||||
get(key: string) {
|
[key, typeof value === "string" ? value : value?.[0]] as [
|
||||||
const item = req.headers[key];
|
string,
|
||||||
return typeof item === "string" ? item ?? null : item?.at(0) ?? null;
|
string,
|
||||||
},
|
],
|
||||||
} as Headers,
|
);
|
||||||
session: {
|
const nextHeaders = new Headers(headers);
|
||||||
// TODO: replace with actual session
|
|
||||||
user: {
|
const store = parseCookies(nextHeaders.get("cookie") ?? "");
|
||||||
id: "1",
|
const sessionToken = store[sessionTokenCookieName];
|
||||||
name: "Test User",
|
|
||||||
email: "",
|
const session = await getSessionFromToken(db, sessionToken);
|
||||||
},
|
|
||||||
expires: new Date().toISOString(),
|
return createTRPCContext({
|
||||||
},
|
headers: nextHeaders,
|
||||||
});
|
session,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return createTRPCContext({
|
||||||
|
headers: new Headers(),
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
wss.on("connection", (ws, incomingMessage) => {
|
wss.on("connection", (websocket, incomingMessage) => {
|
||||||
logger.info(
|
logger.info(
|
||||||
`➕ Connection (${wss.clients.size}) ${incomingMessage.method} ${incomingMessage.url}`,
|
`➕ Connection (${wss.clients.size}) ${incomingMessage.method} ${incomingMessage.url}`,
|
||||||
);
|
);
|
||||||
ws.once("close", (code, reason) => {
|
websocket.once("close", (code, reason) => {
|
||||||
logger.info(
|
logger.info(
|
||||||
`➖ Connection (${wss.clients.size}) ${code} ${reason.toString()}`,
|
`➖ Connection (${wss.clients.size}) ${code} ${reason.toString()}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ export * from "./security";
|
|||||||
export const createHandlers = (isCredentialsRequest: boolean) =>
|
export const createHandlers = (isCredentialsRequest: boolean) =>
|
||||||
createConfiguration(isCredentialsRequest);
|
createConfiguration(isCredentialsRequest);
|
||||||
export const { auth } = createConfiguration(false);
|
export const { auth } = createConfiguration(false);
|
||||||
|
export { getSessionFromToken, sessionTokenCookieName } from "./session";
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
|
import type { Session } from "next-auth";
|
||||||
|
|
||||||
|
import type { Database } from "@homarr/db";
|
||||||
|
|
||||||
export const sessionMaxAgeInSeconds = 30 * 24 * 60 * 60; // 30 days
|
export const sessionMaxAgeInSeconds = 30 * 24 * 60 * 60; // 30 days
|
||||||
export const sessionTokenCookieName = "next-auth.session-token";
|
export const sessionTokenCookieName = "next-auth.session-token";
|
||||||
@@ -10,3 +13,38 @@ export const expireDateAfter = (seconds: number) => {
|
|||||||
export const generateSessionToken = () => {
|
export const generateSessionToken = () => {
|
||||||
return randomUUID();
|
return randomUUID();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getSessionFromToken = async (
|
||||||
|
db: Database,
|
||||||
|
token: string | undefined,
|
||||||
|
): Promise<Session | null> => {
|
||||||
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await db.query.sessions.findFirst({
|
||||||
|
where: ({ sessionToken }, { eq }) => eq(sessionToken, token),
|
||||||
|
columns: {
|
||||||
|
expires: true,
|
||||||
|
},
|
||||||
|
with: {
|
||||||
|
user: {
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
image: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: session.user,
|
||||||
|
expires: session.expires.toISOString(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
17
packages/common/src/cookie.ts
Normal file
17
packages/common/src/cookie.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export function parseCookies(cookieString: string) {
|
||||||
|
const list: Record<string, string> = {};
|
||||||
|
const cookieHeader = cookieString;
|
||||||
|
if (!cookieHeader) return list;
|
||||||
|
|
||||||
|
cookieHeader.split(";").forEach(function (cookie) {
|
||||||
|
const items = cookie.split("=");
|
||||||
|
let name = items.shift();
|
||||||
|
name = name?.trim();
|
||||||
|
if (!name) return;
|
||||||
|
const value = items.join("=").trim();
|
||||||
|
if (!value) return;
|
||||||
|
list[name] = decodeURIComponent(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from "./object";
|
export * from "./object";
|
||||||
export * from "./string";
|
export * from "./string";
|
||||||
|
export * from "./cookie";
|
||||||
|
|||||||
@@ -85,8 +85,10 @@ export const verificationTokens = mysqlTable(
|
|||||||
token: varchar("token", { length: 512 }).notNull(),
|
token: varchar("token", { length: 512 }).notNull(),
|
||||||
expires: timestamp("expires").notNull(),
|
expires: timestamp("expires").notNull(),
|
||||||
},
|
},
|
||||||
(vt) => ({
|
(verificationToken) => ({
|
||||||
compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
|
compoundKey: primaryKey({
|
||||||
|
columns: [verificationToken.identifier, verificationToken.token],
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -115,12 +117,14 @@ export const integrationSecrets = mysqlTable(
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => integrations.id, { onDelete: "cascade" }),
|
.references(() => integrations.id, { onDelete: "cascade" }),
|
||||||
},
|
},
|
||||||
(is) => ({
|
(integrationSecret) => ({
|
||||||
compoundKey: primaryKey({
|
compoundKey: primaryKey({
|
||||||
columns: [is.integrationId, is.kind],
|
columns: [integrationSecret.integrationId, integrationSecret.kind],
|
||||||
}),
|
}),
|
||||||
kindIdx: index("integration_secret__kind_idx").on(is.kind),
|
kindIdx: index("integration_secret__kind_idx").on(integrationSecret.kind),
|
||||||
updatedAtIdx: index("integration_secret__updated_at_idx").on(is.updatedAt),
|
updatedAtIdx: index("integration_secret__updated_at_idx").on(
|
||||||
|
integrationSecret.updatedAt,
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -230,11 +234,17 @@ export const accountRelations = relations(accounts, ({ one }) => ({
|
|||||||
|
|
||||||
export const userRelations = relations(users, ({ many }) => ({
|
export const userRelations = relations(users, ({ many }) => ({
|
||||||
accounts: many(accounts),
|
accounts: many(accounts),
|
||||||
|
|
||||||
boards: many(boards),
|
boards: many(boards),
|
||||||
boardPermissions: many(boardPermissions),
|
boardPermissions: many(boardPermissions),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const sessionRelations = relations(sessions, ({ one }) => ({
|
||||||
|
user: one(users, {
|
||||||
|
fields: [sessions.userId],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
export const boardPermissionRelations = relations(
|
export const boardPermissionRelations = relations(
|
||||||
boardPermissions,
|
boardPermissions,
|
||||||
({ one }) => ({
|
({ one }) => ({
|
||||||
|
|||||||
@@ -82,8 +82,10 @@ export const verificationTokens = sqliteTable(
|
|||||||
token: text("token").notNull(),
|
token: text("token").notNull(),
|
||||||
expires: integer("expires", { mode: "timestamp_ms" }).notNull(),
|
expires: integer("expires", { mode: "timestamp_ms" }).notNull(),
|
||||||
},
|
},
|
||||||
(vt) => ({
|
(verificationToken) => ({
|
||||||
compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
|
compoundKey: primaryKey({
|
||||||
|
columns: [verificationToken.identifier, verificationToken.token],
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -110,12 +112,14 @@ export const integrationSecrets = sqliteTable(
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => integrations.id, { onDelete: "cascade" }),
|
.references(() => integrations.id, { onDelete: "cascade" }),
|
||||||
},
|
},
|
||||||
(is) => ({
|
(integrationSecret) => ({
|
||||||
compoundKey: primaryKey({
|
compoundKey: primaryKey({
|
||||||
columns: [is.integrationId, is.kind],
|
columns: [integrationSecret.integrationId, integrationSecret.kind],
|
||||||
}),
|
}),
|
||||||
kindIdx: index("integration_secret__kind_idx").on(is.kind),
|
kindIdx: index("integration_secret__kind_idx").on(integrationSecret.kind),
|
||||||
updatedAtIdx: index("integration_secret__updated_at_idx").on(is.updatedAt),
|
updatedAtIdx: index("integration_secret__updated_at_idx").on(
|
||||||
|
integrationSecret.updatedAt,
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -229,6 +233,13 @@ export const userRelations = relations(users, ({ many }) => ({
|
|||||||
boardPermissions: many(boardPermissions),
|
boardPermissions: many(boardPermissions),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const sessionRelations = relations(sessions, ({ one }) => ({
|
||||||
|
user: one(users, {
|
||||||
|
fields: [sessions.userId],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
export const boardPermissionRelations = relations(
|
export const boardPermissionRelations = relations(
|
||||||
boardPermissions,
|
boardPermissions,
|
||||||
({ one }) => ({
|
({ one }) => ({
|
||||||
|
|||||||
@@ -9,26 +9,28 @@ import {
|
|||||||
} from "./spotlight-store";
|
} from "./spotlight-store";
|
||||||
import type { SpotlightActionGroup } from "./type";
|
import type { SpotlightActionGroup } from "./type";
|
||||||
|
|
||||||
const disableArrowUpAndDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const disableArrowUpAndDown = (
|
||||||
if (e.key === "ArrowDown") {
|
event: React.KeyboardEvent<HTMLInputElement>,
|
||||||
|
) => {
|
||||||
|
if (event.key === "ArrowDown") {
|
||||||
selectNextAction(spotlightStore);
|
selectNextAction(spotlightStore);
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
} else if (e.key === "ArrowUp") {
|
} else if (event.key === "ArrowUp") {
|
||||||
selectPreviousAction(spotlightStore);
|
selectPreviousAction(spotlightStore);
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
} else if (e.key === "Enter") {
|
} else if (event.key === "Enter") {
|
||||||
triggerSelectedAction(spotlightStore);
|
triggerSelectedAction(spotlightStore);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const focusActiveByDefault = (e: React.FocusEvent<HTMLInputElement>) => {
|
const focusActiveByDefault = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||||
const relatedTarget = e.relatedTarget;
|
const relatedTarget = event.relatedTarget;
|
||||||
|
|
||||||
const isPreviousTargetRadio =
|
const isPreviousTargetRadio =
|
||||||
relatedTarget && "type" in relatedTarget && relatedTarget.type === "radio";
|
relatedTarget && "type" in relatedTarget && relatedTarget.type === "radio";
|
||||||
if (isPreviousTargetRadio) return;
|
if (isPreviousTargetRadio) return;
|
||||||
|
|
||||||
const group = e.currentTarget.parentElement?.parentElement;
|
const group = event.currentTarget.parentElement?.parentElement;
|
||||||
if (!group) return;
|
if (!group) return;
|
||||||
const label = group.querySelector<HTMLLabelElement>("label[data-checked]");
|
const label = group.querySelector<HTMLLabelElement>("label[data-checked]");
|
||||||
if (!label) return;
|
if (!label) return;
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -307,6 +307,9 @@ importers:
|
|||||||
'@homarr/auth':
|
'@homarr/auth':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../auth
|
version: link:../auth
|
||||||
|
'@homarr/common':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../common
|
||||||
'@homarr/db':
|
'@homarr/db':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../db
|
version: link:../db
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const config = {
|
|||||||
"warn",
|
"warn",
|
||||||
{
|
{
|
||||||
min: 3,
|
min: 3,
|
||||||
exceptions: ["_", "i", "z", "t", "id"], // _ for unused variables, i for index, z for zod, t for translation
|
exceptions: ["_", "i", "z", "t", "id", "db"], // _ for unused variables, i for index, z for zod, t for translation
|
||||||
properties: "never", // This allows for example the use of <Grid.Col span={{ sm: 12, md: 6 }}> as sm and md would be too short
|
properties: "never", // This allows for example the use of <Grid.Col span={{ sm: 12, md: 6 }}> as sm and md would be too short
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user