Compare commits
5 Commits
f2b0a6f07e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ef926050b6 | |||
| 82596a9ac4 | |||
| c86b950a0d | |||
| 946dd0d55f | |||
| c9aa69f767 |
@@ -4,3 +4,12 @@ node_modules
|
||||
data
|
||||
.git
|
||||
*.log
|
||||
# Agent/skills files — not part of the app
|
||||
AGENTS.md
|
||||
DEPLOYMENT-PROFILE.md
|
||||
PROJECT-PROFILE-WORKBOOK.md
|
||||
ROUTING-EXAMPLES.md
|
||||
SKILLS.md
|
||||
hubs/
|
||||
skills/
|
||||
memory/
|
||||
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
# CODEDUMP
|
||||
|
||||
Internal dashboard for tracking AI tools and coding projects. Provides a high-level overview of what the team is building, what tools are available, and the completion status of active work — all in one place.
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
CODEDUMP gives teams a single place to:
|
||||
|
||||
- Track AI tools and coding projects with completion percentages
|
||||
- Attach markdown documentation and project files
|
||||
- Link to internal/external URLs and Google Drive sources
|
||||
- Highlight new tools available to the team
|
||||
- Manage access via admin accounts and 4-digit user PINs
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|---|---|
|
||||
| Frontend | React 18 + TypeScript + Vite |
|
||||
| Styling | Tailwind CSS (dark mode, CSS variable theming) |
|
||||
| Backend | Node.js + Express + TypeScript |
|
||||
| Database | SQLite via `better-sqlite3` |
|
||||
| Auth | JWT (12h expiry) + bcryptjs (admin passwords) + HMAC-SHA256 (user PINs) |
|
||||
| File uploads | Multer (local disk storage) |
|
||||
| Markdown | `react-markdown` + `remark-gfm` |
|
||||
| Container | Docker (multi-stage build, single container) |
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
codedump/
|
||||
├── client/ # React frontend
|
||||
│ ├── src/
|
||||
│ │ ├── api/index.ts # Typed fetch wrappers (JWT injected automatically)
|
||||
│ │ ├── hooks/
|
||||
│ │ │ ├── useAuth.ts # AuthContext — JWT storage, login/logout
|
||||
│ │ │ └── useSettings.ts # SettingsContext — branding, accent color
|
||||
│ │ ├── components/
|
||||
│ │ │ ├── layout/
|
||||
│ │ │ │ ├── Layout.tsx # App shell (sidebar + outlet)
|
||||
│ │ │ │ └── Sidebar.tsx # Nav — auth-aware, admin section, logout
|
||||
│ │ │ ├── projects/
|
||||
│ │ │ │ ├── ProjectCard.tsx # Card with progress bar, links, tags
|
||||
│ │ │ │ └── ProjectForm.tsx # Create/edit form with color picker
|
||||
│ │ │ ├── tools/
|
||||
│ │ │ │ └── ToolCard.tsx # Tool card with NEW badge
|
||||
│ │ │ └── ui/
|
||||
│ │ │ ├── Badge.tsx # Status/role badges
|
||||
│ │ │ ├── Button.tsx # Primary/secondary/ghost/danger variants
|
||||
│ │ │ ├── Modal.tsx # Accessible overlay (Escape to close)
|
||||
│ │ │ └── ProgressBar.tsx # Accent-colored progress indicator
|
||||
│ │ ├── pages/
|
||||
│ │ │ ├── Login.tsx # PIN pad + admin login
|
||||
│ │ │ ├── Dashboard.tsx # Stats, new tools spotlight, recent projects
|
||||
│ │ │ ├── Projects.tsx # Filterable project grid
|
||||
│ │ │ ├── ProjectDetail.tsx # Docs viewer, completion editor, links
|
||||
│ │ │ ├── Tools.tsx # Tool catalog with search + filter
|
||||
│ │ │ ├── Settings.tsx # Branding, logo upload, accent color
|
||||
│ │ │ └── AdminUsers.tsx # User management (admin only)
|
||||
│ │ └── types/index.ts # Shared TypeScript interfaces
|
||||
│ ├── index.html
|
||||
│ ├── vite.config.ts # Dev proxy: /api → localhost:3000
|
||||
│ ├── tailwind.config.js
|
||||
│ └── tsconfig.json
|
||||
│
|
||||
├── server/ # Express backend
|
||||
│ ├── src/
|
||||
│ │ ├── db/schema.ts # SQLite schema, migrations, admin bootstrap
|
||||
│ │ ├── lib/pinHash.ts # HMAC-SHA256 PIN hashing (deterministic lookup)
|
||||
│ │ ├── middleware/auth.ts # requireAuth / requireAdmin JWT guards
|
||||
│ │ └── routes/
|
||||
│ │ ├── auth.ts # POST /pin, POST /login, GET /me
|
||||
│ │ ├── projects.ts # CRUD + tag/category support
|
||||
│ │ ├── tools.ts # CRUD
|
||||
│ │ ├── uploads.ts # POST (multer) + DELETE for documents
|
||||
│ │ ├── settings.ts # GET/PUT settings, POST logo upload
|
||||
│ │ └── users.ts # Admin-only user CRUD
|
||||
│ ├── tsconfig.json
|
||||
│ └── package.json
|
||||
│
|
||||
├── Dockerfile # Multi-stage: client build → server build → runtime
|
||||
├── docker-compose.yml
|
||||
├── .dockerignore
|
||||
├── INSTALL.md # Unraid GUI install guide
|
||||
└── package.json # npm workspaces root
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
**`users`** — Authentication accounts
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | TEXT PK | UUID |
|
||||
| `username` | TEXT UNIQUE | Display name |
|
||||
| `role` | TEXT | `admin` or `user` |
|
||||
| `pin_hash` | TEXT UNIQUE | HMAC-SHA256 of 4-digit PIN (users only) |
|
||||
| `password_hash` | TEXT | bcrypt hash (admins only) |
|
||||
| `created_at` | TEXT | ISO datetime |
|
||||
|
||||
**`projects`** — Core project records
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | TEXT PK | UUID |
|
||||
| `name` | TEXT | Required |
|
||||
| `description` | TEXT | |
|
||||
| `category` | TEXT | e.g. AI/ML, DevOps, Research |
|
||||
| `status` | TEXT | `active`, `complete`, `archived` |
|
||||
| `completion` | INTEGER | 0–100 |
|
||||
| `external_url` | TEXT | Internal or external link |
|
||||
| `drive_url` | TEXT | Google Drive link |
|
||||
| `tags` | TEXT | JSON array of strings |
|
||||
| `accent_color` | TEXT | Hex color for card theming |
|
||||
| `is_new` | INTEGER | Boolean flag |
|
||||
| `created_at` / `updated_at` | TEXT | ISO datetime |
|
||||
|
||||
**`documents`** — Files attached to projects
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | TEXT PK | UUID |
|
||||
| `project_id` | TEXT FK | Cascades on project delete |
|
||||
| `filename` | TEXT | UUID-named file on disk |
|
||||
| `original_name` | TEXT | Display name |
|
||||
| `mimetype` | TEXT | |
|
||||
| `created_at` | TEXT | |
|
||||
|
||||
**`tools`** — Available AI/dev tools catalog
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | TEXT PK | UUID |
|
||||
| `name` | TEXT | |
|
||||
| `description` | TEXT | |
|
||||
| `category` | TEXT | |
|
||||
| `external_url` | TEXT | |
|
||||
| `is_new` | INTEGER | Boolean — shown in dashboard spotlight |
|
||||
| `added_at` | TEXT | |
|
||||
| `notes` | TEXT | Usage tips, caveats |
|
||||
|
||||
**`settings`** — Key/value app configuration
|
||||
| Key | Default | Description |
|
||||
|---|---|---|
|
||||
| `app_title` | `CODEDUMP` | Shown in sidebar and login page |
|
||||
| `company_name` | `Your Company` | Shown under the app title |
|
||||
| `logo_url` | `null` | Uploaded or external URL |
|
||||
| `accent_color` | `#6366f1` | CSS variable applied globally |
|
||||
|
||||
---
|
||||
|
||||
## Auth Model
|
||||
|
||||
Two account types with separate login flows:
|
||||
|
||||
**Users** — Standard team members
|
||||
- Select no username; just enter their 4-digit PIN directly on the keypad
|
||||
- PINs are unique system-wide (enforced by DB unique constraint)
|
||||
- Stored as `HMAC-SHA256(JWT_SECRET, pin)` — deterministic, allowing single-query lookup
|
||||
- Can: create/edit/delete projects and tools, upload documents, view all content
|
||||
- Cannot: access Settings or User Management
|
||||
|
||||
**Admins** — Elevated accounts
|
||||
- Log in with username + password via the "Admin Login" link on the login page
|
||||
- Passwords stored as bcrypt hashes (cost factor 10)
|
||||
- Can: everything users can do, plus Settings (branding/theme) and User Management
|
||||
- A bootstrap admin is created on first startup from `ADMIN_USERNAME` / `ADMIN_PASSWORD` env vars
|
||||
- The last admin account cannot be deleted
|
||||
|
||||
**JWT** — 12-hour tokens stored in `localStorage`. Expiry is checked client-side on load; the server re-validates on every request. A 401 response from the server clears the token and redirects to `/login`.
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `PORT` | `3000` | Server listen port |
|
||||
| `DATA_DIR` | `./data` | SQLite DB + uploads root on disk |
|
||||
| `MAX_UPLOAD_MB` | `50` | Max file upload size |
|
||||
| `ADMIN_USERNAME` | `admin` | Bootstrap admin username (first-run only) |
|
||||
| `ADMIN_PASSWORD` | `codedump2024` | Bootstrap admin password (first-run only) — change this |
|
||||
| `JWT_SECRET` | *(insecure default)* | Token signing secret — set to a 32+ char random string |
|
||||
|
||||
Generate a strong `JWT_SECRET`:
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Running Locally
|
||||
|
||||
```bash
|
||||
# Install all workspace dependencies
|
||||
npm install
|
||||
|
||||
# Start both server (port 3000) and client (port 5173) with hot reload
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Visit `http://localhost:5173`. The Vite dev server proxies `/api/*` to `localhost:3000`.
|
||||
|
||||
Default admin credentials on first run: `admin` / `codedump2024`
|
||||
|
||||
---
|
||||
|
||||
## Building for Production
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
# Client builds to client/dist/
|
||||
# Server builds to server/dist/
|
||||
# Server serves client/dist/ as static files
|
||||
npm start
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
# Build image
|
||||
docker build -t codedump:latest .
|
||||
|
||||
# Run with docker-compose (recommended)
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Data persists in `./data/` (local) or `/mnt/user/appdata/codedump/` (Unraid).
|
||||
|
||||
See [INSTALL.md](./INSTALL.md) for the full Unraid GUI setup guide including port mapping, volume paths, and all environment variable fields.
|
||||
|
||||
---
|
||||
|
||||
## File Upload Details
|
||||
|
||||
- Accepted types: `.md`, `.txt`, `.pdf`, `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`
|
||||
- Files are stored in `DATA_DIR/uploads/` with UUID filenames
|
||||
- Served publicly at `/api/uploads/:filename` via `express.static` (no auth required — needed for `<img>` tags and the markdown doc viewer)
|
||||
- Upload and delete operations require authentication
|
||||
- Logo uploads follow the same path; the URL is saved in settings and served the same way
|
||||
|
||||
---
|
||||
|
||||
## Deployment Notes
|
||||
|
||||
- SQLite with WAL mode — suitable for a single-server internal tool; no separate database service needed
|
||||
- All persistent state (DB + uploads) lives under `DATA_DIR` — back up that directory
|
||||
- The bootstrap admin only runs if no admin account exists in the DB; changing env vars after first run does not change existing credentials
|
||||
- `JWT_SECRET` must stay consistent across restarts — changing it invalidates all active sessions
|
||||
@@ -18,10 +18,13 @@ async function req<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const res = await fetch(`${BASE}${path}`, { ...options, headers });
|
||||
|
||||
if (res.status === 401) {
|
||||
// Token expired — clear storage and reload to login
|
||||
const hadToken = Boolean(localStorage.getItem(TOKEN_KEY));
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem('codedump_user');
|
||||
window.location.href = '/login';
|
||||
// Only force-redirect if the user had an active session that expired.
|
||||
// Without this guard, unauthenticated requests (e.g. settings fetch on
|
||||
// the login page) trigger a redirect loop: 401 → /login → fetch → 401 → ...
|
||||
if (hadToken) window.location.href = '/login';
|
||||
throw new Error('Session expired');
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import type { Components } from 'react-markdown';
|
||||
|
||||
const components: Components = {
|
||||
// ── Headings ────────────────────────────────────────────────────────────────
|
||||
h1: ({ children }) => (
|
||||
<h1 className="text-2xl font-bold text-white mt-8 mb-4 pb-3 border-b border-border first:mt-0">
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className="text-lg font-semibold text-white mt-7 mb-3 pb-2 border-b border-border/50 flex items-center gap-2">
|
||||
<span className="inline-block w-1 h-4 rounded-sm shrink-0" style={{ background: 'var(--accent)' }} />
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<h3 className="text-base font-semibold text-white mt-5 mb-2">{children}</h3>
|
||||
),
|
||||
h4: ({ children }) => (
|
||||
<h4 className="text-sm font-semibold text-zinc-200 mt-4 mb-1 uppercase tracking-wide">{children}</h4>
|
||||
),
|
||||
|
||||
// ── Paragraph ───────────────────────────────────────────────────────────────
|
||||
p: ({ children }) => (
|
||||
<p className="text-zinc-300 leading-7 mb-4 last:mb-0">{children}</p>
|
||||
),
|
||||
|
||||
// ── Links ───────────────────────────────────────────────────────────────────
|
||||
a: ({ href, children }) => (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline underline-offset-2 decoration-[var(--accent)]/50 hover:decoration-[var(--accent)] transition-colors"
|
||||
style={{ color: 'var(--accent)' }}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
|
||||
// ── Strong / Em ─────────────────────────────────────────────────────────────
|
||||
strong: ({ children }) => (
|
||||
<strong className="font-semibold text-white">{children}</strong>
|
||||
),
|
||||
em: ({ children }) => (
|
||||
<em className="italic text-zinc-400">{children}</em>
|
||||
),
|
||||
|
||||
// ── Horizontal rule ─────────────────────────────────────────────────────────
|
||||
hr: () => <hr className="my-6 border-border" />,
|
||||
|
||||
// ── Blockquote ──────────────────────────────────────────────────────────────
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="my-4 pl-4 border-l-2 text-zinc-400 italic bg-white/[0.02] rounded-r-lg py-3 pr-3" style={{ borderLeftColor: 'var(--accent)' }}>
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
|
||||
// ── Lists ───────────────────────────────────────────────────────────────────
|
||||
ul: ({ children }) => (
|
||||
<ul className="my-3 space-y-1.5 list-none pl-4">{children}</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol className="my-3 space-y-1.5 list-decimal list-inside pl-2 text-zinc-300">{children}</ol>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li className="text-zinc-300 leading-relaxed flex gap-2 items-start">
|
||||
<span className="mt-2.5 w-1.5 h-1.5 rounded-full shrink-0" style={{ background: 'var(--accent)', opacity: 0.7 }} />
|
||||
<span className="flex-1">{children}</span>
|
||||
</li>
|
||||
),
|
||||
|
||||
// ── Inline code ─────────────────────────────────────────────────────────────
|
||||
code: ({ className, children, ...props }) => {
|
||||
const isBlock = Boolean(className);
|
||||
if (isBlock) return <code className={className} {...props}>{children}</code>;
|
||||
return (
|
||||
<code className="font-mono text-[0.8em] bg-white/10 text-[#e2a4ff] px-1.5 py-0.5 rounded-md border border-white/10">
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
|
||||
// ── Code block ──────────────────────────────────────────────────────────────
|
||||
pre: ({ children }) => (
|
||||
<pre className="my-4 bg-[#0d0d14] border border-border rounded-xl overflow-x-auto">
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-border/50">
|
||||
<div className="flex gap-1.5">
|
||||
<span className="w-3 h-3 rounded-full bg-red-500/40" />
|
||||
<span className="w-3 h-3 rounded-full bg-yellow-500/40" />
|
||||
<span className="w-3 h-3 rounded-full bg-green-500/40" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 text-sm font-mono text-zinc-300 leading-relaxed">
|
||||
{children}
|
||||
</div>
|
||||
</pre>
|
||||
),
|
||||
|
||||
// ── Tables ──────────────────────────────────────────────────────────────────
|
||||
table: ({ children }) => (
|
||||
<div className="my-5 overflow-x-auto rounded-xl border border-border">
|
||||
<table className="w-full text-sm border-collapse">{children}</table>
|
||||
</div>
|
||||
),
|
||||
thead: ({ children }) => (
|
||||
<thead className="bg-white/[0.04] border-b border-border">{children}</thead>
|
||||
),
|
||||
tbody: ({ children }) => (
|
||||
<tbody className="divide-y divide-border">{children}</tbody>
|
||||
),
|
||||
tr: ({ children }) => (
|
||||
<tr className="transition-colors hover:bg-white/[0.03]">{children}</tr>
|
||||
),
|
||||
th: ({ children }) => (
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-muted whitespace-nowrap">
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
td: ({ children }) => (
|
||||
<td className="px-4 py-3 text-zinc-300 align-top">{children}</td>
|
||||
),
|
||||
};
|
||||
|
||||
interface Props {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export default function MarkdownViewer({ content }: Props) {
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import MarkdownViewer from '../components/ui/MarkdownViewer';
|
||||
import {
|
||||
ArrowLeft, ExternalLink, Pencil, Trash2, Upload, FileText,
|
||||
X, Save, FolderOpen, Eye, Code2
|
||||
@@ -244,18 +243,7 @@ export default function ProjectDetail() {
|
||||
</div>
|
||||
<div className="p-6 overflow-auto max-h-[70vh]">
|
||||
{docViewMode === 'preview' ? (
|
||||
<div className="prose prose-invert prose-sm max-w-none
|
||||
prose-headings:text-white prose-headings:font-semibold
|
||||
prose-p:text-zinc-300 prose-p:leading-relaxed
|
||||
prose-a:text-[var(--accent)] prose-a:no-underline hover:prose-a:underline
|
||||
prose-code:bg-white/10 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:font-mono
|
||||
prose-pre:bg-surface prose-pre:border prose-pre:border-border
|
||||
prose-blockquote:border-l-[var(--accent)] prose-blockquote:text-muted
|
||||
prose-li:text-zinc-300 prose-strong:text-white
|
||||
prose-table:text-sm prose-th:text-muted prose-td:text-zinc-300
|
||||
prose-hr:border-border">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{docContent}</ReactMarkdown>
|
||||
</div>
|
||||
<MarkdownViewer content={docContent} />
|
||||
) : (
|
||||
<pre className="text-xs text-zinc-300 font-mono whitespace-pre-wrap">{docContent}</pre>
|
||||
)}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "ai-tools-dashboard",
|
||||
"name": "codedump",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"workspaces": ["client", "server"],
|
||||
|
||||
+10
-3
@@ -2,6 +2,7 @@ import express from 'express';
|
||||
import cors from 'cors';
|
||||
import path from 'path';
|
||||
import './db/schema'; // initialize DB + bootstrap admin
|
||||
import { UPLOAD_PATH } from './db/schema';
|
||||
import { requireAuth } from './middleware/auth';
|
||||
import authRouter from './routes/auth';
|
||||
import projectsRouter from './routes/projects';
|
||||
@@ -16,14 +17,20 @@ const PORT = Number(process.env.PORT || 3000);
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
|
||||
// Public — auth endpoints (login doesn't require token)
|
||||
// Public — auth endpoints
|
||||
app.use('/api/auth', authRouter);
|
||||
|
||||
// Public — serve uploaded files as static assets.
|
||||
// <img> tags and markdown renderers can't send Authorization headers,
|
||||
// so file reads must be unauthenticated. POST/DELETE in uploadsRouter
|
||||
// are still protected by requireAuth inside the router.
|
||||
app.use('/api/uploads', express.static(UPLOAD_PATH));
|
||||
|
||||
// Protected — all other API routes require a valid JWT
|
||||
app.use('/api/projects', requireAuth, projectsRouter);
|
||||
app.use('/api/tools', requireAuth, toolsRouter);
|
||||
app.use('/api/uploads', requireAuth, uploadsRouter);
|
||||
app.use('/api/settings', requireAuth, settingsRouter);
|
||||
app.use('/api/uploads', requireAuth, uploadsRouter); // handles POST + DELETE only
|
||||
app.use('/api/settings', settingsRouter); // GET is public (branding on login page); PUT/POST require admin (per-method in router)
|
||||
app.use('/api/users', usersRouter); // requireAdmin applied inside router
|
||||
|
||||
// Serve built React client in production
|
||||
|
||||
@@ -3,6 +3,7 @@ import multer from 'multer';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import db, { UPLOAD_PATH } from '../db/schema';
|
||||
import { requireAdmin } from '../middleware/auth';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -27,7 +28,7 @@ router.get('/', (_req: Request, res: Response) => {
|
||||
res.json(settings);
|
||||
});
|
||||
|
||||
router.put('/', (req: Request, res: Response) => {
|
||||
router.put('/', requireAdmin, (req: Request, res: Response) => {
|
||||
const allowed = ['app_title', 'logo_url', 'accent_color', 'company_name'];
|
||||
const upsert = db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)');
|
||||
const update = db.transaction((body: Record<string, any>) => {
|
||||
@@ -45,7 +46,7 @@ router.put('/', (req: Request, res: Response) => {
|
||||
res.json(settings);
|
||||
});
|
||||
|
||||
router.post('/logo', logoUpload.single('logo'), (req: Request, res: Response) => {
|
||||
router.post('/logo', requireAdmin, logoUpload.single('logo'), (req: Request, res: Response) => {
|
||||
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
|
||||
const url = `/api/uploads/${req.file.filename}`;
|
||||
db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)').run('logo_url', JSON.stringify(url));
|
||||
|
||||
@@ -3,6 +3,7 @@ import multer from 'multer';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
// Note: GET /api/uploads/:filename is served by express.static in index.ts (no auth needed for <img> tags)
|
||||
import db, { UPLOAD_PATH } from '../db/schema';
|
||||
|
||||
const router = Router();
|
||||
@@ -46,13 +47,6 @@ router.post('/projects/:projectId', upload.single('file'), (req: Request, res: R
|
||||
});
|
||||
});
|
||||
|
||||
// Get raw file
|
||||
router.get('/:filename', (req: Request, res: Response) => {
|
||||
const filePath = path.join(UPLOAD_PATH, path.basename(req.params.filename));
|
||||
if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'File not found' });
|
||||
res.sendFile(filePath);
|
||||
});
|
||||
|
||||
// Delete a document
|
||||
router.delete('/documents/:id', (req: Request, res: Response) => {
|
||||
const doc = db.prepare('SELECT * FROM documents WHERE id = ?').get(req.params.id) as any;
|
||||
|
||||
Reference in New Issue
Block a user