test: increase coverage for existing controllers
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
This commit is contained in:
parent
5ef2026b61
commit
1db50e41f9
5 changed files with 336 additions and 2 deletions
|
|
@ -0,0 +1,197 @@
|
||||||
|
import { beforeEach, describe, expect, test } from "bun:test";
|
||||||
|
import { createApp } from "~/server/app";
|
||||||
|
import {
|
||||||
|
createTestSession,
|
||||||
|
createTestSessionWithGlobalAdmin,
|
||||||
|
createTestSessionWithOrgAdmin,
|
||||||
|
createTestSessionWithRegularMember,
|
||||||
|
getAuthHeaders,
|
||||||
|
} from "~/test/helpers/auth";
|
||||||
|
import { account, invitation, member, organization, ssoProvider, usersTable } from "~/server/db/schema";
|
||||||
|
import { db } from "~/server/db/db";
|
||||||
|
|
||||||
|
const app = createApp();
|
||||||
|
|
||||||
|
describe("auth controller security", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await db.delete(member);
|
||||||
|
await db.delete(account);
|
||||||
|
await db.delete(invitation);
|
||||||
|
await db.delete(ssoProvider);
|
||||||
|
await db.delete(organization);
|
||||||
|
await db.delete(usersTable);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("public endpoints - no auth required", () => {
|
||||||
|
test("GET /api/v1/auth/status should be accessible without authentication", async () => {
|
||||||
|
const res = await app.request("/api/v1/auth/status");
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("GET /api/v1/auth/sso-providers should be accessible without authentication", async () => {
|
||||||
|
const res = await app.request("/api/v1/auth/sso-providers");
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("GET /api/v1/auth/login-error should be accessible without authentication", async () => {
|
||||||
|
const res = await app.request("/api/v1/auth/login-error?error=test");
|
||||||
|
expect(res.status).toBe(302);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("org admin endpoints - require requireAuth + requireOrgAdmin", () => {
|
||||||
|
const orgAdminEndpoints = [
|
||||||
|
{ method: "GET", path: "/api/v1/auth/sso-settings" },
|
||||||
|
{ method: "DELETE", path: "/api/v1/auth/sso-providers/test-provider" },
|
||||||
|
{ method: "PATCH", path: "/api/v1/auth/sso-providers/test-provider/auto-linking" },
|
||||||
|
{ method: "DELETE", path: "/api/v1/auth/sso-invitations/test-invitation" },
|
||||||
|
{ method: "GET", path: "/api/v1/auth/org-members" },
|
||||||
|
{ method: "PATCH", path: "/api/v1/auth/org-members/test-member/role" },
|
||||||
|
{ method: "DELETE", path: "/api/v1/auth/org-members/test-member" },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { method, path } of orgAdminEndpoints) {
|
||||||
|
test(`${method} ${path} should return 401 when unauthenticated`, async () => {
|
||||||
|
const res = await app.request(path, { method });
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`${method} ${path} should return 403 for regular members`, async () => {
|
||||||
|
const { headers } = await createTestSessionWithRegularMember();
|
||||||
|
const res = await app.request(path, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: method !== "GET" && method !== "DELETE" ? JSON.stringify({}) : undefined,
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(403);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.message).toBe("Forbidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`${method} ${path} should be accessible to org admins`, async () => {
|
||||||
|
const { headers } = await createTestSessionWithOrgAdmin();
|
||||||
|
const res = await app.request(path, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: method !== "GET" && method !== "DELETE" ? JSON.stringify({}) : undefined,
|
||||||
|
});
|
||||||
|
// Should not be 401 or 403 - actual response depends on endpoint logic
|
||||||
|
expect(res.status).not.toBe(401);
|
||||||
|
expect(res.status).not.toBe(403);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("PATCH /api/v1/auth/sso-providers/:providerId/auto-linking specific", () => {
|
||||||
|
test("should return 400 for invalid payload", async () => {
|
||||||
|
const { headers } = await createTestSessionWithOrgAdmin();
|
||||||
|
const res = await app.request("/api/v1/auth/sso-providers/test-provider/auto-linking", {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("PATCH /api/v1/auth/org-members/:memberId/role specific", () => {
|
||||||
|
test("should return 400 for invalid payload", async () => {
|
||||||
|
const { headers } = await createTestSessionWithOrgAdmin();
|
||||||
|
const res = await app.request("/api/v1/auth/org-members/test-member/role", {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("global admin endpoints - require requireAuth + requireAdmin", () => {
|
||||||
|
const adminEndpoints = [
|
||||||
|
{ method: "GET", path: "/api/v1/auth/admin-users" },
|
||||||
|
{ method: "DELETE", path: "/api/v1/auth/admin-users/test-user/accounts/test-account" },
|
||||||
|
{ method: "GET", path: "/api/v1/auth/deletion-impact/test-user" },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { method, path } of adminEndpoints) {
|
||||||
|
test(`${method} ${path} should return 401 when unauthenticated`, async () => {
|
||||||
|
const res = await app.request(path, { method });
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`${method} ${path} should return 403 for regular users`, async () => {
|
||||||
|
const { headers } = await createTestSession();
|
||||||
|
const res = await app.request(path, { method, headers });
|
||||||
|
expect(res.status).toBe(403);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.message).toBe("Forbidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`${method} ${path} should return 403 for org admins`, async () => {
|
||||||
|
const { headers } = await createTestSessionWithOrgAdmin();
|
||||||
|
const res = await app.request(path, { method, headers });
|
||||||
|
expect(res.status).toBe(403);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.message).toBe("Forbidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`${method} ${path} should not return 401 for global admins`, async () => {
|
||||||
|
const { headers } = await createTestSessionWithGlobalAdmin();
|
||||||
|
const res = await app.request(path, { method, headers });
|
||||||
|
// Should not be 401 - actual response depends on endpoint logic
|
||||||
|
expect(res.status).not.toBe(401);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("invalid session handling", () => {
|
||||||
|
test("should return 401 for invalid session cookie", async () => {
|
||||||
|
const res = await app.request("/api/v1/auth/sso-settings", {
|
||||||
|
headers: getAuthHeaders("invalid-session-token"),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return 401 when session cookie is missing", async () => {
|
||||||
|
const res = await app.request("/api/v1/auth/admin-users");
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("information disclosure", () => {
|
||||||
|
test("should not disclose org members when unauthenticated", async () => {
|
||||||
|
const res = await app.request("/api/v1/auth/org-members");
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should not disclose SSO settings when unauthenticated", async () => {
|
||||||
|
const res = await app.request("/api/v1/auth/sso-settings");
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should not disclose admin users when unauthenticated", async () => {
|
||||||
|
const res = await app.request("/api/v1/auth/admin-users");
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -51,6 +51,7 @@ describe("backups security", () => {
|
||||||
{ method: "PUT", path: "/api/v1/backups/1/mirrors" },
|
{ method: "PUT", path: "/api/v1/backups/1/mirrors" },
|
||||||
{ method: "GET", path: "/api/v1/backups/1/mirrors/compatibility" },
|
{ method: "GET", path: "/api/v1/backups/1/mirrors/compatibility" },
|
||||||
{ method: "POST", path: "/api/v1/backups/reorder" },
|
{ method: "POST", path: "/api/v1/backups/reorder" },
|
||||||
|
{ method: "GET", path: "/api/v1/backups/1/progress" },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const { method, path } of endpoints) {
|
for (const { method, path } of endpoints) {
|
||||||
|
|
|
||||||
|
|
@ -75,13 +75,17 @@ describe("repositories security", () => {
|
||||||
{ method: "GET", path: "/api/v1/repositories/test-repo/stats" },
|
{ method: "GET", path: "/api/v1/repositories/test-repo/stats" },
|
||||||
{ method: "DELETE", path: "/api/v1/repositories/test-repo" },
|
{ method: "DELETE", path: "/api/v1/repositories/test-repo" },
|
||||||
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots" },
|
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots" },
|
||||||
|
{ method: "POST", path: "/api/v1/repositories/test-repo/snapshots/refresh" },
|
||||||
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot" },
|
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot" },
|
||||||
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot/files" },
|
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot/files" },
|
||||||
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot/dump" },
|
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot/dump" },
|
||||||
{ method: "POST", path: "/api/v1/repositories/test-repo/restore" },
|
{ method: "POST", path: "/api/v1/repositories/test-repo/restore" },
|
||||||
{ method: "POST", path: "/api/v1/repositories/test-repo/doctor" },
|
{ method: "POST", path: "/api/v1/repositories/test-repo/doctor" },
|
||||||
|
{ method: "DELETE", path: "/api/v1/repositories/test-repo/doctor" },
|
||||||
|
{ method: "POST", path: "/api/v1/repositories/test-repo/unlock" },
|
||||||
{ method: "DELETE", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot" },
|
{ method: "DELETE", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot" },
|
||||||
{ method: "DELETE", path: "/api/v1/repositories/test-repo/snapshots" },
|
{ method: "DELETE", path: "/api/v1/repositories/test-repo/snapshots" },
|
||||||
|
{ method: "POST", path: "/api/v1/repositories/test-repo/snapshots/tag" },
|
||||||
{ method: "PATCH", path: "/api/v1/repositories/test-repo" },
|
{ method: "PATCH", path: "/api/v1/repositories/test-repo" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -95,6 +99,21 @@ describe("repositories security", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("dev panel endpoint - requires dev panel access", () => {
|
||||||
|
test("POST /api/v1/repositories/:shortId/exec should return 401 when unauthenticated", async () => {
|
||||||
|
const res = await app.request("/api/v1/repositories/test-repo/exec", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ command: "version" }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("information disclosure", () => {
|
describe("information disclosure", () => {
|
||||||
test("should not disclose if a repository exists when unauthenticated", async () => {
|
test("should not disclose if a repository exists when unauthenticated", async () => {
|
||||||
const res = await app.request("/api/v1/repositories/non-existent-repo");
|
const res = await app.request("/api/v1/repositories/non-existent-repo");
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { test, describe, expect } from "bun:test";
|
import { test, describe, expect } from "bun:test";
|
||||||
import { createApp } from "~/server/app";
|
import { createApp } from "~/server/app";
|
||||||
import { createTestSession, getAuthHeaders } from "~/test/helpers/auth";
|
import { createTestSession, createTestSessionWithGlobalAdmin, getAuthHeaders } from "~/test/helpers/auth";
|
||||||
|
|
||||||
const app = createApp();
|
const app = createApp();
|
||||||
|
|
||||||
|
|
@ -34,7 +34,11 @@ describe("system security", () => {
|
||||||
describe("unauthenticated access", () => {
|
describe("unauthenticated access", () => {
|
||||||
const endpoints: { method: string; path: string }[] = [
|
const endpoints: { method: string; path: string }[] = [
|
||||||
{ method: "GET", path: "/api/v1/system/info" },
|
{ method: "GET", path: "/api/v1/system/info" },
|
||||||
|
{ method: "GET", path: "/api/v1/system/updates" },
|
||||||
|
{ method: "GET", path: "/api/v1/system/registration-status" },
|
||||||
|
{ method: "PUT", path: "/api/v1/system/registration-status" },
|
||||||
{ method: "POST", path: "/api/v1/system/restic-password" },
|
{ method: "POST", path: "/api/v1/system/restic-password" },
|
||||||
|
{ method: "GET", path: "/api/v1/system/dev-panel" },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const { method, path } of endpoints) {
|
for (const { method, path } of endpoints) {
|
||||||
|
|
@ -47,6 +51,62 @@ describe("system security", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("registration-status endpoint", () => {
|
||||||
|
test("GET /api/v1/system/registration-status should be accessible with valid session", async () => {
|
||||||
|
const { headers } = await createTestSession();
|
||||||
|
const res = await app.request("/api/v1/system/registration-status", { headers });
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(typeof body.enabled).toBe("boolean");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("PUT /api/v1/system/registration-status should return 403 for non-admin users", async () => {
|
||||||
|
const { headers } = await createTestSession();
|
||||||
|
const res = await app.request("/api/v1/system/registration-status", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ enabled: false }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(403);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.message).toBe("Forbidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("PUT /api/v1/system/registration-status should be accessible to global admin", async () => {
|
||||||
|
const { headers } = await createTestSessionWithGlobalAdmin();
|
||||||
|
const res = await app.request("/api/v1/system/registration-status", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ enabled: false }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("dev-panel endpoint", () => {
|
||||||
|
test("GET /api/v1/system/dev-panel should be accessible with valid session", async () => {
|
||||||
|
const { headers } = await createTestSession();
|
||||||
|
const res = await app.request("/api/v1/system/dev-panel", { headers });
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(typeof body.enabled).toBe("boolean");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updates endpoint", () => {
|
||||||
|
test("GET /api/v1/system/updates should be accessible with valid session", async () => {
|
||||||
|
const { headers } = await createTestSession();
|
||||||
|
const res = await app.request("/api/v1/system/updates", { headers });
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("input validation", () => {
|
describe("input validation", () => {
|
||||||
test("should return 400 for invalid payload on restic-password", async () => {
|
test("should return 400 for invalid payload on restic-password", async () => {
|
||||||
const { headers } = await createTestSession();
|
const { headers } = await createTestSession();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
import { auth } from "~/server/lib/auth";
|
import { auth } from "~/server/lib/auth";
|
||||||
|
import { db } from "~/server/db/db";
|
||||||
|
import { member, organization, sessionsTable, usersTable } from "~/server/db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
export const COOKIE_PREFIX = "zerobyte";
|
export const COOKIE_PREFIX = "zerobyte";
|
||||||
|
|
||||||
|
|
@ -12,6 +15,12 @@ export async function createTestSession() {
|
||||||
const ctx = await auth.$context;
|
const ctx = await auth.$context;
|
||||||
const user = ctx.test.createUser();
|
const user = ctx.test.createUser();
|
||||||
await ctx.test.saveUser(user);
|
await ctx.test.saveUser(user);
|
||||||
|
|
||||||
|
const allUsers = await db.query.usersTable.findMany();
|
||||||
|
if (allUsers.length === 1 && allUsers[0].role === "admin") {
|
||||||
|
await db.update(usersTable).set({ role: "user" }).where(eq(usersTable.id, user.id));
|
||||||
|
}
|
||||||
|
|
||||||
const { headers, session } = await ctx.test.login({ userId: user.id });
|
const { headers, session } = await ctx.test.login({ userId: user.id });
|
||||||
|
|
||||||
const organizationId = (session as { activeOrganizationId?: string }).activeOrganizationId ?? "";
|
const organizationId = (session as { activeOrganizationId?: string }).activeOrganizationId ?? "";
|
||||||
|
|
@ -19,7 +28,55 @@ export async function createTestSession() {
|
||||||
return {
|
return {
|
||||||
headers: Object.fromEntries(headers.entries()) as Record<string, string>,
|
headers: Object.fromEntries(headers.entries()) as Record<string, string>,
|
||||||
session,
|
session,
|
||||||
user,
|
user: { ...user, role: "user" },
|
||||||
organizationId,
|
organizationId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createTestSessionWithOrgAdmin() {
|
||||||
|
const { headers, user, organizationId } = await createTestSession();
|
||||||
|
|
||||||
|
await db.update(member).set({ role: "admin" }).where(eq(member.userId, user.id));
|
||||||
|
|
||||||
|
return { headers, user, organizationId };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTestSessionWithGlobalAdmin() {
|
||||||
|
const ctx = await auth.$context;
|
||||||
|
const user = ctx.test.createUser();
|
||||||
|
await ctx.test.saveUser(user);
|
||||||
|
|
||||||
|
await db.update(usersTable).set({ role: "admin" }).where(eq(usersTable.id, user.id));
|
||||||
|
|
||||||
|
const [org] = await db
|
||||||
|
.insert(organization)
|
||||||
|
.values({ id: crypto.randomUUID(), name: "Admin Org", slug: `admin-org-${Date.now()}`, createdAt: new Date() })
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
await db.insert(member).values({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
organizationId: org.id,
|
||||||
|
userId: user.id,
|
||||||
|
role: "owner",
|
||||||
|
createdAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.update(sessionsTable).set({ activeOrganizationId: org.id }).where(eq(sessionsTable.userId, user.id));
|
||||||
|
|
||||||
|
const { headers, session } = await ctx.test.login({ userId: user.id });
|
||||||
|
|
||||||
|
return {
|
||||||
|
headers: Object.fromEntries(headers.entries()) as Record<string, string>,
|
||||||
|
session,
|
||||||
|
user,
|
||||||
|
organizationId: org.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTestSessionWithRegularMember() {
|
||||||
|
const { headers, user, organizationId } = await createTestSession();
|
||||||
|
|
||||||
|
await db.update(member).set({ role: "member" }).where(eq(member.userId, user.id));
|
||||||
|
|
||||||
|
return { headers, user, organizationId };
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue