Compare commits
21 Commits
7cecf5326d
...
894c841bb7
| Author | SHA1 | Date | |
|---|---|---|---|
| 894c841bb7 | |||
| e59bb35ac9 | |||
| a345a2adfb | |||
| 00d84d6686 | |||
| 7ed05695b9 | |||
| 03cc4cf4eb | |||
| 2121a6b5de | |||
|
|
6ace2163ed | ||
|
|
93ab4b5d53 | ||
|
|
70ddcb2a94 | ||
|
|
1da81089cd | ||
| f383c6a465 | |||
| e34d47b352 | |||
| de70be996e | |||
| 096a1235b5 | |||
| 4a4dc11aa4 | |||
| 7399cbe785 | |||
| 14d20be9a2 | |||
| 55f5cc699d | |||
| afcb447b2b | |||
| 1c1eeddcbe |
@@ -31,14 +31,14 @@ master process. Key design principles:
|
|||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
**Check shell scripts (shellcheck + shfmt) (eventually go fmt and prettier or similar):**
|
**Check shell scripts (shellcheck + shfmt) (eventually go fmt and biome or similar):**
|
||||||
```bash
|
```bash
|
||||||
./check.sh
|
./check.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
**Format TypeScript code:**
|
**Format TypeScript code:**
|
||||||
```bash
|
```bash
|
||||||
cd express && ../cmd pnpm prettier --write .
|
cd express && ../cmd pnpm biome check --write .
|
||||||
```
|
```
|
||||||
|
|
||||||
**Build Go master process:**
|
**Build Go master process:**
|
||||||
@@ -108,6 +108,10 @@ Early stage - most implementations are stubs:
|
|||||||
|
|
||||||
# meta
|
# meta
|
||||||
|
|
||||||
|
## formatting and sorting
|
||||||
|
|
||||||
|
- When a typescript file exports symbols, they should be listed in order
|
||||||
|
|
||||||
## guidelines for this document
|
## guidelines for this document
|
||||||
|
|
||||||
- Try to keep lines below 80 characters in length, especially prose. But if
|
- Try to keep lines below 80 characters in length, especially prose. But if
|
||||||
|
|||||||
@@ -54,10 +54,9 @@ To run a more complete system, you also need to have docker compose installed.
|
|||||||
|
|
||||||
To hack on diachron itself, you need the following:
|
To hack on diachron itself, you need the following:
|
||||||
|
|
||||||
|
- bash
|
||||||
- docker and docker compose
|
- docker and docker compose
|
||||||
- [fd](https://github.com/sharkdp/fd)
|
- [fd](https://github.com/sharkdp/fd)
|
||||||
- golang, version 1.23.6 or greater
|
- golang, version 1.23.6 or greater
|
||||||
- shellcheck
|
- shellcheck
|
||||||
- shfmt
|
- shfmt
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
75
TODO.md
75
TODO.md
@@ -1,22 +1,23 @@
|
|||||||
## high importance
|
## high importance
|
||||||
|
|
||||||
|
- [ ] nix services/ and split it up into core/ request/
|
||||||
|
|
||||||
- [ ] Add unit tests all over the place.
|
- [ ] Add unit tests all over the place.
|
||||||
- ⚠️ Huge task - needs breakdown before starting
|
- ⚠️ Huge task - needs breakdown before starting
|
||||||
|
|
||||||
- [ ] Create initial docker-compose.yml file for local development
|
|
||||||
- include most recent stable postgres
|
|
||||||
- include beanstalkd
|
|
||||||
- include memcached
|
|
||||||
- include redis
|
|
||||||
- include mailpit
|
|
||||||
|
|
||||||
- [ ] Add first cut at database access. Remember that ORMs are not all that!
|
|
||||||
|
|
||||||
- [ ] Add middleware concept
|
|
||||||
|
|
||||||
|
- [ ] Add default user table(s) to database.
|
||||||
|
|
||||||
|
|
||||||
- [ ] Add authentication
|
- [ ] Add authentication
|
||||||
- password
|
- [ ] password
|
||||||
- third party?
|
- [ ] third party?
|
||||||
|
|
||||||
|
|
||||||
|
- [ ] Add middleware concept
|
||||||
|
|
||||||
- [ ] Add authorization
|
- [ ] Add authorization
|
||||||
- for specific routes / resources / etc
|
- for specific routes / resources / etc
|
||||||
@@ -25,6 +26,9 @@
|
|||||||
Partially done; see the /time route. But we need to figure out where to
|
Partially done; see the /time route. But we need to figure out where to
|
||||||
store templates, static files, etc.
|
store templates, static files, etc.
|
||||||
|
|
||||||
|
- [ ] fix process management: if you control-c `master` process sometimes it
|
||||||
|
leaves around `master-bin`, `logger-bin`, and `diachron:nnnn` processes.
|
||||||
|
Huge problem.
|
||||||
|
|
||||||
## medium importance
|
## medium importance
|
||||||
|
|
||||||
@@ -32,10 +36,33 @@
|
|||||||
- with queries
|
- with queries
|
||||||
- convert to logfmt and is there a viewer UI we could pull in and use
|
- convert to logfmt and is there a viewer UI we could pull in and use
|
||||||
instead?
|
instead?
|
||||||
|
|
||||||
|
- [ ] add nested routes. Note that this might be easy to do without actually
|
||||||
|
changing the logic in express/routes.ts. A function that takes an array
|
||||||
|
of routes and maps over them rewriting them. Maybe.
|
||||||
|
|
||||||
|
- [ ] related: add something to do with default templates and stuff... I
|
||||||
|
think we can make handlers a lot shorter to write, sometimes not even
|
||||||
|
necessary at all, with some sane defaults and an easy to use override
|
||||||
|
mechanism
|
||||||
|
|
||||||
|
- [ ] fill in the rest of express/http-codes.ts
|
||||||
|
|
||||||
|
- [ ] fill out express/content-types.ts
|
||||||
|
|
||||||
|
|
||||||
|
- [ ] identify redundant "old skool" and ajax routes, factor out their
|
||||||
|
commonalities, etc.
|
||||||
|
|
||||||
- [ ] figure out and add logging to disk
|
- [ ] figure out and add logging to disk
|
||||||
|
|
||||||
- [ ] Add email verification
|
- [ ] I don't really feel close to satisfied with template location /
|
||||||
|
rendering / etc. Rethink and rework.
|
||||||
|
|
||||||
|
- [ ] Add email verification (this is partially done already)
|
||||||
|
|
||||||
|
- [ ] Reading .env files and dealing with the environment should be immune to
|
||||||
|
the extent possible from idiotic errors
|
||||||
|
|
||||||
- [ ] Update check script:
|
- [ ] Update check script:
|
||||||
- [x] shellcheck on shell scripts
|
- [x] shellcheck on shell scripts
|
||||||
@@ -48,6 +75,17 @@
|
|||||||
- upgrade docs
|
- upgrade docs
|
||||||
- starting docs
|
- starting docs
|
||||||
- taking over docs
|
- taking over docs
|
||||||
|
- reference
|
||||||
|
- internals
|
||||||
|
|
||||||
|
- [ ] make migration creation default to something like yyyy-mm-dd_ssss (are
|
||||||
|
9999 migrations in a day enough?)
|
||||||
|
|
||||||
|
- [ ] clean up `cmd` and `mgmt`: do the right thing with their commonalities
|
||||||
|
and make very plain which is which for what. Consider additional
|
||||||
|
commands. Maybe `develop` for specific development tasks,
|
||||||
|
`operate` for operational tasks, and we keep `cmd` for project-specific
|
||||||
|
commands. Something like that.
|
||||||
|
|
||||||
|
|
||||||
## low importance
|
## low importance
|
||||||
@@ -72,6 +110,10 @@
|
|||||||
code; repeat
|
code; repeat
|
||||||
- Slow start them: only start a few at first
|
- Slow start them: only start a few at first
|
||||||
|
|
||||||
|
- [ ] in express/user.ts: FIXME: set createdAt and updatedAt to start of epoch
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## finished
|
## finished
|
||||||
|
|
||||||
@@ -99,3 +141,12 @@
|
|||||||
|
|
||||||
- [x] Log to logging service from the express backend
|
- [x] Log to logging service from the express backend
|
||||||
- Fill out types and functions in `express/logging.ts`
|
- Fill out types and functions in `express/logging.ts`
|
||||||
|
|
||||||
|
- [x] Add first cut at database access. Remember that ORMs are not all that!
|
||||||
|
|
||||||
|
- [x] Create initial docker-compose.yml file for local development
|
||||||
|
- include most recent stable postgres
|
||||||
|
- include beanstalkd
|
||||||
|
- include memcached
|
||||||
|
- include redis
|
||||||
|
- include mailpit
|
||||||
|
|||||||
35
docker-compose.yml
Normal file
35
docker-compose.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: diachron
|
||||||
|
POSTGRES_PASSWORD: diachron
|
||||||
|
POSTGRES_DB: diachron
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
|
||||||
|
memcached:
|
||||||
|
image: memcached:1.6
|
||||||
|
ports:
|
||||||
|
- "11211:11211"
|
||||||
|
|
||||||
|
beanstalkd:
|
||||||
|
image: schickling/beanstalkd
|
||||||
|
ports:
|
||||||
|
- "11300:11300"
|
||||||
|
|
||||||
|
mailpit:
|
||||||
|
image: axllent/mailpit
|
||||||
|
ports:
|
||||||
|
- "1025:1025" # SMTP
|
||||||
|
- "8025:8025" # Web UI
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
@@ -6,15 +6,19 @@ import { match } from "path-to-regexp";
|
|||||||
import { Session } from "./auth";
|
import { Session } from "./auth";
|
||||||
import { cli } from "./cli";
|
import { cli } from "./cli";
|
||||||
import { contentTypes } from "./content-types";
|
import { contentTypes } from "./content-types";
|
||||||
|
import { runWithContext } from "./context";
|
||||||
|
import { core } from "./core";
|
||||||
import { httpCodes } from "./http-codes";
|
import { httpCodes } from "./http-codes";
|
||||||
|
import { request } from "./request";
|
||||||
import { routes } from "./routes";
|
import { routes } from "./routes";
|
||||||
import { services } from "./services";
|
|
||||||
// import { URLPattern } from 'node:url';
|
// import { URLPattern } from 'node:url';
|
||||||
import {
|
import {
|
||||||
AuthenticationRequired,
|
AuthenticationRequired,
|
||||||
AuthorizationDenied,
|
AuthorizationDenied,
|
||||||
type Call,
|
type Call,
|
||||||
type InternalHandler,
|
type InternalHandler,
|
||||||
|
isRedirect,
|
||||||
type Method,
|
type Method,
|
||||||
massageMethod,
|
massageMethod,
|
||||||
methodParser,
|
methodParser,
|
||||||
@@ -25,10 +29,11 @@ import {
|
|||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// Parse JSON request bodies
|
// Parse request bodies
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
services.logging.log({ source: "logging", text: ["1"] });
|
core.logging.log({ source: "logging", text: ["1"] });
|
||||||
const processedRoutes: { [K in Method]: ProcessedRoute[] } = {
|
const processedRoutes: { [K in Method]: ProcessedRoute[] } = {
|
||||||
GET: [],
|
GET: [],
|
||||||
POST: [],
|
POST: [],
|
||||||
@@ -47,9 +52,9 @@ routes.forEach((route: Route, _idx: number, _allRoutes: Route[]) => {
|
|||||||
const methodList = route.methods;
|
const methodList = route.methods;
|
||||||
|
|
||||||
const handler: InternalHandler = async (
|
const handler: InternalHandler = async (
|
||||||
request: ExpressRequest,
|
expressRequest: ExpressRequest,
|
||||||
): Promise<Result> => {
|
): Promise<Result> => {
|
||||||
const method = massageMethod(request.method);
|
const method = massageMethod(expressRequest.method);
|
||||||
|
|
||||||
console.log("method", method);
|
console.log("method", method);
|
||||||
|
|
||||||
@@ -57,23 +62,25 @@ routes.forEach((route: Route, _idx: number, _allRoutes: Route[]) => {
|
|||||||
// XXX: Worth asserting this?
|
// XXX: Worth asserting this?
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("request.originalUrl", request.originalUrl);
|
console.log("request.originalUrl", expressRequest.originalUrl);
|
||||||
|
|
||||||
// Authenticate the request
|
// Authenticate the request
|
||||||
const auth = await services.auth.validateRequest(request);
|
const auth = await request.auth.validateRequest(expressRequest);
|
||||||
|
|
||||||
const req: Call = {
|
const req: Call = {
|
||||||
pattern: route.path,
|
pattern: route.path,
|
||||||
path: request.originalUrl,
|
path: expressRequest.originalUrl,
|
||||||
method,
|
method,
|
||||||
parameters: { one: 1, two: 2 },
|
parameters: { one: 1, two: 2 },
|
||||||
request,
|
request: expressRequest,
|
||||||
user: auth.user,
|
user: auth.user,
|
||||||
session: new Session(auth.session, auth.user),
|
session: new Session(auth.session, auth.user),
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const retval = await route.handler(req);
|
const retval = await runWithContext({ user: auth.user }, () =>
|
||||||
|
route.handler(req),
|
||||||
|
);
|
||||||
return retval;
|
return retval;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle authentication errors
|
// Handle authentication errors
|
||||||
@@ -111,8 +118,15 @@ async function handler(
|
|||||||
const method = await methodParser.parseAsync(req.method);
|
const method = await methodParser.parseAsync(req.method);
|
||||||
|
|
||||||
const byMethod = processedRoutes[method];
|
const byMethod = processedRoutes[method];
|
||||||
|
console.log(
|
||||||
|
"DEBUG: req.path =",
|
||||||
|
JSON.stringify(req.path),
|
||||||
|
"method =",
|
||||||
|
method,
|
||||||
|
);
|
||||||
for (const [_idx, pr] of byMethod.entries()) {
|
for (const [_idx, pr] of byMethod.entries()) {
|
||||||
const match = pr.matcher(req.url);
|
const match = pr.matcher(req.path);
|
||||||
|
console.log("DEBUG: trying pattern, match result =", match);
|
||||||
if (match) {
|
if (match) {
|
||||||
console.log("match", match);
|
console.log("match", match);
|
||||||
const resp = await pr.handler(req);
|
const resp = await pr.handler(req);
|
||||||
@@ -124,7 +138,7 @@ async function handler(
|
|||||||
const retval: Result = {
|
const retval: Result = {
|
||||||
code: httpCodes.clientErrors.NotFound,
|
code: httpCodes.clientErrors.NotFound,
|
||||||
contentType: contentTypes.text.plain,
|
contentType: contentTypes.text.plain,
|
||||||
result: "not found",
|
result: "not found!",
|
||||||
};
|
};
|
||||||
|
|
||||||
return retval;
|
return retval;
|
||||||
@@ -138,7 +152,18 @@ app.use(async (req: ExpressRequest, res: ExpressResponse) => {
|
|||||||
|
|
||||||
console.log(result);
|
console.log(result);
|
||||||
|
|
||||||
res.status(code).send(result);
|
// Set any cookies from the result
|
||||||
|
if (result0.cookies) {
|
||||||
|
for (const cookie of result0.cookies) {
|
||||||
|
res.cookie(cookie.name, cookie.value, cookie.options ?? {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRedirect(result0)) {
|
||||||
|
res.redirect(code, result0.redirect);
|
||||||
|
} else {
|
||||||
|
res.status(code).send(result);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
process.title = `diachron:${cli.listen.port}`;
|
process.title = `diachron:${cli.listen.port}`;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { contentTypes } from "../content-types";
|
import { contentTypes } from "../content-types";
|
||||||
import { httpCodes } from "../http-codes";
|
import { httpCodes } from "../http-codes";
|
||||||
import { services } from "../services";
|
import { request } from "../request";
|
||||||
import type { Call, Result, Route } from "../types";
|
import type { Call, Result, Route } from "../types";
|
||||||
import {
|
import {
|
||||||
forgotPasswordInputParser,
|
forgotPasswordInputParser,
|
||||||
@@ -39,7 +39,7 @@ const loginHandler = async (call: Call): Promise<Result> => {
|
|||||||
const body = call.request.body;
|
const body = call.request.body;
|
||||||
const { email, password } = loginInputParser.parse(body);
|
const { email, password } = loginInputParser.parse(body);
|
||||||
|
|
||||||
const result = await services.auth.login(email, password, "cookie", {
|
const result = await request.auth.login(email, password, "cookie", {
|
||||||
userAgent: call.request.get("User-Agent"),
|
userAgent: call.request.get("User-Agent"),
|
||||||
ipAddress: call.request.ip,
|
ipAddress: call.request.ip,
|
||||||
});
|
});
|
||||||
@@ -72,9 +72,9 @@ const loginHandler = async (call: Call): Promise<Result> => {
|
|||||||
|
|
||||||
// POST /auth/logout
|
// POST /auth/logout
|
||||||
const logoutHandler = async (call: Call): Promise<Result> => {
|
const logoutHandler = async (call: Call): Promise<Result> => {
|
||||||
const token = services.auth.extractToken(call.request);
|
const token = request.auth.extractToken(call.request);
|
||||||
if (token) {
|
if (token) {
|
||||||
await services.auth.logout(token);
|
await request.auth.logout(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonResponse(httpCodes.success.OK, { message: "Logged out" });
|
return jsonResponse(httpCodes.success.OK, { message: "Logged out" });
|
||||||
@@ -87,7 +87,7 @@ const registerHandler = async (call: Call): Promise<Result> => {
|
|||||||
const { email, password, displayName } =
|
const { email, password, displayName } =
|
||||||
registerInputParser.parse(body);
|
registerInputParser.parse(body);
|
||||||
|
|
||||||
const result = await services.auth.register(
|
const result = await request.auth.register(
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
displayName,
|
displayName,
|
||||||
@@ -128,7 +128,7 @@ const forgotPasswordHandler = async (call: Call): Promise<Result> => {
|
|||||||
const body = call.request.body;
|
const body = call.request.body;
|
||||||
const { email } = forgotPasswordInputParser.parse(body);
|
const { email } = forgotPasswordInputParser.parse(body);
|
||||||
|
|
||||||
const result = await services.auth.createPasswordResetToken(email);
|
const result = await request.auth.createPasswordResetToken(email);
|
||||||
|
|
||||||
// Always return success (don't reveal if email exists)
|
// Always return success (don't reveal if email exists)
|
||||||
if (result) {
|
if (result) {
|
||||||
@@ -159,7 +159,7 @@ const resetPasswordHandler = async (call: Call): Promise<Result> => {
|
|||||||
const body = call.request.body;
|
const body = call.request.body;
|
||||||
const { token, password } = resetPasswordInputParser.parse(body);
|
const { token, password } = resetPasswordInputParser.parse(body);
|
||||||
|
|
||||||
const result = await services.auth.resetPassword(token, password);
|
const result = await request.auth.resetPassword(token, password);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return errorResponse(
|
return errorResponse(
|
||||||
@@ -195,7 +195,7 @@ const verifyEmailHandler = async (call: Call): Promise<Result> => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await services.auth.verifyEmail(token);
|
const result = await request.auth.verifyEmail(token);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return errorResponse(httpCodes.clientErrors.BadRequest, result.error);
|
return errorResponse(httpCodes.clientErrors.BadRequest, result.error);
|
||||||
|
|||||||
62
express/basic/login.ts
Normal file
62
express/basic/login.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { SESSION_COOKIE_NAME } from "../auth/token";
|
||||||
|
import { tokenLifetimes } from "../auth/types";
|
||||||
|
import { request } from "../request";
|
||||||
|
import { html, redirect, render } from "../request/util";
|
||||||
|
import type { Call, Result, Route } from "../types";
|
||||||
|
|
||||||
|
const loginHandler = async (call: Call): Promise<Result> => {
|
||||||
|
if (call.method === "GET") {
|
||||||
|
const c = await render("basic/login", {});
|
||||||
|
return html(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST - handle login
|
||||||
|
const { email, password } = call.request.body;
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
const c = await render("basic/login", {
|
||||||
|
error: "Email and password are required",
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
return html(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await request.auth.login(email, password, "cookie", {
|
||||||
|
userAgent: call.request.get("User-Agent"),
|
||||||
|
ipAddress: call.request.ip,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
const c = await render("basic/login", {
|
||||||
|
error: result.error,
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
return html(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success - set cookie and redirect to home
|
||||||
|
const redirectResult = redirect("/");
|
||||||
|
redirectResult.cookies = [
|
||||||
|
{
|
||||||
|
name: SESSION_COOKIE_NAME,
|
||||||
|
value: result.token,
|
||||||
|
options: {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: false, // Set to true in production with HTTPS
|
||||||
|
sameSite: "lax",
|
||||||
|
maxAge: tokenLifetimes.session,
|
||||||
|
path: "/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return redirectResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loginRoute: Route = {
|
||||||
|
path: "/login",
|
||||||
|
methods: ["GET", "POST"],
|
||||||
|
handler: loginHandler,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { loginRoute };
|
||||||
38
express/basic/logout.ts
Normal file
38
express/basic/logout.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { SESSION_COOKIE_NAME } from "../auth/token";
|
||||||
|
import { request } from "../request";
|
||||||
|
import { redirect } from "../request/util";
|
||||||
|
import type { Call, Result, Route } from "../types";
|
||||||
|
|
||||||
|
const logoutHandler = async (call: Call): Promise<Result> => {
|
||||||
|
// Extract token from cookie and invalidate the session
|
||||||
|
const token = request.auth.extractToken(call.request);
|
||||||
|
if (token) {
|
||||||
|
await request.auth.logout(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the cookie and redirect to login
|
||||||
|
const redirectResult = redirect("/login");
|
||||||
|
redirectResult.cookies = [
|
||||||
|
{
|
||||||
|
name: SESSION_COOKIE_NAME,
|
||||||
|
value: "",
|
||||||
|
options: {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: false,
|
||||||
|
sameSite: "lax",
|
||||||
|
maxAge: 0,
|
||||||
|
path: "/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return redirectResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
const logoutRoute: Route = {
|
||||||
|
path: "/logout",
|
||||||
|
methods: ["GET", "POST"],
|
||||||
|
handler: logoutHandler,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { logoutRoute };
|
||||||
@@ -1,18 +1,36 @@
|
|||||||
import { DateTime } from "ts-luxon";
|
import { DateTime } from "ts-luxon";
|
||||||
|
import { request } from "../request";
|
||||||
|
import { html, render } from "../request/util";
|
||||||
import type { Call, Result, Route } from "../types";
|
import type { Call, Result, Route } from "../types";
|
||||||
import { html, render } from "../util";
|
import { loginRoute } from "./login";
|
||||||
|
import { logoutRoute } from "./logout";
|
||||||
|
|
||||||
const routes: Record<string, Route> = {
|
const routes: Record<string, Route> = {
|
||||||
hello: {
|
hello: {
|
||||||
path: "/hello",
|
path: "/hello",
|
||||||
methods: ["GET"],
|
methods: ["GET"],
|
||||||
handler: async (call: Call): Promise<Result> => {
|
handler: async (_call: Call): Promise<Result> => {
|
||||||
const now = DateTime.now();
|
const now = DateTime.now();
|
||||||
const c = await render("basic/hello", { now });
|
const c = await render("basic/hello", { now });
|
||||||
|
|
||||||
return html(c);
|
return html(c);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
home: {
|
||||||
|
path: "/",
|
||||||
|
methods: ["GET"],
|
||||||
|
handler: async (_call: Call): Promise<Result> => {
|
||||||
|
const _auth = request.auth;
|
||||||
|
const me = request.session.getUser();
|
||||||
|
|
||||||
|
const email = me.toString();
|
||||||
|
const c = await render("basic/home", { email });
|
||||||
|
|
||||||
|
return html(c);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
login: loginRoute,
|
||||||
|
logout: logoutRoute,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { routes };
|
export { routes };
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// This file belongs to the framework. You are not expected to modify it.
|
||||||
|
|
||||||
export type ContentType = string;
|
export type ContentType = string;
|
||||||
|
|
||||||
// tx claude https://claude.ai/share/344fc7bd-5321-4763-af2f-b82275e9f865
|
// tx claude https://claude.ai/share/344fc7bd-5321-4763-af2f-b82275e9f865
|
||||||
|
|||||||
27
express/context.ts
Normal file
27
express/context.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// context.ts
|
||||||
|
//
|
||||||
|
// Request-scoped context using AsyncLocalStorage.
|
||||||
|
// Allows services to access request data (like the current user) without
|
||||||
|
// needing to pass Call through every function.
|
||||||
|
|
||||||
|
import { AsyncLocalStorage } from "node:async_hooks";
|
||||||
|
import { AnonymousUser, type MaybeUser } from "./user";
|
||||||
|
|
||||||
|
type RequestContext = {
|
||||||
|
user: MaybeUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
const asyncLocalStorage = new AsyncLocalStorage<RequestContext>();
|
||||||
|
|
||||||
|
// Run a function within a request context
|
||||||
|
function runWithContext<T>(context: RequestContext, fn: () => T): T {
|
||||||
|
return asyncLocalStorage.run(context, fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current user from context, or AnonymousUser if not in a request
|
||||||
|
function getCurrentUser(): MaybeUser {
|
||||||
|
const context = asyncLocalStorage.getStore();
|
||||||
|
return context?.user ?? AnonymousUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getCurrentUser, runWithContext, type RequestContext };
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
// services.ts
|
import nunjucks from "nunjucks";
|
||||||
|
import { db, migrate, migrationStatus } from "../database";
|
||||||
import { AuthService } from "../auth";
|
|
||||||
import { db, migrate, migrationStatus, PostgresAuthStore } from "../database";
|
|
||||||
import { getLogs, log } from "../logging";
|
import { getLogs, log } from "../logging";
|
||||||
import { anonymousUser, type User } from "../user";
|
|
||||||
|
// FIXME: This doesn't belong here; move it somewhere else.
|
||||||
|
const conf = {
|
||||||
|
templateEngine: () => {
|
||||||
|
return {
|
||||||
|
renderTemplate: (template: string, context: object) => {
|
||||||
|
return nunjucks.renderString(template, context);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const database = {
|
const database = {
|
||||||
db,
|
db,
|
||||||
@@ -28,24 +36,13 @@ const misc = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const session = {
|
|
||||||
getUser: (): User => {
|
|
||||||
return anonymousUser;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize auth with PostgreSQL store
|
|
||||||
const authStore = new PostgresAuthStore();
|
|
||||||
const auth = new AuthService(authStore);
|
|
||||||
|
|
||||||
// Keep this asciibetically sorted
|
// Keep this asciibetically sorted
|
||||||
const services = {
|
const core = {
|
||||||
auth,
|
conf,
|
||||||
database,
|
database,
|
||||||
logging,
|
logging,
|
||||||
misc,
|
misc,
|
||||||
random,
|
random,
|
||||||
session,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { services };
|
export { core };
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { contentTypes } from "./content-types";
|
import { contentTypes } from "./content-types";
|
||||||
|
import { core } from "./core";
|
||||||
import { httpCodes } from "./http-codes";
|
import { httpCodes } from "./http-codes";
|
||||||
import { services } from "./services";
|
|
||||||
import type { Call, Handler, Result } from "./types";
|
import type { Call, Handler, Result } from "./types";
|
||||||
|
|
||||||
const multiHandler: Handler = async (call: Call): Promise<Result> => {
|
const multiHandler: Handler = async (call: Call): Promise<Result> => {
|
||||||
const code = httpCodes.success.OK;
|
const code = httpCodes.success.OK;
|
||||||
const rn = services.random.randomNumber();
|
const rn = core.random.randomNumber();
|
||||||
|
|
||||||
const retval: Result = {
|
const retval: Result = {
|
||||||
code,
|
code,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// This file belongs to the framework. You are not expected to modify it.
|
||||||
|
|
||||||
export type HttpCode = {
|
export type HttpCode = {
|
||||||
code: number;
|
code: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
69
express/mgmt/add-user.ts
Normal file
69
express/mgmt/add-user.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
// add-user.ts
|
||||||
|
// Management command to create users from the command line
|
||||||
|
|
||||||
|
import { hashPassword } from "../auth/password";
|
||||||
|
import { PostgresAuthStore, pool } from "../database";
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length < 2) {
|
||||||
|
console.error(
|
||||||
|
"Usage: ./mgmt add-user <email> <password> [--display-name <name>] [--active]",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = args[0];
|
||||||
|
const password = args[1];
|
||||||
|
|
||||||
|
// Parse optional flags
|
||||||
|
let displayName: string | undefined;
|
||||||
|
let makeActive = false;
|
||||||
|
|
||||||
|
for (let i = 2; i < args.length; i++) {
|
||||||
|
if (args[i] === "--display-name" && args[i + 1]) {
|
||||||
|
displayName = args[i + 1];
|
||||||
|
i++;
|
||||||
|
} else if (args[i] === "--active") {
|
||||||
|
makeActive = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const store = new PostgresAuthStore();
|
||||||
|
|
||||||
|
// Check if user already exists
|
||||||
|
const existing = await store.getUserByEmail(email);
|
||||||
|
if (existing) {
|
||||||
|
console.error(`Error: User with email '${email}' already exists`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password and create user
|
||||||
|
const passwordHash = await hashPassword(password);
|
||||||
|
const user = await store.createUser({
|
||||||
|
email,
|
||||||
|
passwordHash,
|
||||||
|
displayName,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Optionally activate user immediately
|
||||||
|
if (makeActive) {
|
||||||
|
await store.updateUserEmailVerified(user.id);
|
||||||
|
console.log(
|
||||||
|
`Created and activated user: ${user.email} (${user.id})`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(`Created user: ${user.email} (${user.id})`);
|
||||||
|
console.log(" Status: pending (use --active to create as active)");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error("Failed to create user:", err.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"prettier": "prettier",
|
|
||||||
"nodemon": "nodemon dist/index.js"
|
"nodemon": "nodemon dist/index.js"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
@@ -13,7 +12,6 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"packageManager": "pnpm@10.12.4",
|
"packageManager": "pnpm@10.12.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
|
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/nunjucks": "^3.2.6",
|
"@types/nunjucks": "^3.2.6",
|
||||||
"@vercel/ncc": "^0.38.4",
|
"@vercel/ncc": "^0.38.4",
|
||||||
@@ -23,29 +21,12 @@
|
|||||||
"nunjucks": "^3.2.4",
|
"nunjucks": "^3.2.4",
|
||||||
"path-to-regexp": "^8.3.0",
|
"path-to-regexp": "^8.3.0",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"prettier": "^3.6.2",
|
|
||||||
"ts-luxon": "^6.2.0",
|
"ts-luxon": "^6.2.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.20.6",
|
"tsx": "^4.20.6",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"prettier": {
|
|
||||||
"arrowParens": "always",
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"trailingComma": "all",
|
|
||||||
"tabWidth": 4,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": false,
|
|
||||||
"importOrder": [
|
|
||||||
"<THIRD_PARTY_MODULES>",
|
|
||||||
"^[./]"
|
|
||||||
],
|
|
||||||
"importOrderCaseSensitive": true,
|
|
||||||
"plugins": [
|
|
||||||
"@ianvs/prettier-plugin-sort-imports"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.3.10",
|
"@biomejs/biome": "2.3.10",
|
||||||
"@types/express": "^5.0.5",
|
"@types/express": "^5.0.5",
|
||||||
|
|||||||
159
express/pnpm-lock.yaml
generated
159
express/pnpm-lock.yaml
generated
@@ -8,9 +8,6 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ianvs/prettier-plugin-sort-imports':
|
|
||||||
specifier: ^4.7.0
|
|
||||||
version: 4.7.0(prettier@3.6.2)
|
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^24.10.1
|
specifier: ^24.10.1
|
||||||
version: 24.10.1
|
version: 24.10.1
|
||||||
@@ -38,9 +35,6 @@ importers:
|
|||||||
pg:
|
pg:
|
||||||
specifier: ^8.16.3
|
specifier: ^8.16.3
|
||||||
version: 8.16.3
|
version: 8.16.3
|
||||||
prettier:
|
|
||||||
specifier: ^3.6.2
|
|
||||||
version: 3.6.2
|
|
||||||
ts-luxon:
|
ts-luxon:
|
||||||
specifier: ^6.2.0
|
specifier: ^6.2.0
|
||||||
version: 6.2.0
|
version: 6.2.0
|
||||||
@@ -69,43 +63,6 @@ importers:
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
'@babel/code-frame@7.27.1':
|
|
||||||
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/generator@7.28.5':
|
|
||||||
resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/helper-globals@7.28.0':
|
|
||||||
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/helper-string-parser@7.27.1':
|
|
||||||
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@7.28.5':
|
|
||||||
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/parser@7.28.5':
|
|
||||||
resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==}
|
|
||||||
engines: {node: '>=6.0.0'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
'@babel/template@7.27.2':
|
|
||||||
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/traverse@7.28.5':
|
|
||||||
resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/types@7.28.5':
|
|
||||||
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@biomejs/biome@2.3.10':
|
'@biomejs/biome@2.3.10':
|
||||||
resolution: {integrity: sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ==}
|
resolution: {integrity: sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ==}
|
||||||
engines: {node: '>=14.21.3'}
|
engines: {node: '>=14.21.3'}
|
||||||
@@ -319,27 +276,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@ianvs/prettier-plugin-sort-imports@4.7.0':
|
|
||||||
resolution: {integrity: sha512-soa2bPUJAFruLL4z/CnMfSEKGznm5ebz29fIa9PxYtu8HHyLKNE1NXAs6dylfw1jn/ilEIfO2oLLN6uAafb7DA==}
|
|
||||||
peerDependencies:
|
|
||||||
'@prettier/plugin-oxc': ^0.0.4
|
|
||||||
'@vue/compiler-sfc': 2.7.x || 3.x
|
|
||||||
content-tag: ^4.0.0
|
|
||||||
prettier: 2 || 3 || ^4.0.0-0
|
|
||||||
prettier-plugin-ember-template-tag: ^2.1.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@prettier/plugin-oxc':
|
|
||||||
optional: true
|
|
||||||
'@vue/compiler-sfc':
|
|
||||||
optional: true
|
|
||||||
content-tag:
|
|
||||||
optional: true
|
|
||||||
prettier-plugin-ember-template-tag:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
|
||||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
|
||||||
|
|
||||||
'@jridgewell/resolve-uri@3.1.2':
|
'@jridgewell/resolve-uri@3.1.2':
|
||||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
@@ -347,9 +283,6 @@ packages:
|
|||||||
'@jridgewell/sourcemap-codec@1.5.5':
|
'@jridgewell/sourcemap-codec@1.5.5':
|
||||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||||
|
|
||||||
'@jridgewell/trace-mapping@0.3.31':
|
|
||||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
|
||||||
|
|
||||||
'@jridgewell/trace-mapping@0.3.9':
|
'@jridgewell/trace-mapping@0.3.9':
|
||||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||||
|
|
||||||
@@ -649,14 +582,6 @@ packages:
|
|||||||
is-promise@4.0.0:
|
is-promise@4.0.0:
|
||||||
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
|
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
|
||||||
|
|
||||||
jsesc@3.1.0:
|
|
||||||
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
kysely@0.28.9:
|
kysely@0.28.9:
|
||||||
resolution: {integrity: sha512-3BeXMoiOhpOwu62CiVpO6lxfq4eS6KMYfQdMsN/2kUCRNuF2YiEr7u0HLHaQU+O4Xu8YXE3bHVkwaQ85i72EuA==}
|
resolution: {integrity: sha512-3BeXMoiOhpOwu62CiVpO6lxfq4eS6KMYfQdMsN/2kUCRNuF2YiEr7u0HLHaQU+O4Xu8YXE3bHVkwaQ85i72EuA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -765,9 +690,6 @@ packages:
|
|||||||
pgpass@1.0.5:
|
pgpass@1.0.5:
|
||||||
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
|
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
|
||||||
|
|
||||||
picocolors@1.1.1:
|
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
|
||||||
|
|
||||||
picomatch@2.3.1:
|
picomatch@2.3.1:
|
||||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||||
engines: {node: '>=8.6'}
|
engines: {node: '>=8.6'}
|
||||||
@@ -788,11 +710,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
prettier@3.6.2:
|
|
||||||
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
|
|
||||||
engines: {node: '>=14'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
@@ -958,53 +875,6 @@ packages:
|
|||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@babel/code-frame@7.27.1':
|
|
||||||
dependencies:
|
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
|
||||||
js-tokens: 4.0.0
|
|
||||||
picocolors: 1.1.1
|
|
||||||
|
|
||||||
'@babel/generator@7.28.5':
|
|
||||||
dependencies:
|
|
||||||
'@babel/parser': 7.28.5
|
|
||||||
'@babel/types': 7.28.5
|
|
||||||
'@jridgewell/gen-mapping': 0.3.13
|
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
|
||||||
jsesc: 3.1.0
|
|
||||||
|
|
||||||
'@babel/helper-globals@7.28.0': {}
|
|
||||||
|
|
||||||
'@babel/helper-string-parser@7.27.1': {}
|
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@7.28.5': {}
|
|
||||||
|
|
||||||
'@babel/parser@7.28.5':
|
|
||||||
dependencies:
|
|
||||||
'@babel/types': 7.28.5
|
|
||||||
|
|
||||||
'@babel/template@7.27.2':
|
|
||||||
dependencies:
|
|
||||||
'@babel/code-frame': 7.27.1
|
|
||||||
'@babel/parser': 7.28.5
|
|
||||||
'@babel/types': 7.28.5
|
|
||||||
|
|
||||||
'@babel/traverse@7.28.5':
|
|
||||||
dependencies:
|
|
||||||
'@babel/code-frame': 7.27.1
|
|
||||||
'@babel/generator': 7.28.5
|
|
||||||
'@babel/helper-globals': 7.28.0
|
|
||||||
'@babel/parser': 7.28.5
|
|
||||||
'@babel/template': 7.27.2
|
|
||||||
'@babel/types': 7.28.5
|
|
||||||
debug: 4.4.3(supports-color@5.5.0)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@babel/types@7.28.5':
|
|
||||||
dependencies:
|
|
||||||
'@babel/helper-string-parser': 7.27.1
|
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
|
||||||
|
|
||||||
'@biomejs/biome@2.3.10':
|
'@biomejs/biome@2.3.10':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@biomejs/cli-darwin-arm64': 2.3.10
|
'@biomejs/cli-darwin-arm64': 2.3.10
|
||||||
@@ -1122,31 +992,10 @@ snapshots:
|
|||||||
'@esbuild/win32-x64@0.25.12':
|
'@esbuild/win32-x64@0.25.12':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@ianvs/prettier-plugin-sort-imports@4.7.0(prettier@3.6.2)':
|
|
||||||
dependencies:
|
|
||||||
'@babel/generator': 7.28.5
|
|
||||||
'@babel/parser': 7.28.5
|
|
||||||
'@babel/traverse': 7.28.5
|
|
||||||
'@babel/types': 7.28.5
|
|
||||||
prettier: 3.6.2
|
|
||||||
semver: 7.7.3
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
|
||||||
dependencies:
|
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
|
||||||
|
|
||||||
'@jridgewell/resolve-uri@3.1.2': {}
|
'@jridgewell/resolve-uri@3.1.2': {}
|
||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||||
|
|
||||||
'@jridgewell/trace-mapping@0.3.31':
|
|
||||||
dependencies:
|
|
||||||
'@jridgewell/resolve-uri': 3.1.2
|
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
|
||||||
|
|
||||||
'@jridgewell/trace-mapping@0.3.9':
|
'@jridgewell/trace-mapping@0.3.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
@@ -1497,10 +1346,6 @@ snapshots:
|
|||||||
|
|
||||||
is-promise@4.0.0: {}
|
is-promise@4.0.0: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
|
||||||
|
|
||||||
jsesc@3.1.0: {}
|
|
||||||
|
|
||||||
kysely@0.28.9: {}
|
kysely@0.28.9: {}
|
||||||
|
|
||||||
make-error@1.3.6: {}
|
make-error@1.3.6: {}
|
||||||
@@ -1597,8 +1442,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
split2: 4.2.0
|
split2: 4.2.0
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
|
||||||
|
|
||||||
picomatch@2.3.1: {}
|
picomatch@2.3.1: {}
|
||||||
|
|
||||||
postgres-array@2.0.0: {}
|
postgres-array@2.0.0: {}
|
||||||
@@ -1611,8 +1454,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
xtend: 4.0.2
|
xtend: 4.0.2
|
||||||
|
|
||||||
prettier@3.6.2: {}
|
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
forwarded: 0.2.0
|
forwarded: 0.2.0
|
||||||
|
|||||||
25
express/request/index.ts
Normal file
25
express/request/index.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { AuthService } from "../auth";
|
||||||
|
import { getCurrentUser } from "../context";
|
||||||
|
import { PostgresAuthStore } from "../database";
|
||||||
|
import type { MaybeUser } from "../user";
|
||||||
|
import { html, redirect, render } from "./util";
|
||||||
|
|
||||||
|
const util = { html, redirect, render };
|
||||||
|
|
||||||
|
const session = {
|
||||||
|
getUser: (): MaybeUser => {
|
||||||
|
return getCurrentUser();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize auth with PostgreSQL store
|
||||||
|
const authStore = new PostgresAuthStore();
|
||||||
|
const auth = new AuthService(authStore);
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
auth,
|
||||||
|
session,
|
||||||
|
util,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { request };
|
||||||
45
express/request/util.ts
Normal file
45
express/request/util.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { contentTypes } from "../content-types";
|
||||||
|
import { core } from "../core";
|
||||||
|
import { executionContext } from "../execution-context";
|
||||||
|
import { httpCodes } from "../http-codes";
|
||||||
|
import type { RedirectResult, Result } from "../types";
|
||||||
|
import { loadFile } from "../util";
|
||||||
|
import { request } from "./index";
|
||||||
|
|
||||||
|
type NoUser = {
|
||||||
|
[key: string]: unknown;
|
||||||
|
} & {
|
||||||
|
user?: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
const render = async (path: string, ctx?: NoUser): Promise<string> => {
|
||||||
|
const fullPath = `${executionContext.diachron_root}/templates/${path}.html.njk`;
|
||||||
|
const template = await loadFile(fullPath);
|
||||||
|
const user = request.session.getUser();
|
||||||
|
const context = { user, ...ctx };
|
||||||
|
const engine = core.conf.templateEngine();
|
||||||
|
const retval = engine.renderTemplate(template, context);
|
||||||
|
|
||||||
|
return retval;
|
||||||
|
};
|
||||||
|
|
||||||
|
const html = (payload: string): Result => {
|
||||||
|
const retval: Result = {
|
||||||
|
code: httpCodes.success.OK,
|
||||||
|
result: payload,
|
||||||
|
contentType: contentTypes.text.html,
|
||||||
|
};
|
||||||
|
|
||||||
|
return retval;
|
||||||
|
};
|
||||||
|
|
||||||
|
const redirect = (location: string): RedirectResult => {
|
||||||
|
return {
|
||||||
|
code: httpCodes.redirection.SeeOther,
|
||||||
|
contentType: contentTypes.text.plain,
|
||||||
|
result: "",
|
||||||
|
redirect: location,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { html, redirect, render };
|
||||||
@@ -5,9 +5,9 @@ import { DateTime } from "ts-luxon";
|
|||||||
import { authRoutes } from "./auth/routes";
|
import { authRoutes } from "./auth/routes";
|
||||||
import { routes as basicRoutes } from "./basic/routes";
|
import { routes as basicRoutes } from "./basic/routes";
|
||||||
import { contentTypes } from "./content-types";
|
import { contentTypes } from "./content-types";
|
||||||
|
import { core } from "./core";
|
||||||
import { multiHandler } from "./handlers";
|
import { multiHandler } from "./handlers";
|
||||||
import { httpCodes } from "./http-codes";
|
import { httpCodes } from "./http-codes";
|
||||||
import { services } from "./services";
|
|
||||||
import type { Call, Result, Route } from "./types";
|
import type { Call, Result, Route } from "./types";
|
||||||
|
|
||||||
// FIXME: Obviously put this somewhere else
|
// FIXME: Obviously put this somewhere else
|
||||||
@@ -25,14 +25,17 @@ const okText = (result: string): Result => {
|
|||||||
|
|
||||||
const routes: Route[] = [
|
const routes: Route[] = [
|
||||||
...authRoutes,
|
...authRoutes,
|
||||||
|
basicRoutes.home,
|
||||||
basicRoutes.hello,
|
basicRoutes.hello,
|
||||||
|
basicRoutes.login,
|
||||||
|
basicRoutes.logout,
|
||||||
{
|
{
|
||||||
path: "/slow",
|
path: "/slow",
|
||||||
methods: ["GET"],
|
methods: ["GET"],
|
||||||
handler: async (_call: Call): Promise<Result> => {
|
handler: async (_call: Call): Promise<Result> => {
|
||||||
console.log("starting slow request");
|
console.log("starting slow request");
|
||||||
|
|
||||||
await services.misc.sleep(2);
|
await core.misc.sleep(2);
|
||||||
|
|
||||||
console.log("finishing slow request");
|
console.log("finishing slow request");
|
||||||
const retval = okText("that was slow");
|
const retval = okText("that was slow");
|
||||||
|
|||||||
@@ -49,12 +49,35 @@ export type ProcessedRoute = {
|
|||||||
handler: InternalHandler;
|
handler: InternalHandler;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CookieOptions = {
|
||||||
|
httpOnly?: boolean;
|
||||||
|
secure?: boolean;
|
||||||
|
sameSite?: "strict" | "lax" | "none";
|
||||||
|
maxAge?: number;
|
||||||
|
path?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Cookie = {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
options?: CookieOptions;
|
||||||
|
};
|
||||||
|
|
||||||
export type Result = {
|
export type Result = {
|
||||||
code: HttpCode;
|
code: HttpCode;
|
||||||
contentType: ContentType;
|
contentType: ContentType;
|
||||||
result: string;
|
result: string;
|
||||||
|
cookies?: Cookie[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RedirectResult = Result & {
|
||||||
|
redirect: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isRedirect(result: Result): result is RedirectResult {
|
||||||
|
return "redirect" in result;
|
||||||
|
}
|
||||||
|
|
||||||
export type Route = {
|
export type Route = {
|
||||||
path: string;
|
path: string;
|
||||||
methods: Method[];
|
methods: Method[];
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import nunjucks from "nunjucks";
|
|
||||||
import { contentTypes } from "./content-types";
|
|
||||||
import { executionContext } from "./execution-context";
|
|
||||||
import { httpCodes } from "./http-codes";
|
|
||||||
import type { Result } from "./types";
|
|
||||||
|
|
||||||
// FIXME: Handle the error here
|
// FIXME: Handle the error here
|
||||||
const loadFile = async (path: string): Promise<string> => {
|
const loadFile = async (path: string): Promise<string> => {
|
||||||
@@ -13,24 +8,4 @@ const loadFile = async (path: string): Promise<string> => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const render = async (path: string, ctx: object): Promise<string> => {
|
export { loadFile };
|
||||||
const fullPath = `${executionContext.diachron_root}/templates/${path}.html.njk`;
|
|
||||||
|
|
||||||
const template = await loadFile(fullPath);
|
|
||||||
|
|
||||||
const retval = nunjucks.renderString(template, ctx);
|
|
||||||
|
|
||||||
return retval;
|
|
||||||
};
|
|
||||||
|
|
||||||
const html = (payload: string): Result => {
|
|
||||||
const retval: Result = {
|
|
||||||
code: httpCodes.success.OK,
|
|
||||||
result: payload,
|
|
||||||
contentType: contentTypes.text.html,
|
|
||||||
};
|
|
||||||
|
|
||||||
return retval;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { render, html };
|
|
||||||
|
|||||||
9
framework/mgmt.d/add-user
Executable file
9
framework/mgmt.d/add-user
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT="$DIR/../.."
|
||||||
|
|
||||||
|
cd "$ROOT/express"
|
||||||
|
"$DIR"/../cmd.d/tsx mgmt/add-user.ts "$@"
|
||||||
@@ -5,10 +5,8 @@
|
|||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
node_shim_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
node_shim_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
export node_shim_DIR
|
|
||||||
|
|
||||||
source "$node_shim_DIR"/../versions
|
|
||||||
|
|
||||||
|
# shellcheck source=node.common
|
||||||
source "$node_shim_DIR"/node.common
|
source "$node_shim_DIR"/node.common
|
||||||
|
|
||||||
exec "$nodejs_binary_dir/node" "$@"
|
exec "$nodejs_binary_dir/node" "$@"
|
||||||
|
|||||||
@@ -2,23 +2,19 @@
|
|||||||
# shellcheck shell=bash
|
# shellcheck shell=bash
|
||||||
|
|
||||||
node_common_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
node_common_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
project_root="$node_common_DIR/../.."
|
||||||
|
|
||||||
# FIXME this shouldn't be hardcoded here of course
|
# shellcheck source=../versions
|
||||||
nodejs_binary_dir="$node_common_DIR/../binaries/node-v22.15.1-linux-x64/bin"
|
source "$node_common_DIR"/../versions
|
||||||
|
|
||||||
|
nodejs_binary_dir="$project_root/$nodejs_bin_dir"
|
||||||
|
|
||||||
# This might be too restrictive. Or not restrictive enough.
|
# This might be too restrictive. Or not restrictive enough.
|
||||||
PATH="$nodejs_binary_dir":/bin:/usr/bin
|
PATH="$nodejs_binary_dir":/bin:/usr/bin
|
||||||
|
|
||||||
project_root="$node_common_DIR/../.."
|
node_dist_dir="$project_root/$nodejs_dist_dir"
|
||||||
|
|
||||||
node_dir="$project_root/$nodejs_binary_dir"
|
export NPM_CONFIG_PREFIX="$node_dist_dir/npm"
|
||||||
|
export NPM_CONFIG_CACHE="$node_dist_dir/cache"
|
||||||
export NPM_CONFIG_PREFIX="$node_dir/npm"
|
export NPM_CONFIG_TMP="$node_dist_dir/tmp"
|
||||||
export NPM_CONFIG_CACHE="$node_dir/cache"
|
export NODE_PATH="$node_dist_dir/node_modules"
|
||||||
export NPM_CONFIG_TMP="$node_dir/tmp"
|
|
||||||
export NODE_PATH="$node_dir/node_modules"
|
|
||||||
|
|
||||||
# echo $NPM_CONFIG_PREFIX
|
|
||||||
# echo $NPM_CONFIG_CACHE
|
|
||||||
# echo $NPM_CONFIG_TMP
|
|
||||||
# echo $NODE_PATH
|
|
||||||
|
|||||||
19
framework/versions
Normal file
19
framework/versions
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# shellcheck shell=bash
|
||||||
|
|
||||||
|
# This file belongs to the framework. You are not expected to modify it.
|
||||||
|
|
||||||
|
# https://nodejs.org/dist
|
||||||
|
nodejs_binary_linux_x86_64=https://nodejs.org/dist/v24.12.0/node-v24.12.0-linux-x64.tar.xz
|
||||||
|
nodejs_checksum_linux_x86_64=bdebee276e58d0ef5448f3d5ac12c67daa963dd5e0a9bb621a53d1cefbc852fd
|
||||||
|
nodejs_dist_dir=framework/binaries/node-v22.15.1-linux-x64
|
||||||
|
nodejs_bin_dir="$nodejs_dist_dir/bin"
|
||||||
|
|
||||||
|
caddy_binary_linux_x86_64=fixme
|
||||||
|
caddy_checksum_linux_x86_64=fixmetoo
|
||||||
|
|
||||||
|
# https://github.com/pnpm/pnpm/releases
|
||||||
|
pnpm_binary_linux_x86_64=https://github.com/pnpm/pnpm/releases/download/v10.28.0/pnpm-linux-x64
|
||||||
|
pnpm_checksum_linux_x86_64=sha256:348e863d17a62411a65f900e8d91395acabae9e9237653ccc3c36cb385965f28
|
||||||
|
|
||||||
|
|
||||||
|
golangci_lint=v2.7.2-alpine
|
||||||
7
logger/logger
Executable file
7
logger/logger
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
cd "$DIR"
|
||||||
|
|
||||||
|
./logger-bin "$@"
|
||||||
27
mgmt
Executable file
27
mgmt
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# This file belongs to the framework. You are not expected to modify it.
|
||||||
|
|
||||||
|
# Management command runner - parallel to ./cmd for operational tasks
|
||||||
|
# Usage: ./mgmt <command> [args...]
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
if [ $# -lt 1 ]; then
|
||||||
|
echo "Usage: ./mgmt <command> [args...]"
|
||||||
|
echo ""
|
||||||
|
echo "Available commands:"
|
||||||
|
for cmd in "$DIR"/framework/mgmt.d/*; do
|
||||||
|
if [ -x "$cmd" ]; then
|
||||||
|
basename "$cmd"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
subcmd="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
exec "$DIR"/framework/mgmt.d/"$subcmd" "$@"
|
||||||
66
sync.sh
Executable file
66
sync.sh
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Note: This is kind of AI slop and needs to be more carefully reviewed.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# shellcheck source=framework/versions
|
||||||
|
source "$DIR/framework/versions"
|
||||||
|
|
||||||
|
# Ensure correct node version is installed
|
||||||
|
node_installed_checksum_file="$DIR/framework/binaries/.node.checksum"
|
||||||
|
node_installed_checksum=""
|
||||||
|
if [ -f "$node_installed_checksum_file" ]; then
|
||||||
|
node_installed_checksum=$(cat "$node_installed_checksum_file")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$node_installed_checksum" != "$nodejs_checksum_linux_x86_64" ]; then
|
||||||
|
echo "Downloading Node.js..."
|
||||||
|
node_archive="$DIR/framework/downloads/node.tar.xz"
|
||||||
|
curl -fsSL "$nodejs_binary_linux_x86_64" -o "$node_archive"
|
||||||
|
|
||||||
|
echo "Verifying checksum..."
|
||||||
|
echo "$nodejs_checksum_linux_x86_64 $node_archive" | sha256sum -c -
|
||||||
|
|
||||||
|
echo "Extracting Node.js..."
|
||||||
|
tar -xf "$node_archive" -C "$DIR/framework/binaries"
|
||||||
|
rm "$node_archive"
|
||||||
|
|
||||||
|
echo "$nodejs_checksum_linux_x86_64" >"$node_installed_checksum_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure correct pnpm version is installed
|
||||||
|
pnpm_binary="$DIR/framework/binaries/pnpm"
|
||||||
|
pnpm_installed_checksum_file="$DIR/framework/binaries/.pnpm.checksum"
|
||||||
|
pnpm_installed_checksum=""
|
||||||
|
if [ -f "$pnpm_installed_checksum_file" ]; then
|
||||||
|
pnpm_installed_checksum=$(cat "$pnpm_installed_checksum_file")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# pnpm checksum includes "sha256:" prefix, strip it for sha256sum
|
||||||
|
pnpm_checksum="${pnpm_checksum_linux_x86_64#sha256:}"
|
||||||
|
|
||||||
|
if [ "$pnpm_installed_checksum" != "$pnpm_checksum" ]; then
|
||||||
|
echo "Downloading pnpm..."
|
||||||
|
curl -fsSL "$pnpm_binary_linux_x86_64" -o "$pnpm_binary"
|
||||||
|
|
||||||
|
echo "Verifying checksum..."
|
||||||
|
echo "$pnpm_checksum $pnpm_binary" | sha256sum -c -
|
||||||
|
|
||||||
|
chmod +x "$pnpm_binary"
|
||||||
|
|
||||||
|
echo "$pnpm_checksum" >"$pnpm_installed_checksum_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get golang binaries in place
|
||||||
|
cd "$DIR/master"
|
||||||
|
go build
|
||||||
|
|
||||||
|
cd "$DIR/logger"
|
||||||
|
go build
|
||||||
|
|
||||||
|
# Update framework code
|
||||||
|
cd "$DIR/express"
|
||||||
|
../cmd pnpm install
|
||||||
13
templates/basic/home.html.njk
Normal file
13
templates/basic/home.html.njk
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<html>
|
||||||
|
<head></head>
|
||||||
|
<body>
|
||||||
|
<p>
|
||||||
|
home
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
|
||||||
|
{{ email }}
|
||||||
|
</p>
|
||||||
|
<a href="/logout">logout</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
55
templates/basic/login.html.njk
Normal file
55
templates/basic/login.html.njk
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Login</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 50px auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
padding: 10px;
|
||||||
|
background: #fee;
|
||||||
|
border: 1px solid #fcc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Login</h1>
|
||||||
|
{% if error %}
|
||||||
|
<div class="error">{{ error }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<form method="POST" action="/login">
|
||||||
|
<label>
|
||||||
|
Email
|
||||||
|
<input type="email" name="email" required value="{{ email | default('') }}">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Password
|
||||||
|
<input type="password" name="password" required>
|
||||||
|
</label>
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user