umpordez 0.0.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +16 -0
- package/build.mjs +409 -0
- package/cli.mjs +87 -109
- package/create.mjs +334 -0
- package/package.json +2 -7
- package/template/.claude/settings.local.json +20 -0
- package/template/.editorconfig +11 -0
- package/template/.github/copilot-instructions.md +186 -0
- package/template/.nvimrc +7 -0
- package/template/.vscode/extensions.json +9 -0
- package/template/.vscode/settings.json +28 -0
- package/template/AGENTS.md +305 -0
- package/template/CLAUDE.md +140 -0
- package/template/architecture.md +518 -0
- package/template/code.md +211 -0
- package/template/dev.sh +45 -0
- package/template/install.sh +47 -0
- package/template/misc/nginx.conf +151 -0
- package/template/misc/systemd/{{PROJECT_SLUG}}-api.service +31 -0
- package/template/misc/systemd/{{PROJECT_SLUG}}-site-api.service +31 -0
- package/template/misc/systemd/{{PROJECT_SLUG}}-site.service +30 -0
- package/template/seed.sh +23 -0
- package/template/server/.env.sample +40 -0
- package/template/server/.prettierrc +4 -0
- package/template/server/apps/api/app.ts +42 -0
- package/template/server/apps/api/routes/account.ts +98 -0
- package/template/server/apps/api/routes/admin.ts +80 -0
- package/template/server/apps/api/routes/auth.ts +222 -0
- package/template/server/apps/api/routes/files.ts +89 -0
- package/template/server/apps/api/routes/index.ts +19 -0
- package/template/server/apps/api/routes/user.ts +17 -0
- package/template/server/apps/shared/middlewares/demand-account-access.ts +52 -0
- package/template/server/apps/shared/middlewares/demand-admin-user.ts +25 -0
- package/template/server/apps/shared/middlewares/demand-user.ts +17 -0
- package/template/server/apps/shared/middlewares/error-handler.ts +28 -0
- package/template/server/apps/shared/middlewares/request-logger.ts +40 -0
- package/template/server/apps/shared/middlewares/try-set-user-by-token.ts +26 -0
- package/template/server/apps/shared/utils.ts +71 -0
- package/template/server/apps/site-api/app.ts +42 -0
- package/template/server/apps/site-api/routes/index.ts +10 -0
- package/template/server/apps/site-api/routes/public.ts +25 -0
- package/template/server/console/index.ts +26 -0
- package/template/server/console/runner.ts +67 -0
- package/template/server/console/tasks/example.ts +29 -0
- package/template/server/console/tasks/seed-admin.ts +71 -0
- package/template/server/core/context.ts +49 -0
- package/template/server/core/db.ts +12 -0
- package/template/server/core/email.ts +87 -0
- package/template/server/core/errors.ts +44 -0
- package/template/server/core/knexfile.ts +28 -0
- package/template/server/core/logger.ts +60 -0
- package/template/server/core/models/account.ts +319 -0
- package/template/server/core/models/auth.ts +317 -0
- package/template/server/core/models/base.ts +19 -0
- package/template/server/core/models/user.ts +343 -0
- package/template/server/core/s3.ts +183 -0
- package/template/server/emails/_styles.css +5 -0
- package/template/server/emails/_template.html +28 -0
- package/template/server/emails/accountWelcome.html +4 -0
- package/template/server/emails/forgetPassword.html +5 -0
- package/template/server/eslint.config.js +16 -0
- package/template/server/knex.sh +4 -0
- package/template/server/migrations/20260208000000_initial-schema.ts +56 -0
- package/template/server/migrations/20260208000001_seed-admin.ts +42 -0
- package/template/server/migrations/20260208000002_add-user-avatar.ts +13 -0
- package/template/server/package.json +58 -0
- package/template/server/tsconfig.json +27 -0
- package/template/server/types/api.ts +20 -0
- package/template/server/types/express.d.ts +35 -0
- package/template/ui/admin/.prettierrc +4 -0
- package/template/ui/admin/components.json +20 -0
- package/template/ui/admin/eslint.config.js +16 -0
- package/template/ui/admin/index.html +12 -0
- package/template/ui/admin/package.json +57 -0
- package/template/ui/admin/postcss.config.js +6 -0
- package/template/ui/admin/src/app.tsx +13 -0
- package/template/ui/admin/src/components/nav-user.tsx +95 -0
- package/template/ui/admin/src/components/theme-provider.tsx +65 -0
- package/template/ui/admin/src/components/theme-toggle.tsx +17 -0
- package/template/ui/admin/src/components/ui/badge.tsx +36 -0
- package/template/ui/admin/src/components/ui/button.tsx +56 -0
- package/template/ui/admin/src/components/ui/calendar.tsx +68 -0
- package/template/ui/admin/src/components/ui/card.tsx +79 -0
- package/template/ui/admin/src/components/ui/checkbox.tsx +28 -0
- package/template/ui/admin/src/components/ui/date-picker.tsx +78 -0
- package/template/ui/admin/src/components/ui/date-range-picker.tsx +208 -0
- package/template/ui/admin/src/components/ui/dialog.tsx +121 -0
- package/template/ui/admin/src/components/ui/dropdown-menu.tsx +200 -0
- package/template/ui/admin/src/components/ui/input.tsx +22 -0
- package/template/ui/admin/src/components/ui/label.tsx +24 -0
- package/template/ui/admin/src/components/ui/popover.tsx +29 -0
- package/template/ui/admin/src/components/ui/select.tsx +158 -0
- package/template/ui/admin/src/components/ui/separator.tsx +29 -0
- package/template/ui/admin/src/components/ui/sheet.tsx +136 -0
- package/template/ui/admin/src/components/ui/sidebar.tsx +759 -0
- package/template/ui/admin/src/components/ui/skeleton.tsx +15 -0
- package/template/ui/admin/src/components/ui/sonner.tsx +26 -0
- package/template/ui/admin/src/components/ui/switch.tsx +27 -0
- package/template/ui/admin/src/components/ui/table.tsx +117 -0
- package/template/ui/admin/src/components/ui/tabs.tsx +53 -0
- package/template/ui/admin/src/components/ui/textarea.tsx +22 -0
- package/template/ui/admin/src/components/ui/tooltip.tsx +28 -0
- package/template/ui/admin/src/hooks/queries/use-accounts.ts +68 -0
- package/template/ui/admin/src/hooks/queries/use-auth.ts +61 -0
- package/template/ui/admin/src/hooks/queries/use-dashboard.ts +70 -0
- package/template/ui/admin/src/hooks/queries/use-members.ts +90 -0
- package/template/ui/admin/src/hooks/queries/use-users.ts +99 -0
- package/template/ui/admin/src/hooks/use-confirm.tsx +92 -0
- package/template/ui/admin/src/hooks/use-mobile.tsx +22 -0
- package/template/ui/admin/src/hooks/use-sidebar.tsx +4 -0
- package/template/ui/admin/src/hooks/use-user.tsx +86 -0
- package/template/ui/admin/src/index.css +22 -0
- package/template/ui/admin/src/layouts/account.tsx +278 -0
- package/template/ui/admin/src/layouts/admin.tsx +182 -0
- package/template/ui/admin/src/layouts/public.tsx +9 -0
- package/template/ui/admin/src/layouts/user.tsx +107 -0
- package/template/ui/admin/src/lib/config.ts +9 -0
- package/template/ui/admin/src/lib/fetch-api.ts +101 -0
- package/template/ui/admin/src/lib/query-client.ts +12 -0
- package/template/ui/admin/src/lib/query-keys.ts +44 -0
- package/template/ui/admin/src/lib/utils.ts +24 -0
- package/template/ui/admin/src/main.tsx +17 -0
- package/template/ui/admin/src/pages/account/dashboard.tsx +128 -0
- package/template/ui/admin/src/pages/account/members.tsx +414 -0
- package/template/ui/admin/src/pages/account/settings.tsx +210 -0
- package/template/ui/admin/src/pages/admin/accounts.tsx +164 -0
- package/template/ui/admin/src/pages/admin/dashboard.tsx +243 -0
- package/template/ui/admin/src/pages/admin/users.tsx +395 -0
- package/template/ui/admin/src/pages/public/error.tsx +24 -0
- package/template/ui/admin/src/pages/public/forgot-password.tsx +74 -0
- package/template/ui/admin/src/pages/public/login.tsx +102 -0
- package/template/ui/admin/src/pages/public/reset-password.tsx +106 -0
- package/template/ui/admin/src/pages/public/signup.tsx +118 -0
- package/template/ui/admin/src/pages/user/profile.tsx +313 -0
- package/template/ui/admin/src/pages/user/select-account.tsx +114 -0
- package/template/ui/admin/src/router.tsx +71 -0
- package/template/ui/admin/tailwind.config.ts +71 -0
- package/template/ui/admin/tsconfig.json +24 -0
- package/template/ui/admin/vite.config.ts +15 -0
- package/template/ui/site/.env.sample +3 -0
- package/template/ui/site/.prettierrc +4 -0
- package/template/ui/site/app.ts +50 -0
- package/template/ui/site/eslint.config.js +16 -0
- package/template/ui/site/package.json +33 -0
- package/template/ui/site/styles/input.css +3 -0
- package/template/ui/site/tailwind.config.js +8 -0
- package/template/ui/site/tsconfig.json +15 -0
- package/template/ui/site/views/pages/home.ejs +305 -0
- package/template/ui/site/views/partials/footer.ejs +44 -0
- package/template/ui/site/views/partials/header.ejs +83 -0
- package/template-variables.mjs +186 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import logger from '#core/logger.js';
|
|
3
|
+
|
|
4
|
+
export default function requestLogger(
|
|
5
|
+
req: Request,
|
|
6
|
+
res: Response,
|
|
7
|
+
next: NextFunction
|
|
8
|
+
): void {
|
|
9
|
+
const { ip, method, url } = req;
|
|
10
|
+
const started = Date.now();
|
|
11
|
+
|
|
12
|
+
const userId = req.user?.id;
|
|
13
|
+
let idsMsg = '';
|
|
14
|
+
if (userId) {
|
|
15
|
+
idsMsg = ` u=${userId}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (process.env.DEBUG) {
|
|
19
|
+
logger.debug(`[${ip}] {${method}} Receiving ${url}${idsMsg}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
res.on('finish', () => {
|
|
23
|
+
const usr = req.user?.id;
|
|
24
|
+
if (usr && !idsMsg) {
|
|
25
|
+
idsMsg = ` u=${usr}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const msg = `{req} [${ip}] ${method} ${url}${idsMsg} : http=${res.statusCode} ${Date.now() - started}ms`;
|
|
29
|
+
|
|
30
|
+
if (res.statusCode >= 500) {
|
|
31
|
+
logger.error(msg);
|
|
32
|
+
} else if (res.statusCode >= 400) {
|
|
33
|
+
logger.warn(msg);
|
|
34
|
+
} else {
|
|
35
|
+
logger.info(msg);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
next();
|
|
40
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
export default async function trySetUserByToken(
|
|
4
|
+
req: Request,
|
|
5
|
+
_res: Response,
|
|
6
|
+
next: NextFunction
|
|
7
|
+
): Promise<void> {
|
|
8
|
+
try {
|
|
9
|
+
const token = req.cookies?.auth_token;
|
|
10
|
+
|
|
11
|
+
if (!token) {
|
|
12
|
+
next();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const user = await req.context.auth.getUserByToken(token);
|
|
17
|
+
|
|
18
|
+
if (user) {
|
|
19
|
+
req.user = user;
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
// Token invalid or expired — continue without user
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
next();
|
|
26
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { AppError } from '#core/errors.js';
|
|
3
|
+
import { ZodError } from 'zod';
|
|
4
|
+
import logger from '#core/logger.js';
|
|
5
|
+
|
|
6
|
+
type AsyncHandler = (req: Request, res: Response) => Promise<void>;
|
|
7
|
+
|
|
8
|
+
export function buildHandler(handler: AsyncHandler) {
|
|
9
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
10
|
+
try {
|
|
11
|
+
await handler(req, res);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (error instanceof ZodError) {
|
|
14
|
+
const messages = error.errors.map((e) => e.message).join(', ');
|
|
15
|
+
logger.warn(`{route} ValidationError: ${messages}`, {
|
|
16
|
+
path: req.originalUrl,
|
|
17
|
+
method: req.method,
|
|
18
|
+
userId: req.user?.id
|
|
19
|
+
});
|
|
20
|
+
res.status(400).json({
|
|
21
|
+
ok: false,
|
|
22
|
+
message: messages,
|
|
23
|
+
errors: error.errors.map((e) => ({
|
|
24
|
+
field: e.path.join('.'),
|
|
25
|
+
message: e.message
|
|
26
|
+
}))
|
|
27
|
+
});
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (error instanceof AppError) {
|
|
32
|
+
if (error.statusCode >= 500) {
|
|
33
|
+
logger.error(error.message, {
|
|
34
|
+
status: String(error.statusCode),
|
|
35
|
+
path: req.originalUrl,
|
|
36
|
+
method: req.method,
|
|
37
|
+
userId: req.user?.id,
|
|
38
|
+
ip: req.ip,
|
|
39
|
+
stack: error.stack
|
|
40
|
+
});
|
|
41
|
+
} else {
|
|
42
|
+
logger.warn(`{route} ${error.name}: ${error.message}`, {
|
|
43
|
+
status: String(error.statusCode),
|
|
44
|
+
path: req.originalUrl,
|
|
45
|
+
method: req.method,
|
|
46
|
+
userId: req.user?.id
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
res.status(error.statusCode).json({
|
|
51
|
+
ok: false,
|
|
52
|
+
message: error.message
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
logger.error(
|
|
58
|
+
error instanceof Error ? error.message : String(error),
|
|
59
|
+
{
|
|
60
|
+
path: req.originalUrl,
|
|
61
|
+
method: req.method,
|
|
62
|
+
userId: req.user?.id,
|
|
63
|
+
ip: req.ip,
|
|
64
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
next(error);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import dotenv from 'dotenv';
|
|
2
|
+
dotenv.config();
|
|
3
|
+
|
|
4
|
+
import express from 'express';
|
|
5
|
+
import cors from 'cors';
|
|
6
|
+
import cookieParser from 'cookie-parser';
|
|
7
|
+
import Context from '#core/context.js';
|
|
8
|
+
import logger from '#core/logger.js';
|
|
9
|
+
import makeRoutes from './routes/index.js';
|
|
10
|
+
import requestLoggerMiddleware from '#shared/middlewares/request-logger.js';
|
|
11
|
+
import errorHandler from '#shared/middlewares/error-handler.js';
|
|
12
|
+
|
|
13
|
+
const app = express();
|
|
14
|
+
const port = parseInt(process.env.SITE_API_PORT || '{{SITE_API_PORT}}', 10);
|
|
15
|
+
|
|
16
|
+
app.use(cors({
|
|
17
|
+
origin: process.env.SITE_DOMAIN || 'http://localhost:{{SITE_PORT}}',
|
|
18
|
+
credentials: true
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
app.use(cookieParser());
|
|
22
|
+
app.use(express.json({ limit: '10mb' }));
|
|
23
|
+
app.use(express.urlencoded({ extended: true }));
|
|
24
|
+
app.use(requestLoggerMiddleware);
|
|
25
|
+
|
|
26
|
+
// Context injection — fresh Context per request
|
|
27
|
+
app.use((req: express.Request, _res: express.Response, next: express.NextFunction) => {
|
|
28
|
+
req.context = new Context();
|
|
29
|
+
next();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Mount all routes
|
|
33
|
+
makeRoutes(app);
|
|
34
|
+
|
|
35
|
+
// Error handler (must be last)
|
|
36
|
+
app.use(errorHandler);
|
|
37
|
+
|
|
38
|
+
app.listen(port, () => {
|
|
39
|
+
logger.info(`Site API server running on port ${port}`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export default app;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Express } from 'express';
|
|
2
|
+
import makePublicRoute from './public.js';
|
|
3
|
+
|
|
4
|
+
export default function makeRoutes(app: Express): void {
|
|
5
|
+
makePublicRoute(app);
|
|
6
|
+
|
|
7
|
+
// Add new site API route modules here as the project grows:
|
|
8
|
+
// makeCustomerRoute(app);
|
|
9
|
+
// makeCheckoutRoute(app);
|
|
10
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Express, Request, Response } from 'express';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import { buildHandler } from '#shared/utils.js';
|
|
4
|
+
|
|
5
|
+
const router = express.Router();
|
|
6
|
+
|
|
7
|
+
async function handleHealthCheck(_req: Request, res: Response): Promise<void> {
|
|
8
|
+
res.json({ ok: true, message: 'Site API is running' });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function handleGetPublicInfo(req: Request, res: Response): Promise<void> {
|
|
12
|
+
const accounts = await req.context.account.listPublic();
|
|
13
|
+
res.json({ ok: true, accounts });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
router.get('/health', buildHandler(handleHealthCheck));
|
|
17
|
+
router.get('/info', buildHandler(handleGetPublicInfo));
|
|
18
|
+
|
|
19
|
+
// Add public routes for your site here:
|
|
20
|
+
// router.get('/products', buildHandler(handleListProducts));
|
|
21
|
+
// router.get('/products/:slug', buildHandler(handleGetProduct));
|
|
22
|
+
|
|
23
|
+
export default function makeEndpoint(app: Express): void {
|
|
24
|
+
app.use('/site', router);
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import dotenv from 'dotenv';
|
|
2
|
+
dotenv.config();
|
|
3
|
+
|
|
4
|
+
import { parseArgs, runTask, listTasks, type ConsoleTask } from './runner.js';
|
|
5
|
+
|
|
6
|
+
// Import all tasks
|
|
7
|
+
import exampleTask from './tasks/example.js';
|
|
8
|
+
import seedAdminTask from './tasks/seed-admin.js';
|
|
9
|
+
|
|
10
|
+
// Register all tasks here
|
|
11
|
+
const tasks: ConsoleTask[] = [
|
|
12
|
+
exampleTask,
|
|
13
|
+
seedAdminTask,
|
|
14
|
+
// Add new tasks here as they are created
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const args = parseArgs(process.argv);
|
|
18
|
+
|
|
19
|
+
if (!args.task) {
|
|
20
|
+
listTasks(tasks);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
runTask(tasks, args.task as string, args).then(() => {
|
|
25
|
+
process.exit(0);
|
|
26
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import Context from '#core/context.js';
|
|
2
|
+
import logger from '#core/logger.js';
|
|
3
|
+
|
|
4
|
+
export interface ConsoleTaskArgs {
|
|
5
|
+
[key: string]: string | boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ConsoleTask {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
run: (context: Context, args: ConsoleTaskArgs) => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parseArgs(argv: string[]): ConsoleTaskArgs {
|
|
15
|
+
const args: ConsoleTaskArgs = {};
|
|
16
|
+
|
|
17
|
+
for (const arg of argv.slice(2)) {
|
|
18
|
+
if (arg.startsWith('--')) {
|
|
19
|
+
const [key, value] = arg.substring(2).split('=');
|
|
20
|
+
args[key] = value ?? true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return args;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function runTask(
|
|
28
|
+
tasks: ConsoleTask[],
|
|
29
|
+
taskName: string,
|
|
30
|
+
args: ConsoleTaskArgs
|
|
31
|
+
): Promise<void> {
|
|
32
|
+
const task = tasks.find((t) => t.name === taskName);
|
|
33
|
+
|
|
34
|
+
if (!task) {
|
|
35
|
+
logger.error(`Task not found: ${taskName}`);
|
|
36
|
+
logger.info('Available tasks:');
|
|
37
|
+
for (const t of tasks) {
|
|
38
|
+
logger.info(` --task=${t.name} ${t.description}`);
|
|
39
|
+
}
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const context = new Context();
|
|
44
|
+
|
|
45
|
+
logger.info(`Running task: ${task.name}`);
|
|
46
|
+
const start = Date.now();
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await task.run(context, args);
|
|
50
|
+
const duration = Date.now() - start;
|
|
51
|
+
logger.info(`Task completed in ${duration}ms`);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
logger.error('Task failed', {
|
|
54
|
+
error: error instanceof Error ? error.message : String(error),
|
|
55
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
56
|
+
});
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function listTasks(tasks: ConsoleTask[]): void {
|
|
62
|
+
console.log('\nAvailable console tasks:\n');
|
|
63
|
+
for (const task of tasks) {
|
|
64
|
+
console.log(` ${task.name.padEnd(25)} ${task.description}`);
|
|
65
|
+
}
|
|
66
|
+
console.log('\nUsage: npm run console -- --task=TASK_NAME [--arg=value]\n');
|
|
67
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ConsoleTask } from '../runner.js';
|
|
2
|
+
import logger from '#core/logger.js';
|
|
3
|
+
|
|
4
|
+
const task: ConsoleTask = {
|
|
5
|
+
name: 'example',
|
|
6
|
+
description: 'Example task that demonstrates the console runner pattern',
|
|
7
|
+
|
|
8
|
+
async run(context, args) {
|
|
9
|
+
logger.info('Running example task', { args });
|
|
10
|
+
|
|
11
|
+
// Access all models via context, just like in route handlers
|
|
12
|
+
const knex = context.auth.knex;
|
|
13
|
+
|
|
14
|
+
const [result] = await knex('users').count('id as count');
|
|
15
|
+
logger.info(`Total users in database: ${result.count}`);
|
|
16
|
+
|
|
17
|
+
// Example: process with arguments
|
|
18
|
+
if (args.verbose) {
|
|
19
|
+
const users = await knex('users').select('id', 'email', 'name');
|
|
20
|
+
for (const user of users) {
|
|
21
|
+
logger.info(` - ${user.name} (${user.email})`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
logger.info('Example task finished');
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default task;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ConsoleTask } from '../runner.js';
|
|
2
|
+
import logger from '#core/logger.js';
|
|
3
|
+
import { sendEmail, isEmailConfigured } from '#core/email.js';
|
|
4
|
+
|
|
5
|
+
const task: ConsoleTask = {
|
|
6
|
+
name: 'seed-admin',
|
|
7
|
+
description: 'Create the initial admin user and account',
|
|
8
|
+
|
|
9
|
+
async run(context, args) {
|
|
10
|
+
if (!args.email || !args.password) {
|
|
11
|
+
logger.error('Usage: npm run console -- --task=seed-admin --email=admin@example.com --password=yourpassword [--name=Admin]');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const email = args.email as string;
|
|
16
|
+
const password = args.password as string;
|
|
17
|
+
const name = (args.name as string) || 'Admin';
|
|
18
|
+
|
|
19
|
+
const knex = context.auth.knex;
|
|
20
|
+
|
|
21
|
+
const existing = await knex('users').where({ email }).first();
|
|
22
|
+
if (existing) {
|
|
23
|
+
logger.warn(`User ${email} already exists, skipping`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const user = await context.user.create({
|
|
28
|
+
email,
|
|
29
|
+
password,
|
|
30
|
+
name,
|
|
31
|
+
role: 'admin'
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const [account] = await knex('accounts')
|
|
35
|
+
.insert({
|
|
36
|
+
name: 'Admin Account',
|
|
37
|
+
status: 'active',
|
|
38
|
+
settings: JSON.stringify({})
|
|
39
|
+
})
|
|
40
|
+
.returning('*');
|
|
41
|
+
|
|
42
|
+
await knex('user_in_accounts').insert({
|
|
43
|
+
user_id: user.id,
|
|
44
|
+
account_id: account.id,
|
|
45
|
+
role: 'owner'
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
logger.info('Admin user created', {
|
|
49
|
+
email,
|
|
50
|
+
userId: user.id,
|
|
51
|
+
accountId: account.id
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (isEmailConfigured()) {
|
|
55
|
+
try {
|
|
56
|
+
await sendEmail(email, 'Bem-vindo!', 'accountWelcome', {
|
|
57
|
+
user: { name }
|
|
58
|
+
});
|
|
59
|
+
logger.info('Welcome email sent', { email });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
logger.error('Failed to send welcome email', {
|
|
62
|
+
error: error instanceof Error ? error.message : String(error)
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
logger.warn('Email not configured, skipping welcome email');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default task;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import AuthModel from './models/auth.js';
|
|
2
|
+
import UserModel from './models/user.js';
|
|
3
|
+
import AccountModel from './models/account.js';
|
|
4
|
+
import BaseModel from './models/base.js';
|
|
5
|
+
import Db from './db.js';
|
|
6
|
+
|
|
7
|
+
class Context {
|
|
8
|
+
auth: AuthModel;
|
|
9
|
+
user: UserModel;
|
|
10
|
+
account: AccountModel;
|
|
11
|
+
|
|
12
|
+
// Add new models here as the project grows:
|
|
13
|
+
// company: CompanyModel;
|
|
14
|
+
// employee: EmployeeModel;
|
|
15
|
+
// document: DocumentModel;
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
this.auth = new AuthModel(this);
|
|
19
|
+
this.user = new UserModel(this);
|
|
20
|
+
this.account = new AccountModel(this);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async knexTransaction(fn: () => Promise<void>): Promise<void> {
|
|
24
|
+
const db = new Db();
|
|
25
|
+
const { knex } = db;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
await knex.transaction(async (trx) => {
|
|
29
|
+
for (const key in this) {
|
|
30
|
+
const model = (this as any)[key];
|
|
31
|
+
if (model instanceof BaseModel) {
|
|
32
|
+
model.knex = trx;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
await fn();
|
|
37
|
+
});
|
|
38
|
+
} finally {
|
|
39
|
+
for (const key in this) {
|
|
40
|
+
const model = (this as any)[key];
|
|
41
|
+
if (model instanceof BaseModel) {
|
|
42
|
+
model.knex = knex;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default Context;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import nodemailer from 'nodemailer';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import _ from 'lodash';
|
|
6
|
+
import logger from './logger.js';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
10
|
+
let _transporter: nodemailer.Transporter | null = null;
|
|
11
|
+
|
|
12
|
+
function getTransporter(): nodemailer.Transporter {
|
|
13
|
+
if (!_transporter) {
|
|
14
|
+
_transporter = nodemailer.createTransport({
|
|
15
|
+
host: process.env.EMAIL_HOST,
|
|
16
|
+
port: parseInt(process.env.EMAIL_PORT || '465', 10),
|
|
17
|
+
secure: process.env.EMAIL_SECURE === 'true',
|
|
18
|
+
auth: {
|
|
19
|
+
user: process.env.EMAIL_USER,
|
|
20
|
+
pass: process.env.EMAIL_PASSWORD
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return _transporter;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadTemplate(templateName: string, variables: Record<string, unknown>): string {
|
|
29
|
+
const emailsDir = path.resolve(__dirname, '../emails');
|
|
30
|
+
|
|
31
|
+
const stylesPath = path.join(emailsDir, '_styles.css');
|
|
32
|
+
const styles = fs.existsSync(stylesPath)
|
|
33
|
+
? fs.readFileSync(stylesPath, 'utf-8')
|
|
34
|
+
: '';
|
|
35
|
+
|
|
36
|
+
const basePath = path.join(emailsDir, '_template.html');
|
|
37
|
+
const baseTemplate = fs.existsSync(basePath)
|
|
38
|
+
? fs.readFileSync(basePath, 'utf-8')
|
|
39
|
+
: '{{content}}';
|
|
40
|
+
|
|
41
|
+
const templatePath = path.join(emailsDir, `${templateName}.html`);
|
|
42
|
+
if (!fs.existsSync(templatePath)) {
|
|
43
|
+
throw new Error(`Email template not found: ${templateName}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const templateContent = fs.readFileSync(templatePath, 'utf-8');
|
|
47
|
+
|
|
48
|
+
const compiledContent = _.template(templateContent)(variables);
|
|
49
|
+
const fullHtml = baseTemplate
|
|
50
|
+
.replace('{{styles}}', styles)
|
|
51
|
+
.replace('{{content}}', compiledContent);
|
|
52
|
+
|
|
53
|
+
return fullHtml;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isEmailConfigured(): boolean {
|
|
57
|
+
return !!(process.env.EMAIL_HOST && process.env.EMAIL_USER && process.env.EMAIL_PASSWORD);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function sendEmail(
|
|
61
|
+
to: string,
|
|
62
|
+
subject: string,
|
|
63
|
+
templateName: string,
|
|
64
|
+
variables: Record<string, unknown>
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
try {
|
|
67
|
+
const html = loadTemplate(templateName, variables);
|
|
68
|
+
|
|
69
|
+
await getTransporter().sendMail({
|
|
70
|
+
from: `"${process.env.EMAIL_NAME}" <${process.env.EMAIL_FROM}>`,
|
|
71
|
+
replyTo: process.env.EMAIL_REPLY_TO,
|
|
72
|
+
to,
|
|
73
|
+
subject,
|
|
74
|
+
html
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
logger.info('Email sent', { to, subject, template: templateName });
|
|
78
|
+
} catch (error) {
|
|
79
|
+
logger.error('Failed to send email', {
|
|
80
|
+
error: error instanceof Error ? error.message : String(error),
|
|
81
|
+
to,
|
|
82
|
+
subject,
|
|
83
|
+
template: templateName
|
|
84
|
+
});
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export class AppError extends Error {
|
|
2
|
+
statusCode: number;
|
|
3
|
+
|
|
4
|
+
constructor(message: string, statusCode: number = 500) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'AppError';
|
|
7
|
+
this.statusCode = statusCode;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class ValidationError extends AppError {
|
|
12
|
+
constructor(message: string) {
|
|
13
|
+
super(message, 400);
|
|
14
|
+
this.name = 'ValidationError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class UnauthorizedError extends AppError {
|
|
19
|
+
constructor(message: string = 'Não autorizado') {
|
|
20
|
+
super(message, 401);
|
|
21
|
+
this.name = 'UnauthorizedError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class ForbiddenError extends AppError {
|
|
26
|
+
constructor(message: string = 'Acesso negado') {
|
|
27
|
+
super(message, 403);
|
|
28
|
+
this.name = 'ForbiddenError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class NotFoundError extends AppError {
|
|
33
|
+
constructor(message: string = 'Recurso não encontrado') {
|
|
34
|
+
super(message, 404);
|
|
35
|
+
this.name = 'NotFoundError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class ConflictError extends AppError {
|
|
40
|
+
constructor(message: string) {
|
|
41
|
+
super(message, 409);
|
|
42
|
+
this.name = 'ConflictError';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import dotenv from 'dotenv';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
dotenv.config({ path: path.resolve(__dirname, '../.env') });
|
|
8
|
+
|
|
9
|
+
const config = {
|
|
10
|
+
client: 'pg',
|
|
11
|
+
connection: {
|
|
12
|
+
host: process.env.DB_HOST || 'localhost',
|
|
13
|
+
port: parseInt(process.env.DB_PORT || '5432', 10),
|
|
14
|
+
database: process.env.DB_NAME,
|
|
15
|
+
user: process.env.DB_USER || 'postgres',
|
|
16
|
+
password: process.env.DB_PASSWORD || 'postgres'
|
|
17
|
+
},
|
|
18
|
+
migrations: {
|
|
19
|
+
directory: path.resolve(__dirname, '../migrations'),
|
|
20
|
+
extension: 'ts'
|
|
21
|
+
},
|
|
22
|
+
pool: {
|
|
23
|
+
min: 2,
|
|
24
|
+
max: 10
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default config;
|