5 Commits

11 changed files with 167 additions and 40 deletions

View File

@@ -18,18 +18,21 @@ Quick summary of current rewrite/migration goal.
| User Authentication | ~~Login~~, ~~Signup~~, API Key | 🟨 In Progress | | User Authentication | ~~Login~~, ~~Signup~~, API Key | 🟨 In Progress |
| User Profile | Edit profile, upload avatar | ⏳ Not Started | | User Profile | Edit profile, upload avatar | ⏳ Not Started |
| User Admin | Edit user, create user, remove user, alplaprod user integration | ⏳ Not Started | | User Admin | Edit user, create user, remove user, alplaprod user integration | ⏳ Not Started |
| Notifications | Subscribe, Create, Update | ⏳ Not Started | | Notifications | Subscribe, Create, Update, Remove, Manual Trigger | ⏳ Not Started |
| Datamart | Create, Update, Run | 🔧 In Progress | | Datamart | Create, Update, Run, Deactivate | 🔧 In Progress |
| Frontend | Analytics and charts | ⏳ Not Started | | Frontend | Analytics and charts | ⏳ Not Started |
| One Click Print | Get printers, monitor printers, label process, material process | ⏳ Not Started | | Docs | Instructions and trouble shooting | ⏳ Not Started |
| Silo Adjustments | Adjustments | ⏳ Not Started | | One Click Print | Get printers, monitor printers, label process, material process, Special processes | ⏳ Not Started |
| Demand Management | Orders, Forecast | ⏳ Not Started | | Silo Adjustments | Create, History, Comments | ⏳ Not Started |
| Demand Management | Orders, Forecast, Special Mappings, Create trucks, Load Trucks (tablet scanning) | ⏳ Not Started |
| Open Docks | Integrations | ⏳ Not Started | | Open Docks | Integrations | ⏳ Not Started |
| Transport Insight | Integrations | ⏳ Not Started | | Transport Insight | Integrations | ⏳ Not Started |
| Quality | Request Tool | ⏳ Not Started | | Quality Request Tool | Add Pallet, Monitor for moved, status changes, alerts | ⏳ Not Started |
| Logistics | Consume material, return and print, label info, relocate | ⏳ Not Started |
| EOM | Endpoints, Report Pull for finance | ⏳ Not Started |
| OCME | Custom integration | ⏳ Not Started | | OCME | Custom integration | ⏳ Not Started |
| API Migration | Moving to new REST endpoints | 🔧 In Progress | | API Migration | Moving to new REST endpoints | 🔧 In Progress |
| System | Tests, Updates, Remote Logging | ⏳ Not Started | | System | Tests,Builds, Updates, Remote Logging, DB Backups, Alerting | ⏳ Not Started |
_Status legend:_ _Status legend:_
✅ Complete🟨 In Progress ⏳ Not Started ✅ Complete🟨 In Progress ⏳ Not Started

View File

@@ -8,6 +8,32 @@ import { user } from "../db/schema/auth.schema.js";
import { auth } from "../utils/auth.utils.js"; import { auth } from "../utils/auth.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js"; import { apiReturn } from "../utils/returnHelper.utils.js";
// interface EmailLoginRequest {
// email: string;
// password: string;
// }
// interface LoginResponse {
// redirect: boolean;
// token: string;
// user: {
// name: string;
// email: string;
// emailVerified: boolean;
// image: string | null;
// createdAt: string;
// updatedAt: string;
// role: string;
// banned: boolean;
// banReason: string | null;
// banExpires: string | null;
// username: string;
// displayUsername: string;
// lastLogin: string;
// id: string;
// };
// }
const base = { const base = {
password: z.string().min(8, "Password must be at least 8 characters"), password: z.string().min(8, "Password must be at least 8 characters"),
}; };
@@ -28,7 +54,7 @@ const signin = z.union([
const r = Router(); const r = Router();
r.post("/", async (req, res) => { r.post("/", async (req, res) => {
let login: unknown = []; let login: unknown;
try { try {
const validated = signin.parse(req.body); const validated = signin.parse(req.body);
if ("email" in validated) { if ("email" in validated) {
@@ -69,6 +95,19 @@ r.post("/", async (req, res) => {
}); });
} }
// make sure we update the lastLogin
// if (login?.user?.id) {
// const updated = await db
// .update(user)
// .set({ lastLogin: sql`NOW()` })
// .where(eq(user.id, login.user.id))
// .returning({ lastLogin: user.lastLogin });
// const lastLoginTimestamp = updated[0]?.lastLogin;
// console.log("Updated lastLogin:", lastLoginTimestamp);
// } else
// console.warn("User ID unavailable — skipping lastLogin update");
return apiReturn(res, { return apiReturn(res, {
success: true, success: true,
level: "info", //connect.success ? "info" : "error", level: "info", //connect.success ? "info" : "error",
@@ -108,6 +147,16 @@ r.post("/", async (req, res) => {
status: 400, //connect.success ? 200 : 400, status: 400, //connect.success ? 200 : 400,
}); });
} }
return apiReturn(res, {
success: false,
level: "error",
module: "routes",
subModule: "auth",
message: "System Error",
data: [err],
status: 400,
});
} }
}); });

