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 Profile | Edit profile, upload avatar | ⏳ Not Started |
| User Admin | Edit user, create user, remove user, alplaprod user integration | ⏳ Not Started |
| Notifications | Subscribe, Create, Update | ⏳ Not Started |
| Datamart | Create, Update, Run | 🔧 In Progress |
| Notifications | Subscribe, Create, Update, Remove, Manual Trigger | ⏳ Not Started |
| Datamart | Create, Update, Run, Deactivate | 🔧 In Progress |
| Frontend | Analytics and charts | ⏳ Not Started |
| One Click Print | Get printers, monitor printers, label process, material process | ⏳ Not Started |
| Silo Adjustments | Adjustments | ⏳ Not Started |
| Demand Management | Orders, Forecast | ⏳ Not Started |
| Docs | Instructions and trouble shooting | ⏳ Not Started |
| One Click Print | Get printers, monitor printers, label process, material process, Special processes | ⏳ 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 |
| 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 |
| 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:_
✅ 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 { 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 = {
password: z.string().min(8, "Password must be at least 8 characters"),
};
@@ -28,7 +54,7 @@ const signin = z.union([
const r = Router();
r.post("/", async (req, res) => {
let login: unknown = [];
let login: unknown;
try {
const validated = signin.parse(req.body);
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, {
success: true,
level: "info", //connect.success ? "info" : "error",
@@ -108,6 +147,16 @@ r.post("/", async (req, res) => {
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",
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: {
// Error: {
@@ -47,12 +58,24 @@ export const openApiBase: OpenAPIV3_1.Document = {
// message: { type: "string" },
// },
// },
// },
// },.
},
tags: [
// { name: "Health", description: "Health check endpoints" },
// { name: "Printing", description: "Label printing operations" },
// { name: "Silo", description: "Silo management" },
{
name: "Auth",
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" },
],
paths: {}, // Will be populated
@@ -96,6 +119,7 @@ export const setupApiDocsRoutes = (baseUrl: string, app: Express) => {
targetKey: "node",
clientKey: "axios",
},
documentDownloadType: "json",
hideClientButton: true,
hiddenClients: {

View File

@@ -1,6 +1,6 @@
/**
* 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.
* example
@@ -8,7 +8,7 @@
* "name": "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",
* "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

View File

@@ -1,18 +1,56 @@
import { Router } from "express";
import z from "zod";
import type { NewDatamart } from "../db/schema/datamart.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
r.post("/add", async (_, res) => {
apiReturn(res, {
success: true,
level: "info",
const newQuery = z.object({
name: z.string().min(5),
description: z.string().min(30),
query: z.string().min(10),
options: z
.string()
.describe("This should be a set of keys separated by a comma")
.optional(),
});
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: "prodSql",
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;

View File

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

View File

@@ -14,7 +14,7 @@ export const datamart = pgTable("datamart", {
name: text("name"),
description: text("description").notNull(),
query: text("query"),
version: integer("version").notNull(),
version: integer("version").default(1).notNull(),
active: boolean("active").default(true),
options: text("checked").default(""),
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 {
admin,
apiKey,
createAuthMiddleware,
customSession,
jwt,
lastLoginMethod,
username,
} from "better-auth/plugins";
import { eq } from "drizzle-orm";
@@ -38,7 +40,7 @@ export const auth = betterAuth({
},
lastLogin: {
type: "date",
required: false,
required: true,
input: false,
},
},
@@ -47,6 +49,7 @@ export const auth = betterAuth({
jwt({ jwt: { expirationTime: "1h" } }),
apiKey(),
admin(),
lastLoginMethod(),
username({
minUsernameLength: 5,
usernameValidator: (username) => {
@@ -119,12 +122,22 @@ export const auth = betterAuth({
secure: false,
httpOnly: true,
},
events: {
async onSignInSuccess({ user }: { user: User }) {
await db
.update(rawSchema.user)
.set({ lastLogin: new Date() })
.where(eq(schema.user.id, user.id));
hooks: {
after: createAuthMiddleware(async (ctx) => {
if (ctx.path.startsWith("/login")) {
const newSession = ctx.context.newSession;
if (newSession) {
// something here later
}
}
}),
},
events: {
// async onSignInSuccess({ user }: { user: User }) {
// await db
// .update(rawSchema.user)
// .set({ lastLogin: new Date() })
// .where(eq(schema.user.id, user.id));
// },
},
});

View File

@@ -11,7 +11,8 @@ interface Data {
| "prodSql"
| "query"
| "sendmail"
| "auth";
| "auth"
| "datamart";
level: "info" | "error" | "debug" | "fatal";
message: string;
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 nodemailer from "nodemailer";
import type Mail 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 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 { tryCatch } from "./trycatch.utils.js";

View File

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