Compare commits
No commits in common. "f006552ac13625b1270ab8828a8ad177502d2b5a" and "4c1445a4d7d3de1ad96528162c8deaad440651bf" have entirely different histories.
f006552ac1
...
4c1445a4d7
|
|
@ -13,8 +13,6 @@ services:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- DB_TYPE=sqlite
|
- DB_TYPE=sqlite
|
||||||
- DATABASE_URL=file:/app/data/app.db
|
- DATABASE_URL=file:/app/data/app.db
|
||||||
- ENABLE_PASSCODE=true
|
|
||||||
- PASSCODE=aabbcc
|
|
||||||
volumes:
|
volumes:
|
||||||
# Mount the data directory for SQLite persistence
|
# Mount the data directory for SQLite persistence
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,17 @@ sudo docker compose up -d
|
||||||
```
|
```
|
||||||
Save this token securely.
|
Save this token securely.
|
||||||
|
|
||||||
|
6. (Optional) Run migrations:
|
||||||
|
```bash
|
||||||
|
# Set environment variables temporarily
|
||||||
|
export DB_TYPE=turso
|
||||||
|
export TURSO_URL=libsql://your-database-url
|
||||||
|
export TURSO_TOKEN=your-token
|
||||||
|
|
||||||
|
# Push schema to Turso
|
||||||
|
npx drizzle-kit push
|
||||||
|
```
|
||||||
|
|
||||||
### Step 2: Deploy to Vercel
|
### Step 2: Deploy to Vercel
|
||||||
|
|
||||||
1. **Connect Repository**:
|
1. **Connect Repository**:
|
||||||
|
|
@ -156,25 +167,15 @@ sudo docker compose up -d
|
||||||
3. **Set Environment Variables**:
|
3. **Set Environment Variables**:
|
||||||
In the Vercel dashboard, go to Project Settings → Environment Variables, add:
|
In the Vercel dashboard, go to Project Settings → Environment Variables, add:
|
||||||
|
|
||||||
| Variable | Value | Required |
|
| Variable | Value |
|
||||||
|----------|-------|----------|
|
|----------|-------|
|
||||||
| `DB_TYPE` | `turso` | Yes |
|
| `DB_TYPE` | `turso` |
|
||||||
| `TURSO_URL` | Your Turso database URL | Yes |
|
| `TURSO_URL` | Your Turso database URL |
|
||||||
| `TURSO_TOKEN` | Your Turso auth token | Yes |
|
| `TURSO_TOKEN` | Your Turso auth token |
|
||||||
| `ENABLE_PASSCODE` | `true` (recommended for public sites) | No (default: false) |
|
|
||||||
| `PASSCODE` | Your secret passcode | If ENABLE_PASSCODE=true |
|
|
||||||
|
|
||||||
> **Note**: The build process will automatically run database migrations using the `prebuild` script. Make sure the environment variables are set before the first build.
|
|
||||||
>
|
|
||||||
> **Security**: For public deployments, set `ENABLE_PASSCODE=true` and `PASSCODE` to protect the application with a simple passcode. The cookie lasts 7 days.
|
|
||||||
|
|
||||||
4. **Deploy**:
|
4. **Deploy**:
|
||||||
- Click "Deploy"
|
- Click "Deploy"
|
||||||
- Vercel will build and deploy automatically
|
- Vercel will build and deploy automatically
|
||||||
- The build process will:
|
|
||||||
1. Run `prebuild` script to create database tables
|
|
||||||
2. Build the Next.js application
|
|
||||||
3. Deploy to Vercel's edge network
|
|
||||||
|
|
||||||
### Updating on Vercel
|
### Updating on Vercel
|
||||||
|
|
||||||
|
|
@ -244,34 +245,12 @@ sudo docker compose up -d
|
||||||
|
|
||||||
### Vercel Issues
|
### Vercel Issues
|
||||||
|
|
||||||
**Issue**: Build fails with "no such table" error
|
|
||||||
**Solution**: This happens when the database tables don't exist during build. The `prebuild` script should create them automatically. If it fails:
|
|
||||||
- Ensure `TURSO_URL` and `TURSO_TOKEN` are set in Vercel Environment Variables
|
|
||||||
- Check that the Turso database exists and is accessible
|
|
||||||
- Try running `npx drizzle-kit push` locally with the same credentials to verify
|
|
||||||
|
|
||||||
**Issue**: Build fails with database errors
|
**Issue**: Build fails with database errors
|
||||||
**Solution**: Ensure environment variables are set in Vercel dashboard (not just in `.env.local`)
|
**Solution**: Ensure environment variables are set in Vercel dashboard (not just in `.env.local`)
|
||||||
|
|
||||||
**Issue**: Data doesn't persist between deployments
|
**Issue**: Data doesn't persist between deployments
|
||||||
**Solution**: This is expected with SQLite on Vercel. You must use Turso for persistence.
|
**Solution**: This is expected with SQLite on Vercel. You must use Turso for persistence.
|
||||||
|
|
||||||
**Issue**: Passcode not working
|
|
||||||
**Solution**:
|
|
||||||
- Ensure both `ENABLE_PASSCODE=true` and `PASSCODE` are set
|
|
||||||
- The passcode is case-sensitive
|
|
||||||
- Clear browser cookies and try again
|
|
||||||
- Check browser console for API errors
|
|
||||||
|
|
||||||
### Authentication Issues
|
|
||||||
|
|
||||||
**Issue**: Cannot access site even with correct passcode
|
|
||||||
**Solution**:
|
|
||||||
- Check that cookies are enabled in your browser
|
|
||||||
- Try accessing in an incognito/private window
|
|
||||||
- Verify the `AUTH_COOKIE_NAME` hasn't changed between deployments
|
|
||||||
- The cookie is set to expire after 7 days; if you cleared cookies, you'll need to re-enter the passcode
|
|
||||||
|
|
||||||
### Turso Issues
|
### Turso Issues
|
||||||
|
|
||||||
**Issue**: Connection refused or timeout
|
**Issue**: Connection refused or timeout
|
||||||
|
|
@ -308,46 +287,20 @@ Note: Some SQLite-specific syntax may need adjustment for Turso compatibility.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Authentication
|
## Security Considerations
|
||||||
|
|
||||||
The application supports optional passcode-based authentication, controlled by environment variables.
|
### Docker Deployment
|
||||||
|
|
||||||
### Configuration
|
- Container runs as root (uid=0) by design for simplicity
|
||||||
|
- SQLite database file is owned by root
|
||||||
|
- Ensure proper firewall rules on your server
|
||||||
|
- Consider using HTTPS reverse proxy (nginx/traefik)
|
||||||
|
|
||||||
| Variable | Description | Default |
|
### Vercel Deployment
|
||||||
|----------|-------------|---------|
|
|
||||||
| `ENABLE_PASSCODE` | Enable passcode protection | `false` |
|
|
||||||
| `PASSCODE` | The secret passcode | - |
|
|
||||||
| `AUTH_COOKIE_NAME` | Name of the auth cookie | `release_tracker_auth` |
|
|
||||||
|
|
||||||
### Use Cases
|
- Turso provides encryption at rest and in transit
|
||||||
|
- Vercel provides HTTPS by default
|
||||||
**Private Docker Deployment** (no auth needed):
|
- Keep `TURSO_TOKEN` secure and rotate periodically
|
||||||
```bash
|
|
||||||
ENABLE_PASSCODE=false
|
|
||||||
```
|
|
||||||
|
|
||||||
**Public Vercel Deployment** (auth required):
|
|
||||||
```bash
|
|
||||||
ENABLE_PASSCODE=true
|
|
||||||
PASSCODE=your-secure-passcode-here
|
|
||||||
```
|
|
||||||
|
|
||||||
### How It Works
|
|
||||||
|
|
||||||
1. When `ENABLE_PASSCODE=true`, all routes require authentication
|
|
||||||
2. Users are redirected to `/login` page
|
|
||||||
3. After entering the correct passcode, a 7-day cookie is set
|
|
||||||
4. Cookie is `httpOnly`, `secure` (in production), and `SameSite=strict`
|
|
||||||
5. No logout functionality (users must clear cookies or wait 7 days)
|
|
||||||
|
|
||||||
### Security Considerations
|
|
||||||
|
|
||||||
- Passcode is never sent to the client (verified server-side)
|
|
||||||
- Cookie cannot be accessed by JavaScript (`httpOnly`)
|
|
||||||
- Cookie is only sent over HTTPS in production (`secure`)
|
|
||||||
- Cookie expires after 7 days (`maxAge: 604800`)
|
|
||||||
- Single passcode for all users (simple but not suitable for multi-user scenarios)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.1.6",
|
"eslint-config-next": "16.1.6",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"tsx": "^4.21.0",
|
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
|
|
@ -6250,21 +6249,6 @@
|
||||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fsevents": {
|
|
||||||
"version": "2.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
|
@ -9322,510 +9306,6 @@
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/tsx": {
|
|
||||||
"version": "4.21.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
|
||||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"esbuild": "~0.27.0",
|
|
||||||
"get-tsconfig": "^4.7.5"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"tsx": "dist/cli.mjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"fsevents": "~2.3.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"aix"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/android-arm": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/android-x64": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
|
|
||||||
"cpu": [
|
|
||||||
"loong64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
|
|
||||||
"cpu": [
|
|
||||||
"mips64el"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
|
|
||||||
"cpu": [
|
|
||||||
"s390x"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"netbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"netbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openharmony"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"sunos"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/esbuild": {
|
|
||||||
"version": "0.27.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
|
|
||||||
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"esbuild": "bin/esbuild"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@esbuild/aix-ppc64": "0.27.2",
|
|
||||||
"@esbuild/android-arm": "0.27.2",
|
|
||||||
"@esbuild/android-arm64": "0.27.2",
|
|
||||||
"@esbuild/android-x64": "0.27.2",
|
|
||||||
"@esbuild/darwin-arm64": "0.27.2",
|
|
||||||
"@esbuild/darwin-x64": "0.27.2",
|
|
||||||
"@esbuild/freebsd-arm64": "0.27.2",
|
|
||||||
"@esbuild/freebsd-x64": "0.27.2",
|
|
||||||
"@esbuild/linux-arm": "0.27.2",
|
|
||||||
"@esbuild/linux-arm64": "0.27.2",
|
|
||||||
"@esbuild/linux-ia32": "0.27.2",
|
|
||||||
"@esbuild/linux-loong64": "0.27.2",
|
|
||||||
"@esbuild/linux-mips64el": "0.27.2",
|
|
||||||
"@esbuild/linux-ppc64": "0.27.2",
|
|
||||||
"@esbuild/linux-riscv64": "0.27.2",
|
|
||||||
"@esbuild/linux-s390x": "0.27.2",
|
|
||||||
"@esbuild/linux-x64": "0.27.2",
|
|
||||||
"@esbuild/netbsd-arm64": "0.27.2",
|
|
||||||
"@esbuild/netbsd-x64": "0.27.2",
|
|
||||||
"@esbuild/openbsd-arm64": "0.27.2",
|
|
||||||
"@esbuild/openbsd-x64": "0.27.2",
|
|
||||||
"@esbuild/openharmony-arm64": "0.27.2",
|
|
||||||
"@esbuild/sunos-x64": "0.27.2",
|
|
||||||
"@esbuild/win32-arm64": "0.27.2",
|
|
||||||
"@esbuild/win32-ia32": "0.27.2",
|
|
||||||
"@esbuild/win32-x64": "0.27.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tunnel-agent": {
|
"node_modules/tunnel-agent": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"prebuild": "tsx scripts/migrate.ts",
|
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint",
|
"lint": "eslint"
|
||||||
"migrate": "drizzle-kit push"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|
@ -48,7 +46,6 @@
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.1.6",
|
"eslint-config-next": "16.1.6",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"tsx": "^4.21.0",
|
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import { initDb } from '../src/lib/db';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log('Running database migrations...');
|
|
||||||
try {
|
|
||||||
await initDb();
|
|
||||||
console.log('Migrations completed successfully');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Migration failed:', error);
|
|
||||||
// Don't fail the build if migrations fail (tables might already exist)
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
// Environment configuration
|
|
||||||
const ENABLE_PASSCODE = process.env.ENABLE_PASSCODE === 'true';
|
|
||||||
const PASSCODE = process.env.PASSCODE;
|
|
||||||
const AUTH_COOKIE_NAME = process.env.AUTH_COOKIE_NAME || 'release_tracker_auth';
|
|
||||||
|
|
||||||
// Cookie configuration
|
|
||||||
const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days in seconds
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
// If passcode protection is disabled, return success
|
|
||||||
if (!ENABLE_PASSCODE) {
|
|
||||||
return NextResponse.json({ success: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if passcode is configured
|
|
||||||
if (!PASSCODE) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Passcode not configured' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const body = await request.json();
|
|
||||||
const { passcode } = body;
|
|
||||||
|
|
||||||
if (!passcode || typeof passcode !== 'string') {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Passcode is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify passcode
|
|
||||||
if (passcode !== PASSCODE) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Invalid passcode' },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set authentication cookie
|
|
||||||
const response = NextResponse.json({ success: true });
|
|
||||||
|
|
||||||
response.cookies.set({
|
|
||||||
name: AUTH_COOKIE_NAME,
|
|
||||||
value: 'authenticated',
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'strict',
|
|
||||||
maxAge: COOKIE_MAX_AGE,
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Invalid request' },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional: DELETE endpoint for logout (if needed in future)
|
|
||||||
export async function DELETE() {
|
|
||||||
const response = NextResponse.json({ success: true });
|
|
||||||
|
|
||||||
response.cookies.set({
|
|
||||||
name: AUTH_COOKIE_NAME,
|
|
||||||
value: '',
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'strict',
|
|
||||||
maxAge: 0,
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
@ -4,9 +4,6 @@ import { Button } from '@/components/ui/button';
|
||||||
import { ClusterCard } from '@/components/clusters/cluster-card';
|
import { ClusterCard } from '@/components/clusters/cluster-card';
|
||||||
import { listClusters } from '@/lib/actions/clusters';
|
import { listClusters } from '@/lib/actions/clusters';
|
||||||
|
|
||||||
// Force dynamic rendering to avoid static generation during build
|
|
||||||
export const dynamic = 'force-dynamic';
|
|
||||||
|
|
||||||
export default async function ClustersPage() {
|
export default async function ClustersPage() {
|
||||||
const clusters = await listClusters();
|
const clusters = await listClusters();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,6 @@ import { CustomerCard } from '@/components/customers/customer-card';
|
||||||
import { listClusters } from '@/lib/actions/clusters';
|
import { listClusters } from '@/lib/actions/clusters';
|
||||||
import { listCustomers } from '@/lib/actions/customers';
|
import { listCustomers } from '@/lib/actions/customers';
|
||||||
|
|
||||||
// Force dynamic rendering to avoid static generation during build
|
|
||||||
export const dynamic = 'force-dynamic';
|
|
||||||
|
|
||||||
export default async function CustomersPage() {
|
export default async function CustomersPage() {
|
||||||
const [customers, clusters] = await Promise.all([
|
const [customers, clusters] = await Promise.all([
|
||||||
listCustomers(),
|
listCustomers(),
|
||||||
|
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { Suspense, useState } from 'react';
|
|
||||||
import { useSearchParams, useRouter } from 'next/navigation';
|
|
||||||
import { Lock, Eye, EyeOff, ArrowRight } from 'lucide-react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
|
|
||||||
// Export dynamic to prevent static generation
|
|
||||||
export const dynamic = 'force-dynamic';
|
|
||||||
|
|
||||||
function LoginForm() {
|
|
||||||
const [passcode, setPasscode] = useState('');
|
|
||||||
const [showPasscode, setShowPasscode] = useState(false);
|
|
||||||
const [error, setError] = useState('');
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const redirectUrl = searchParams.get('redirect') || '/';
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setError('');
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/auth', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ passcode }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
// Redirect to original URL or home
|
|
||||||
router.push(redirectUrl);
|
|
||||||
router.refresh();
|
|
||||||
} else {
|
|
||||||
const data = await response.json();
|
|
||||||
setError(data.error || 'Invalid passcode');
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setError('An error occurred. Please try again.');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center bg-slate-100 p-4">
|
|
||||||
<Card className="w-full max-w-md">
|
|
||||||
<CardHeader className="space-y-1 text-center">
|
|
||||||
<div className="flex justify-center mb-4">
|
|
||||||
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
|
|
||||||
<Lock className="w-6 h-6 text-blue-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<CardTitle className="text-2xl font-bold">Release Tracker</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Enter the passcode to access the application
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
id="passcode"
|
|
||||||
type={showPasscode ? 'text' : 'password'}
|
|
||||||
placeholder="Enter passcode"
|
|
||||||
value={passcode}
|
|
||||||
onChange={(e) => setPasscode(e.target.value)}
|
|
||||||
className="pr-10"
|
|
||||||
disabled={isLoading}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowPasscode(!showPasscode)}
|
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600"
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
{showPasscode ? (
|
|
||||||
<EyeOff className="w-4 h-4" />
|
|
||||||
) : (
|
|
||||||
<Eye className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="text-sm text-red-600 bg-red-50 p-3 rounded-lg">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="w-full"
|
|
||||||
disabled={isLoading || !passcode.trim()}
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
'Verifying...'
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
Access Application
|
|
||||||
<ArrowRight className="w-4 h-4 ml-2" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LoginPage() {
|
|
||||||
return (
|
|
||||||
<Suspense fallback={
|
|
||||||
<div className="min-h-screen flex items-center justify-center bg-slate-100">
|
|
||||||
<Card className="w-full max-w-md">
|
|
||||||
<CardContent className="py-12">
|
|
||||||
<div className="text-center text-slate-500">Loading...</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
}>
|
|
||||||
<LoginForm />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -7,9 +7,6 @@ import { listClusters } from '@/lib/actions/clusters';
|
||||||
import { listCustomers, listCustomersByCluster } from '@/lib/actions/customers';
|
import { listCustomers, listCustomersByCluster } from '@/lib/actions/customers';
|
||||||
import { getActiveReleases, getReleaseStats } from '@/lib/actions/releases';
|
import { getActiveReleases, getReleaseStats } from '@/lib/actions/releases';
|
||||||
|
|
||||||
// Force dynamic rendering to avoid static generation during build
|
|
||||||
export const dynamic = 'force-dynamic';
|
|
||||||
|
|
||||||
export default async function DashboardPage() {
|
export default async function DashboardPage() {
|
||||||
const [clusters, customersList, activeReleases, stats] = await Promise.all([
|
const [clusters, customersList, activeReleases, stats] = await Promise.all([
|
||||||
listClusters(),
|
listClusters(),
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { ReleaseList } from '@/components/releases/release-list';
|
import { ReleaseList } from '@/components/releases/release-list';
|
||||||
import { listReleases } from '@/lib/actions/releases';
|
import { listReleases } from '@/lib/actions/releases';
|
||||||
|
|
||||||
// Force dynamic rendering to avoid static generation during build
|
|
||||||
export const dynamic = 'force-dynamic';
|
|
||||||
|
|
||||||
export default async function ReleasesPage() {
|
export default async function ReleasesPage() {
|
||||||
const releases = await listReleases();
|
const releases = await listReleases();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,6 @@ export function ReleaseMatrixClient({ stepsByCluster, category, releaseId }: Rel
|
||||||
const clusters = Object.values(stepsByCluster);
|
const clusters = Object.values(stepsByCluster);
|
||||||
|
|
||||||
const handleStepClick = (step: any, template: any = null) => {
|
const handleStepClick = (step: any, template: any = null) => {
|
||||||
console.log('[ReleaseMatrixClient] handleStepClick called:', {
|
|
||||||
stepId: step?.id,
|
|
||||||
stepName: step?.name,
|
|
||||||
stepContent: step?.content?.substring(0, 50),
|
|
||||||
templateId: template?.id,
|
|
||||||
templateContent: template?.content?.substring(0, 50),
|
|
||||||
});
|
|
||||||
setSelectedStep(step);
|
setSelectedStep(step);
|
||||||
setSelectedTemplate(template);
|
setSelectedTemplate(template);
|
||||||
setIsPanelOpen(true);
|
setIsPanelOpen(true);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useMemo } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Copy, Check } from 'lucide-react';
|
import { Copy, Check } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
|
|
@ -11,17 +11,15 @@ interface CodeBlockProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CodeBlock({ code, type, showLineNumbers = true }: CodeBlockProps) {
|
export function CodeBlock({ code, type, showLineNumbers = true }: CodeBlockProps) {
|
||||||
console.log('[CodeBlock] Render - code length:', code?.length, 'code preview:', code?.substring(0, 50), 'type:', type);
|
const [highlighted, setHighlighted] = useState<string>('');
|
||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
// Compute highlighted code synchronously
|
useEffect(() => {
|
||||||
const highlighted = useMemo(() => {
|
async function highlight() {
|
||||||
if (!code) return '';
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
const Prism = require('prismjs');
|
const Prism = require('prismjs');
|
||||||
|
|
||||||
|
// Load language components dynamically
|
||||||
if (type === 'sql') {
|
if (type === 'sql') {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
require('prismjs/components/prism-sql');
|
require('prismjs/components/prism-sql');
|
||||||
|
|
@ -32,14 +30,12 @@ export function CodeBlock({ code, type, showLineNumbers = true }: CodeBlockProps
|
||||||
|
|
||||||
const language = type === 'text' ? 'text' : type;
|
const language = type === 'text' ? 'text' : type;
|
||||||
const grammar = Prism.languages[language] || Prism.languages.text;
|
const grammar = Prism.languages[language] || Prism.languages.text;
|
||||||
return Prism.highlight(code, grammar, language);
|
const highlightedCode = Prism.highlight(code, grammar, language);
|
||||||
} catch (e) {
|
setHighlighted(highlightedCode);
|
||||||
console.error('[CodeBlock] Prism error:', e);
|
|
||||||
return code.replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
}
|
}
|
||||||
}, [code, type]);
|
|
||||||
|
|
||||||
console.log('[CodeBlock] highlighted length:', highlighted?.length);
|
highlight();
|
||||||
|
}, [code, type]);
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
await navigator.clipboard.writeText(code);
|
await navigator.clipboard.writeText(code);
|
||||||
|
|
@ -47,16 +43,14 @@ export function CodeBlock({ code, type, showLineNumbers = true }: CodeBlockProps
|
||||||
setTimeout(() => setCopied(false), 2000);
|
setTimeout(() => setCopied(false), 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Split code into lines for line numbers
|
const lines = code.split('\n');
|
||||||
const lines = code ? code.split('\n') : [];
|
|
||||||
const highlightedLines = highlighted ? highlighted.split('\n') : [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10"
|
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
>
|
>
|
||||||
{copied ? (
|
{copied ? (
|
||||||
|
|
@ -65,35 +59,33 @@ export function CodeBlock({ code, type, showLineNumbers = true }: CodeBlockProps
|
||||||
<Copy className="w-4 h-4" />
|
<Copy className="w-4 h-4" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="rounded-lg bg-slate-900 p-4 overflow-x-auto text-sm">
|
<pre className="rounded-lg bg-slate-900 p-4 overflow-x-auto text-sm">
|
||||||
<pre className={`language-${type} m-0`}>
|
<code className="language-{type}">
|
||||||
{showLineNumbers ? (
|
{showLineNumbers ? (
|
||||||
<div className="flex">
|
<table className="border-collapse">
|
||||||
{/* Line numbers column */}
|
<tbody>
|
||||||
<div className="flex flex-col text-slate-500 text-right pr-4 select-none min-w-[3rem]">
|
{lines.map((line, i) => (
|
||||||
{lines.map((_, i) => (
|
<tr key={i}>
|
||||||
<span key={i}>{i + 1}</span>
|
<td className="text-slate-500 text-right pr-4 select-none w-12">
|
||||||
))}
|
{i + 1}
|
||||||
</div>
|
</td>
|
||||||
{/* Code column */}
|
<td
|
||||||
<div className="flex flex-col">
|
className="text-slate-100"
|
||||||
{highlightedLines.map((line, i) => (
|
dangerouslySetInnerHTML={{
|
||||||
<span
|
__html: highlighted
|
||||||
key={i}
|
? highlighted.split('\n')[i] || ''
|
||||||
className="text-slate-100 whitespace-pre"
|
: line
|
||||||
dangerouslySetInnerHTML={{ __html: line || ' ' }}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</tr>
|
||||||
))}
|
))}
|
||||||
</div>
|
</tbody>
|
||||||
</div>
|
</table>
|
||||||
) : (
|
) : (
|
||||||
<code
|
<span dangerouslySetInnerHTML={{ __html: highlighted || code }} />
|
||||||
className="text-slate-100 whitespace-pre"
|
|
||||||
dangerouslySetInnerHTML={{ __html: highlighted || code || '' }}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import { X, RotateCcw, Pencil, Trash2, FileText, AlertCircle, CheckCircle, Check, SkipForward, Copy } from 'lucide-react';
|
import { X, RotateCcw, Pencil, Trash2, FileText, AlertCircle, CheckCircle, Check, SkipForward, Copy } from 'lucide-react';
|
||||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
@ -64,33 +64,13 @@ export function StepDetailPanel({
|
||||||
onEditCustom,
|
onEditCustom,
|
||||||
onDeleteCustom,
|
onDeleteCustom,
|
||||||
}: StepDetailPanelProps) {
|
}: StepDetailPanelProps) {
|
||||||
const [notes, setNotes] = useState('');
|
const [notes, setNotes] = useState(step?.notes || '');
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [editContent, setEditContent] = useState('');
|
const [editContent, setEditContent] = useState(step?.content || '');
|
||||||
const [showOriginal, setShowOriginal] = useState(false);
|
const [showOriginal, setShowOriginal] = useState(false);
|
||||||
const [skipReason, setSkipReason] = useState('');
|
const [skipReason, setSkipReason] = useState('');
|
||||||
const [isSkipping, setIsSkipping] = useState(false);
|
const [isSkipping, setIsSkipping] = useState(false);
|
||||||
|
|
||||||
// Sync state when step changes
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('[StepDetailPanel] useEffect triggered, step?.id:', step?.id);
|
|
||||||
if (step) {
|
|
||||||
console.log('[StepDetailPanel] Syncing state with step:', {
|
|
||||||
stepId: step.id,
|
|
||||||
stepName: step.name,
|
|
||||||
stepContent: step.content?.substring(0, 50),
|
|
||||||
});
|
|
||||||
setNotes(step.notes || '');
|
|
||||||
setEditContent(step.content || '');
|
|
||||||
setIsEditing(false);
|
|
||||||
setShowOriginal(false);
|
|
||||||
setSkipReason('');
|
|
||||||
setIsSkipping(false);
|
|
||||||
}
|
|
||||||
}, [step?.id]);
|
|
||||||
|
|
||||||
console.log('[StepDetailPanel] Render - step:', step?.id, 'step.content:', step?.content?.substring(0, 50), 'editContent:', editContent?.substring(0, 50));
|
|
||||||
|
|
||||||
if (!step) return null;
|
if (!step) return null;
|
||||||
|
|
||||||
const isCustom = step.isCustom;
|
const isCustom = step.isCustom;
|
||||||
|
|
@ -189,7 +169,7 @@ export function StepDetailPanel({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={isOpen} onOpenChange={onClose}>
|
<Sheet open={isOpen} onOpenChange={onClose}>
|
||||||
<SheetContent key={step?.id} className="w-[600px] sm:max-w-[600px]">
|
<SheetContent className="w-[600px] sm:max-w-[600px]">
|
||||||
<SheetHeader className="px-6">
|
<SheetHeader className="px-6">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
import { NextResponse } from 'next/server';
|
|
||||||
import type { NextRequest } from 'next/server';
|
|
||||||
|
|
||||||
// Environment configuration
|
|
||||||
const ENABLE_PASSCODE = process.env.ENABLE_PASSCODE === 'true';
|
|
||||||
const AUTH_COOKIE_NAME = process.env.AUTH_COOKIE_NAME || 'release_tracker_auth';
|
|
||||||
|
|
||||||
// Paths that should be excluded from auth check
|
|
||||||
const PUBLIC_PATHS = ['/login', '/api/auth'];
|
|
||||||
|
|
||||||
export function middleware(request: NextRequest) {
|
|
||||||
// If passcode protection is disabled, allow all requests
|
|
||||||
if (!ENABLE_PASSCODE) {
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { pathname } = request.nextUrl;
|
|
||||||
|
|
||||||
// Check if the path is public (login page or auth API)
|
|
||||||
if (PUBLIC_PATHS.some(path => pathname.startsWith(path))) {
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for auth cookie
|
|
||||||
const authCookie = request.cookies.get(AUTH_COOKIE_NAME);
|
|
||||||
|
|
||||||
if (!authCookie || authCookie.value !== 'authenticated') {
|
|
||||||
// Redirect to login page
|
|
||||||
const loginUrl = new URL('/login', request.url);
|
|
||||||
// Store the original URL to redirect back after login
|
|
||||||
loginUrl.searchParams.set('redirect', pathname);
|
|
||||||
return NextResponse.redirect(loginUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
// User is authenticated, allow the request
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure which paths the middleware runs on
|
|
||||||
export const config = {
|
|
||||||
matcher: [
|
|
||||||
// Match all paths except static files and api routes that are public
|
|
||||||
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
Loading…
Reference in New Issue