View File

@@ -38,6 +38,17 @@ export const openApiBase: OpenAPIV3_1.Document = {
scheme: "bearer", scheme: "bearer",
bearerFormat: "JWT", bearerFormat: "JWT",
}, },
ApiKeyAuth: {
type: "apiKey",
description: "API key required for authentication",
name: "api_key",
in: "header",
},
basicAuth: {
type: "http",
scheme: "basic",
description: "Basic authentication using username and password",
},
}, },
// schemas: { // schemas: {
// Error: { // Error: {
@@ -47,12 +58,24 @@ export const openApiBase: OpenAPIV3_1.Document = {
// message: { type: "string" }, // message: { type: "string" },
// }, // },
// }, // },
// }, // },.
}, },
tags: [ tags: [
// { name: "Health", description: "Health check endpoints" }, {
// { name: "Printing", description: "Label printing operations" }, name: "Auth",
// { name: "Silo", description: "Silo management" }, description:
"Authentication section where you get and create users and api keys",
},
{
name: "System",
description: "All system endpoints that will be available to run",
},
{
name: "Datamart",
description:
"All Special queries to run based on there names.\n Refer to the docs to see all possible queries that can be ran here, you can also run the getQueries to see available.",
},
// { name: "TMS", description: "TMS integration" }, // { name: "TMS", description: "TMS integration" },
], ],
paths: {}, // Will be populated paths: {}, // Will be populated
@@ -96,6 +119,7 @@ export const setupApiDocsRoutes = (baseUrl: string, app: Express) => {
targetKey: "node", targetKey: "node",
clientKey: "axios", clientKey: "axios",
}, },
documentDownloadType: "json", documentDownloadType: "json",
hideClientButton: true, hideClientButton: true,
hiddenClients: { hiddenClients: {

View File

@@ -1,6 +1,6 @@
/** /**
* each endpoint will be something like * each endpoint will be something like
* /api/datamart/{name}?{options} * /api/datamart/{name}?{criteria}
* *
* when getting the current queries we will need to map through the available queries we currently have and send back. * when getting the current queries we will need to map through the available queries we currently have and send back.
* example * example
@@ -8,7 +8,7 @@
* "name": "getopenorders", * "name": "getopenorders",
* "endpoint": "/api/datamart/getopenorders", * "endpoint": "/api/datamart/getopenorders",
* "description": "Returns open orders based on day count sent over, sDay 15 days in the past eDay 5 days in the future, can be left empty for this default days", * "description": "Returns open orders based on day count sent over, sDay 15 days in the past eDay 5 days in the future, can be left empty for this default days",
* "criteria": "sDay,eDay" * "options": "sDay,eDay"
* }, * },
* *
* when a criteria is password over we will handle it by counting how many were passed up to 3 then deal with each one respectively * when a criteria is password over we will handle it by counting how many were passed up to 3 then deal with each one respectively

View File

@@ -1,18 +1,56 @@
import { Router } from "express"; import { Router } from "express";
import z from "zod";
import type { NewDatamart } from "../db/schema/datamart.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js"; import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router(); const r = Router();
r.post("/add", async (_, res) => { const newQuery = z.object({
apiReturn(res, { name: z.string().min(5),
success: true, description: z.string().min(30),
level: "info", query: z.string().min(10),
module: "routes", options: z
subModule: "prodSql", .string()
message: "connect.message", .describe("This should be a set of keys separated by a comma")
data: [{ connect: "" }], .optional(),
status: 200, });
});
r.post("/add", async (req, res) => {
try {
const v = newQuery.parse(req.body);
const query: NewDatamart = { ...v };
console.log(query);
} catch (err) {
if (err instanceof z.ZodError) {
const flattened = z.flattenError(err);
// return res.status(400).json({
// error: "Validation failed",
// details: flattened,
// });
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "routes",
subModule: "auth",
message: "Validation failed",
data: [flattened.fieldErrors],
status: 400, //connect.success ? 200 : 400,
});
}
return apiReturn(res, {
success: false,
level: "error",
module: "routes",
subModule: "datamart",
message: "connect.message",
data: [{ connect: "" }],
status: 200,
});
}
}); });
export default r; export default r;

View File

@@ -25,7 +25,6 @@ export const user = pgTable("user", {
banExpires: timestamp("ban_expires"), banExpires: timestamp("ban_expires"),
username: text("username").unique(), username: text("username").unique(),
displayUsername: text("display_username"), displayUsername: text("display_username"),
lastLogin: timestamp("last_login").defaultNow(),
}); });
export const session = pgTable( export const session = pgTable(

View File

@@ -14,7 +14,7 @@ export const datamart = pgTable("datamart", {
name: text("name"), name: text("name"),
description: text("description").notNull(), description: text("description").notNull(),
query: text("query"), query: text("query"),
version: integer("version").notNull(), version: integer("version").default(1).notNull(),
active: boolean("active").default(true), active: boolean("active").default(true),
options: text("checked").default(""), options: text("checked").default(""),
add_date: timestamp("add_date").defaultNow(), add_date: timestamp("add_date").defaultNow(),

View File

@@ -1,10 +1,12 @@
import { betterAuth, type User } from "better-auth"; import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { import {
admin, admin,
apiKey, apiKey,
createAuthMiddleware,
customSession, customSession,
jwt, jwt,
lastLoginMethod,
username, username,
} from "better-auth/plugins"; } from "better-auth/plugins";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
@@ -38,7 +40,7 @@ export const auth = betterAuth({
}, },
lastLogin: { lastLogin: {
type: "date", type: "date",
required: false, required: true,
input: false, input: false,
}, },
}, },
@@ -47,6 +49,7 @@ export const auth = betterAuth({
jwt({ jwt: { expirationTime: "1h" } }), jwt({ jwt: { expirationTime: "1h" } }),
apiKey(), apiKey(),
admin(), admin(),
lastLoginMethod(),
username({ username({
minUsernameLength: 5, minUsernameLength: 5,
usernameValidator: (username) => { usernameValidator: (username) => {
@@ -119,12 +122,22 @@ export const auth = betterAuth({
secure: false, secure: false,
httpOnly: true, httpOnly: true,
}, },
hooks: {
after: createAuthMiddleware(async (ctx) => {
if (ctx.path.startsWith("/login")) {
const newSession = ctx.context.newSession;
if (newSession) {
// something here later
}
}
}),
},
events: { events: {
async onSignInSuccess({ user }: { user: User }) { // async onSignInSuccess({ user }: { user: User }) {
await db // await db
.update(rawSchema.user) // .update(rawSchema.user)
.set({ lastLogin: new Date() }) // .set({ lastLogin: new Date() })
.where(eq(schema.user.id, user.id)); // .where(eq(schema.user.id, user.id));
}, // },
}, },
}); });

View File

@@ -11,7 +11,8 @@ interface Data {
| "prodSql" | "prodSql"
| "query" | "query"
| "sendmail" | "sendmail"
| "auth"; | "auth"
| "datamart";
level: "info" | "error" | "debug" | "fatal"; level: "info" | "error" | "debug" | "fatal";
message: string; message: string;
data?: unknown[]; data?: unknown[];

View File

@@ -1,13 +1,13 @@
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import type { Transporter } from "nodemailer"; import type { Transporter } from "nodemailer";
import nodemailer from "nodemailer"; import nodemailer from "nodemailer";
import type Mail from "nodemailer/lib/mailer/index.js"; import type Mail from "nodemailer/lib/mailer/index.js";
import type { Address } from "nodemailer/lib/mailer/index.js"; import type { Address } from "nodemailer/lib/mailer/index.js";
import type SMTPTransport from "nodemailer/lib/smtp-transport/index.js"; import type SMTPTransport from "nodemailer/lib/smtp-transport/index.js";
import hbs from "nodemailer-express-handlebars"; import hbs from "nodemailer-express-handlebars";
import os from "os";
import path from "path";
import { fileURLToPath } from "url";
import { promisify } from "util";
import { returnFunc } from "./returnHelper.utils.js"; import { returnFunc } from "./returnHelper.utils.js";
import { tryCatch } from "./trycatch.utils.js"; import { tryCatch } from "./trycatch.utils.js";

View File

@@ -12,7 +12,7 @@ describe("Prod SQL connection", () => {
}); });
afterAll(async () => { afterAll(async () => {
if (pool && pool.close) await pool.close(); if (pool?.close) await pool.close();
}); });
it("should connect and return expected data", async () => { it("should connect and return expected data", async () => {