-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #368 from Open-Earth-Foundation/feature/admin-account
Create script for making default admin account and add route to change user role
- Loading branch information
Showing
7 changed files
with
222 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) { | ||
|
@@ -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(); | ||
|
@@ -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(), | ||
}; | ||
}); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |