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

Tbcct 67 integrate backend auth system to be used in the backoffice also checking the admin role #41

1 change: 1 addition & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ jobs:
DB_NAME=${{ secrets.DB_NAME }}
DB_USERNAME=${{ secrets.DB_USERNAME }}
DB_PASSWORD=${{ secrets.DB_PASSWORD }}
API_URL=${{ vars.NEXT_PUBLIC_API_URL }}
context: .
cache-from: type=gha
cache-to: type=gha,mode=max
Expand Down
2 changes: 2 additions & 0 deletions admin/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ ARG DB_PORT
ARG DB_NAME
ARG DB_USERNAME
ARG DB_PASSWORD
ARG API_URL

ENV DB_HOST $DB_HOST
ENV DB_PORT $DB_PORT
ENV DB_NAME $DB_NAME
ENV DB_USERNAME $DB_USERNAME
ENV DB_PASSWORD $DB_PASSWORD
ENV API_URL $API_URL

WORKDIR /app

Expand Down
24 changes: 13 additions & 11 deletions admin/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import "reflect-metadata";
import AdminJS, { ComponentLoader, locales } from "adminjs";
import AdminJS, { ComponentLoader } from "adminjs";
import AdminJSExpress from "@adminjs/express";
import express from "express";
import * as AdminJSTypeorm from "@adminjs/typeorm";
import { User } from "@shared/entities/users/user.entity.js";
import { dataSource } from "./datasource.js";
import { CarbonInputEntity } from "@api/modules/model/entities/carbon-input.entity.js";
import { CostInput } from "@api/modules/model/entities/cost-input.entity.js";
import { Country } from "@api/modules/model/entities/country.entity.js";
import { AuthProvider } from "./providers/auth.provider.js";
import { userResource } from "./resources/users/user.resource.js";

AdminJS.registerAdapter({
Database: AdminJSTypeorm.Database,
Resource: AdminJSTypeorm.Resource,
});

const PORT = 1000;
export const API_URL = process.env.API_URL || "http://localhost:4000";

const componentLoader = new ComponentLoader();
const authProvider = new AuthProvider();

const start = async () => {
await dataSource.initialize();
Expand All @@ -28,9 +31,10 @@ const start = async () => {
};

const admin = new AdminJS({
rootPath: "/",
rootPath: "/admin",
componentLoader,
resources: [
userResource,
{
resource: CostInput,
name: "Cost Input",
Expand All @@ -47,13 +51,7 @@ const start = async () => {
icon: "Globe",
},
},
{
resource: User,
options: {
parent: databaseNavigation,
icon: "User",
},
},

{
resource: CarbonInputEntity,
name: "Andresito",
Expand All @@ -65,7 +63,11 @@ const start = async () => {
],
});

const adminRouter = AdminJSExpress.buildRouter(admin);
const adminRouter = AdminJSExpress.buildAuthenticatedRouter(admin, {
provider: authProvider,
cookiePassword: "some-secret",
});

app.use(admin.options.rootPath, adminRouter);

app.listen(PORT, () => {
Expand Down
35 changes: 35 additions & 0 deletions admin/providers/auth.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { BaseAuthProvider, LoginHandlerOptions } from "adminjs";

import { ROLES } from "@shared/entities/users/roles.enum.js";
import { UserDto, UserWithAccessToken } from "@shared/dtos/users/user.dto.js";
import { API_URL } from "../index.js";

export class AuthProvider extends BaseAuthProvider {
override async handleLogin(opts: LoginHandlerOptions, context?: any) {
const { email, password } = opts.data;
try {
const response = await fetch(`${API_URL}/authentication/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (response.ok) {
const data: UserWithAccessToken = await response.json();
const { user, accessToken } = data;
if (this.isAdmin(user)) {
return { ...user, accessToken };
}
return null;
}

return null;
} catch (error) {
console.error(error);
return null;
}
}

private isAdmin(user: UserDto) {
return user.role === ROLES.ADMIN;
}
}
61 changes: 61 additions & 0 deletions admin/resources/users/user.actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ActionContext, ActionRequest, ActionResponse } from "adminjs";
import { CreateUserDto } from "@shared/dtos/users/create-user.dto.js";
import { API_URL } from "../../index.js";

export const createUserAction = async (
request: ActionRequest,
response: ActionResponse,
context: ActionContext,
) => {
if (request.method === "post") {
const { resource, currentAdmin, records } = context;
const { email, role, name, partnerName } = request.payload as CreateUserDto;
const record = resource.build({ email, role, name });
const accessToken = currentAdmin?.accessToken;
if (!accessToken) {
throw new Error("Current Admin token not found");
}
try {
const apiResponse = await fetch(`${API_URL}/admin/users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
Origin: response.req.headers.origin,
},
body: JSON.stringify(request.payload),
});

if (!apiResponse.ok) {
const res = await apiResponse.json();
return {
record,
redirectUrl: "/admin/resources/User",
notice: {
message: JSON.stringify(res.errors),
type: "error",
},
};
}

return {
redirectUrl: "/admin/resources/User",
notice: {
message: "User created successfully",
type: "success",
},
record,
};
} catch (error) {
console.error("Error creating user", error);
return {
record,
notice: {
message: "Error creating user: Contact administrator",
type: "error",
},
redirectUrl: "/admin/resources/User",
};
}
}
};
27 changes: 27 additions & 0 deletions admin/resources/users/user.resource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ResourceWithOptions } from "adminjs";
import { User } from "@shared/entities/users/user.entity.js";
import { createUserAction } from "./user.actions.js";

export const userResource: ResourceWithOptions = {
resource: User,
options: {
navigation: {
name: "Data Management",
icon: "Database",
},
properties: {
id: { isVisible: false, isId: true },
password: { isVisible: false },
isActive: { isVisible: false },
email: { isRequired: true },
role: { isRequired: true },
partnerName: { isRequired: true },
},
actions: {
new: {
actionType: "resource",
handler: createUserAction,
},
},
},
};
4 changes: 2 additions & 2 deletions api/src/modules/admin/admin.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import { ROLES } from '@shared/entities/users/roles.enum';

@Controller()
@UseGuards(JwtAuthGuard, RolesGuard)
@RequiredRoles(ROLES.ADMIN)
export class AdminController {
constructor(private readonly auth: AuthenticationService) {}

@RequiredRoles(ROLES.ADMIN)
@TsRestHandler(adminContract.addUser)
async createUser(
async addUser(
@Headers('origin') origin: string,
): Promise<ControllerResponse> {
return tsRestHandler(adminContract.addUser, async ({ body }) => {
Expand Down
2 changes: 1 addition & 1 deletion infrastructure/source_bundle/proxy/conf.d/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ server {
proxy_pass_request_headers on;
client_max_body_size 200m;
}
location /administration/ {
location /admin/ {
proxy_pass http://admin;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Host $host;
Expand Down
2 changes: 2 additions & 0 deletions shared/config/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ DB_NAME=blc
DB_USERNAME=blue-carbon-cost
DB_PASSWORD=blue-carbon-cost

API_URL=http://localhost:4000

# Access Token Configuration
ACCESS_TOKEN_SECRET=access_token_secret
ACCESS_TOKEN_EXPIRES_IN=2h
Expand Down
2 changes: 2 additions & 0 deletions shared/schemas/users/create-user.schema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { z } from "zod";
import { ROLES } from "@shared/entities/users/roles.enum";

export const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().optional(),
partnerName: z.string(),
role: z.enum([ROLES.ADMIN, ROLES.PARTNER]).optional(),
});
Loading