@unbrained/pm-web 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/CHANGELOG.md +7 -0
- package/README.md +107 -0
- package/dist/auth.js +20 -0
- package/dist/auth.js.map +1 -0
- package/dist/crypto.js +42 -0
- package/dist/crypto.js.map +1 -0
- package/dist/db.js +111 -0
- package/dist/db.js.map +1 -0
- package/dist/index.js +88 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.js +16 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/routes/admin.js +207 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/auth.js +163 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/github.js +354 -0
- package/dist/routes/github.js.map +1 -0
- package/dist/routes/groups.js +180 -0
- package/dist/routes/groups.js.map +1 -0
- package/dist/routes/pm.js +2446 -0
- package/dist/routes/pm.js.map +1 -0
- package/dist/routes/projects.js +151 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/sharing.js +155 -0
- package/dist/routes/sharing.js.map +1 -0
- package/dist/server.js +64 -0
- package/dist/server.js.map +1 -0
- package/dist/services/pm-runner.js +190 -0
- package/dist/services/pm-runner.js.map +1 -0
- package/dist/services/sse.js +111 -0
- package/dist/services/sse.js.map +1 -0
- package/manifest.json +15 -0
- package/package.json +111 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/index.html +265 -0
- package/public/manifest.json +66 -0
- package/public/src/api.js +28 -0
- package/public/src/api.js.map +1 -0
- package/public/src/api.ts +29 -0
- package/public/src/app.js +926 -0
- package/public/src/app.js.map +1 -0
- package/public/src/app.ts +929 -0
- package/public/src/components/modals.js +62 -0
- package/public/src/components/modals.js.map +1 -0
- package/public/src/components/modals.ts +73 -0
- package/public/src/components/toast.js +10 -0
- package/public/src/components/toast.js.map +1 -0
- package/public/src/components/toast.ts +13 -0
- package/public/src/constants.js +30 -0
- package/public/src/constants.js.map +1 -0
- package/public/src/constants.ts +41 -0
- package/public/src/state.js +15 -0
- package/public/src/state.js.map +1 -0
- package/public/src/state.ts +19 -0
- package/public/src/types.js +5 -0
- package/public/src/types.js.map +1 -0
- package/public/src/types.ts +253 -0
- package/public/src/utils.js +57 -0
- package/public/src/utils.js.map +1 -0
- package/public/src/utils.ts +56 -0
- package/public/src/views/activity.js +47 -0
- package/public/src/views/activity.js.map +1 -0
- package/public/src/views/activity.ts +41 -0
- package/public/src/views/admin.js +435 -0
- package/public/src/views/admin.js.map +1 -0
- package/public/src/views/admin.ts +504 -0
- package/public/src/views/auth.js +81 -0
- package/public/src/views/auth.js.map +1 -0
- package/public/src/views/auth.ts +74 -0
- package/public/src/views/calendar.js +133 -0
- package/public/src/views/calendar.js.map +1 -0
- package/public/src/views/calendar.ts +129 -0
- package/public/src/views/comments-audit.js +109 -0
- package/public/src/views/comments-audit.js.map +1 -0
- package/public/src/views/comments-audit.ts +108 -0
- package/public/src/views/config.js +322 -0
- package/public/src/views/config.js.map +1 -0
- package/public/src/views/config.ts +344 -0
- package/public/src/views/context.js +98 -0
- package/public/src/views/context.js.map +1 -0
- package/public/src/views/context.ts +100 -0
- package/public/src/views/create.js +293 -0
- package/public/src/views/create.js.map +1 -0
- package/public/src/views/create.ts +246 -0
- package/public/src/views/dedupe.js +51 -0
- package/public/src/views/dedupe.js.map +1 -0
- package/public/src/views/dedupe.ts +43 -0
- package/public/src/views/export.js +300 -0
- package/public/src/views/export.js.map +1 -0
- package/public/src/views/export.ts +274 -0
- package/public/src/views/github.js +360 -0
- package/public/src/views/github.js.map +1 -0
- package/public/src/views/github.ts +308 -0
- package/public/src/views/graph-canvas.js +1986 -0
- package/public/src/views/graph-canvas.js.map +1 -0
- package/public/src/views/graph-canvas.ts +2218 -0
- package/public/src/views/graph.js +1824 -0
- package/public/src/views/graph.js.map +1 -0
- package/public/src/views/graph.ts +1891 -0
- package/public/src/views/groups.js +186 -0
- package/public/src/views/groups.js.map +1 -0
- package/public/src/views/groups.ts +172 -0
- package/public/src/views/guide.js +151 -0
- package/public/src/views/guide.js.map +1 -0
- package/public/src/views/guide.ts +162 -0
- package/public/src/views/health.js +105 -0
- package/public/src/views/health.js.map +1 -0
- package/public/src/views/health.ts +102 -0
- package/public/src/views/items.js +1306 -0
- package/public/src/views/items.js.map +1 -0
- package/public/src/views/items.ts +1196 -0
- package/public/src/views/normalize.js +67 -0
- package/public/src/views/normalize.js.map +1 -0
- package/public/src/views/normalize.ts +58 -0
- package/public/src/views/plan.js +454 -0
- package/public/src/views/plan.js.map +1 -0
- package/public/src/views/plan.ts +496 -0
- package/public/src/views/projects.js +204 -0
- package/public/src/views/projects.js.map +1 -0
- package/public/src/views/projects.ts +196 -0
- package/public/src/views/router.js +227 -0
- package/public/src/views/router.js.map +1 -0
- package/public/src/views/router.ts +188 -0
- package/public/src/views/search.js +103 -0
- package/public/src/views/search.js.map +1 -0
- package/public/src/views/search.ts +94 -0
- package/public/src/views/settings.js +272 -0
- package/public/src/views/settings.js.map +1 -0
- package/public/src/views/settings.ts +190 -0
- package/public/src/views/shared.js +49 -0
- package/public/src/views/shared.js.map +1 -0
- package/public/src/views/shared.ts +49 -0
- package/public/src/views/sharing.js +152 -0
- package/public/src/views/sharing.js.map +1 -0
- package/public/src/views/sharing.ts +139 -0
- package/public/src/views/stats.js +92 -0
- package/public/src/views/stats.js.map +1 -0
- package/public/src/views/stats.ts +88 -0
- package/public/src/views/templates.js +117 -0
- package/public/src/views/templates.js.map +1 -0
- package/public/src/views/templates.ts +113 -0
- package/public/src/views/validate.js +54 -0
- package/public/src/views/validate.js.map +1 -0
- package/public/src/views/validate.ts +48 -0
- package/public/styles.css +2231 -0
- package/public/sw.js +318 -0
- package/public/tsconfig.json +20 -0
- package/sql/schema.sql +105 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# pm-web
|
|
2
|
+
|
|
3
|
+
Full web UI for [pm-cli](https://github.com/unbraind/pm-cli) — browse, create, update, search, dedupe-audit, validate and manage pm projects in the browser.
|
|
4
|
+
|
|
5
|
+
Features user auth, multi-project support, sharing, groups, GitHub import/sync, admin-only management, local Ollama semantic search configuration, and pm-graph/Neo4j relationship graphs. Hosted at **pm-web.unbrained.dev** or self-host via Docker.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Quick Start (Self-Hosted)
|
|
10
|
+
|
|
11
|
+
### Docker
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
docker build -t pm-web .
|
|
15
|
+
docker run -p 4000:4000 -e DATABASE_URL=postgres://... pm-web
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Node.js
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
git clone https://github.com/unbraind/pm-web.git
|
|
22
|
+
cd pm-web
|
|
23
|
+
npm install
|
|
24
|
+
npm run build
|
|
25
|
+
|
|
26
|
+
# Set environment variables
|
|
27
|
+
export PORT=4000
|
|
28
|
+
export DATABASE_URL=postgres://user:pass@localhost:5432/pmweb
|
|
29
|
+
export JWT_SECRET=change-me
|
|
30
|
+
export OLLAMA_BASE_URL=http://localhost:11434
|
|
31
|
+
export PM_OLLAMA_MODEL=qwen3-embedding:0.6b
|
|
32
|
+
export NEO4J_URI=bolt://localhost:7687
|
|
33
|
+
export NEO4J_USER=neo4j
|
|
34
|
+
export NEO4J_PASSWORD=change-me
|
|
35
|
+
|
|
36
|
+
npm start
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Open http://localhost:4000 in your browser.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Installation as pm Package
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pm install github.com/unbraind/pm-web --global
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The package repository is at **github.com/unbraind/pm-web**.
|
|
50
|
+
|
|
51
|
+
### Commands
|
|
52
|
+
|
|
53
|
+
| Command | Description |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `pm web` | Start the pm-web server |
|
|
56
|
+
| `pm web --port 8080` | Start on a custom port |
|
|
57
|
+
|
|
58
|
+
### Environment Variables
|
|
59
|
+
|
|
60
|
+
| Variable | Required | Description |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `DATABASE_URL` | Yes | PostgreSQL connection string |
|
|
63
|
+
| `JWT_SECRET` | Yes | Secret for signing JWT tokens |
|
|
64
|
+
| `PM_WEB_SECRET_KEY` | Recommended | At-rest encryption key for saved GitHub PATs. Falls back to `JWT_SECRET`; use at least 32 characters |
|
|
65
|
+
| `PM_WEB_BOOTSTRAP_ADMIN_EMAIL` | Recommended | Email of the user account to auto-promote to admin on schema init. Leave unset to skip auto-promotion (manage admins via the admin UI). |
|
|
66
|
+
| `PORT` | No | Server port (default: 4000) |
|
|
67
|
+
| `NODE_ENV` | No | `production` enables caching |
|
|
68
|
+
| `OLLAMA_BASE_URL` / `OLLAMA_HOST` | No | Local Ollama endpoint for semantic pm search |
|
|
69
|
+
| `PM_OLLAMA_MODEL` | No | Embedding model for new projects, default `qwen3-embedding:0.6b` |
|
|
70
|
+
| `NEO4J_URI` | No | Neo4j Bolt URI for graph sync |
|
|
71
|
+
| `NEO4J_USER` / `NEO4J_USERNAME` | No | Neo4j username |
|
|
72
|
+
| `NEO4J_PASSWORD` | No | Neo4j password |
|
|
73
|
+
| `PM_GRAPH_EXTENSION_PATH` | No | Bundled pm-graph extension path, default `extensions/pm-graph` |
|
|
74
|
+
|
|
75
|
+
New pm-web projects configure local Ollama search automatically and install the bundled `pm-graph` extension into the project workspace. Neo4j graph rows are scoped per pm-web project so syncing one project does not overwrite another.
|
|
76
|
+
|
|
77
|
+
Saved GitHub personal access tokens are encrypted at rest before they are written to PostgreSQL. Existing plaintext tokens from older installs still work when read, and are replaced with encrypted values the next time the user saves a token.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Architecture
|
|
82
|
+
|
|
83
|
+
- **Backend**: Express.js with PostgreSQL
|
|
84
|
+
- **Frontend**: Single-page app in `public/`
|
|
85
|
+
- **Auth**: JWT-based user authentication
|
|
86
|
+
- **API**: RESTful API at `/api/*`
|
|
87
|
+
|
|
88
|
+
### API Routes
|
|
89
|
+
|
|
90
|
+
| Route | Description |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `/api/auth` | Authentication (login, register) |
|
|
93
|
+
| `/api/projects` | Project CRUD |
|
|
94
|
+
| `/api/projects/:id/pm` | PM item operations |
|
|
95
|
+
| `/api/groups` | Group management |
|
|
96
|
+
| `/api/projects/:id/shares` | Sharing |
|
|
97
|
+
| `/api/projects/:id/github` | GitHub integration |
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
|
104
|
+
|
|
105
|
+
## Release Automation
|
|
106
|
+
|
|
107
|
+
This package is release-ready for GitHub, npm, and Bun-compatible installs. CI runs type checking, build, production dependency audit, package packing, Bun install verification, and pm-changelog validation. The daily release workflow publishes only when commits exist after the latest release tag and uses pm-changelog to generate CHANGELOG.md and GitHub release notes.
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import jwt from "jsonwebtoken";
|
|
2
|
+
const JWT_SECRET = process.env.JWT_SECRET || "pm-web-dev-secret-change-in-prod";
|
|
3
|
+
const JWT_EXPIRES = "30d";
|
|
4
|
+
export function signToken(payload) {
|
|
5
|
+
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES });
|
|
6
|
+
}
|
|
7
|
+
export function verifyToken(token) {
|
|
8
|
+
return jwt.verify(token, JWT_SECRET);
|
|
9
|
+
}
|
|
10
|
+
export function extractToken(req) {
|
|
11
|
+
const authHeader = req.headers.authorization;
|
|
12
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
13
|
+
return authHeader.slice(7);
|
|
14
|
+
}
|
|
15
|
+
const cookie = req.cookies?.pm_token;
|
|
16
|
+
if (cookie)
|
|
17
|
+
return cookie;
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,cAAc,CAAC;AAG/B,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,kCAAkC,CAAC;AAChF,MAAM,WAAW,GAAG,KAAK,CAAC;AAO1B,MAAM,UAAU,SAAS,CAAC,OAAmB;IAC3C,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAe,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAY;IACvC,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IAC7C,IAAI,UAAU,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACtC,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7B,CAAC;IACD,MAAM,MAAM,GAAI,GAAsD,CAAC,OAAO,EAAE,QAAQ,CAAC;IACzF,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
const TOKEN_PREFIX = "pmweb:v1";
|
|
3
|
+
function secretMaterial() {
|
|
4
|
+
const value = process.env.PM_WEB_SECRET_KEY || process.env.JWT_SECRET;
|
|
5
|
+
if (!value || value.length < 32) {
|
|
6
|
+
throw new Error("Set PM_WEB_SECRET_KEY or a JWT_SECRET of at least 32 characters before storing GitHub tokens.");
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
function encryptionKey() {
|
|
11
|
+
return crypto.createHash("sha256").update(secretMaterial(), "utf8").digest();
|
|
12
|
+
}
|
|
13
|
+
export function encryptSecret(plainText) {
|
|
14
|
+
const iv = crypto.randomBytes(12);
|
|
15
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", encryptionKey(), iv);
|
|
16
|
+
const encrypted = Buffer.concat([cipher.update(plainText, "utf8"), cipher.final()]);
|
|
17
|
+
const tag = cipher.getAuthTag();
|
|
18
|
+
return [
|
|
19
|
+
TOKEN_PREFIX,
|
|
20
|
+
iv.toString("base64url"),
|
|
21
|
+
tag.toString("base64url"),
|
|
22
|
+
encrypted.toString("base64url"),
|
|
23
|
+
].join(":");
|
|
24
|
+
}
|
|
25
|
+
export function decryptSecret(stored) {
|
|
26
|
+
if (!stored)
|
|
27
|
+
return null;
|
|
28
|
+
if (!stored.startsWith(`${TOKEN_PREFIX}:`)) {
|
|
29
|
+
return stored;
|
|
30
|
+
}
|
|
31
|
+
const [, , ivRaw, tagRaw, encryptedRaw] = stored.split(":");
|
|
32
|
+
if (!ivRaw || !tagRaw || !encryptedRaw) {
|
|
33
|
+
throw new Error("Stored GitHub token is malformed.");
|
|
34
|
+
}
|
|
35
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", encryptionKey(), Buffer.from(ivRaw, "base64url"));
|
|
36
|
+
decipher.setAuthTag(Buffer.from(tagRaw, "base64url"));
|
|
37
|
+
return Buffer.concat([
|
|
38
|
+
decipher.update(Buffer.from(encryptedRaw, "base64url")),
|
|
39
|
+
decipher.final(),
|
|
40
|
+
]).toString("utf8");
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAC;AAEjC,MAAM,YAAY,GAAG,UAAU,CAAC;AAEhC,SAAS,cAAc;IACrB,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;IACtE,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,+FAA+F,CAAC,CAAC;IACnH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,aAAa;IACpB,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,cAAc,EAAE,EAAE,MAAM,CAAC,CAAC,MAAM,EAAE,CAAC;AAC/E,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,SAAiB;IAC7C,MAAM,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IAClC,MAAM,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC,aAAa,EAAE,aAAa,EAAE,EAAE,EAAE,CAAC,CAAC;IACzE,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACpF,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;IAChC,OAAO;QACL,YAAY;QACZ,EAAE,CAAC,QAAQ,CAAC,WAAW,CAAC;QACxB,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC;QACzB,SAAS,CAAC,QAAQ,CAAC,WAAW,CAAC;KAChC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACd,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAiC;IAC7D,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,YAAY,GAAG,CAAC,EAAE,CAAC;QAC3C,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,CAAC,EAAE,AAAD,EAAG,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC5D,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,gBAAgB,CACtC,aAAa,EACb,aAAa,EAAE,EACf,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,WAAW,CAAC,CAChC,CAAC;IACF,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;IACtD,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;QACvD,QAAQ,CAAC,KAAK,EAAE;KACjB,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC"}
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import pg from "pg";
|
|
2
|
+
const { Pool } = pg;
|
|
3
|
+
export const pool = new Pool({
|
|
4
|
+
...(process.env.DATABASE_URL
|
|
5
|
+
? { connectionString: process.env.DATABASE_URL }
|
|
6
|
+
: {
|
|
7
|
+
host: process.env.POSTGRES_HOST || "pm-telemetry-postgres",
|
|
8
|
+
port: parseInt(process.env.POSTGRES_PORT || "5432", 10),
|
|
9
|
+
user: process.env.POSTGRES_USER,
|
|
10
|
+
password: process.env.POSTGRES_PASSWORD,
|
|
11
|
+
database: process.env.POSTGRES_DB,
|
|
12
|
+
}),
|
|
13
|
+
max: 10,
|
|
14
|
+
idleTimeoutMillis: 30_000,
|
|
15
|
+
connectionTimeoutMillis: 5_000,
|
|
16
|
+
});
|
|
17
|
+
const bootstrapAdminEmail = (process.env.PM_WEB_BOOTSTRAP_ADMIN_EMAIL || "")
|
|
18
|
+
.trim()
|
|
19
|
+
.toLowerCase();
|
|
20
|
+
export async function initSchema() {
|
|
21
|
+
await pool.query(`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS pm_users (
|
|
23
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
24
|
+
email TEXT UNIQUE NOT NULL,
|
|
25
|
+
password_hash TEXT NOT NULL,
|
|
26
|
+
display_name TEXT,
|
|
27
|
+
is_admin BOOLEAN NOT NULL DEFAULT FALSE,
|
|
28
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
29
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE IF NOT EXISTS pm_projects (
|
|
33
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
34
|
+
user_id UUID NOT NULL REFERENCES pm_users(id) ON DELETE CASCADE,
|
|
35
|
+
name TEXT NOT NULL,
|
|
36
|
+
slug TEXT NOT NULL,
|
|
37
|
+
description TEXT DEFAULT '',
|
|
38
|
+
prefix TEXT NOT NULL,
|
|
39
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
40
|
+
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
41
|
+
UNIQUE(user_id, slug)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE INDEX IF NOT EXISTS pm_projects_user_id ON pm_projects(user_id);
|
|
45
|
+
|
|
46
|
+
CREATE TABLE IF NOT EXISTS pm_groups (
|
|
47
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
48
|
+
owner_id UUID NOT NULL REFERENCES pm_users(id) ON DELETE CASCADE,
|
|
49
|
+
name TEXT NOT NULL,
|
|
50
|
+
description TEXT DEFAULT '',
|
|
51
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
52
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS pm_group_members (
|
|
56
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
57
|
+
group_id UUID NOT NULL REFERENCES pm_groups(id) ON DELETE CASCADE,
|
|
58
|
+
user_id UUID NOT NULL REFERENCES pm_users(id) ON DELETE CASCADE,
|
|
59
|
+
role TEXT NOT NULL DEFAULT 'member',
|
|
60
|
+
invited_at TIMESTAMPTZ DEFAULT NOW(),
|
|
61
|
+
UNIQUE(group_id, user_id)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE TABLE IF NOT EXISTS pm_project_shares (
|
|
65
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
66
|
+
project_id UUID NOT NULL REFERENCES pm_projects(id) ON DELETE CASCADE,
|
|
67
|
+
shared_with_user_id UUID REFERENCES pm_users(id) ON DELETE CASCADE,
|
|
68
|
+
shared_with_group_id UUID REFERENCES pm_groups(id) ON DELETE CASCADE,
|
|
69
|
+
permission TEXT NOT NULL DEFAULT 'view',
|
|
70
|
+
shared_at TIMESTAMPTZ DEFAULT NOW(),
|
|
71
|
+
CONSTRAINT share_target CHECK (
|
|
72
|
+
(shared_with_user_id IS NOT NULL AND shared_with_group_id IS NULL) OR
|
|
73
|
+
(shared_with_user_id IS NULL AND shared_with_group_id IS NOT NULL)
|
|
74
|
+
),
|
|
75
|
+
UNIQUE(project_id, shared_with_user_id),
|
|
76
|
+
UNIQUE(project_id, shared_with_group_id)
|
|
77
|
+
);
|
|
78
|
+
`);
|
|
79
|
+
// Migrations for new columns (idempotent)
|
|
80
|
+
await pool.query(`
|
|
81
|
+
ALTER TABLE pm_users ADD COLUMN IF NOT EXISTS github_token TEXT;
|
|
82
|
+
ALTER TABLE pm_users ADD COLUMN IF NOT EXISTS is_admin BOOLEAN NOT NULL DEFAULT FALSE;
|
|
83
|
+
ALTER TABLE pm_projects ADD COLUMN IF NOT EXISTS github_owner TEXT;
|
|
84
|
+
ALTER TABLE pm_projects ADD COLUMN IF NOT EXISTS github_repo TEXT;
|
|
85
|
+
ALTER TABLE pm_projects ADD COLUMN IF NOT EXISTS github_sync_enabled BOOLEAN DEFAULT FALSE;
|
|
86
|
+
`);
|
|
87
|
+
await pool.query(`CREATE TABLE IF NOT EXISTS pm_admin_audit (
|
|
88
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
89
|
+
actor_id UUID NOT NULL REFERENCES pm_users(id) ON DELETE CASCADE,
|
|
90
|
+
action TEXT NOT NULL,
|
|
91
|
+
description TEXT DEFAULT '',
|
|
92
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
93
|
+
)`);
|
|
94
|
+
await pool.query(`CREATE INDEX IF NOT EXISTS pm_admin_audit_created_at ON pm_admin_audit(created_at DESC)`);
|
|
95
|
+
await pool.query(`
|
|
96
|
+
CREATE TABLE IF NOT EXISTS pm_github_item_links (
|
|
97
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
98
|
+
project_id UUID NOT NULL REFERENCES pm_projects(id) ON DELETE CASCADE,
|
|
99
|
+
pm_item_id TEXT NOT NULL,
|
|
100
|
+
issue_number INTEGER NOT NULL,
|
|
101
|
+
issue_url TEXT NOT NULL,
|
|
102
|
+
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
103
|
+
UNIQUE (project_id, pm_item_id)
|
|
104
|
+
);
|
|
105
|
+
`);
|
|
106
|
+
await pool.query(`CREATE INDEX IF NOT EXISTS pm_github_item_links_project ON pm_github_item_links(project_id)`);
|
|
107
|
+
if (bootstrapAdminEmail) {
|
|
108
|
+
await pool.query(`UPDATE pm_users SET is_admin = TRUE, updated_at = NOW() WHERE lower(email) = lower($1)`, [bootstrapAdminEmail]);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=db.js.map
|
package/dist/db.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;AAEpB,MAAM,CAAC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC;IAC3B,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY;QAC1B,CAAC,CAAC,EAAE,gBAAgB,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE;QAChD,CAAC,CAAC;YACE,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,uBAAuB;YAC1D,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,MAAM,EAAE,EAAE,CAAC;YACvD,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa;YAC/B,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,iBAAiB;YACvC,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW;SAClC,CAAC;IACN,GAAG,EAAE,EAAE;IACP,iBAAiB,EAAE,MAAM;IACzB,uBAAuB,EAAE,KAAK;CAC/B,CAAC,CAAC;AAEH,MAAM,mBAAmB,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,4BAA4B,IAAI,EAAE,CAAC;KACzE,IAAI,EAAE;KACN,WAAW,EAAE,CAAC;AAEjB,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,MAAM,IAAI,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDhB,CAAC,CAAC;IAEH,0CAA0C;IAC1C,MAAM,IAAI,CAAC,KAAK,CAAC;;;;;;GAMhB,CAAC,CAAC;IAEH,MAAM,IAAI,CAAC,KAAK,CACd;;;;;;MAME,CACH,CAAC;IAEF,MAAM,IAAI,CAAC,KAAK,CACd,yFAAyF,CAC1F,CAAC;IAEF,MAAM,IAAI,CAAC,KAAK,CAAC;;;;;;;;;;GAUhB,CAAC,CAAC;IAEH,MAAM,IAAI,CAAC,KAAK,CACd,6FAA6F,CAC9F,CAAC;IAEF,IAAI,mBAAmB,EAAE,CAAC;QACxB,MAAM,IAAI,CAAC,KAAK,CACd,wFAAwF,EACxF,CAAC,mBAAmB,CAAC,CACtB,CAAC;IACJ,CAAC;AACH,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// pm-web — Extension wrapper for the pm-web server
|
|
2
|
+
// This file registers the web server as a pm extension command.
|
|
3
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
// Inline defineExtension helper (avoids runtime dependency on @unbrained/pm-cli/sdk)
|
|
8
|
+
function defineExtension(m) { return m; }
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
11
|
+
let serverProcess = null;
|
|
12
|
+
function ensureRuntimeDependencies() {
|
|
13
|
+
const expressPackage = path.join(packageRoot, "node_modules", "express", "package.json");
|
|
14
|
+
if (fs.existsSync(expressPackage))
|
|
15
|
+
return;
|
|
16
|
+
console.error("Installing pm-web runtime dependencies...");
|
|
17
|
+
const install = spawnSync("npm", ["install", "--omit=dev"], {
|
|
18
|
+
cwd: packageRoot,
|
|
19
|
+
stdio: "inherit",
|
|
20
|
+
env: { ...process.env, NODE_ENV: "production" },
|
|
21
|
+
});
|
|
22
|
+
if (install.error)
|
|
23
|
+
throw install.error;
|
|
24
|
+
if (install.status !== 0) {
|
|
25
|
+
throw new Error(`npm install --omit=dev failed with exit code ${install.status ?? "unknown"}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export default defineExtension({
|
|
29
|
+
name: "pm-web",
|
|
30
|
+
version: "1.0.0",
|
|
31
|
+
activate(api) {
|
|
32
|
+
// -----------------------------------------------------------------------
|
|
33
|
+
// Command: pm web [--port <port>]
|
|
34
|
+
// -----------------------------------------------------------------------
|
|
35
|
+
api.registerCommand({
|
|
36
|
+
name: "web",
|
|
37
|
+
description: "Start the pm-web server. Opens a browser-based UI for managing pm projects.",
|
|
38
|
+
intent: "launch the pm-web server",
|
|
39
|
+
examples: [
|
|
40
|
+
"pm web",
|
|
41
|
+
"pm web --port 8080",
|
|
42
|
+
],
|
|
43
|
+
flags: [
|
|
44
|
+
{ long: "--port", value_name: "port", description: "Port to listen on (default: 4000 or PORT env var)" },
|
|
45
|
+
{ long: "--detach", description: "Run the server in the background" },
|
|
46
|
+
],
|
|
47
|
+
async run(ctx) {
|
|
48
|
+
const port = ctx.options["port"] ?? process.env["PORT"] ?? "4000";
|
|
49
|
+
const detach = Boolean(ctx.options["detach"]);
|
|
50
|
+
const serverPath = path.resolve(__dirname, "server.js");
|
|
51
|
+
ensureRuntimeDependencies();
|
|
52
|
+
if (detach) {
|
|
53
|
+
if (serverProcess) {
|
|
54
|
+
console.error(`pm-web is already running (PID ${serverProcess.pid})`);
|
|
55
|
+
return { status: "already_running", pid: serverProcess.pid };
|
|
56
|
+
}
|
|
57
|
+
serverProcess = spawn("node", [serverPath], {
|
|
58
|
+
env: { ...process.env, PORT: String(port) },
|
|
59
|
+
detached: true,
|
|
60
|
+
stdio: "ignore",
|
|
61
|
+
});
|
|
62
|
+
serverProcess.unref();
|
|
63
|
+
console.error(`pm-web started on port ${port} (PID ${serverProcess.pid})`);
|
|
64
|
+
return { status: "started", port: Number(port), pid: serverProcess.pid };
|
|
65
|
+
}
|
|
66
|
+
// Foreground mode — the server takes over the process
|
|
67
|
+
console.error(`Starting pm-web on port ${port}…`);
|
|
68
|
+
console.error("Press Ctrl+C to stop.\n");
|
|
69
|
+
const child = spawn("node", [serverPath], {
|
|
70
|
+
env: { ...process.env, PORT: String(port) },
|
|
71
|
+
stdio: "inherit",
|
|
72
|
+
});
|
|
73
|
+
await new Promise((resolve, reject) => {
|
|
74
|
+
child.on("error", reject);
|
|
75
|
+
child.on("exit", (code, signal) => {
|
|
76
|
+
if (code === 0 || signal === "SIGINT" || signal === "SIGTERM") {
|
|
77
|
+
resolve();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
reject(new Error(`pm-web exited with code ${code ?? `signal ${signal}`}`));
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
return { status: "stopped", port: Number(port) };
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { verifyToken, extractToken } from "../auth.js";
|
|
2
|
+
export function requireAuth(req, res, next) {
|
|
3
|
+
const token = extractToken(req);
|
|
4
|
+
if (!token) {
|
|
5
|
+
res.status(401).json({ error: "Authentication required" });
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
req.user = verifyToken(token);
|
|
10
|
+
next();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
res.status(401).json({ error: "Invalid or expired token" });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAMvD,MAAM,UAAU,WAAW,CAAC,GAAgB,EAAE,GAAa,EAAE,IAAkB;IAC7E,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC,CAAC;QAC3D,OAAO;IACT,CAAC;IACD,IAAI,CAAC;QACH,GAAG,CAAC,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;QAC9B,IAAI,EAAE,CAAC;IACT,CAAC;IAAC,MAAM,CAAC;QACP,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAC;IAC9D,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { pool } from "../db.js";
|
|
3
|
+
import { requireAuth } from "../middleware/auth.js";
|
|
4
|
+
const router = Router();
|
|
5
|
+
router.use(requireAuth);
|
|
6
|
+
async function getAdminCount() {
|
|
7
|
+
const result = await pool.query(`SELECT COUNT(*)::int AS count FROM pm_users WHERE is_admin = TRUE`);
|
|
8
|
+
return result.rows[0]?.count ?? 0;
|
|
9
|
+
}
|
|
10
|
+
async function isUserAdmin(userId) {
|
|
11
|
+
const result = await pool.query(`SELECT is_admin FROM pm_users WHERE id = $1`, [userId]);
|
|
12
|
+
return result.rows[0]?.is_admin === true;
|
|
13
|
+
}
|
|
14
|
+
async function requireAdmin(req, res, next) {
|
|
15
|
+
try {
|
|
16
|
+
const result = await pool.query(`SELECT is_admin FROM pm_users WHERE id = $1`, [req.user.userId]);
|
|
17
|
+
if (result.rows[0]?.is_admin === true) {
|
|
18
|
+
next();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
res.status(403).json({ error: "Admin access is required" });
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
console.error("Admin auth check failed:", err);
|
|
25
|
+
res.status(500).json({ error: "Failed to verify admin access" });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
router.use(requireAdmin);
|
|
29
|
+
router.get("/overview", async (_req, res) => {
|
|
30
|
+
try {
|
|
31
|
+
const [users, projects, shares, groups] = await Promise.all([
|
|
32
|
+
pool.query(`
|
|
33
|
+
SELECT id, email, display_name, is_admin, github_token IS NOT NULL AS has_github_token, created_at, updated_at
|
|
34
|
+
FROM pm_users
|
|
35
|
+
ORDER BY is_admin DESC, created_at DESC
|
|
36
|
+
`),
|
|
37
|
+
pool.query(`
|
|
38
|
+
SELECT p.id, p.name, p.slug, p.prefix, p.description, p.github_owner, p.github_repo,
|
|
39
|
+
p.github_sync_enabled, p.created_at, p.updated_at,
|
|
40
|
+
u.email AS owner_email, u.display_name AS owner_display_name
|
|
41
|
+
FROM pm_projects p
|
|
42
|
+
JOIN pm_users u ON u.id = p.user_id
|
|
43
|
+
ORDER BY p.updated_at DESC NULLS LAST, p.created_at DESC
|
|
44
|
+
`),
|
|
45
|
+
pool.query(`SELECT COUNT(*)::int AS count FROM pm_project_shares`),
|
|
46
|
+
pool.query(`
|
|
47
|
+
SELECT g.id, g.name, g.description, owner.email AS owner_email, COUNT(m.id)::int AS member_count, g.created_at
|
|
48
|
+
FROM pm_groups g
|
|
49
|
+
JOIN pm_users owner ON owner.id = g.owner_id
|
|
50
|
+
LEFT JOIN pm_group_members m ON m.group_id = g.id
|
|
51
|
+
GROUP BY g.id, owner.email
|
|
52
|
+
ORDER BY g.created_at DESC
|
|
53
|
+
`),
|
|
54
|
+
]);
|
|
55
|
+
res.json({
|
|
56
|
+
users: users.rows,
|
|
57
|
+
projects: projects.rows,
|
|
58
|
+
groups: groups.rows,
|
|
59
|
+
stats: {
|
|
60
|
+
users: users.rowCount,
|
|
61
|
+
admins: users.rows.filter((user) => user.is_admin === true).length,
|
|
62
|
+
projects: projects.rowCount,
|
|
63
|
+
sharedProjects: shares.rows[0]?.count ?? 0,
|
|
64
|
+
groups: groups.rowCount,
|
|
65
|
+
},
|
|
66
|
+
serverVersion: process.env.npm_package_version || '1.0.0',
|
|
67
|
+
uptimeSeconds: Math.floor(process.uptime()),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
console.error("Admin overview failed:", err);
|
|
72
|
+
res.status(500).json({ error: "Failed to load admin overview" });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
router.patch("/users/:id", async (req, res) => {
|
|
76
|
+
const { isAdmin } = req.body;
|
|
77
|
+
if (typeof isAdmin !== "boolean") {
|
|
78
|
+
res.status(400).json({ error: "isAdmin boolean is required" });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const currentAdmin = await isUserAdmin(req.params.id);
|
|
83
|
+
if (currentAdmin && !isAdmin) {
|
|
84
|
+
const adminCount = await getAdminCount();
|
|
85
|
+
if (adminCount <= 1) {
|
|
86
|
+
res.status(409).json({ error: "Cannot remove the last admin user." });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const result = await pool.query(`UPDATE pm_users SET is_admin = $1, updated_at = NOW()
|
|
91
|
+
WHERE id = $2
|
|
92
|
+
RETURNING id, email, display_name, is_admin, github_token IS NOT NULL AS has_github_token, created_at, updated_at`, [isAdmin, req.params.id]);
|
|
93
|
+
if (result.rows.length === 0) {
|
|
94
|
+
res.status(404).json({ error: "User not found" });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await logAudit(req.user.userId, "user.update", `Set is_admin=${isAdmin} for user ${req.params.id}`);
|
|
98
|
+
res.json({ user: result.rows[0] });
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
console.error("Admin user update failed:", err);
|
|
102
|
+
res.status(500).json({ error: "Failed to update user" });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// DELETE /admin/users/:id — Delete a user and all their data
|
|
106
|
+
router.delete("/users/:id", async (req, res) => {
|
|
107
|
+
try {
|
|
108
|
+
if (await isUserAdmin(req.params.id)) {
|
|
109
|
+
const adminCount = await getAdminCount();
|
|
110
|
+
if (adminCount <= 1) {
|
|
111
|
+
res.status(409).json({ error: "Cannot delete the last admin user." });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const result = await pool.query(`DELETE FROM pm_users WHERE id = $1 RETURNING id, email`, [req.params.id]);
|
|
116
|
+
if (result.rows.length === 0) {
|
|
117
|
+
res.status(404).json({ error: "User not found" });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
await logAudit(req.user.userId, "user.delete", `Deleted user ${result.rows[0].email} (${req.params.id})`);
|
|
121
|
+
res.json({ ok: true, deleted: result.rows[0] });
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
console.error("Admin user delete failed:", err);
|
|
125
|
+
res.status(500).json({ error: "Failed to delete user" });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
// DELETE /admin/projects/:id — Delete a project
|
|
129
|
+
router.delete("/projects/:id", async (req, res) => {
|
|
130
|
+
try {
|
|
131
|
+
const result = await pool.query(`DELETE FROM pm_projects WHERE id = $1 RETURNING id, name, slug`, [req.params.id]);
|
|
132
|
+
if (result.rows.length === 0) {
|
|
133
|
+
res.status(404).json({ error: "Project not found" });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
await logAudit(req.user.userId, "project.delete", `Deleted project ${result.rows[0].name} (${req.params.id})`);
|
|
137
|
+
res.json({ ok: true, deleted: result.rows[0] });
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
console.error("Admin project delete failed:", err);
|
|
141
|
+
res.status(500).json({ error: "Failed to delete project" });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
// POST /admin/groups — Create a new group
|
|
145
|
+
router.post("/groups", async (req, res) => {
|
|
146
|
+
const { name, description } = req.body;
|
|
147
|
+
if (!name?.trim()) {
|
|
148
|
+
res.status(400).json({ error: "Group name is required" });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const result = await pool.query(`INSERT INTO pm_groups (owner_id, name, description) VALUES ($1, $2, $3)
|
|
153
|
+
RETURNING id, name, description, created_at`, [req.user.userId, name.trim(), description?.trim() || ""]);
|
|
154
|
+
await logAudit(req.user.userId, "group.create", `Created group "${name.trim()}"`);
|
|
155
|
+
res.status(201).json({ group: result.rows[0] });
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
console.error("Admin group create failed:", err);
|
|
159
|
+
res.status(500).json({ error: "Failed to create group" });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
// DELETE /admin/groups/:id — Delete a group
|
|
163
|
+
router.delete("/groups/:id", async (req, res) => {
|
|
164
|
+
try {
|
|
165
|
+
const result = await pool.query(`DELETE FROM pm_groups WHERE id = $1 RETURNING id, name`, [req.params.id]);
|
|
166
|
+
if (result.rows.length === 0) {
|
|
167
|
+
res.status(404).json({ error: "Group not found" });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await logAudit(req.user.userId, "group.delete", `Deleted group "${result.rows[0].name}" (${req.params.id})`);
|
|
171
|
+
res.json({ ok: true, deleted: result.rows[0] });
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
console.error("Admin group delete failed:", err);
|
|
175
|
+
res.status(500).json({ error: "Failed to delete group" });
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
// GET /admin/audit — Retrieve audit log entries
|
|
179
|
+
router.get("/audit", async (req, res) => {
|
|
180
|
+
const limit = Math.min(parseInt(req.query.limit) || 100, 500);
|
|
181
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
182
|
+
try {
|
|
183
|
+
const [entries, countResult] = await Promise.all([
|
|
184
|
+
pool.query(`SELECT a.id, a.action, a.description, a.created_at, u.email AS actor_email, u.display_name AS actor_name
|
|
185
|
+
FROM pm_admin_audit a
|
|
186
|
+
JOIN pm_users u ON u.id = a.actor_id
|
|
187
|
+
ORDER BY a.created_at DESC
|
|
188
|
+
LIMIT $1 OFFSET $2`, [limit, offset]),
|
|
189
|
+
pool.query(`SELECT COUNT(*)::int AS count FROM pm_admin_audit`),
|
|
190
|
+
]);
|
|
191
|
+
res.json({ entries: entries.rows, total: countResult.rows[0]?.count ?? 0, limit, offset });
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
console.error("Admin audit log failed:", err);
|
|
195
|
+
res.status(500).json({ error: "Failed to load audit log" });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
async function logAudit(actorId, action, description) {
|
|
199
|
+
try {
|
|
200
|
+
await pool.query(`INSERT INTO pm_admin_audit (actor_id, action, description) VALUES ($1, $2, $3)`, [actorId, action, description]);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
console.error("Audit log write failed:", err);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
export { router as adminRouter };
|
|
207
|
+
//# sourceMappingURL=admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin.js","sourceRoot":"","sources":["../../src/routes/admin.ts"],"names":[],"mappings":"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"}
|