Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create script for making default admin account and add route to change user role #368

Merged
merged 11 commits into from
Mar 19, 2024
Merged
3 changes: 3 additions & 0 deletions .github/workflows/web-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,7 @@ jobs:
kubectl set env deployment/cc-web-deploy CHAT_PROVIDER=huggingface
kubectl set env deployment/cc-web-deploy OPENAI_API_KEY=${{secrets.OPENAI_API_KEY}}
kubectl set env deployment/cc-web-deploy HUGGINGFACE_API_KEY=${{secrets.HUGGINGFACE_API_KEY}}
kubectl set env deployment/cc-web-deploy DEFAULT_ADMIN_EMAIL=${{secrets.DEFAULT_ADMIN_EMAIL}}
kubectl set env deployment/cc-web-deploy DEFAULT_ADMIN_PASSWORD=${{secrets.DEFAULT_ADMIN_PASSWORD}}
kubectl create -f k8s/cc-create-admin.yml -n default
kubectl rollout restart deployment cc-web-deploy -n default
3 changes: 2 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"cy:run": "cypress run",
"cy:test": "start-server-and-test start http://localhost:3000/en/auth/login cy:run",
"prettier": "npx prettier . --write",
"email": "email dev --dir src/lib/emails"
"email": "email dev --dir src/lib/emails",
"create-admin": "tsx scripts/create-admin.ts"
lemilonkh marked this conversation as resolved.
Show resolved Hide resolved
},
"dependencies": {
"@chakra-ui/icons": "^2.1.0",
Expand Down
43 changes: 43 additions & 0 deletions app/scripts/create-admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { db } from "@/models";
import env from "@next/env";
import { randomUUID } from "node:crypto";
import { logger } from "@/services/logger";
import bcrypt from "bcrypt";
import { Roles } from "@/lib/auth";

async function createAdmin() {
const projectDir = process.cwd();
env.loadEnvConfig(projectDir);

if (!db.initialized) {
await db.initialize();
}

if (!process.env.DEFAULT_ADMIN_EMAIL || !process.env.DEFAULT_ADMIN_PASSWORD) {
logger.error(
"create-admin.ts: Missing default admin credentials DEFAULT_ADMIN_EMAIL and DEFAULT_ADMIN_PASSWORD in env!",
);
return;
}

const passwordHash = await bcrypt.hash(
process.env.DEFAULT_ADMIN_PASSWORD,
12,
);
const user = await db.models.User.create({
lemilonkh marked this conversation as resolved.
Show resolved Hide resolved
userId: randomUUID(),
name: "Admin",
email: process.env.DEFAULT_ADMIN_EMAIL.toLowerCase(),
passwordHash,
role: Roles.Admin,
});

logger.info(
"Created admin user with email %s and ID %s",
user.email,
user.userId,
);
await db.sequelize?.close();
}

createAdmin();
31 changes: 31 additions & 0 deletions app/src/app/api/v0/auth/role/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Roles } from "@/lib/auth";
import { db } from "@/models";
import { apiHandler } from "@/util/api";
import createHttpError from "http-errors";
import { NextResponse } from "next/server";
import { z } from "zod";

const changeRoleRequest = z.object({
email: z.string().email(),
role: z.nativeEnum(Roles),
});

export const POST = apiHandler(async (req, { session }) => {
if (session?.user.role !== Roles.Admin) {
throw new createHttpError.Forbidden("Can only be used by admin accounts");
}
const body = changeRoleRequest.parse(await req.json());
const user = await db.models.User.findOne({ where: { email: body.email } });

if (!user) {

Check warning on line 20 in app/src/app/api/v0/auth/role/route.ts

View check run for this annotation

Codecov / codecov/patch

app/src/app/api/v0/auth/role/route.ts#L19-L20

Added lines #L19 - L20 were not covered by tests
throw new createHttpError.NotFound("User not found");
}
if (user.role === body.role) {
throw new createHttpError.BadRequest("User already has role " + body.role);
}

user.role = body.role;
await user.save();

return NextResponse.json({ success: true });
});
90 changes: 90 additions & 0 deletions app/tests/api/admin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { POST as changeRole } from "@/app/api/v0/auth/role/route";
import { db } from "@/models";
import assert from "node:assert";
import { after, before, beforeEach, describe, it, mock } from "node:test";
import { mockRequest, setupTests, testUserData, testUserID } from "../helpers";
import { AppSession, Auth, Roles } from "@/lib/auth";

