feat(auth): signupm, forgot passowrd, reset password all added
This commit is contained in:
@@ -6,7 +6,7 @@ lstWrapper/publish/LstWrapper.exe
|
|||||||
lstWrapper/publish/LstWrapper.pdb
|
lstWrapper/publish/LstWrapper.pdb
|
||||||
lstWrapper/publish/LstWrapper.runtimeconfig.json
|
lstWrapper/publish/LstWrapper.runtimeconfig.json
|
||||||
lstWrapper/publish/LstWrapper.staticwebassets.endpoints.json
|
lstWrapper/publish/LstWrapper.staticwebassets.endpoints.json
|
||||||
web.config
|
lstWrapper/publish/web.config
|
||||||
controller/lst_ctl.exe
|
controller/lst_ctl.exe
|
||||||
controller/.env-example
|
controller/.env-example
|
||||||
scripts/update-controller-bumpBuild.ps1
|
scripts/update-controller-bumpBuild.ps1
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
meta {
|
||||||
|
name: Request Resetpassword
|
||||||
|
type: http
|
||||||
|
seq: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{url}}/lst/api/user/resetPassword
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"email": "blake.matthes@alpla.com",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
}
|
||||||
25
LogisticsSupportTool_API_DOCS/app/auth/Resetpassword.bru
Normal file
25
LogisticsSupportTool_API_DOCS/app/auth/Resetpassword.bru
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
meta {
|
||||||
|
name: Resetpassword
|
||||||
|
type: http
|
||||||
|
seq: 11
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: http://localhost:4200/api/auth/reset-password/gCo7OUP6CH2Qu7obhvOrhuo9?callbackURL
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
params:query {
|
||||||
|
callbackURL:
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"newPassword": "nova0511"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
}
|
||||||
38
app/src/internal/auth/routes/resetPassword.ts
Normal file
38
app/src/internal/auth/routes/resetPassword.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Router, type Request, type Response } from "express";
|
||||||
|
|
||||||
|
import { authClient } from "../../../pkg/auth/auth-client.js";
|
||||||
|
import z from "zod";
|
||||||
|
import { auth } from "../../../pkg/auth/auth.js";
|
||||||
|
|
||||||
|
const resetSchema = z.object({
|
||||||
|
email: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.post("/", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const validated = resetSchema.parse(req.body);
|
||||||
|
|
||||||
|
const data = await auth.api.requestPasswordReset({
|
||||||
|
body: {
|
||||||
|
email: validated.email,
|
||||||
|
redirectTo: `${process.env.BETTER_AUTH_URL}/user/resetpassword`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return res.json(data);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof z.ZodError) {
|
||||||
|
const flattened = z.flattenError(err);
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Validation failed",
|
||||||
|
details: flattened,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(err);
|
||||||
|
return res.status(400).json({ error: err });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -2,10 +2,12 @@ import type { Express, Request, Response } from "express";
|
|||||||
import me from "./me.js";
|
import me from "./me.js";
|
||||||
import register from "./register.js";
|
import register from "./register.js";
|
||||||
import userRoles from "./userroles.js";
|
import userRoles from "./userroles.js";
|
||||||
|
import resetPassword from "./resetPassword.js";
|
||||||
import { requireAuth } from "../../../pkg/middleware/authMiddleware.js";
|
import { requireAuth } from "../../../pkg/middleware/authMiddleware.js";
|
||||||
|
|
||||||
export const setupAuthRoutes = (app: Express, basePath: string) => {
|
export const setupAuthRoutes = (app: Express, basePath: string) => {
|
||||||
app.use(basePath + "/api/user/me", requireAuth(), me);
|
app.use(basePath + "/api/user/me", requireAuth(), me);
|
||||||
|
app.use(basePath + "/api/user/resetpassword", resetPassword);
|
||||||
app.use(basePath + "/api/user/register", register);
|
app.use(basePath + "/api/user/register", register);
|
||||||
app.use(basePath + "/api/user/roles", requireAuth(), userRoles);
|
app.use(basePath + "/api/user/roles", requireAuth(), userRoles);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import type { auth } from "./auth.js";
|
import type { auth } from "./auth.js";
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
baseURL: "http://localhost:3000",
|
baseURL: process.env.BETTER_AUTH_URL,
|
||||||
plugins: [
|
plugins: [
|
||||||
inferAdditionalFields<typeof auth>(),
|
inferAdditionalFields<typeof auth>(),
|
||||||
usernameClient(),
|
usernameClient(),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { betterAuth } from "better-auth";
|
|||||||
import * as rawSchema from "../db/schema/auth-schema.js";
|
import * as rawSchema from "../db/schema/auth-schema.js";
|
||||||
import type { User } from "better-auth/types";
|
import type { User } from "better-auth/types";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
import { sendEmail } from "../utils/mail/sendMail.js";
|
||||||
|
|
||||||
export const schema = {
|
export const schema = {
|
||||||
user: rawSchema.user,
|
user: rawSchema.user,
|
||||||
@@ -14,6 +15,7 @@ export const schema = {
|
|||||||
jwks: rawSchema.jwks,
|
jwks: rawSchema.jwks,
|
||||||
apiKey: rawSchema.apikey, // 🔑 rename to apiKey
|
apiKey: rawSchema.apikey, // 🔑 rename to apiKey
|
||||||
};
|
};
|
||||||
|
const RESET_EXPIRY_SECONDS = 3600; // 1 hour
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
database: drizzleAdapter(db, {
|
database: drizzleAdapter(db, {
|
||||||
@@ -24,11 +26,40 @@ export const auth = betterAuth({
|
|||||||
"*.alpla.net",
|
"*.alpla.net",
|
||||||
"http://localhost:5173",
|
"http://localhost:5173",
|
||||||
"http://localhost:5500",
|
"http://localhost:5500",
|
||||||
|
"http://localhost:4200",
|
||||||
|
"http://localhost:4000",
|
||||||
],
|
],
|
||||||
appName: "lst",
|
appName: "lst",
|
||||||
emailAndPassword: {
|
emailAndPassword: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
minPasswordLength: 8, // optional config
|
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: [
|
plugins: [
|
||||||
//jwt({ jwt: { expirationTime: "1h" } }),
|
//jwt({ jwt: { expirationTime: "1h" } }),
|
||||||
|
|||||||
@@ -2,6 +2,24 @@ import type { Request, Response, NextFunction } from "express";
|
|||||||
import { db } from "../db/db.js";
|
import { db } from "../db/db.js";
|
||||||
import { apiHits } from "../db/schema/apiHits.js";
|
import { apiHits } from "../db/schema/apiHits.js";
|
||||||
|
|
||||||
|
type StripPasswords<T> = T extends Array<infer U>
|
||||||
|
? StripPasswords<U>[]
|
||||||
|
: T extends object
|
||||||
|
? { [K in keyof T as Exclude<K, "password">]: StripPasswords<T[K]> }
|
||||||
|
: T;
|
||||||
|
|
||||||
|
function stripPasswords<T>(input: T): StripPasswords<T> {
|
||||||
|
if (Array.isArray(input)) {
|
||||||
|
return input.map(stripPasswords) as StripPasswords<T>;
|
||||||
|
} else if (input && typeof input === "object") {
|
||||||
|
const entries = Object.entries(input as object)
|
||||||
|
.filter(([key]) => key !== "password")
|
||||||
|
.map(([key, value]) => [key, stripPasswords(value)]);
|
||||||
|
return Object.fromEntries(entries) as StripPasswords<T>;
|
||||||
|
}
|
||||||
|
return input as StripPasswords<T>;
|
||||||
|
}
|
||||||
|
|
||||||
export function apiHitMiddleware(
|
export function apiHitMiddleware(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
@@ -15,7 +33,7 @@ export function apiHitMiddleware(
|
|||||||
await db.insert(apiHits).values({
|
await db.insert(apiHits).values({
|
||||||
method: req.method,
|
method: req.method,
|
||||||
path: req.originalUrl,
|
path: req.originalUrl,
|
||||||
body: JSON.stringify(req.body ?? {}),
|
body: JSON.stringify(req.body ? stripPasswords(req.body) : {}),
|
||||||
status: res.statusCode,
|
status: res.statusCode,
|
||||||
ip: req.ip,
|
ip: req.ip,
|
||||||
duration: Date.now() - start,
|
duration: Date.now() - start,
|
||||||
|
|||||||
123
app/src/pkg/utils/mail/sendMail.ts
Normal file
123
app/src/pkg/utils/mail/sendMail.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import type { Address } from "nodemailer/lib/mailer/index.js";
|
||||||
|
import type { Transporter } from "nodemailer";
|
||||||
|
import type SMTPTransport from "nodemailer/lib/smtp-transport/index.js";
|
||||||
|
import type Mail from "nodemailer/lib/mailer/index.js";
|
||||||
|
import os from "os";
|
||||||
|
import nodemailer from "nodemailer";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { promisify } from "util";
|
||||||
|
import hbs from "nodemailer-express-handlebars";
|
||||||
|
import { createLogger } from "../../logger/logger.js";
|
||||||
|
|
||||||
|
interface HandlebarsMailOptions extends Mail.Options {
|
||||||
|
template: string;
|
||||||
|
context: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EmailData {
|
||||||
|
email: string;
|
||||||
|
subject: string;
|
||||||
|
template: string;
|
||||||
|
context: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sendEmail = async (data: EmailData): Promise<any> => {
|
||||||
|
const log = createLogger({ module: "pkg", subModule: "sendMail" });
|
||||||
|
let transporter: Transporter;
|
||||||
|
let fromEmail: string | Address;
|
||||||
|
|
||||||
|
if (
|
||||||
|
os.hostname().includes("OLP") &&
|
||||||
|
process.env.EMAIL_USER &&
|
||||||
|
process.env.EMAIL_PASSWORD
|
||||||
|
) {
|
||||||
|
transporter = nodemailer.createTransport({
|
||||||
|
service: "gmail",
|
||||||
|
auth: {
|
||||||
|
user: process.env.EMAIL_USER,
|
||||||
|
pass: process.env.EMAIL_PASSWORD,
|
||||||
|
},
|
||||||
|
//debug: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// update the from email
|
||||||
|
fromEmail = process.env.EMAIL_USER;
|
||||||
|
} else {
|
||||||
|
// convert to the correct plant token.
|
||||||
|
|
||||||
|
let host = `${os.hostname().replace("VMS006", "")}-smtp.alpla.net`;
|
||||||
|
|
||||||
|
//const testServers = ["vms036", "VMS036"];
|
||||||
|
|
||||||
|
if (os.hostname().includes("VMS036")) {
|
||||||
|
host = "USMCD1-smtp.alpla.net";
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (plantToken[0].value === "usiow2") {
|
||||||
|
// host = "USIOW1-smtp.alpla.net";
|
||||||
|
// }
|
||||||
|
|
||||||
|
transporter = nodemailer.createTransport({
|
||||||
|
host: host,
|
||||||
|
port: 25,
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
//secure: false,
|
||||||
|
// auth: {
|
||||||
|
// user: "alplaprod",
|
||||||
|
// pass: "obelix",
|
||||||
|
// },
|
||||||
|
debug: true,
|
||||||
|
} as SMTPTransport.Options);
|
||||||
|
|
||||||
|
// update the from email
|
||||||
|
fromEmail = `noreply@alpla.com`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// creating the handlbar options
|
||||||
|
const viewPath = path.resolve(
|
||||||
|
path.dirname(fileURLToPath(import.meta.url)),
|
||||||
|
"./views/"
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlebarOptions = {
|
||||||
|
viewEngine: {
|
||||||
|
extname: ".hbs",
|
||||||
|
//layoutsDir: path.resolve(viewPath, "layouts"), // Path to layouts directory
|
||||||
|
defaultLayout: "", // Specify the default layout
|
||||||
|
partialsDir: viewPath,
|
||||||
|
},
|
||||||
|
viewPath: viewPath,
|
||||||
|
extName: ".hbs", // File extension for Handlebars templates
|
||||||
|
};
|
||||||
|
|
||||||
|
transporter.use("compile", hbs(handlebarOptions));
|
||||||
|
|
||||||
|
const mailOptions: HandlebarsMailOptions = {
|
||||||
|
from: fromEmail,
|
||||||
|
to: data.email,
|
||||||
|
subject: data.subject,
|
||||||
|
//text: "You will have a reset token here and only have 30min to click the link before it expires.",
|
||||||
|
//html: emailTemplate("BlakesTest", "This is an example with css"),
|
||||||
|
template: data.template, // Name of the Handlebars template (e.g., 'welcome.hbs')
|
||||||
|
context: data.context,
|
||||||
|
};
|
||||||
|
|
||||||
|
// now verify and send the email
|
||||||
|
const sendMailPromise = promisify(transporter.sendMail).bind(transporter);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send email and await the result
|
||||||
|
const info = await sendMailPromise(mailOptions);
|
||||||
|
log.info(null, `Email was sent to: ${data.email}`);
|
||||||
|
return { success: true, message: "Email sent.", data: info };
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
log.error(
|
||||||
|
{ error: err },
|
||||||
|
|
||||||
|
`Error sending Email to : ${data.email}`
|
||||||
|
);
|
||||||
|
return { success: false, message: "Error sending email.", error: err };
|
||||||
|
}
|
||||||
|
};
|
||||||
69
app/src/pkg/utils/mail/views/forgotPassword.hbs
Normal file
69
app/src/pkg/utils/mail/views/forgotPassword.hbs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Password Reset</title>
|
||||||
|
{{> styles}}
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
.email-container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 24px;
|
||||||
|
margin: 20px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #4f46e5;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666666;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="email-container">
|
||||||
|
<h2>Password Reset Request</h2>
|
||||||
|
<p>Hi {{username}},</p>
|
||||||
|
|
||||||
|
<p>We received a request to reset the password for your account ({{email}}). If this was you, you can create a new password by clicking the button below:</p>
|
||||||
|
|
||||||
|
<p style="text-align:center;">
|
||||||
|
<a href="{{url}}" class="btn">Reset Your Password</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>If the button above doesn’t work, copy and paste the following link into your web browser:</p>
|
||||||
|
<p style="word-break: break-all; color:#4f46e5;">{{url}}</p>
|
||||||
|
|
||||||
|
<p><strong>Note:</strong> This link will expire in <b>{{expiry}}</b> for your security.</p>
|
||||||
|
|
||||||
|
<p>If you did not request a password reset, you can safely ignore this email — your account will remain secure.</p>
|
||||||
|
|
||||||
|
<p>Thanks,<br/>The LST Team</p>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>You are receiving this email because a password reset was requested for your account.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
6
app/src/pkg/utils/mail/views/styles.hbs
Normal file
6
app/src/pkg/utils/mail/views/styles.hbs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<style>
|
||||||
|
table { width: 100%; background-color: #ffffff; border-collapse: collapse;
|
||||||
|
border-width: 2px; border-color: #14BDEA; border-style: solid; color:
|
||||||
|
#000000; } th, td { border: 1px solid #ddd; padding: 8px; } th {
|
||||||
|
background-color: #f4f4f4; }
|
||||||
|
</style>
|
||||||
@@ -79,6 +79,7 @@
|
|||||||
<button id="USBET1VMS006">USBET1VMS006</button>
|
<button id="USBET1VMS006">USBET1VMS006</button>
|
||||||
<button id="USBOW1VMS006">USBOW1VMS006</button>
|
<button id="USBOW1VMS006">USBOW1VMS006</button>
|
||||||
<button id="USKSC1VMS006">USKSC1VMS006</button>
|
<button id="USKSC1VMS006">USKSC1VMS006</button>
|
||||||
|
<button id="USMCD1VMS006">USMCD1VMS006</button>
|
||||||
<button id="USSLC1VMS006">USSLC1VMS006</button>
|
<button id="USSLC1VMS006">USSLC1VMS006</button>
|
||||||
<button id="USSTP1VMS006">USSTP1VMS006</button>
|
<button id="USSTP1VMS006">USSTP1VMS006</button>
|
||||||
<button id="USDAY1VMS006">USDAY1VMS006</button>
|
<button id="USDAY1VMS006">USDAY1VMS006</button>
|
||||||
@@ -266,6 +267,14 @@
|
|||||||
}); // "frontend" = payload target
|
}); // "frontend" = payload target
|
||||||
logMessage("info", "Copying to USDAY1VMS006");
|
logMessage("info", "Copying to USDAY1VMS006");
|
||||||
};
|
};
|
||||||
|
document.getElementById("USMCD1VMS006").onclick = () => {
|
||||||
|
socket.emit("update", {
|
||||||
|
action: "copy",
|
||||||
|
target: "USMCD1VMS006",
|
||||||
|
drive: "e",
|
||||||
|
}); // "frontend" = payload target
|
||||||
|
logMessage("info", "Copying to USMCD1VMS006");
|
||||||
|
};
|
||||||
|
|
||||||
socket.on("logs", (msg) => logMessage("logs", msg));
|
socket.on("logs", (msg) => logMessage("logs", msg));
|
||||||
socket.on("errors", (msg) => logMessage("errors", msg));
|
socket.on("errors", (msg) => logMessage("errors", msg));
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Server, Settings, User, type LucideIcon } from "lucide-react";
|
import { Server, Settings, User, type LucideIcon } from "lucide-react";
|
||||||
|
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||||
import { userAccess, type UserRoles } from "../../lib/authClient";
|
import { userAccess, type UserRoles } from "../../lib/authClient";
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
@@ -65,6 +66,10 @@ export default function Admin() {
|
|||||||
</>
|
</>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{userAccess(null, ["systemAdmin"]) && (
|
||||||
|
<TanStackRouterDevtools position="bottom-right" />
|
||||||
|
)}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
|
|||||||
@@ -21,16 +21,17 @@ export default function Nav() {
|
|||||||
<div className="m-1">
|
<div className="m-1">
|
||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
</div>
|
</div>
|
||||||
{/* <div className="mr-1 ml-1">
|
<div className="m-1">
|
||||||
{settings.length > 0 && (
|
<Button>
|
||||||
<a
|
<a
|
||||||
href={`https://${server[0].value}.alpla.net/lst/d`}
|
href={`${window.location.origin}/lst/d`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
LST - Docs |
|
LST - Docs
|
||||||
</a>
|
</a>
|
||||||
)}
|
</Button>
|
||||||
</div> */}
|
</div>
|
||||||
|
|
||||||
{session ? (
|
{session ? (
|
||||||
<div className="m-1">
|
<div className="m-1">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { Sidebar, SidebarFooter, SidebarTrigger } from "../ui/sidebar";
|
import {
|
||||||
|
Sidebar,
|
||||||
|
SidebarContent,
|
||||||
|
SidebarFooter,
|
||||||
|
SidebarTrigger,
|
||||||
|
} from "../ui/sidebar";
|
||||||
import { Header } from "./Header";
|
import { Header } from "./Header";
|
||||||
import Admin from "./Admin";
|
import Admin from "./Admin";
|
||||||
import { userAccess } from "../../lib/authClient";
|
import { userAccess } from "../../lib/authClient";
|
||||||
@@ -8,7 +13,9 @@ export default function SideBarNav() {
|
|||||||
<div className="flex min-h-screen">
|
<div className="flex min-h-screen">
|
||||||
<Sidebar collapsible="icon">
|
<Sidebar collapsible="icon">
|
||||||
<Header />
|
<Header />
|
||||||
{userAccess(null, ["systemAdmin", "admin"]) && <Admin />}
|
<SidebarContent>
|
||||||
|
{userAccess(null, ["systemAdmin", "admin"]) && <Admin />}
|
||||||
|
</SidebarContent>
|
||||||
|
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<SidebarTrigger />
|
<SidebarTrigger />
|
||||||
|
|||||||
52
frontend/src/lib/formStuff/components/InputPasswordField.tsx
Normal file
52
frontend/src/lib/formStuff/components/InputPasswordField.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useFieldContext } from "..";
|
||||||
|
import { FieldErrors } from "./FieldErrors";
|
||||||
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
|
import { Label } from "../../../components/ui/label";
|
||||||
|
import { Input } from "../../../components/ui/input";
|
||||||
|
import { Button } from "../../../components/ui/button";
|
||||||
|
|
||||||
|
type PasswordInputProps = {
|
||||||
|
label: string;
|
||||||
|
required?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InputPasswordField = ({
|
||||||
|
label,
|
||||||
|
required = false,
|
||||||
|
}: PasswordInputProps) => {
|
||||||
|
const field = useFieldContext<any>();
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<Label htmlFor={field.name}>{label}</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
type={show ? "text" : "password"}
|
||||||
|
value={field.state.value}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
required={required}
|
||||||
|
className="pr-10"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setShow((p) => !p)}
|
||||||
|
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7"
|
||||||
|
tabIndex={-1} // prevent messing with tab order
|
||||||
|
>
|
||||||
|
{show ? (
|
||||||
|
<EyeOff className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<FieldErrors meta={field.state.meta} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,12 +3,18 @@ import { SubmitButton } from "./components/SubmitButton";
|
|||||||
import { InputField } from "./components/InputField";
|
import { InputField } from "./components/InputField";
|
||||||
import { SelectField } from "./components/SelectField";
|
import { SelectField } from "./components/SelectField";
|
||||||
import { CheckboxField } from "./components/CheckBox";
|
import { CheckboxField } from "./components/CheckBox";
|
||||||
|
import { InputPasswordField } from "./components/InputPasswordField";
|
||||||
|
|
||||||
export const { fieldContext, useFieldContext, formContext, useFormContext } =
|
export const { fieldContext, useFieldContext, formContext, useFormContext } =
|
||||||
createFormHookContexts();
|
createFormHookContexts();
|
||||||
|
|
||||||
export const { useAppForm } = createFormHook({
|
export const { useAppForm } = createFormHook({
|
||||||
fieldComponents: { InputField, SelectField, CheckboxField },
|
fieldComponents: {
|
||||||
|
InputField,
|
||||||
|
InputPasswordField,
|
||||||
|
SelectField,
|
||||||
|
CheckboxField,
|
||||||
|
},
|
||||||
formComponents: { SubmitButton },
|
formComponents: { SubmitButton },
|
||||||
fieldContext,
|
fieldContext,
|
||||||
formContext,
|
formContext,
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { Route as authLoginRouteImport } from './routes/(auth)/login'
|
|||||||
import { Route as AdminLayoutAdminUsersRouteImport } from './routes/_adminLayout/admin/users'
|
import { Route as AdminLayoutAdminUsersRouteImport } from './routes/_adminLayout/admin/users'
|
||||||
import { Route as AdminLayoutAdminSettingsRouteImport } from './routes/_adminLayout/admin/settings'
|
import { Route as AdminLayoutAdminSettingsRouteImport } from './routes/_adminLayout/admin/settings'
|
||||||
import { Route as AdminLayoutAdminServersRouteImport } from './routes/_adminLayout/admin/servers'
|
import { Route as AdminLayoutAdminServersRouteImport } from './routes/_adminLayout/admin/servers'
|
||||||
|
import { Route as authUserSignupRouteImport } from './routes/(auth)/user/signup'
|
||||||
|
import { Route as authUserResetpasswordRouteImport } from './routes/(auth)/user/resetpassword'
|
||||||
|
|
||||||
const AdminLayoutRouteRoute = AdminLayoutRouteRouteImport.update({
|
const AdminLayoutRouteRoute = AdminLayoutRouteRouteImport.update({
|
||||||
id: '/_adminLayout',
|
id: '/_adminLayout',
|
||||||
@@ -46,10 +48,22 @@ const AdminLayoutAdminServersRoute = AdminLayoutAdminServersRouteImport.update({
|
|||||||
path: '/admin/servers',
|
path: '/admin/servers',
|
||||||
getParentRoute: () => AdminLayoutRouteRoute,
|
getParentRoute: () => AdminLayoutRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const authUserSignupRoute = authUserSignupRouteImport.update({
|
||||||
|
id: '/(auth)/user/signup',
|
||||||
|
path: '/user/signup',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const authUserResetpasswordRoute = authUserResetpasswordRouteImport.update({
|
||||||
|
id: '/(auth)/user/resetpassword',
|
||||||
|
path: '/user/resetpassword',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/login': typeof authLoginRoute
|
'/login': typeof authLoginRoute
|
||||||
|
'/user/resetpassword': typeof authUserResetpasswordRoute
|
||||||
|
'/user/signup': typeof authUserSignupRoute
|
||||||
'/admin/servers': typeof AdminLayoutAdminServersRoute
|
'/admin/servers': typeof AdminLayoutAdminServersRoute
|
||||||
'/admin/settings': typeof AdminLayoutAdminSettingsRoute
|
'/admin/settings': typeof AdminLayoutAdminSettingsRoute
|
||||||
'/admin/users': typeof AdminLayoutAdminUsersRoute
|
'/admin/users': typeof AdminLayoutAdminUsersRoute
|
||||||
@@ -57,6 +71,8 @@ export interface FileRoutesByFullPath {
|
|||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/login': typeof authLoginRoute
|
'/login': typeof authLoginRoute
|
||||||
|
'/user/resetpassword': typeof authUserResetpasswordRoute
|
||||||
|
'/user/signup': typeof authUserSignupRoute
|
||||||
'/admin/servers': typeof AdminLayoutAdminServersRoute
|
'/admin/servers': typeof AdminLayoutAdminServersRoute
|
||||||
'/admin/settings': typeof AdminLayoutAdminSettingsRoute
|
'/admin/settings': typeof AdminLayoutAdminSettingsRoute
|
||||||
'/admin/users': typeof AdminLayoutAdminUsersRoute
|
'/admin/users': typeof AdminLayoutAdminUsersRoute
|
||||||
@@ -66,6 +82,8 @@ export interface FileRoutesById {
|
|||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/_adminLayout': typeof AdminLayoutRouteRouteWithChildren
|
'/_adminLayout': typeof AdminLayoutRouteRouteWithChildren
|
||||||
'/(auth)/login': typeof authLoginRoute
|
'/(auth)/login': typeof authLoginRoute
|
||||||
|
'/(auth)/user/resetpassword': typeof authUserResetpasswordRoute
|
||||||
|
'/(auth)/user/signup': typeof authUserSignupRoute
|
||||||
'/_adminLayout/admin/servers': typeof AdminLayoutAdminServersRoute
|
'/_adminLayout/admin/servers': typeof AdminLayoutAdminServersRoute
|
||||||
'/_adminLayout/admin/settings': typeof AdminLayoutAdminSettingsRoute
|
'/_adminLayout/admin/settings': typeof AdminLayoutAdminSettingsRoute
|
||||||
'/_adminLayout/admin/users': typeof AdminLayoutAdminUsersRoute
|
'/_adminLayout/admin/users': typeof AdminLayoutAdminUsersRoute
|
||||||
@@ -75,16 +93,27 @@ export interface FileRouteTypes {
|
|||||||
fullPaths:
|
fullPaths:
|
||||||
| '/'
|
| '/'
|
||||||
| '/login'
|
| '/login'
|
||||||
|
| '/user/resetpassword'
|
||||||
|
| '/user/signup'
|
||||||
| '/admin/servers'
|
| '/admin/servers'
|
||||||
| '/admin/settings'
|
| '/admin/settings'
|
||||||
| '/admin/users'
|
| '/admin/users'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/' | '/login' | '/admin/servers' | '/admin/settings' | '/admin/users'
|
to:
|
||||||
|
| '/'
|
||||||
|
| '/login'
|
||||||
|
| '/user/resetpassword'
|
||||||
|
| '/user/signup'
|
||||||
|
| '/admin/servers'
|
||||||
|
| '/admin/settings'
|
||||||
|
| '/admin/users'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
| '/_adminLayout'
|
| '/_adminLayout'
|
||||||
| '/(auth)/login'
|
| '/(auth)/login'
|
||||||
|
| '/(auth)/user/resetpassword'
|
||||||
|
| '/(auth)/user/signup'
|
||||||
| '/_adminLayout/admin/servers'
|
| '/_adminLayout/admin/servers'
|
||||||
| '/_adminLayout/admin/settings'
|
| '/_adminLayout/admin/settings'
|
||||||
| '/_adminLayout/admin/users'
|
| '/_adminLayout/admin/users'
|
||||||
@@ -94,6 +123,8 @@ export interface RootRouteChildren {
|
|||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
AdminLayoutRouteRoute: typeof AdminLayoutRouteRouteWithChildren
|
AdminLayoutRouteRoute: typeof AdminLayoutRouteRouteWithChildren
|
||||||
authLoginRoute: typeof authLoginRoute
|
authLoginRoute: typeof authLoginRoute
|
||||||
|
authUserResetpasswordRoute: typeof authUserResetpasswordRoute
|
||||||
|
authUserSignupRoute: typeof authUserSignupRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
@@ -140,6 +171,20 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AdminLayoutAdminServersRouteImport
|
preLoaderRoute: typeof AdminLayoutAdminServersRouteImport
|
||||||
parentRoute: typeof AdminLayoutRouteRoute
|
parentRoute: typeof AdminLayoutRouteRoute
|
||||||
}
|
}
|
||||||
|
'/(auth)/user/signup': {
|
||||||
|
id: '/(auth)/user/signup'
|
||||||
|
path: '/user/signup'
|
||||||
|
fullPath: '/user/signup'
|
||||||
|
preLoaderRoute: typeof authUserSignupRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/(auth)/user/resetpassword': {
|
||||||
|
id: '/(auth)/user/resetpassword'
|
||||||
|
path: '/user/resetpassword'
|
||||||
|
fullPath: '/user/resetpassword'
|
||||||
|
preLoaderRoute: typeof authUserResetpasswordRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +207,8 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
AdminLayoutRouteRoute: AdminLayoutRouteRouteWithChildren,
|
AdminLayoutRouteRoute: AdminLayoutRouteRouteWithChildren,
|
||||||
authLoginRoute: authLoginRoute,
|
authLoginRoute: authLoginRoute,
|
||||||
|
authUserResetpasswordRoute: authUserResetpasswordRoute,
|
||||||
|
authUserSignupRoute: authUserSignupRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export default function LoginForm() {
|
|||||||
|
|
||||||
const session = await getSession();
|
const session = await getSession();
|
||||||
setSession(session);
|
setSession(session);
|
||||||
|
form.reset();
|
||||||
fetchRoles();
|
fetchRoles();
|
||||||
|
|
||||||
toast.success(
|
toast.success(
|
||||||
@@ -54,12 +55,13 @@ export default function LoginForm() {
|
|||||||
router.invalidate();
|
router.invalidate();
|
||||||
router.history.push(search.redirect ? search.redirect : "/");
|
router.history.push(search.redirect ? search.redirect : "/");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
// @ts-ignore
|
||||||
|
toast.error(error?.message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div className="ml-[25%]">
|
<div className="">
|
||||||
<LstCard className="p-3 w-96">
|
<LstCard className="p-3 w-96">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Login to your account</CardTitle>
|
<CardTitle>Login to your account</CardTitle>
|
||||||
@@ -87,9 +89,8 @@ export default function LoginForm() {
|
|||||||
<form.AppField
|
<form.AppField
|
||||||
name="password"
|
name="password"
|
||||||
children={(field) => (
|
children={(field) => (
|
||||||
<field.InputField
|
<field.InputPasswordField
|
||||||
label="Password"
|
label="Password"
|
||||||
inputType="password"
|
|
||||||
required={true}
|
required={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -102,7 +103,7 @@ export default function LoginForm() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/user/resetpassword"
|
||||||
className="inline-block text-sm underline-offset-4 hover:underline"
|
className="inline-block text-sm underline-offset-4 hover:underline"
|
||||||
>
|
>
|
||||||
Forgot your password?
|
Forgot your password?
|
||||||
@@ -118,9 +119,12 @@ export default function LoginForm() {
|
|||||||
|
|
||||||
<div className="mt-4 text-center text-sm">
|
<div className="mt-4 text-center text-sm">
|
||||||
Don't have an account?{" "}
|
Don't have an account?{" "}
|
||||||
<a href="#" className="underline underline-offset-4">
|
<Link
|
||||||
|
to={"/user/signup"}
|
||||||
|
className="underline underline-offset-4"
|
||||||
|
>
|
||||||
Sign up
|
Sign up
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</LstCard>
|
</LstCard>
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { LstCard } from "../../../components/ui/lstCard";
|
||||||
|
import {
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "../../../components/ui/card";
|
||||||
|
import { useAppForm } from "../../../lib/formStuff";
|
||||||
|
import { api } from "../../../lib/axiosAPI";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export default function RequestResetPassword() {
|
||||||
|
const form = useAppForm({
|
||||||
|
defaultValues: {
|
||||||
|
email: "",
|
||||||
|
},
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
try {
|
||||||
|
const res = await api.post("api/user/resetpassword", {
|
||||||
|
email: value.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(res);
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.success(
|
||||||
|
res.data.message
|
||||||
|
? res.data.message
|
||||||
|
: "If this email exists in our system, check your email for the reset link"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
<LstCard className="p-6 w-96">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Reset your password</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter your email address and we’ll send you a reset link
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form.AppField
|
||||||
|
name="email"
|
||||||
|
children={(field) => (
|
||||||
|
<field.InputField
|
||||||
|
label="Email address"
|
||||||
|
inputType="email"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-6">
|
||||||
|
<form.AppForm>
|
||||||
|
<form.SubmitButton>
|
||||||
|
Send Reset Link
|
||||||
|
</form.SubmitButton>
|
||||||
|
</form.AppForm>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center text-sm text-gray-600">
|
||||||
|
Remembered your password?{" "}
|
||||||
|
<Link
|
||||||
|
to="/login"
|
||||||
|
className="text-primary underline underline-offset-4 hover:text-primary/80"
|
||||||
|
>
|
||||||
|
Back to login
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</LstCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
114
frontend/src/routes/(auth)/-components/ResetPasswordForm.tsx
Normal file
114
frontend/src/routes/(auth)/-components/ResetPasswordForm.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { useAppForm } from "../../../lib/formStuff";
|
||||||
|
import { LstCard } from "../../../components/ui/lstCard";
|
||||||
|
import {
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "../../../components/ui/card";
|
||||||
|
import { api } from "../../../lib/axiosAPI";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Link, useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export default function ResetPasswordForm({ token }: { token: string }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const form = useAppForm({
|
||||||
|
defaultValues: {
|
||||||
|
password: "",
|
||||||
|
confirmPassword: "",
|
||||||
|
},
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
if (value.password != value.confirmPassword) {
|
||||||
|
toast.error("Passwords do not match");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await api.post("/api/auth/reset-password", {
|
||||||
|
newPassword: value.password,
|
||||||
|
token: token,
|
||||||
|
});
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.success("Password has been reset");
|
||||||
|
form.reset();
|
||||||
|
navigate({ to: "/login" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
// @ts-ignore
|
||||||
|
toast.error(error?.response.data.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
<LstCard className="p-6 w-96">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Set a new password</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter your new password below and confirm it to continue
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form.AppField
|
||||||
|
name="password"
|
||||||
|
children={(field) => (
|
||||||
|
<field.InputPasswordField
|
||||||
|
label="New Password"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<form.AppField
|
||||||
|
name="confirmPassword"
|
||||||
|
// validators={{
|
||||||
|
// onChangeListenTo: ["password"],
|
||||||
|
// onChange: ({ value, fieldApi }) => {
|
||||||
|
// if (
|
||||||
|
// value !==
|
||||||
|
// fieldApi.form.getFieldValue("password")
|
||||||
|
// ) {
|
||||||
|
// return "Passwords do not match";
|
||||||
|
// }
|
||||||
|
// return undefined;
|
||||||
|
// },
|
||||||
|
// }}
|
||||||
|
children={(field) => (
|
||||||
|
<field.InputPasswordField
|
||||||
|
label="Confirm Password"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-6">
|
||||||
|
<form.AppForm>
|
||||||
|
<form.SubmitButton>
|
||||||
|
Reset Password
|
||||||
|
</form.SubmitButton>
|
||||||
|
</form.AppForm>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center text-sm text-gray-600">
|
||||||
|
Remembered your account?{" "}
|
||||||
|
<Link
|
||||||
|
to="/login"
|
||||||
|
className="text-primary underline underline-offset-4 hover:text-primary/80"
|
||||||
|
>
|
||||||
|
Back to login
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</LstCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
129
frontend/src/routes/(auth)/-components/SignupForm.tsx
Normal file
129
frontend/src/routes/(auth)/-components/SignupForm.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { toast } from "sonner";
|
||||||
|
import {
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "../../../components/ui/card";
|
||||||
|
import { LstCard } from "../../../components/ui/lstCard";
|
||||||
|
import { api } from "../../../lib/axiosAPI";
|
||||||
|
import { useAppForm } from "../../../lib/formStuff";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export default function SignupForm() {
|
||||||
|
const form = useAppForm({
|
||||||
|
defaultValues: {
|
||||||
|
username: "",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
confirmPassword: "",
|
||||||
|
},
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
if (value.password != value.confirmPassword) {
|
||||||
|
toast.error("Passwords do not match");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await api.post("/api/user/register", {
|
||||||
|
username: value.username,
|
||||||
|
name: value.username,
|
||||||
|
email: value.email,
|
||||||
|
password: value.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.success(`Welcome ${value.username}, to lst.`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
// @ts-ignore
|
||||||
|
toast.error(error?.response.data.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
<LstCard className="p-6 w-96">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Create an account</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Fill in your details to get started
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Username */}
|
||||||
|
<form.AppField
|
||||||
|
name="username"
|
||||||
|
children={(field) => (
|
||||||
|
<field.InputField
|
||||||
|
label="Username"
|
||||||
|
inputType="text"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Email */}
|
||||||
|
<form.AppField
|
||||||
|
name="email"
|
||||||
|
children={(field) => (
|
||||||
|
<field.InputField
|
||||||
|
label="Email address"
|
||||||
|
inputType="email"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Password */}
|
||||||
|
<form.AppField
|
||||||
|
name="password"
|
||||||
|
children={(field) => (
|
||||||
|
<field.InputField
|
||||||
|
label="Password"
|
||||||
|
inputType="password"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Confirm Password */}
|
||||||
|
<form.AppField
|
||||||
|
name="confirmPassword"
|
||||||
|
children={(field) => (
|
||||||
|
<field.InputField
|
||||||
|
label="Confirm Password"
|
||||||
|
inputType="password"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-6">
|
||||||
|
<form.AppForm>
|
||||||
|
<form.SubmitButton>Sign Up</form.SubmitButton>
|
||||||
|
</form.AppForm>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center text-sm text-gray-600">
|
||||||
|
Already have an account?{" "}
|
||||||
|
<Link
|
||||||
|
to={"/login"}
|
||||||
|
className="text-primary underline underline-offset-4 hover:text-primary/80"
|
||||||
|
>
|
||||||
|
Log in
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</LstCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ export const Route = createFileRoute("/(auth)/login")({
|
|||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
return (
|
return (
|
||||||
<div className="m-[25%]">
|
<div className="ml-[25%] mt-[0.5%]">
|
||||||
<LoginForm />
|
<LoginForm />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
27
frontend/src/routes/(auth)/user/resetpassword.tsx
Normal file
27
frontend/src/routes/(auth)/user/resetpassword.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import z from "zod";
|
||||||
|
import RequestResetPassword from "../-components/RequestResetPassword";
|
||||||
|
import ResetPasswordForm from "../-components/ResetPasswordForm";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/(auth)/user/resetpassword")({
|
||||||
|
// beforeLoad: ({ search }) => {
|
||||||
|
// return { token: search.token };
|
||||||
|
// },
|
||||||
|
validateSearch: z.object({
|
||||||
|
token: z.string().optional(),
|
||||||
|
}),
|
||||||
|
component: RouteComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const { token } = Route.useSearch();
|
||||||
|
return (
|
||||||
|
<div className="ml-[25%] mt-[0.5%]">
|
||||||
|
{token ? (
|
||||||
|
<ResetPasswordForm token={token} />
|
||||||
|
) : (
|
||||||
|
<RequestResetPassword />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
frontend/src/routes/(auth)/user/signup.tsx
Normal file
14
frontend/src/routes/(auth)/user/signup.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import SignupForm from "../-components/SignupForm";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/(auth)/user/signup")({
|
||||||
|
component: RouteComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return (
|
||||||
|
<div className="ml-[25%] mt-[0.5%]">
|
||||||
|
<SignupForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
|
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
|
||||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
|
||||||
import type { QueryClient } from "@tanstack/react-query";
|
import type { QueryClient } from "@tanstack/react-query";
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
@@ -18,7 +18,6 @@ interface RootRouteContext {
|
|||||||
|
|
||||||
const RootLayout = () => {
|
const RootLayout = () => {
|
||||||
//const { logout, login } = Route.useRouteContext();
|
//const { logout, login } = Route.useRouteContext();
|
||||||
|
|
||||||
const defaultOpen = Cookies.get("sidebar_state") === "true";
|
const defaultOpen = Cookies.get("sidebar_state") === "true";
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -26,16 +25,15 @@ const RootLayout = () => {
|
|||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<div className="flex flex-col h-screen overflow-hidden">
|
<div className="flex flex-col h-screen overflow-hidden">
|
||||||
<Nav />
|
<Nav />
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
<SidebarProvider defaultOpen={defaultOpen}>
|
<SidebarProvider defaultOpen={defaultOpen}>
|
||||||
<SideBarNav />
|
<SideBarNav />
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-2 overflow-y-auto">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</div>
|
</div>
|
||||||
<Toaster expand richColors closeButton />
|
<Toaster expand richColors closeButton />
|
||||||
<TanStackRouterDevtools />
|
|
||||||
</div>
|
</div>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</SessionGuard>
|
</SessionGuard>
|
||||||
|
|||||||
@@ -1,25 +1,13 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { Button } from "../components/ui/button";
|
|
||||||
import { useAuth } from "../lib/authClient";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
component: Index,
|
component: Index,
|
||||||
});
|
});
|
||||||
|
|
||||||
function Index() {
|
function Index() {
|
||||||
const { session } = useAuth();
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="h-screen flex flex-col items-center justify-center">
|
<div className="h-screen flex flex-col items-center justify-center"></div>
|
||||||
<div>Welcome, {session ? session.user.username : "Guest"}</div>
|
|
||||||
<div>
|
|
||||||
<Button className="h-96 w-96">
|
|
||||||
<a href="/lst/d" target="_blank" className="text-4xl">
|
|
||||||
LST-DOCS
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { useFieldContext } from "..";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { FieldErrors } from "./FieldErrors";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
|
|
||||||
|
type PasswordInputProps = {
|
||||||
|
label: string;
|
||||||
|
required?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InputPassword = ({
|
||||||
|
label,
|
||||||
|
required = false,
|
||||||
|
}: PasswordInputProps) => {
|
||||||
|
const field = useFieldContext<any>();
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<Label htmlFor={field.name}>{label}</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
type={show ? "text" : "password"}
|
||||||
|
value={field.state.value}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
required={required}
|
||||||
|
className="pr-10"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setShow((p) => !p)}
|
||||||
|
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7"
|
||||||
|
tabIndex={-1} // prevent messing with tab order
|
||||||
|
>
|
||||||
|
{show ? (
|
||||||
|
<EyeOff className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<FieldErrors meta={field.state.meta} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -154,6 +154,14 @@ app.Run(async (HttpContext context) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Inject client IP into X-Forwarded-For
|
||||||
|
var remoteIp = context.Connection.RemoteIpAddress?.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(remoteIp))
|
||||||
|
{
|
||||||
|
request.Headers.Remove("X-Forwarded-For");
|
||||||
|
request.Headers.TryAddWithoutValidation("X-Forwarded-For", remoteIp);
|
||||||
|
}
|
||||||
|
|
||||||
if (context.Request.ContentLength > 0 && request.Content == null)
|
if (context.Request.ContentLength > 0 && request.Content == null)
|
||||||
{
|
{
|
||||||
request.Content = new StreamContent(context.Request.Body);
|
request.Content = new StreamContent(context.Request.Body);
|
||||||
|
|||||||
10
migrations/0007_common_kronos.sql
Normal file
10
migrations/0007_common_kronos.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
CREATE TABLE "apiHits" (
|
||||||
|
"apiHit_id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"method" text NOT NULL,
|
||||||
|
"path" text NOT NULL,
|
||||||
|
"body" jsonb,
|
||||||
|
"status" integer,
|
||||||
|
"ip" text,
|
||||||
|
"duration" integer,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
887
migrations/meta/0007_snapshot.json
Normal file
887
migrations/meta/0007_snapshot.json
Normal file
@@ -0,0 +1,887 @@
|
|||||||
|
{
|
||||||
|
"id": "33b56ace-aa18-498f-922f-513e13ccc6ef",
|
||||||
|
"prevId": "2e845c85-5248-4082-a69a-77c18e50f044",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.apiHits": {
|
||||||
|
"name": "apiHits",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"apiHit_id": {
|
||||||
|
"name": "apiHit_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"method": {
|
||||||
|
"name": "method",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"name": "path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"name": "body",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ip": {
|
||||||
|
"name": "ip",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"name": "duration",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"provider_id": {
|
||||||
|
"name": "provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"name": "id_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"access_token_expires_at": {
|
||||||
|
"name": "access_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token_expires_at": {
|
||||||
|
"name": "refresh_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"account_user_id_user_id_fk": {
|
||||||
|
"name": "account_user_id_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.apikey": {
|
||||||
|
"name": "apikey",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"name": "start",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"prefix": {
|
||||||
|
"name": "prefix",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"refill_interval": {
|
||||||
|
"name": "refill_interval",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refill_amount": {
|
||||||
|
"name": "refill_amount",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"last_refill_at": {
|
||||||
|
"name": "last_refill_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"name": "enabled",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"rate_limit_enabled": {
|
||||||
|
"name": "rate_limit_enabled",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"rate_limit_time_window": {
|
||||||
|
"name": "rate_limit_time_window",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": 86400000
|
||||||
|
},
|
||||||
|
"rate_limit_max": {
|
||||||
|
"name": "rate_limit_max",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": 10
|
||||||
|
},
|
||||||
|
"request_count": {
|
||||||
|
"name": "request_count",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"remaining": {
|
||||||
|
"name": "remaining",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"last_request": {
|
||||||
|
"name": "last_request",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"name": "permissions",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"name": "metadata",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"apikey_user_id_user_id_fk": {
|
||||||
|
"name": "apikey_user_id_user_id_fk",
|
||||||
|
"tableFrom": "apikey",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.jwks": {
|
||||||
|
"name": "jwks",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"public_key": {
|
||||||
|
"name": "public_key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"private_key": {
|
||||||
|
"name": "private_key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.session": {
|
||||||
|
"name": "session",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"ip_address": {
|
||||||
|
"name": "ip_address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_agent": {
|
||||||
|
"name": "user_agent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"impersonated_by": {
|
||||||
|
"name": "impersonated_by",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"session_user_id_user_id_fk": {
|
||||||
|
"name": "session_user_id_user_id_fk",
|
||||||
|
"tableFrom": "session",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"session_token_unique": {
|
||||||
|
"name": "session_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email_verified": {
|
||||||
|
"name": "email_verified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"banned": {
|
||||||
|
"name": "banned",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"ban_reason": {
|
||||||
|
"name": "ban_reason",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ban_expires": {
|
||||||
|
"name": "ban_expires",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"display_username": {
|
||||||
|
"name": "display_username",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"last_login": {
|
||||||
|
"name": "last_login",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"user_username_unique": {
|
||||||
|
"name": "user_username_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"username"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.verification": {
|
||||||
|
"name": "verification",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.logs": {
|
||||||
|
"name": "logs",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"log_id": {
|
||||||
|
"name": "log_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"name": "level",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"module": {
|
||||||
|
"name": "module",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"subModule": {
|
||||||
|
"name": "subModule",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"name": "message",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"stack": {
|
||||||
|
"name": "stack",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'[]'::jsonb"
|
||||||
|
},
|
||||||
|
"checked": {
|
||||||
|
"name": "checked",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"hostname": {
|
||||||
|
"name": "hostname",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.settings": {
|
||||||
|
"name": "settings",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"settings_id": {
|
||||||
|
"name": "settings_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"moduleName": {
|
||||||
|
"name": "moduleName",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"active": {
|
||||||
|
"name": "active",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"name": "roles",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'[\"systemAdmin\"]'::jsonb"
|
||||||
|
},
|
||||||
|
"add_User": {
|
||||||
|
"name": "add_User",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'LST_System'"
|
||||||
|
},
|
||||||
|
"add_Date": {
|
||||||
|
"name": "add_Date",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"upd_User": {
|
||||||
|
"name": "upd_User",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'LST_System'"
|
||||||
|
},
|
||||||
|
"upd_date": {
|
||||||
|
"name": "upd_date",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "name",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user_roles": {
|
||||||
|
"name": "user_roles",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"user_role_id": {
|
||||||
|
"name": "user_role_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"module": {
|
||||||
|
"name": "module",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"unique_user_module": {
|
||||||
|
"name": "unique_user_module",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "user_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "module",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"user_roles_user_id_user_id_fk": {
|
||||||
|
"name": "user_roles_user_id_user_id_fk",
|
||||||
|
"tableFrom": "user_roles",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,13 @@
|
|||||||
"when": 1758588364052,
|
"when": 1758588364052,
|
||||||
"tag": "0006_loud_reavers",
|
"tag": "0006_loud_reavers",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 7,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1758755206107,
|
||||||
|
"tag": "0007_common_kronos",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
1877
package-lock.json
generated
1877
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dotenvx/dotenvx": "^1.49.0",
|
"@dotenvx/dotenvx": "^1.49.0",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"better-auth": "^1.3.9",
|
"better-auth": "^1.3.9",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
@@ -49,8 +50,11 @@
|
|||||||
"drizzle-orm": "^0.44.5",
|
"drizzle-orm": "^0.44.5",
|
||||||
"drizzle-zod": "^0.8.3",
|
"drizzle-zod": "^0.8.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"handlebars": "^4.7.8",
|
||||||
"morgan": "^1.10.1",
|
"morgan": "^1.10.1",
|
||||||
"mssql": "^11.0.1",
|
"mssql": "^11.0.1",
|
||||||
|
"nodemailer": "^7.0.6",
|
||||||
|
"nodemailer-express-handlebars": "^7.0.0",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"pino": "^9.9.0",
|
"pino": "^9.9.0",
|
||||||
"pino-pretty": "^13.1.1",
|
"pino-pretty": "^13.1.1",
|
||||||
@@ -62,6 +66,8 @@
|
|||||||
"@types/morgan": "^1.9.10",
|
"@types/morgan": "^1.9.10",
|
||||||
"@types/mssql": "^9.1.7",
|
"@types/mssql": "^9.1.7",
|
||||||
"@types/node": "^24.3.0",
|
"@types/node": "^24.3.0",
|
||||||
|
"@types/nodemailer": "^7.0.1",
|
||||||
|
"@types/nodemailer-express-handlebars": "^4.0.5",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"cz-conventional-changelog": "^3.3.0",
|
"cz-conventional-changelog": "^3.3.0",
|
||||||
"standard-version": "^9.5.0",
|
"standard-version": "^9.5.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user