diff --git a/.gitignore b/.gitignore index 3266418..aa484e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ dist frontend/dist server/dist +dist +apiDocs/ # ---> Node bun.lock .nx @@ -137,3 +139,6 @@ dist .yarn/install-state.gz .pnp.* + +nssm.exe + diff --git a/package.json b/package.json index b97ea96..59a3dad 100644 --- a/package.json +++ b/package.json @@ -7,14 +7,15 @@ "dev:server": "dotenvx run -f .env -- tsx watch server/index.ts", "dev:frontend": "cd frontend && npm run dev", "build": "npm run build:server && npm run build:frontend", - "build:server": "rimraf dist && tsc --build", + "build:server": "rimraf dist && tsc --build && xcopy server\\scripts dist\\server\\scripts /E /I /Y", "build:frontend": "cd frontend && npm run build", "start": "npm run start:server", "start:server": "dotenvx run -f .env -- node dist/server/index.js", "db:generate": "npx drizzle-kit generate", "db:migrate": "npx drizzle-kit push", "deploy": "standard-version --conventional-commits", - "commit": "cz" + "commit": "cz", + "prodinstall": "npm i --omit=dev" }, "dependencies": { "@dotenvx/dotenvx": "^1.38.3", @@ -33,13 +34,13 @@ "jsonwebtoken": "^9.0.2", "pg": "^8.13.3", "postgres": "^3.4.5", - "zod": "^3.24.2" + "zod": "^3.24.2", + "drizzle-kit": "^0.30.4" }, "devDependencies": { "@types/node": "^22.13.5", "concurrently": "^8.2.0", "dotenv": "^16.3.1", - "drizzle-kit": "^0.30.4", "tsx": "^4.7.1", "@types/bcrypt": "^5.0.2", "@types/js-cookie": "^3.0.6", diff --git a/server/.gitignore b/server/.gitignore deleted file mode 100644 index 36fabb6..0000000 --- a/server/.gitignore +++ /dev/null @@ -1,28 +0,0 @@ -# dev -.yarn/ -!.yarn/releases -.vscode/* -!.vscode/launch.json -!.vscode/*.code-snippets -.idea/workspace.xml -.idea/usage.statistics.xml -.idea/shelf - -# deps -node_modules/ - -# env -.env -.env.production - -# logs -logs/ -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -# misc -.DS_Store diff --git a/server/drizzle.config.js b/server/drizzle.config.js deleted file mode 100644 index 66a4b1c..0000000 --- a/server/drizzle.config.js +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from "drizzle-kit"; -const database = process.env.DATABASE_URL || ""; -export default defineConfig({ - dialect: "postgresql", - schema: "database/schema", - out: "database/migrations", - dbCredentials: { - url: database, - }, -}); diff --git a/server/globalUtils/apiHits.d.ts b/server/globalUtils/apiHits.d.ts deleted file mode 100644 index 61de24b..0000000 --- a/server/globalUtils/apiHits.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { Context } from "hono"; -import { z } from "zod"; -declare const requestSchema: z.ZodObject<{ - ip: z.ZodOptional; - endpoint: z.ZodString; - action: z.ZodOptional; - stats: z.ZodOptional; -}, "strip", z.ZodTypeAny, { - ip?: string; - endpoint?: string; - action?: string; - stats?: string; -}, { - ip?: string; - endpoint?: string; - action?: string; - stats?: string; -}>; -type ApiHitData = z.infer; -export declare const apiHit: (c: Context, data: unknown) => Promise<{ - success: boolean; - data?: ApiHitData; - errors?: any[]; -}>; -export {}; -//# sourceMappingURL=apiHits.d.ts.map \ No newline at end of file diff --git a/server/globalUtils/apiHits.d.ts.map b/server/globalUtils/apiHits.d.ts.map deleted file mode 100644 index 5d877fc..0000000 --- a/server/globalUtils/apiHits.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"apiHits.d.ts","sourceRoot":"","sources":["apiHits.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,MAAM,CAAC;AAClC,OAAO,EAAC,CAAC,EAAW,MAAM,KAAK,CAAC;AAGhC,QAAA,MAAM,aAAa;;;;;;;;;;;;;;;EAKjB,CAAC;AAEH,KAAK,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAEhD,eAAO,MAAM,MAAM,MACZ,OAAO,QACJ,OAAO,KACd,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,UAAU,CAAC;IAAC,MAAM,CAAC,EAAE,GAAG,EAAE,CAAA;CAAC,CAuB/D,CAAC"} \ No newline at end of file diff --git a/server/globalUtils/apiHits.js b/server/globalUtils/apiHits.js deleted file mode 100644 index 57c0028..0000000 --- a/server/globalUtils/apiHits.js +++ /dev/null @@ -1,20 +0,0 @@ -import { z, ZodError } from "zod"; -const requestSchema = z.object({ - ip: z.string().optional(), - endpoint: z.string(), - action: z.string().optional(), - stats: z.string().optional(), -}); -export const apiHit = async (c, data) => { - try { - const forwarded = c.req.header("host"); - const validatedData = requestSchema.parse(data); - return { success: true, data: validatedData }; - } - catch (error) { - if (error instanceof ZodError) { - return { success: false, errors: error.errors }; - } - return { success: false, errors: [{ message: "An unknown error occurred" }] }; - } -}; diff --git a/server/globalUtils/apiReturn.ts b/server/globalUtils/apiReturn.ts new file mode 100644 index 0000000..87516a7 --- /dev/null +++ b/server/globalUtils/apiReturn.ts @@ -0,0 +1,15 @@ +import type {Context} from "hono"; +import type {ContentfulStatusCode} from "hono/utils/http-status"; + +export const apiReturn = async ( + c: Context, + success: boolean, + message: string, + data: any, + code: ContentfulStatusCode +): Promise => { + /** + * This is just a global return function to reduce constacnt typing the same thing lol + */ + return c.json({success, message, data}, code); +}; diff --git a/server/index.ts b/server/index.ts index 233120e..06e9374 100644 --- a/server/index.ts +++ b/server/index.ts @@ -4,9 +4,6 @@ import {serveStatic} from "@hono/node-server/serve-static"; import {logger} from "hono/logger"; import {cors} from "hono/cors"; -import {db} from "../database/dbclient.js"; -import {modules} from "../database/schema/modules.js"; - // custom routes import scalar from "./services/general/route/scalar.js"; import system from "./services/server/systemServer.js"; @@ -63,7 +60,7 @@ app.use("*", serveStatic({path: "./frontend/dist/index.html"})); serve( { fetch: app.fetch, - port: Number(process.env.SERVER_PORT), + port: Number(process.env.VITE_SERVER_PORT), }, (info) => { console.log(`Server is running on http://localhost:${info.port}`); diff --git a/server/scripts/services.ps1 b/server/scripts/services.ps1 new file mode 100644 index 0000000..4a6f9fe --- /dev/null +++ b/server/scripts/services.ps1 @@ -0,0 +1,85 @@ +param ( + [string]$serviceName, + [string]$option, + [string]$appPath, + [string]$command, # just the command like run startadm or what ever you have in npm. + [string]$description +) + +# Example string to run with the parameters in it. +# .\services.ps1 -serviceName "LST-Admin" -option "install" -appPath "C:\Users\matthes01\Documents\lstV2" -description "The Admin DashBoard" -command "npm run startadm" + +$nssmPath = $AppPath + "\nssm.exe" +$npmPath = "C:\Program Files\nodejs\npm.cmd" # Path to npm.cmd + +if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { + Write-Host "Error: This script must be run as Administrator." + exit 1 +} + +if(-not $serviceName -or -not $option){ + Write-host "The service name or option is missing please enter one of them and try again." + exit 1 +} + +if ($option -eq "start"){ + write-host "Starting $($serviceName)." + Start-Service $serviceName +} + +if ($option -eq "stop"){ + write-host "Stoping $($serviceName)." + Stop-Service $serviceName +} + +if ($option -eq "restart"){ + write-host "Stoping $($serviceName) to be restarted" + Stop-Service $serviceName + Start-Sleep 3 # so we give it enough time to fully stop + write-host "Starting $($serviceName)" + Start-Service $serviceName +} + +if ($option -eq "delete"){ + if(-not $appPath){ + Write-host "The path to the app is missing please add it in and try again." + exit 1 + } + & $nssmPath stop $serviceName + write-host "Removing $($serviceName)" + & $nssmPath remove $serviceName confirm + +} + +if($option -eq "install"){ + if(-not $appPath -or -not $description -or -not $command){ + Write-host "Please check all parameters are passed to install the app.." + exit 1 + } + + + $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + + if(-not $service){ + write-host $serviceName "is not installed we will install it now" + + Write-Host "Installing $serviceName..." + & $nssmPath install $serviceName $npmPath $command + & $nssmPath set $serviceName AppDirectory $appPath + & $nssmPath set $serviceName Description $description + # Set recovery options + sc.exe failure $serviceName reset= 0 actions= restart/5000/restart/5000/restart/5000 + & $nssmPath start $serviceName + }else{ + write-host $serviceName "is already installed will push the updated info" + Write-Host "Updating $serviceName..." + & $nssmPath stop $serviceName + & $nssmPath set $serviceName AppDirectory $appPath + & $nssmPath set $serviceName Description $description + # Set recovery options + sc.exe failure $serviceName reset= 0 actions= restart/5000/restart/5000/restart/5000 + Start-Sleep 4 + & $nssmPath start $serviceName + } +} + diff --git a/server/services/auth/authService.ts b/server/services/auth/authService.ts index e1aad40..5b20c93 100644 --- a/server/services/auth/authService.ts +++ b/server/services/auth/authService.ts @@ -1,10 +1,10 @@ import {OpenAPIHono} from "@hono/zod-openapi"; - +import {authMiddleware} from "./middleware/authMiddleware.js"; import login from "./routes/login.js"; import register from "./routes/register.js"; import session from "./routes/session.js"; -import getAccess from "./routes/getUserRoles.js"; -import {authMiddleware} from "./middleware/authMiddleware.js"; +import getAccess from "./routes/userRoles/getUserRoles.js"; +import setAccess from "./routes/userRoles/setUserRoles.js"; const app = new OpenAPIHono(); app.route("auth/login", login); @@ -13,6 +13,10 @@ app.route("auth/session", session); // required to login app.use("auth/getuseraccess", authMiddleware); + app.route("/auth/getuseraccess", getAccess); +app.use("auth/setuseraccess", authMiddleware); +app.route("/auth/setuseraccess", setAccess); + export default app; diff --git a/server/services/auth/controllers/login.ts b/server/services/auth/controllers/login.ts index fcf11f8..6e39c34 100644 --- a/server/services/auth/controllers/login.ts +++ b/server/services/auth/controllers/login.ts @@ -3,7 +3,7 @@ import {db} from "../../../../database/dbclient.js"; import {users} from "../../../../database/schema/users.js"; import {eq, sql} from "drizzle-orm"; import {checkPassword} from "../utils/checkPassword.js"; -import {roleCheck} from "./getUserAccess.js"; +import {roleCheck} from "./userRoles/getUserAccess.js"; /** * Authenticate a user and return a JWT. diff --git a/server/services/auth/controllers/getUserAccess.ts b/server/services/auth/controllers/userRoles/getUserAccess.ts similarity index 55% rename from server/services/auth/controllers/getUserAccess.ts rename to server/services/auth/controllers/userRoles/getUserAccess.ts index 54bbb93..328273a 100644 --- a/server/services/auth/controllers/getUserAccess.ts +++ b/server/services/auth/controllers/userRoles/getUserAccess.ts @@ -4,10 +4,13 @@ in the login route we attach it to user under roles. */ import {eq} from "drizzle-orm"; -import {db} from "../../../../database/dbclient.js"; -import {userRoles} from "../../../../database/schema/userRoles.js"; +import {db} from "../../../../../database/dbclient.js"; +import {userRoles} from "../../../../../database/schema/userRoles.js"; -export const roleCheck = async (user_id: any) => { +export const roleCheck = async (user_id: string | undefined) => { + if (!user_id) { + throw Error("Missing user_id"); + } // get the user roles by the user_id const roles = await db.select().from(userRoles).where(eq(userRoles.user_id, user_id)); diff --git a/server/services/auth/controllers/userRoles/setSysAdmin.ts b/server/services/auth/controllers/userRoles/setSysAdmin.ts new file mode 100644 index 0000000..18d42b8 --- /dev/null +++ b/server/services/auth/controllers/userRoles/setSysAdmin.ts @@ -0,0 +1,35 @@ +import {users} from "../../../../../database/schema/users.js"; +import {eq} from "drizzle-orm"; +import {db} from "../../../../../database/dbclient.js"; +import {userRoles} from "../../../../../database/schema/userRoles.js"; +import {modules} from "../../../../../database/schema/modules.js"; +import {roles} from "../../../../../database/schema/roles.js"; + +export const setSysAdmin = async (user: any, roleName: any): Promise => { + // remove all userRoles to prevent errors + try { + const remove = await db.delete(userRoles).where(eq(userRoles.user_id, user[0].user_id)); + } catch (error) { + console.log(error); + } + + // now we want to add the user to the system admin. + const module = await db.select().from(modules); + const role = await db.select().from(roles).where(eq(roles.name, roleName)); + + for (let i = 0; i < module.length; i++) { + try { + const userRole = await db.insert(userRoles).values({ + user_id: user[0].user_id, + role_id: role[0].role_id, + module_id: module[i].module_id, + role: roleName, + }); + console.log(`${user[0].username} has been granted access to ${module[i].name} with the role ${roleName}`); + } catch (error) { + console.log(error); + } + } + + return; +}; diff --git a/server/services/auth/controllers/userRoles/setUserRoles.ts b/server/services/auth/controllers/userRoles/setUserRoles.ts new file mode 100644 index 0000000..3c78dfa --- /dev/null +++ b/server/services/auth/controllers/userRoles/setUserRoles.ts @@ -0,0 +1,57 @@ +/* +pass over a users uuid and return all modules they have permission too. +in the login route we attach it to user under roles. +*/ + +import {eq} from "drizzle-orm"; +import {db} from "../../../../../database/dbclient.js"; +import {userRoles} from "../../../../../database/schema/userRoles.js"; +import {users} from "../../../../../database/schema/users.js"; +import {modules} from "../../../../../database/schema/modules.js"; +import {roles} from "../../../../../database/schema/roles.js"; +import {setSysAdmin} from "./setSysAdmin.js"; + +export const setUserAccess = async (username: string, moduleName: string, roleName: string, override?: string) => { + // get the user roles by the user_id + const user = await db.select().from(users).where(eq(users.username, username)); + const module = await db.select().from(modules).where(eq(modules.name, moduleName)); + + if (process.env.SECRETOVERRIDECODE != override && roleName === "systemAdmin") { + return {success: false, message: "The override code provided is invalid."}; + } + + const role = await db.select().from(roles).where(eq(roles.name, roleName)); + + /** + * For system admin we want to do a little more + */ + + if (roleName === "systemAdmin") { + await setSysAdmin(user, roleName); + return { + success: true, + message: `${username} has been granted access to ${moduleName} with the role ${roleName}`, + }; + } + //console.log(user, module, role); + + // set the user + try { + const userRole = await db + .insert(userRoles) + .values({user_id: user[0].user_id, role_id: role[0].role_id, module_id: module[0].module_id, role: roleName}); + //.returning({user: users.username, email: users.email}); + + // return c.json({message: "User Registered", user}, 200); + return { + success: true, + message: `${username} has been granted access to ${moduleName} with the role ${roleName}`, + }; + } catch (error) { + return { + success: false, + message: `There was an error granting ${username} access to ${moduleName} with the role ${roleName}`, + data: error, + }; + } +}; diff --git a/server/services/auth/routes/getUserRoles.ts b/server/services/auth/routes/getUserRoles.ts deleted file mode 100644 index 9f3f6d4..0000000 --- a/server/services/auth/routes/getUserRoles.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {z, createRoute, OpenAPIHono} from "@hono/zod-openapi"; -import {apiHit} from "../../../globalUtils/apiHits.js"; - -const app = new OpenAPIHono(); - -const responseSchema = z.object({ - message: z.string().optional().openapi({example: "User Created"}), -}); - -app.openapi( - createRoute({ - tags: ["Auth"], - summary: "Returns the useraccess table", - method: "get", - path: "/", - - responses: { - 200: { - content: {"application/json": {schema: responseSchema}}, - description: "Retrieve the user", - }, - }, - }), - async (c) => { - // apit hit - apiHit(c, {endpoint: "api/auth/register"}); - return c.json({message: "UserRoles coming over"}); - } -); - -export default app; diff --git a/server/services/auth/routes/userRoles/getUserRoles.ts b/server/services/auth/routes/userRoles/getUserRoles.ts new file mode 100644 index 0000000..a9a9421 --- /dev/null +++ b/server/services/auth/routes/userRoles/getUserRoles.ts @@ -0,0 +1,52 @@ +import {z, createRoute, OpenAPIHono} from "@hono/zod-openapi"; +import {apiHit} from "../../../../globalUtils/apiHits.js"; +import jwt from "jsonwebtoken"; +import {roleCheck} from "../../controllers/userRoles/getUserAccess.js"; +import type {CustomJwtPayload} from "../../../../types/jwtToken.js"; + +const {verify} = jwt; +const app = new OpenAPIHono(); + +const responseSchema = z.object({ + message: z.string().optional().openapi({example: "User Created"}), +}); + +app.openapi( + createRoute({ + tags: ["Auth"], + summary: "Returns the useraccess table", + method: "get", + path: "/", + + responses: { + 200: { + content: {"application/json": {schema: responseSchema}}, + description: "Retrieve the user", + }, + }, + }), + async (c) => { + // apit hit + apiHit(c, {endpoint: "api/auth/getUserRoles"}); + const authHeader = c.req.header("Authorization"); + const token = authHeader?.split("Bearer ")[1] || ""; + try { + const secret = process.env.JWT_SECRET!; + if (!secret) { + throw new Error("JWT_SECRET is not defined in environment variables"); + } + + const payload = verify(token, secret) as CustomJwtPayload; + + const canAccess = await roleCheck(payload.user?.user_id); + + return c.json({sucess: true, message: `User ${payload.user?.username} can access`, data: canAccess}, 200); + } catch (error) { + console.log(error); + } + + return c.json({message: "UserRoles coming over"}); + } +); + +export default app; diff --git a/server/services/auth/routes/userRoles/setUserRoles.ts b/server/services/auth/routes/userRoles/setUserRoles.ts new file mode 100644 index 0000000..300349c --- /dev/null +++ b/server/services/auth/routes/userRoles/setUserRoles.ts @@ -0,0 +1,63 @@ +import {createRoute, OpenAPIHono, z} from "@hono/zod-openapi"; +import {setUserAccess} from "../../controllers/userRoles/setUserRoles.js"; +import {apiHit} from "../../../../globalUtils/apiHits.js"; +import {apiReturn} from "../../../../globalUtils/apiReturn.js"; + +const app = new OpenAPIHono(); + +const responseSchema = z.object({ + success: z.boolean().openapi({example: true}), + message: z.string().optional().openapi({example: "user access"}), + data: z.array(z.object({})).optional().openapi({example: []}), +}); + +const UserAccess = z.object({ + username: z + .string() + .regex(/^[a-zA-Z0-9_]{3,30}$/) + .openapi({example: "smith034"}), + module: z.string().openapi({example: "production"}), + role: z.string().openapi({example: "viewer"}), + override: z.string().optional().openapi({example: "secretString"}), +}); + +app.openapi( + createRoute({ + tags: ["Auth"], + summary: "Sets Users access", + method: "post", + path: "/", + description: "When logged in you will be able to grant new permissions", + request: { + body: { + content: { + "application/json": {schema: UserAccess}, + }, + }, + }, + responses: { + 200: { + content: {"application/json": {schema: responseSchema}}, + description: "Retrieve the user", + }, + 400: { + content: {"application/json": {schema: responseSchema}}, + description: "Failed to get user access", + }, + }, + }), + async (c) => { + apiHit(c, {endpoint: "api/auth/setUserRoles"}); + const {username, module, role, override} = await c.req.json(); + try { + const access = await setUserAccess(username, module, role, override); + //return apiReturn(c, true, access?.message, access?.data, 200); + return c.json({success: access.success, message: access.message, data: access.data}, 200); + } catch (error) { + console.log(error); + //return apiReturn(c, false, "Error in setting the user access", error, 400); + return c.json({success: false, message: "Error in setting the user access", data: error}, 400); + } + } +); +export default app; diff --git a/server/types/jwtToken.ts b/server/types/jwtToken.ts new file mode 100644 index 0000000..abd3403 --- /dev/null +++ b/server/types/jwtToken.ts @@ -0,0 +1,6 @@ +import type {JwtPayload} from "jsonwebtoken"; +import type {User} from "./users.js"; + +export type CustomJwtPayload = JwtPayload & { + user: User | undefined; +}; diff --git a/server/types/modules.ts b/server/types/modules.ts new file mode 100644 index 0000000..95a2f11 --- /dev/null +++ b/server/types/modules.ts @@ -0,0 +1,10 @@ +export interface Modules { + module_id: string; + name: string; + active: boolean; + roles: string; + add_user: string; + add_date: Date; + upd_user: string; + upd_date: Date; +} diff --git a/server/types/roles.ts b/server/types/roles.ts new file mode 100644 index 0000000..8a1cf5b --- /dev/null +++ b/server/types/roles.ts @@ -0,0 +1,4 @@ +export interface Roles { + role: string; + module_id: string; +} diff --git a/server/types/users.ts b/server/types/users.ts new file mode 100644 index 0000000..30c9352 --- /dev/null +++ b/server/types/users.ts @@ -0,0 +1,9 @@ +import type {Roles} from "./roles.js"; + +export type User = { + user_id?: string; + email?: string; + username?: string; + roles?: Roles[]; + role?: string; +};