Skip to content

Commit

Permalink
added a not found page (404) and admin routes
Browse files Browse the repository at this point in the history
  • Loading branch information
progof committed Nov 7, 2024
1 parent e3b647a commit 29fb53a
Show file tree
Hide file tree
Showing 15 changed files with 371 additions and 15 deletions.
53 changes: 53 additions & 0 deletions backend/db/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { drizzle } from "drizzle-orm/postgres-js";
import { z } from "zod";
import * as schema from "./schema.js";
import dotenv from "dotenv";
import postgres from "postgres";

dotenv.config();

const env = z
.object({
DB_CONNECTION_URI: z.string().url(),
ADMIN_EMAILS: z
.string()
.transform((value) => {
return value.split(",").filter((maybeEmail) => {
const result = z.string().email().safeParse(maybeEmail);

if (!result.success) {
console.warn("Skipping invalid email:", maybeEmail);
return false;
}

return true;
});
})
.optional(),
})
.parse(process.env);

const sql = postgres(env.DB_CONNECTION_URI, { max: 1 });
const db = drizzle(sql, { schema });



if (env.ADMIN_EMAILS) {
await Promise.all(
env.ADMIN_EMAILS.map(async (email) => {
try {
await db.insert(schema.adminsTable).values({ email });

console.log("Created admin:", email);
} catch (error) {
if (error instanceof postgres.PostgresError && error.code === "23505") {
console.warn("Admin already exists:", email);
return;
}
console.error(error);
}
}),
);
}

await sql.end({ timeout: 1 });
3 changes: 2 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio",
"db:push": "drizzle-kit push"
"db:push": "drizzle-kit push",
"db:seed": "tsx ./db/seed.ts"
},
"dependencies": {
"@aws-sdk/client-s3": "3.622.0",
Expand Down
36 changes: 36 additions & 0 deletions backend/src/admin/admin.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Request, Response, Router } from "express";
import { db } from "@/db.js";

import { AdminService } from "./admin.service.js";
import { checkAdmin } from "@/middlewares/admin.middlewares.js";

export class AdminController {
public readonly router = Router();

// Constructor initialization of routes
constructor(
private readonly adminService: AdminService,
) {
this.router.get(
"/admin/users",
checkAdmin,
this.getAllUsers.bind(this)
);

}

// Get all users with their details

public async getAllUsers(req: Request, res: Response) {
try {
const users = await this.adminService.getAllUsers(db);

return res.status(200).send({ users });
} catch (error) {
console.error(error);
res.status(500).send({ errors: [{ message: "Internal server error" }] });
}
}


}
51 changes: 51 additions & 0 deletions backend/src/admin/admin.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { dateToUTCTimestamp } from "@/utils.js";
import { adminsTable, usersTable } from "@db/schema.js";
import { User } from "@shared/index.js";
import { z, ZodType } from "zod";


export const userSchema = z.object({
id: z.string(),
username: z.string(),
email: z.string(),
emailVerified: z.boolean(),
avatarKey: z.string().nullable(),
createdAt: z.number(),
role: z.string(),
score: z.number(),
level: z.number(),
currency: z.number(),
isAdmin: z.boolean(),
}) satisfies ZodType<User>;

export class AdminService {
constructor() {}

async isAdminEmail(db: any, email: string): Promise<boolean> {
const admin = await db
.select(adminsTable)
.where({ email })
.single();
return !!admin;
}

async getAllUsers(db: any): Promise<User[]> {
const user = await db
.select(usersTable)
.all();
return user.map((u: any) => userSchema.parse({
id: u.id,
username: u.username,
email: u.email,
emailVerified: u.emailVerified,
avatarKey: u.avatarKey,
createdAt: dateToUTCTimestamp(u.createdAt),
role: u.role,
score: u.score,
level: u.level,
currency: u.currency,
isAdmin: u.isAdmin,
} satisfies User));
}

}
73 changes: 59 additions & 14 deletions backend/src/auth/password/password.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import {
userSessionsTable,
passwordAccountsTable,
PasswordAccount,
adminsTable,
} from "../../../db/schema.js";
import { eq } from "drizzle-orm";
import { HTTPError, dateToUTCTimestamp } from "@/utils.js";
import { Database, HTTPError, Transaction, dateToUTCTimestamp } from "@/utils.js";
import { User } from "@shared/index.js";
import { z, ZodType } from "zod";

Expand All @@ -26,6 +27,7 @@ export const userSchema = z.object({
score: z.number(),
level: z.number(),
currency: z.number(),
isAdmin: z.boolean(),
}) satisfies ZodType<User>;