const mockSession: AppSession = {
user: { id: testUserID, role: "user" },
expires: "1h",
};
const mockAdminSession: AppSession = {
user: { id: testUserID, role: "admin" },
expires: "1h",
};

describe("Admin API", () => {
let prevGetServerSession = Auth.getServerSession;

before(async () => {
setupTests();
await db.initialize();
});

after(async () => {
Auth.getServerSession = prevGetServerSession;
if (db.sequelize) await db.sequelize.close();
});

beforeEach(async () => {
Auth.getServerSession = mock.fn(() => Promise.resolve(mockSession));
await db.models.User.upsert({
userId: testUserID,
name: testUserData.name,
email: testUserData.email,
role: Roles.User,
});
});

it("should change the user role when logged in as admin", async () => {
const req = mockRequest({ email: testUserData.email, role: Roles.Admin });
Auth.getServerSession = mock.fn(() => Promise.resolve(mockAdminSession));
const res = await changeRole(req, { params: {} });
assert.equal(res.status, 200);
const body = await res.json();
assert.equal(body.success, true);

const user = await db.models.User.findOne({
where: { email: testUserData.email },
});
assert.equal(user?.role, Roles.Admin);
});

it("should not change the user role when logged in as normal user", async () => {
const req = mockRequest({ email: testUserData.email, role: Roles.Admin });
const res = await changeRole(req, { params: {} });
assert.equal(res.status, 403);

const user = await db.models.User.findOne({
where: { email: testUserData.email },
});
assert.equal(user?.role, Roles.User);
});

it("should return a 404 error when user does not exist", async () => {
const req = mockRequest({
email: "not-existing@example.com",
role: Roles.Admin,
});
Auth.getServerSession = mock.fn(() => Promise.resolve(mockAdminSession));
const res = await changeRole(req, { params: {} });
assert.equal(res.status, 404);
});

it("should validate the request", async () => {
Auth.getServerSession = mock.fn(() => Promise.resolve(mockAdminSession));

const req = mockRequest({ email: testUserData.email, role: "invalid" });
const res = await changeRole(req, { params: {} });
assert.equal(res.status, 400);

const req2 = mockRequest({ email: "not-an-email", role: "Admin" });
const res2 = await changeRole(req2, { params: {} });
assert.equal(res2.status, 400);

const req3 = mockRequest({});
const res3 = await changeRole(req3, { params: {} });
assert.equal(res3.status, 400);
});
});
20 changes: 12 additions & 8 deletions app/tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ export function createRequest(url: string, body?: any) {
return request;
}

export function mockRequest(body?: any, searchParams?: Record<string, string>): NextRequest {
export function mockRequest(
body?: any,
searchParams?: Record<string, string>,
): NextRequest {
const request = new NextRequest(new URL(mockUrl));
request.json = mock.fn(() => Promise.resolve(body));
for (const param in searchParams) {
Expand Down Expand Up @@ -72,6 +75,13 @@ export const testFileFormat = {
};

export const testUserID = "beb9634a-b68c-4c1b-a20b-2ab0ced5e3c2";
export const testUserData = {
id: testUserID,
name: "Test User",
email: "test@example.com",
image: null,
role: "user",
};

export function setupTests() {
const projectDir = process.cwd();
Expand All @@ -82,13 +92,7 @@ export function setupTests() {
const expires = new Date();
expires.setDate(expires.getDate() + 1);
return {
user: {
id: testUserID,
name: "Test User",
email: "test@example.com",
image: null,
role: "user",
},
user: testUserData,
expires: expires.toISOString(),
};
});
Expand Down
29 changes: 29 additions & 0 deletions k8s/cc-create-admin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
apiVersion: batch/v1
kind: Job
metadata:
generateName: cc-create-admin-
spec:
ttlSecondsAfterFinished: 86400
template:
spec:
restartPolicy: OnFailure
containers:
- name: cc-migrate
image: ghcr.io/open-earth-foundation/citycatalyst:latest
imagePullPolicy: Always
env:
- name: NODE_ENV
value: development
- name: DATABASE_NAME
value: "citycatalyst"
- name: DATABASE_HOST
value: "cc-db"
- name: DATABASE_USER
value: "citycatalyst"
- name: DATABASE_PASSWORD
value: "development"
command: ["npm", "run", "create-admin"]
resources:
limits:
memory: "1024Mi"
cpu: "1000m"
Loading