Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions apps/dokploy/__test__/services/admin-present.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

const mockDb = vi.hoisted(() => ({
query: {
member: {
findFirst: vi.fn(),
},
organization: {
findFirst: vi.fn(),
},
user: {
findFirst: vi.fn(),
},
},
}));

vi.mock("@dokploy/server/db", () => ({ db: mockDb }));

import { isAdminPresent } from "@dokploy/server/services/admin";

beforeEach(() => {
vi.clearAllMocks();
mockDb.query.member.findFirst.mockResolvedValue(undefined);
mockDb.query.organization.findFirst.mockResolvedValue(undefined);
mockDb.query.user.findFirst.mockResolvedValue(undefined);
});

describe("isAdminPresent", () => {
it("returns false on a truly uninitialized instance", async () => {
await expect(isAdminPresent()).resolves.toBe(false);
});

it("returns true when an owner membership exists", async () => {
mockDb.query.member.findFirst.mockResolvedValue({
id: "member-1",
role: "owner",
userId: "user-1",
organizationId: "org-1",
});

await expect(isAdminPresent()).resolves.toBe(true);
});

it("returns true when member.owner is missing but an organization owner exists", async () => {
mockDb.query.organization.findFirst.mockResolvedValue({
id: "org-1",
ownerId: "user-1",
owner: { id: "user-1", email: "admin@example.com" },
});

await expect(isAdminPresent()).resolves.toBe(true);
});

it("returns true when users exist even if owner membership and organizations are missing", async () => {
mockDb.query.user.findFirst.mockResolvedValue({
id: "user-1",
email: "admin@example.com",
});

await expect(isAdminPresent()).resolves.toBe(true);
});
});
12 changes: 5 additions & 7 deletions packages/server/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getTrustedOrigins,
getTrustedProviders,
getUserByToken,
isAdminPresent,
} from "../services/admin";
import { createAuditLog } from "../services/proprietary/audit-log";
import { resolveOrganizationDefaultRole } from "../services/proprietary/license-key";
Expand Down Expand Up @@ -206,10 +207,7 @@ const createBetterAuth = () =>
if (isSSORequest) {
return;
}
const isAdminPresent = await db.query.member.findFirst({
where: eq(schema.member.role, "owner"),
});
if (isAdminPresent) {
if (await isAdminPresent()) {
throw new APIError("BAD_REQUEST", {
message: "Admin is already created",
});
Expand All @@ -220,11 +218,11 @@ const createBetterAuth = () =>
after: async (user, context) => {
const isSSORequest = context?.path.includes("/sso");
const isSCIMRequest = context?.path.includes("/scim");
const isAdminPresent = await db.query.member.findFirst({
const ownerMember = await db.query.member.findFirst({
where: eq(schema.member.role, "owner"),
});

if (!IS_CLOUD && !isAdminPresent) {
if (!IS_CLOUD && !ownerMember) {
await updateWebServerSettings({
serverIp: await getPublicIpWithFallback(),
});
Expand Down Expand Up @@ -273,7 +271,7 @@ const createBetterAuth = () =>
return;
}

if (IS_CLOUD || !isAdminPresent) {
if (IS_CLOUD || !ownerMember) {
await db.transaction(async (tx) => {
const organization = await tx
.insert(schema.organization)
Expand Down
20 changes: 17 additions & 3 deletions packages/server/src/services/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,24 @@ export const isAdminPresent = async () => {
where: eq(member.role, "owner"),
});

if (!admin) {
return false;
if (admin) {
return true;
}
return true;

// Partial DB restore can drop member.owner while user/org rows remain.
// Those instances are initialized, not a fresh Setup.
const org = await db.query.organization.findFirst({
with: {
owner: true,
},
});

if (org?.owner) {
return true;
}
Comment on lines +50 to +58

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant organization owner query

The organization-owner lookup cannot affect the result because every loaded owner is also an existing user covered by the subsequent user lookup. Removing this query avoids unnecessary database work and simplifies the fallback logic.

Suggested change
const org = await db.query.organization.findFirst({
with: {
owner: true,
},
});
if (org?.owner) {
return true;
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


const existingUser = await db.query.user.findFirst();
return Boolean(existingUser);
};

export const findOwner = async () => {
Expand Down