Skip to content

Commit

Permalink
Merge pull request #368 from Open-Earth-Foundation/feature/admin-account
Browse files Browse the repository at this point in the history
Create script for making default admin account and add route to change user role
  • Loading branch information
lemilonkh authored Mar 19, 2024
2 parents 79c9808 + 0a32216 commit 9e393db
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 11 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/web-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,7 @@ jobs:
kubectl set env deployment/cc-web-deploy CHAT_PROVIDER=openai
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
7 changes: 4 additions & 3 deletions 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"
},
"dependencies": {
"@chakra-ui/icons": "^2.1.0",
Expand Down Expand Up @@ -91,6 +92,7 @@
"sequelize": "^6.37.1",
"sequelize-cli": "^6.6.2",
"tailwindcss": "3.4.1",
"tsx": "^4.7.0",
"typescript": "5.3.3",
"uuid": "^9.0.1",
"wellknown": "^0.5.0",
Expand All @@ -108,8 +110,7 @@
"prettier": "3.2.5",
"sequelize-auto": "^0.8.8",
"start-server-and-test": "^2.0.3",
"storybook": "^7.6.17",
"tsx": "^4.7.0"
"storybook": "^7.6.17"
},
"engines": {
"node": ">=20.5.0"
Expand Down
53 changes: 53 additions & 0 deletions app/scripts/create-admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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!",
);
await db.sequelize?.close();
return;
}

const email = process.env.DEFAULT_ADMIN_EMAIL.toLowerCase();
const user = await db.models.User.findOne({ where: { email } });

if (user) {
logger.info("Admin user already exists. Exiting.");
await db.sequelize?.close();
return;
}

const passwordHash = await bcrypt.hash(
process.env.DEFAULT_ADMIN_PASSWORD,
12,
);
const newUser = await db.models.User.create({
userId: randomUUID(),
name: "Admin",
email: email,
passwordHash,
role: Roles.Admin,
});

logger.info(
"Created admin user with email %s and ID %s",
newUser.email,
newUser.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) {
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: "[email protected]",
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: "[email protected]",
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: "[email protected]",
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"

0 comments on commit 9e393db

Please sign in to comment.