export class AuthPasswordService {
Expand Down Expand Up @@ -118,33 +120,39 @@ export class AuthPasswordService {
.where(eq(resetPasswordRequestsTable.userId, userId));
}

async getUserById(userId: string):Promise<User> {



async getUserById(userId: string): Promise<User> {
const user = (
await db
.select()
.from(usersTable)
.leftJoin(adminsTable, eq(usersTable.email, adminsTable.email))
.where(eq(usersTable.id, userId))
).at(0);

if (!user) {
throw new HTTPError({ status: 404, message: "User not found" });
}

return userSchema.parse({
id: user.id,
username: user.username,
email: user.email,
emailVerified: user.emailVerified,
avatarKey: user.avatarKey,
createdAt: dateToUTCTimestamp(user.createdAt),
role: user.role,
score: user.score,
level: user.level,
currency: user.currency,
id: user.users.id,
username: user.users.username,
email: user.users.email,
emailVerified: user.users.emailVerified,
avatarKey: user.users.avatarKey,
createdAt: dateToUTCTimestamp(user.users.createdAt),
role: user.users.role,
score: user.users.score,
level: user.users.level,
currency: user.users.currency,
isAdmin: user.admins !== null,

} satisfies User);
}

// async getUserById(db: Database | Transaction, userId: string): Promise<User> {
// async getUserById(userId: string):Promise<User> {
// const user = (
// await db
// .select()
Expand All @@ -155,7 +163,39 @@ export class AuthPasswordService {
// if (!user) {
// throw new HTTPError({ status: 404, message: "User not found" });
// }

// return userSchema.parse({
// id: user.id,
// username: user.username,
// email: user.email,
// emailVerified: user.emailVerified,
// avatarKey: user.avatarKey,
// createdAt: dateToUTCTimestamp(user.createdAt),
// role: user.role,
// score: user.score,
// level: user.level,
// currency: user.currency,
// } satisfies User);
// }



// function for get user by userId with checking if the user is an admin join with adminsTable


// async getUserById(userId: string): Promise<User> {
// const user = (
// await db
// .select()
// .from(usersTable)
// .leftJoin(adminsTable, eq(usersTable.email, adminsTable.email))
// .where(eq(usersTable.id, userId))
// ).at(0);

// if (!user) {
// throw new HTTPError({ status: 404, message: "User not found" });
// }

// return userSchema.parse({
// id: user.id,
// username: user.username,
Expand All @@ -165,9 +205,14 @@ export class AuthPasswordService {
// createdAt: dateToUTCTimestamp(user.createdAt),
// role: user.role,
// score: user.score,
// level: user.level,
// currency: user.currency,
// isAdmin: user.admins !== null,
// } satisfies User);
// }



async getUserRoleById(userId: string) {
const [user] = await db
.select({ role: usersTable.role })
Expand Down
10 changes: 10 additions & 0 deletions backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ import { CommentsService } from "@/dayquest/comment.service.js";

import { AuthGoogleService } from "./auth/google/google.service.js";
import { AuthGoogleController } from "./auth/google/google.controller.js";

import { AdminService } from "./admin/admin.service.js";
import { AdminController } from "./admin/admin.controller.js";

import { db } from "./db.js";


const app = express();

app.use(
Expand Down Expand Up @@ -72,6 +77,11 @@ const dayQuestController = new DayQuestController(
);
app.use(dayQuestController.router);


export const adminService = new AdminService();
const adminController = new AdminController(adminService);
app.use(adminController.router);

app.listen(config.APP_PORT, () => {
console.log(`Server running on port: ${config.APP_PORT}`);
});
20 changes: 20 additions & 0 deletions backend/src/middlewares/admin.middlewares.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { NextFunction, Request, Response } from "express";
import { AdminService } from "@/admin/admin.service.js";
import { db } from "@/db.js";


// Middleware to check if the user is an admin.
export async function checkAdmin(req: Request, res: Response, next: NextFunction) {
if (!req.session.user) {
return res.status(401).send({ errors: [{ message: "Unauthorized" }] });
}

const adminService = new AdminService();
const isAdmin = await adminService.isAdminEmail(db, req.session.user.email);

if (!isAdmin) {
return res.status(403).send({ errors: [{ message: "Forbidden" }] });
}

next();
}
5 changes: 5 additions & 0 deletions frontend/src/assets/icons/people.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/src/components/ui/button/Button.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const buttonVariants = cva(
primary:
"bg-blue-500 text-zinc-200 hover:bg-blue-600 disabled:bg-zinc-400",
secondary: "text-zinc-100 disabled:bg-zinc-400 border border-zinc-100",
tertiary: "text-blue-500 disabled:bg-zinc-400 border border-blue-500",
},
},
defaultVariants: {
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/layouts/ProfileDropdownMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import ScalesIcon from "@/assets/icons/scales.svg?component";
import SettingsIcon from "@/assets/icons/settings.svg?component";
import LogoutIcon from "@/assets/icons/logout.svg?component";
import LoaderIcon from "@/assets/icons/loader.svg?component";
import PeopleIcon from "@/assets/icons/people.svg?component";
import { getMeQueryOptions } from "@/services/user.service";
import { computed } from "vue";
import type { User } from "@shared/index";
Expand Down Expand Up @@ -82,6 +83,15 @@ const avatar = computed(() => {
<span class="text-zinc-800">Balances</span>
</DropdownMenuItem>
</DropdownMenuGroup>
<template v-if="user.isAdmin">
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem as="a" href="/admin/users/">
<PeopleIcon class="mr-2 size-4 stroke-[1.5] text-zinc-800" />
<span class="text-zinc-800">Users</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</template>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem as="a" href="/user/settings" disabled>
Expand Down
Loading

0 comments on commit 29fb53a

Please sign in to comment.