diff --git a/LogisticsSupportTool_API_DOCS/LstV2/ocp/printers/Update Printers.bru b/LogisticsSupportTool_API_DOCS/LstV2/ocp/printers/Update Printers.bru index c2a2dc6..6b3f6bc 100644 --- a/LogisticsSupportTool_API_DOCS/LstV2/ocp/printers/Update Printers.bru +++ b/LogisticsSupportTool_API_DOCS/LstV2/ocp/printers/Update Printers.bru @@ -4,7 +4,7 @@ meta { seq: 1 } -post { +get { url: {{url}}/lst/old/api/ocp/updateprinters body: none auth: inherit diff --git a/LogisticsSupportTool_API_DOCS/app/admin/User/Change user password.bru b/LogisticsSupportTool_API_DOCS/app/admin/User/Change user password.bru new file mode 100644 index 0000000..1c9f80b --- /dev/null +++ b/LogisticsSupportTool_API_DOCS/app/admin/User/Change user password.bru @@ -0,0 +1,26 @@ +meta { + name: Change user password + type: http + seq: 5 +} + +patch { + url: {{url}}/lst/api/admin/users/changePassword/:userId + body: json + auth: inherit +} + +params:path { + userId: 0hlO48C7Jw1J804FxrCnonKjQ2zh48R6 +} + +body:json { + { + "password":"nova0511" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/LogisticsSupportTool_API_DOCS/app/admin/User/Create user.bru b/LogisticsSupportTool_API_DOCS/app/admin/User/Create user.bru new file mode 100644 index 0000000..2cacd9f --- /dev/null +++ b/LogisticsSupportTool_API_DOCS/app/admin/User/Create user.bru @@ -0,0 +1,25 @@ +meta { + name: Create user + type: http + seq: 4 +} + +post { + url: {{url}}/lst/api/admin/users + body: none + auth: inherit +} + +body:json { + { + "username":"matthes01", + "name":"blake", + "email":"blake.matthes@alpla.com", + "password":"nova0511" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/LogisticsSupportTool_API_DOCS/app/admin/User/Delete User.bru b/LogisticsSupportTool_API_DOCS/app/admin/User/Delete User.bru new file mode 100644 index 0000000..0ad7845 --- /dev/null +++ b/LogisticsSupportTool_API_DOCS/app/admin/User/Delete User.bru @@ -0,0 +1,26 @@ +meta { + name: Delete User + type: http + seq: 6 +} + +delete { + url: {{url}}/lst/api/admin/users/delete/:userId + body: json + auth: inherit +} + +params:path { + userId: 0hlO48C7Jw1J804FxrCnonKjQ2zh48R6 +} + +body:json { + { + "password":"nova0511" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/LogisticsSupportTool_API_DOCS/app/admin/User/Get Users.bru b/LogisticsSupportTool_API_DOCS/app/admin/User/Get Users.bru index 5983f00..590f128 100644 --- a/LogisticsSupportTool_API_DOCS/app/admin/User/Get Users.bru +++ b/LogisticsSupportTool_API_DOCS/app/admin/User/Get Users.bru @@ -21,4 +21,5 @@ body:json { settings { encodeUrl: true + timeout: 0 } diff --git a/LogisticsSupportTool_API_DOCS/app/admin/User/GrantROle by ID.bru b/LogisticsSupportTool_API_DOCS/app/admin/User/GrantROle by ID.bru index faad09c..72d62e1 100644 --- a/LogisticsSupportTool_API_DOCS/app/admin/User/GrantROle by ID.bru +++ b/LogisticsSupportTool_API_DOCS/app/admin/User/GrantROle by ID.bru @@ -5,7 +5,7 @@ meta { } patch { - url: {{url}}/lst/api/admin/:userID/grant + url: {{url}}/lst/api/admin/users/:userID/grant body: json auth: inherit } diff --git a/LogisticsSupportTool_API_DOCS/app/admin/User/RevokeRole by ID.bru b/LogisticsSupportTool_API_DOCS/app/admin/User/RevokeRole by ID.bru index 60943e3..fa2555e 100644 --- a/LogisticsSupportTool_API_DOCS/app/admin/User/RevokeRole by ID.bru +++ b/LogisticsSupportTool_API_DOCS/app/admin/User/RevokeRole by ID.bru @@ -5,7 +5,7 @@ meta { } patch { - url: {{url}}/lst/api/admin/:userID/revoke + url: {{url}}/lst/api/admin/users/:userID/revoke body: json auth: inherit } diff --git a/LogisticsSupportTool_API_DOCS/environments/lst.bru b/LogisticsSupportTool_API_DOCS/environments/lst.bru index 756bd84..e1d91e8 100644 --- a/LogisticsSupportTool_API_DOCS/environments/lst.bru +++ b/LogisticsSupportTool_API_DOCS/environments/lst.bru @@ -1,5 +1,5 @@ vars { - url: https://usiow2prod.alpla.net + url: http://localhost:4200 session_cookie: urlv2: http://localhost:3000 jwtV2: diff --git a/app/src/internal/admin/controller/users/newUser.ts b/app/src/internal/admin/controller/users/newUser.ts new file mode 100644 index 0000000..66ad512 --- /dev/null +++ b/app/src/internal/admin/controller/users/newUser.ts @@ -0,0 +1,42 @@ +import type { User } from "better-auth"; +import { DrizzleQueryError } from "drizzle-orm"; +import { auth } from "../../../../pkg/auth/auth.js"; +import { tryCatch } from "../../../../pkg/utils/tryCatch.js"; + +export type NewUser = { + email: string; + password: string; + username: string; + statusCode: number; + message: string; +}; + +export const createNewUser = async (userData: NewUser) => { + const { data, error } = await tryCatch( + auth.api.createUser({ + body: { + email: userData.email, // required + password: userData.password, // required + name: userData.username, // required + role: "user", + data: { username: userData.username }, + }, + }), + ); + + if (error) { + if (error instanceof DrizzleQueryError) { + // @ts-ignore + if (error?.cause.message.includes("unique constraint")) { + return { + statusCode: 400, + message: `${userData.username} already exists`, + }; + } + } + + return error; + } + + return data; +}; diff --git a/app/src/internal/admin/routes.ts b/app/src/internal/admin/routes.ts index 19f5946..592eb25 100644 --- a/app/src/internal/admin/routes.ts +++ b/app/src/internal/admin/routes.ts @@ -1,11 +1,9 @@ import type { Express, Request, Response } from "express"; import { requireAuth } from "../../pkg/middleware/authMiddleware.js"; import { mainServerSync } from "./controller/servers/matchServers.js"; -//admin routes -import users from "./routes/getUserRoles.js"; -import grantRoles from "./routes/grantRole.js"; -import revokeRoles from "./routes/revokeRole.js"; import servers from "./routes/servers/serverRoutes.js"; +//admin routes +import users from "./routes/users/userRoutes.js"; export const setupAdminRoutes = (app: Express, basePath: string) => { app.use( @@ -15,22 +13,10 @@ export const setupAdminRoutes = (app: Express, basePath: string) => { app.use( basePath + "/api/admin/users", - requireAuth("user", ["systemAdmin"]), // will pass bc system admin but this is just telling us we need this + requireAuth("user", ["systemAdmin", "admin"]), // will pass bc system admin but this is just telling us we need this users, ); - app.use( - basePath + "/api/admin", - requireAuth("user", ["systemAdmin", "admin"]), // will pass bc system admin but this is just telling us we need this - grantRoles, - ); - - app.use( - basePath + "/api/admin", - requireAuth("user", ["systemAdmin", "admin"]), // will pass bc system admin but this is just telling us we need this - revokeRoles, - ); - // run the sync only on startup setTimeout(() => { mainServerSync(); diff --git a/app/src/internal/admin/routes/getUserRoles.ts b/app/src/internal/admin/routes/getUserRoles.ts deleted file mode 100644 index 2415626..0000000 --- a/app/src/internal/admin/routes/getUserRoles.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Router } from "express"; -import type { Request, Response } from "express"; -import { tryCatch } from "../../../pkg/utils/tryCatch.js"; -import { db } from "../../../pkg/db/db.js"; -import { user } from "../../../pkg/db/schema/auth-schema.js"; -import { userRoles } from "../../../pkg/db/schema/user_roles.js"; - -const router = Router(); - -router.post("/", async (req: Request, res: Response) => { - // should get all users - const { data: users, error: userError } = await tryCatch( - db.select().from(user) - ); - - if (userError) { - return res.status(500).json({ - success: false, - message: "Failed to get users", - error: userError, - }); - } - // should get all roles - - const { data: userRole, error: userRoleError } = await tryCatch( - db.select().from(userRoles) - ); - - if (userRoleError) { - return res.status(500).json({ - success: false, - message: "Failed to get userRoless", - error: userRoleError, - }); - } - - // add the roles and return - - const usersWithRoles = users.map((user) => { - const roles = userRole - .filter((ur) => ur.userId === user.id) - .map((ur) => ({ module: ur.module, role: ur.role })); - - return { ...user, roles }; - }); - - return res - .status(200) - .json({ success: true, message: "User data", data: usersWithRoles }); -}); - -export default router; diff --git a/app/src/internal/admin/routes/users/changeUserPassword.ts b/app/src/internal/admin/routes/users/changeUserPassword.ts new file mode 100644 index 0000000..a635577 --- /dev/null +++ b/app/src/internal/admin/routes/users/changeUserPassword.ts @@ -0,0 +1,25 @@ +import { type Request, type Response, Router } from "express"; +import { auth } from "../../../../pkg/auth/auth.js"; + +const router = Router(); + +router.patch("/:userId", async (req: Request, res: Response) => { + const userId = req.params.userId; + const cookieHeader = req.headers.cookie ?? ""; + const authorization = req.headers.authorization ?? ""; + + const data = await auth.api.setUserPassword({ + body: { + newPassword: req.body.password, // required + userId: userId, // required + }, + // This endpoint requires session cookies. + headers: { + cookie: cookieHeader, + authorization, + }, + }); + + return res.status(200).json({ message: "Password was just changed." }); +}); +export default router; diff --git a/app/src/internal/admin/routes/users/createUser.ts b/app/src/internal/admin/routes/users/createUser.ts new file mode 100644 index 0000000..7b90165 --- /dev/null +++ b/app/src/internal/admin/routes/users/createUser.ts @@ -0,0 +1,20 @@ +import { type Request, type Response, Router } from "express"; +import { createNewUser, type NewUser } from "../../controller/users/newUser.js"; + +const router = Router(); + +router.post("/", async (req: Request, res: Response) => { + const body = req.body; + const user = (await createNewUser(body)) as NewUser; + + if (user?.statusCode === 400) { + return res.status(user?.statusCode).json({ + message: user?.message, + }); + } + + return res + .status(200) + .json({ message: `${body.username}, was just created` }); +}); +export default router; diff --git a/app/src/internal/admin/routes/users/deleteUser.ts b/app/src/internal/admin/routes/users/deleteUser.ts new file mode 100644 index 0000000..46f1a46 --- /dev/null +++ b/app/src/internal/admin/routes/users/deleteUser.ts @@ -0,0 +1,24 @@ +import { type Request, type Response, Router } from "express"; +import { auth } from "../../../../pkg/auth/auth.js"; + +const router = Router(); + +router.delete("/:userId", async (req: Request, res: Response) => { + const userId = req.params.userId; + const cookieHeader = req.headers.cookie ?? ""; + const authorization = req.headers.authorization ?? ""; + + const data = await auth.api.removeUser({ + body: { + userId: userId, // required + }, + // This endpoint requires session cookies. + headers: { + cookie: cookieHeader, + authorization, + }, + }); + + return res.status(200).json({ message: "User was just deleted." }); +}); +export default router; diff --git a/app/src/internal/admin/routes/users/getActiveSessions.ts b/app/src/internal/admin/routes/users/getActiveSessions.ts new file mode 100644 index 0000000..a635577 --- /dev/null +++ b/app/src/internal/admin/routes/users/getActiveSessions.ts @@ -0,0 +1,25 @@ +import { type Request, type Response, Router } from "express"; +import { auth } from "../../../../pkg/auth/auth.js"; + +const router = Router(); + +router.patch("/:userId", async (req: Request, res: Response) => { + const userId = req.params.userId; + const cookieHeader = req.headers.cookie ?? ""; + const authorization = req.headers.authorization ?? ""; + + const data = await auth.api.setUserPassword({ + body: { + newPassword: req.body.password, // required + userId: userId, // required + }, + // This endpoint requires session cookies. + headers: { + cookie: cookieHeader, + authorization, + }, + }); + + return res.status(200).json({ message: "Password was just changed." }); +}); +export default router; diff --git a/app/src/internal/admin/routes/users/getUserRoles.ts b/app/src/internal/admin/routes/users/getUserRoles.ts new file mode 100644 index 0000000..329c4ec --- /dev/null +++ b/app/src/internal/admin/routes/users/getUserRoles.ts @@ -0,0 +1,52 @@ +import type { Request, Response } from "express"; +import { Router } from "express"; +import { db } from "../../../../pkg/db/db.js"; +import { user } from "../../../../pkg/db/schema/auth-schema.js"; +import { userRoles } from "../../../../pkg/db/schema/user_roles.js"; +import { tryCatch } from "../../../../pkg/utils/tryCatch.js"; + +const router = Router(); + +router.post("/", async (req: Request, res: Response) => { + // should get all users + const { data: users, error: userError } = await tryCatch( + db.select().from(user), + ); + + if (userError) { + return res.status(500).json({ + success: false, + message: "Failed to get users", + error: userError, + }); + } + // should get all roles + + const { data: userRole, error: userRoleError } = await tryCatch( + db.select().from(userRoles), + ); + + if (userRoleError) { + return res.status(500).json({ + success: false, + message: "Failed to get userRoless", + error: userRoleError, + }); + } + + // add the roles and return + + const usersWithRoles = users.map((user) => { + const roles = userRole + .filter((ur) => ur.userId === user.id) + .map((ur) => ({ module: ur.module, role: ur.role })); + + return { ...user, roles }; + }); + + return res + .status(200) + .json({ success: true, message: "User data", data: usersWithRoles }); +}); + +export default router; diff --git a/app/src/internal/admin/routes/grantRole.ts b/app/src/internal/admin/routes/users/grantRole.ts similarity index 87% rename from app/src/internal/admin/routes/grantRole.ts rename to app/src/internal/admin/routes/users/grantRole.ts index 67e6d21..90159a9 100644 --- a/app/src/internal/admin/routes/grantRole.ts +++ b/app/src/internal/admin/routes/users/grantRole.ts @@ -1,10 +1,9 @@ import type { Request, Response } from "express"; import { Router } from "express"; import z from "zod"; -import { db } from "../../../pkg/db/db.js"; -import { userRoles } from "../../../pkg/db/schema/user_roles.js"; -import { createLogger } from "../../../pkg/logger/logger.js"; -import { tryCatch } from "../../../pkg/utils/tryCatch.js"; +import { db } from "../../../../pkg/db/db.js"; +import { userRoles } from "../../../../pkg/db/schema/user_roles.js"; +import { createLogger } from "../../../../pkg/logger/logger.js"; const roleSchema = z.object({ module: z.enum([ diff --git a/app/src/internal/admin/routes/revokeRole.ts b/app/src/internal/admin/routes/users/revokeRole.ts similarity index 85% rename from app/src/internal/admin/routes/revokeRole.ts rename to app/src/internal/admin/routes/users/revokeRole.ts index c40ca2a..dc5e54f 100644 --- a/app/src/internal/admin/routes/revokeRole.ts +++ b/app/src/internal/admin/routes/users/revokeRole.ts @@ -2,10 +2,9 @@ import { and, eq } from "drizzle-orm"; import type { Request, Response } from "express"; import { Router } from "express"; import z from "zod"; -import { db } from "../../../pkg/db/db.js"; -import { userRoles } from "../../../pkg/db/schema/user_roles.js"; -import { createLogger } from "../../../pkg/logger/logger.js"; -import { tryCatch } from "../../../pkg/utils/tryCatch.js"; +import { db } from "../../../../pkg/db/db.js"; +import { userRoles } from "../../../../pkg/db/schema/user_roles.js"; +import { createLogger } from "../../../../pkg/logger/logger.js"; const roleSchema = z.object({ module: z.enum([ diff --git a/app/src/internal/admin/routes/users/userRoutes.ts b/app/src/internal/admin/routes/users/userRoutes.ts new file mode 100644 index 0000000..d4905c5 --- /dev/null +++ b/app/src/internal/admin/routes/users/userRoutes.ts @@ -0,0 +1,51 @@ +import { fromNodeHeaders } from "better-auth/node"; +import type { Request, Response } from "express"; +import { Router } from "express"; +import { auth } from "../../../../pkg/auth/auth.js"; +import { requireAuth } from "../../../../pkg/middleware/authMiddleware.js"; +import changePassword from "./changeUserPassword.js"; +import createUser from "./createUser.js"; +import deleteUser from "./deleteUser.js"; +import users from "./getUserRoles.js"; +import grantRoles from "./grantRole.js"; +import revokeRoles from "./revokeRole.js"; + +const router = Router(); + +router.use( + "/", + requireAuth("user", ["systemAdmin", "admin"]), // will pass bc system admin but this is just telling us we need this + grantRoles, +); + +router.use( + "/new", + requireAuth("user", ["systemAdmin", "admin"]), // will pass bc system admin but this is just telling us we need this + createUser, +); + +router.use( + "/", + requireAuth("user", ["systemAdmin", "admin"]), // will pass bc system admin but this is just telling us we need this + revokeRoles, +); + +router.use( + "/", + requireAuth("user", ["systemAdmin", "admin"]), // will pass bc system admin but this is just telling us we need this + users, +); + +router.use( + "/changePassword", + requireAuth("user", ["systemAdmin", "admin"]), // will pass bc system admin but this is just telling us we need this + changePassword, +); + +router.use( + "/delete", + requireAuth("user", ["systemAdmin", "admin"]), // will pass bc system admin but this is just telling us we need this + deleteUser, +); + +export default router; diff --git a/app/src/pkg/auth/auth.ts b/app/src/pkg/auth/auth.ts index 260bef6..6d0dde9 100644 --- a/app/src/pkg/auth/auth.ts +++ b/app/src/pkg/auth/auth.ts @@ -1,89 +1,93 @@ -import { drizzleAdapter } from "better-auth/adapters/drizzle"; -import { db } from "../db/db.js"; -import { username, admin, apiKey, jwt } from "better-auth/plugins"; import { betterAuth } from "better-auth"; -import * as rawSchema from "../db/schema/auth-schema.js"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { admin, apiKey, jwt, username } from "better-auth/plugins"; import type { User } from "better-auth/types"; import { eq } from "drizzle-orm"; +import { db } from "../db/db.js"; +import * as rawSchema from "../db/schema/auth-schema.js"; import { sendEmail } from "../utils/mail/sendMail.js"; export const schema = { - user: rawSchema.user, - session: rawSchema.session, - account: rawSchema.account, - verification: rawSchema.verification, - jwks: rawSchema.jwks, - apiKey: rawSchema.apikey, // 🔑 rename to apiKey + user: rawSchema.user, + session: rawSchema.session, + account: rawSchema.account, + verification: rawSchema.verification, + jwks: rawSchema.jwks, + apiKey: rawSchema.apikey, // 🔑 rename to apiKey }; const RESET_EXPIRY_SECONDS = 3600; // 1 hour export const auth = betterAuth({ - database: drizzleAdapter(db, { - provider: "pg", - schema, - }), - trustedOrigins: [ - "*.alpla.net", - "http://localhost:5173", - "http://localhost:5500", - "http://localhost:4200", - "http://localhost:4000", - ], - appName: "lst", - emailAndPassword: { - enabled: true, - minPasswordLength: 8, // optional config - resetPasswordTokenExpirySeconds: RESET_EXPIRY_SECONDS, // time in seconds - sendResetPassword: async ({ user, token }) => { - const frontendUrl = `${process.env.BETTER_AUTH_URL}/lst/app/user/resetpassword?token=${token}`; - const expiryMinutes = Math.floor(RESET_EXPIRY_SECONDS / 60); - const expiryText = - expiryMinutes >= 60 - ? `${expiryMinutes / 60} hour${ - expiryMinutes === 60 ? "" : "s" - }` - : `${expiryMinutes} minutes`; - const emailData = { - email: user.email, - subject: "LST- Forgot password request", - template: "forgotPassword", - context: { - username: user.name, - email: user.email, - url: frontendUrl, - expiry: expiryText, - }, - }; - await sendEmail(emailData); - }, - // onPasswordReset: async ({ user }, request) => { - // // your logic here - // console.log(`Password for user ${user.email} has been reset.`); - // }, - }, - plugins: [ - //jwt({ jwt: { expirationTime: "1h" } }), - apiKey(), - admin(), - username(), - ], - session: { - expiresIn: 60 * 60, - updateAge: 60 * 5, - freshAge: 60 * 2, - cookieCache: { - enabled: true, - maxAge: 5 * 60, // Cache duration in seconds - }, - }, - events: { - async onSignInSuccess({ user }: { user: User }) { - await db - .update(schema.user) - .set({ lastLogin: new Date() }) - .where(eq(schema.user.id, user.id)); - }, - }, + database: drizzleAdapter(db, { + provider: "pg", + schema, + }), + trustedOrigins: [ + "*.alpla.net", + "http://localhost:5173", + "http://localhost:5500", + "http://localhost:4200", + "http://localhost:4000", + ], + appName: "lst", + emailAndPassword: { + enabled: true, + minPasswordLength: 8, // optional config + resetPasswordTokenExpirySeconds: RESET_EXPIRY_SECONDS, // time in seconds + sendResetPassword: async ({ user, token }) => { + const frontendUrl = `${process.env.BETTER_AUTH_URL}/lst/app/user/resetpassword?token=${token}`; + const expiryMinutes = Math.floor(RESET_EXPIRY_SECONDS / 60); + const expiryText = + expiryMinutes >= 60 + ? `${expiryMinutes / 60} hour${expiryMinutes === 60 ? "" : "s"}` + : `${expiryMinutes} minutes`; + const emailData = { + email: user.email, + subject: "LST- Forgot password request", + template: "forgotPassword", + context: { + username: user.name, + email: user.email, + url: frontendUrl, + expiry: expiryText, + }, + }; + await sendEmail(emailData); + }, + // onPasswordReset: async ({ user }, request) => { + // // your logic here + // console.log(`Password for user ${user.email} has been reset.`); + // }, + }, + plugins: [ + //jwt({ jwt: { expirationTime: "1h" } }), + apiKey(), + admin(), + username(), + ], + session: { + expiresIn: 60 * 60, + updateAge: 60 * 5, + freshAge: 60 * 2, + cookieCache: { + enabled: true, + maxAge: 5 * 60, + }, + }, + cookie: { + path: "/lst/app", + sameSite: "lax", + secure: false, + httpOnly: true, + }, + events: { + async onSignInSuccess({ user }: { user: User }) { + await db + .update(schema.user) + .set({ lastLogin: new Date() }) + .where(eq(schema.user.id, user.id)); + }, + }, }); export type Auth = typeof auth; diff --git a/frontend/src/routes/_app/_adminLayout/-components/ExpandedRow.tsx b/frontend/src/routes/_app/_adminLayout/-components/ExpandedRow.tsx index 9e4bbaf..9c99e19 100644 --- a/frontend/src/routes/_app/_adminLayout/-components/ExpandedRow.tsx +++ b/frontend/src/routes/_app/_adminLayout/-components/ExpandedRow.tsx @@ -59,7 +59,7 @@ export default function ExpandedRow({ row }: { row: any }) { // user, // }); try { - const result = await api.patch(`/api/admin/${user.id}/grant`, { + const result = await api.patch(`/api/admin/users/${user.id}/grant`, { module: module, role: role, }); @@ -83,7 +83,7 @@ export default function ExpandedRow({ row }: { row: any }) { const onDeleteRole = async (module: string) => { try { - const result = await api.patch(`/api/admin/${user.id}/revoke`, { + const result = await api.patch(`/api/admin/users/${user.id}/revoke`, { module: module, });