mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 14:12:10 +01:00
Compare commits
10 Commits
developmen
...
8464ac9c52
| Author | SHA1 | Date | |
|---|---|---|---|
| 8464ac9c52 | |||
| 247616cda8 | |||
| 10b1f91a2f | |||
| dd3fb55122 | |||
|
ebd35a88fb
|
|||
|
c0daab55ed
|
|||
| 5aaca6de99 | |||
|
bde0b44ea0
|
|||
|
71c8aff125
|
|||
|
0c18e8be57
|
36
.editorconfig
Normal file
36
.editorconfig
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.{css,scss,sass}]
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
|
[*.{ps1,psm1}]
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[*.{json,yaml,yml}]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
|
[*.{js,ts,jsx,tsx}]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
|
[*.{html,htm}]
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
@ -1,9 +0,0 @@
|
|||||||
# Development environment settings
|
|
||||||
# This file ensures NextAuth always has necessary environment variables in development
|
|
||||||
|
|
||||||
# NextAuth.js configuration
|
|
||||||
NEXTAUTH_URL=http://192.168.1.2:3000
|
|
||||||
NEXTAUTH_SECRET=this_is_a_fixed_secret_for_development_only
|
|
||||||
NODE_ENV=development
|
|
||||||
|
|
||||||
# Database connection - already configured in your prisma/schema.prisma
|
|
||||||
2779
.github/instructions/auth.js.instructions.md
vendored
Normal file
2779
.github/instructions/auth.js.instructions.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2428
.github/instructions/cloudflare-d1-database.instructions.md
vendored
Normal file
2428
.github/instructions/cloudflare-d1-database.instructions.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
335
.github/instructions/cloudflare-workers-nextjs.instructions.md
vendored
Normal file
335
.github/instructions/cloudflare-workers-nextjs.instructions.md
vendored
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
---
|
||||||
|
applyTo: '**'
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
title: Next.js · Cloudflare Workers docs
|
||||||
|
description: Create an Next.js application and deploy it to Cloudflare Workers with Workers Assets.
|
||||||
|
lastUpdated: 2025-05-16T19:09:44.000Z
|
||||||
|
source_url:
|
||||||
|
html: https://developers.cloudflare.com/workers/frameworks/framework-guides/nextjs/
|
||||||
|
md: https://developers.cloudflare.com/workers/frameworks/framework-guides/nextjs/index.md
|
||||||
|
---
|
||||||
|
|
||||||
|
**Start from CLI** - scaffold a Next.js project on Workers.
|
||||||
|
|
||||||
|
* npm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm create cloudflare@latest -- my-next-app --framework=next
|
||||||
|
```
|
||||||
|
|
||||||
|
* yarn
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn create cloudflare my-next-app --framework=next
|
||||||
|
```
|
||||||
|
|
||||||
|
* pnpm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm create cloudflare@latest my-next-app --framework=next
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a simple getting started guide. For detailed documentation on how the to use the Cloudflare OpenNext adapter, visit the [OpenNext website](https://opennext.js.org/cloudflare).
|
||||||
|
|
||||||
|
## What is Next.js?
|
||||||
|
|
||||||
|
[Next.js](https://nextjs.org/) is a [React](https://react.dev/) framework for building full stack applications.
|
||||||
|
|
||||||
|
Next.js supports Server-side and Client-side rendering, as well as Partial Prerendering which lets you combine static and dynamic components in the same route.
|
||||||
|
|
||||||
|
You can deploy your Next.js app to Cloudflare Workers using the OpenNext adaptor.
|
||||||
|
|
||||||
|
## Next.js supported features
|
||||||
|
|
||||||
|
Most Next.js features are supported by the Cloudflare OpenNext adapter:
|
||||||
|
|
||||||
|
| Feature | Cloudflare adapter | Notes |
|
||||||
|
| - | - | - |
|
||||||
|
| App Router | 🟢 supported | |
|
||||||
|
| Pages Router | 🟢 supported | |
|
||||||
|
| Route Handlers | 🟢 supported | |
|
||||||
|
| React Server Components | 🟢 supported | |
|
||||||
|
| Static Site Generation (SSG) | 🟢 supported | |
|
||||||
|
| Server-Side Rendering (SSR) | 🟢 supported | |
|
||||||
|
| Incremental Static Regeneration (ISR) | 🟢 supported | |
|
||||||
|
| Server Actions | 🟢 supported | |
|
||||||
|
| Response streaming | 🟢 supported | |
|
||||||
|
| asynchronous work with `next/after` | 🟢 supported | |
|
||||||
|
| Middleware | 🟢 supported | |
|
||||||
|
| Image optimization | 🟢 supported | Supported via [Cloudflare Images](https://developers.cloudflare.com/images/) |
|
||||||
|
| Partial Prerendering (PPR) | 🟢 supported | PPR is experimental in Next.js |
|
||||||
|
| Composable Caching ('use cache') | 🟢 supported | Composable Caching is experimental in Next.js |
|
||||||
|
| Node.js in Middleware | ⚪ not yet supported | Node.js middleware introduced in 15.2 are not yet supported |
|
||||||
|
|
||||||
|
## Deploy a new Next.js project on Workers
|
||||||
|
|
||||||
|
1. **Create a new project with the create-cloudflare CLI (C3).**
|
||||||
|
|
||||||
|
* npm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm create cloudflare@latest -- my-next-app --framework=next
|
||||||
|
```
|
||||||
|
|
||||||
|
* yarn
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn create cloudflare my-next-app --framework=next
|
||||||
|
```
|
||||||
|
|
||||||
|
* pnpm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm create cloudflare@latest my-next-app --framework=next
|
||||||
|
```
|
||||||
|
|
||||||
|
What's happening behind the scenes?
|
||||||
|
|
||||||
|
When you run this command, C3 creates a new project directory, initiates [Next.js's official setup tool](https://nextjs.org/docs/app/api-reference/cli/create-next-app), and configures the project for Cloudflare. It then offers the option to instantly deploy your application to Cloudflare.
|
||||||
|
|
||||||
|
2. **Develop locally.**
|
||||||
|
|
||||||
|
After creating your project, run the following command in your project directory to start a local development server. The command uses the Next.js development server. It offers the best developer experience by quickly reloading your app every time the source code is updated.
|
||||||
|
|
||||||
|
* npm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
* yarn
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
* pnpm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Test and preview your site with the Cloudflare adapter.**
|
||||||
|
|
||||||
|
* npm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
* yarn
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
* pnpm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
What's the difference between dev and preview?
|
||||||
|
|
||||||
|
The command used in the previous step uses the Next.js development server, which runs in Node.js. However, your deployed application will run on Cloudflare Workers, which uses the `workerd` runtime. Therefore when running integration tests and previewing your application, you should use the preview command, which is more accurate to production, as it executes your application in the `workerd` runtime using `wrangler dev`.
|
||||||
|
|
||||||
|
4. **Deploy your project.**
|
||||||
|
|
||||||
|
You can deploy your project to a [`*.workers.dev` subdomain](https://developers.cloudflare.com/workers/configuration/routing/workers-dev/) or a [custom domain](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/) from your local machine or any CI/CD system (including [Workers Builds](https://developers.cloudflare.com/workers/ci-cd/#workers-builds)). Use the following command to build and deploy. If you're using a CI service, be sure to update your "deploy command" accordingly.
|
||||||
|
|
||||||
|
* npm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
* yarn
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn run deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
* pnpm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm run deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploy an existing Next.js project on Workers
|
||||||
|
|
||||||
|
You can convert an existing Next.js application to run on Cloudflare
|
||||||
|
|
||||||
|
1. **Install [`@opennextjs/cloudflare`](https://www.npmjs.com/package/@opennextjs/cloudflare)**
|
||||||
|
|
||||||
|
* npm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm i @opennextjs/cloudflare@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
* yarn
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn add @opennextjs/cloudflare@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
* pnpm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm add @opennextjs/cloudflare@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install [`wrangler CLI`](https://developers.cloudflare.com/workers/wrangler) as a devDependency**
|
||||||
|
|
||||||
|
* npm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm i -D wrangler@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
* yarn
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn add -D wrangler@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
* pnpm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm add -D wrangler@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Add a Wrangler configuration file**
|
||||||
|
|
||||||
|
In your project root, create a [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) with the following content:
|
||||||
|
|
||||||
|
* wrangler.jsonc
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"main": ".open-next/worker.js",
|
||||||
|
"name": "my-app",
|
||||||
|
"compatibility_date": "2025-03-25",
|
||||||
|
"compatibility_flags": [
|
||||||
|
"nodejs_compat"
|
||||||
|
],
|
||||||
|
"assets": {
|
||||||
|
"directory": ".open-next/assets",
|
||||||
|
"binding": "ASSETS"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* wrangler.toml
|
||||||
|
|
||||||
|
```toml
|
||||||
|
main = ".open-next/worker.js"
|
||||||
|
name = "my-app"
|
||||||
|
compatibility_date = "2025-03-25"
|
||||||
|
compatibility_flags = ["nodejs_compat"]
|
||||||
|
[assets]
|
||||||
|
directory = ".open-next/assets"
|
||||||
|
binding = "ASSETS"
|
||||||
|
```
|
||||||
|
|
||||||
|
Note
|
||||||
|
|
||||||
|
As shown above, you must enable the [`nodejs_compat` compatibility flag](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) *and* set your [compatibility date](https://developers.cloudflare.com/workers/configuration/compatibility-dates/) to `2024-09-23` or later for your Next.js app to work with @opennextjs/cloudflare.
|
||||||
|
|
||||||
|
4. **Add a configuration file for OpenNext**
|
||||||
|
|
||||||
|
In your project root, create an OpenNext configuration file named `open-next.config.ts` with the following content:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
|
||||||
|
|
||||||
|
|
||||||
|
export default defineCloudflareConfig();
|
||||||
|
```
|
||||||
|
|
||||||
|
Note
|
||||||
|
|
||||||
|
`open-next.config.ts` is where you can configure the caching, see the [adapter documentation](https://opennext.js.org/cloudflare/caching) for more information
|
||||||
|
|
||||||
|
5. **Update `package.json`**
|
||||||
|
|
||||||
|
You can add the following scripts to your `package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
||||||
|
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
||||||
|
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage
|
||||||
|
|
||||||
|
* `preview`: Builds your app and serves it locally, allowing you to quickly preview your app running locally in the Workers runtime, via a single command. - `deploy`: Builds your app, and then deploys it to Cloudflare - `cf-typegen`: Generates a `cloudflare-env.d.ts` file at the root of your project containing the types for the env.
|
||||||
|
|
||||||
|
6. **Develop locally.**
|
||||||
|
|
||||||
|
After creating your project, run the following command in your project directory to start a local development server. The command uses the Next.js development server. It offers the best developer experience by quickly reloading your app after your source code is updated.
|
||||||
|
|
||||||
|
* npm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
* yarn
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
* pnpm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Test your site with the Cloudflare adapter.**
|
||||||
|
|
||||||
|
The command used in the previous step uses the Next.js development server to offer a great developer experience. However your application will run on Cloudflare Workers so you want to run your integration tests and verify that your application workers correctly in this environment.
|
||||||
|
|
||||||
|
* npm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
* yarn
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
* pnpm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
8. **Deploy your project.**
|
||||||
|
|
||||||
|
You can deploy your project to a [`*.workers.dev` subdomain](https://developers.cloudflare.com/workers/configuration/routing/workers-dev/) or a [custom domain](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/) from your local machine or any CI/CD system (including [Workers Builds](https://developers.cloudflare.com/workers/ci-cd/#workers-builds)). Use the following command to build and deploy. If you're using a CI service, be sure to update your "deploy command" accordingly.
|
||||||
|
|
||||||
|
* npm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
* yarn
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn run deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
* pnpm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm run deploy
|
||||||
|
```
|
||||||
9
.github/workflows/playwright.yml
vendored
9
.github/workflows/playwright.yml
vendored
@ -1,9 +1,13 @@
|
|||||||
name: Playwright Tests
|
name: Playwright Tests
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, master]
|
branches:
|
||||||
|
- master
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, master]
|
branches:
|
||||||
|
- master
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
@ -20,6 +24,7 @@ jobs:
|
|||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: npx playwright install --with-deps
|
run: npx playwright install --with-deps
|
||||||
- name: Run Playwright tests
|
- name: Run Playwright tests
|
||||||
|
continue-on-error: true
|
||||||
run: npx playwright test
|
run: npx playwright test
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|||||||
207
.gitignore
vendored
207
.gitignore
vendored
@ -33,6 +33,12 @@ yarn-error.log*
|
|||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|
||||||
|
# Cloudflare Workers
|
||||||
|
.dev.vars
|
||||||
|
wrangler.toml.local
|
||||||
|
.wrangler/
|
||||||
|
.open-next/
|
||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
@ -218,6 +224,12 @@ yarn-error.log*
|
|||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|
||||||
|
# Cloudflare Workers
|
||||||
|
.dev.vars
|
||||||
|
wrangler.toml.local
|
||||||
|
.wrangler/
|
||||||
|
.open-next/
|
||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
@ -227,6 +239,9 @@ next-env.d.ts
|
|||||||
*.db-shm
|
*.db-shm
|
||||||
*.db-wal
|
*.db-wal
|
||||||
*.sqlite?
|
*.sqlite?
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
prisma/dev.db
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/*
|
.vscode/*
|
||||||
@ -261,3 +276,195 @@ Thumbs.db
|
|||||||
/playwright-report/
|
/playwright-report/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/node,macos
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=node,macos
|
||||||
|
|
||||||
|
### macOS ###
|
||||||
|
# General
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Icon must end with two \r
|
||||||
|
Icon
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
### Node ###
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Moved from ./templates for ignoring all locks in templates
|
||||||
|
templates/**/*-lock.*
|
||||||
|
templates/**/*.lock
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
### Node Patch ###
|
||||||
|
# Serverless Webpack directories
|
||||||
|
.webpack/
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
# SvelteKit build / generate output
|
||||||
|
.svelte-kit
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/node,macos
|
||||||
|
|
||||||
|
# Wrangler output
|
||||||
|
.wrangler/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Turbo output
|
||||||
|
.turbo/
|
||||||
|
|
||||||
|
.dev.vars*
|
||||||
|
test-transcript-format.js
|
||||||
|
my-next-app/
|
||||||
|
|
||||||
|
# Wiki
|
||||||
|
.wiki/
|
||||||
|
|
||||||
|
.specstory/
|
||||||
|
|||||||
29
.open-next/.build/open-next.config.edge.mjs
Normal file
29
.open-next/.build/open-next.config.edge.mjs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// open-next.config.ts
|
||||||
|
var config = {
|
||||||
|
default: {
|
||||||
|
override: {
|
||||||
|
wrapper: "cloudflare-node",
|
||||||
|
converter: "edge",
|
||||||
|
proxyExternalRequest: "fetch",
|
||||||
|
incrementalCache: "dummy",
|
||||||
|
tagCache: "dummy",
|
||||||
|
queue: "dummy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
edgeExternals: ["node:crypto"],
|
||||||
|
middleware: {
|
||||||
|
external: true,
|
||||||
|
override: {
|
||||||
|
wrapper: "cloudflare-edge",
|
||||||
|
converter: "edge",
|
||||||
|
proxyExternalRequest: "fetch",
|
||||||
|
incrementalCache: "dummy",
|
||||||
|
tagCache: "dummy",
|
||||||
|
queue: "dummy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var open_next_config_default = config;
|
||||||
|
export {
|
||||||
|
open_next_config_default as default
|
||||||
|
};
|
||||||
31
.open-next/.build/open-next.config.mjs
Normal file
31
.open-next/.build/open-next.config.mjs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { createRequire as topLevelCreateRequire } from 'module';const require = topLevelCreateRequire(import.meta.url);import bannerUrl from 'url';const __dirname = bannerUrl.fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
|
||||||
|
// open-next.config.ts
|
||||||
|
var config = {
|
||||||
|
default: {
|
||||||
|
override: {
|
||||||
|
wrapper: "cloudflare-node",
|
||||||
|
converter: "edge",
|
||||||
|
proxyExternalRequest: "fetch",
|
||||||
|
incrementalCache: "dummy",
|
||||||
|
tagCache: "dummy",
|
||||||
|
queue: "dummy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
edgeExternals: ["node:crypto"],
|
||||||
|
middleware: {
|
||||||
|
external: true,
|
||||||
|
override: {
|
||||||
|
wrapper: "cloudflare-edge",
|
||||||
|
converter: "edge",
|
||||||
|
proxyExternalRequest: "fetch",
|
||||||
|
incrementalCache: "dummy",
|
||||||
|
tagCache: "dummy",
|
||||||
|
queue: "dummy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var open_next_config_default = config;
|
||||||
|
export {
|
||||||
|
open_next_config_default as default
|
||||||
|
};
|
||||||
54
.prettierignore
Normal file
54
.prettierignore
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
prisma/migrations/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
|
||||||
|
# Coverage reports
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
playwright/.cache/
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
*.generated.*
|
||||||
|
|
||||||
|
pnpm-lock.yaml
|
||||||
112
README.md
112
README.md
@ -2,65 +2,65 @@
|
|||||||
|
|
||||||
A real-time analytics dashboard for monitoring user sessions and interactions with interactive data visualizations and detailed metrics.
|
A real-time analytics dashboard for monitoring user sessions and interactions with interactive data visualizations and detailed metrics.
|
||||||
|
|
||||||
.*%22&replace=%24%3Cversion%3E&logo=nextdotjs&label=Nextjs&color=%23000000)
|
.*%22&replace=%24%3Cversion%3E&logo=nextdotjs&label=Nextjs&color=%23000000>)
|
||||||
.*%22&replace=%24%3Cversion%3E&logo=react&label=React&color=%2361DAFB)
|
.*%22&replace=%24%3Cversion%3E&logo=react&label=React&color=%2361DAFB>)
|
||||||
.*%22&replace=%24%3Cversion%3E&logo=typescript&label=TypeScript&color=%233178C6)
|
.*%22&replace=%24%3Cversion%3E&logo=typescript&label=TypeScript&color=%233178C6>)
|
||||||
.*%22&replace=%24%3Cversion%3E&logo=prisma&label=Prisma&color=%232D3748)
|
.*%22&replace=%24%3Cversion%3E&logo=prisma&label=Prisma&color=%232D3748>)
|
||||||
.*%22&replace=%24%3Cversion%3E&logo=tailwindcss&label=TailwindCSS&color=%2306B6D4)
|
.*%22&replace=%24%3Cversion%3E&logo=tailwindcss&label=TailwindCSS&color=%2306B6D4>)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Real-time Session Monitoring**: Track and analyze user sessions as they happen
|
- **Real-time Session Monitoring**: Track and analyze user sessions as they happen
|
||||||
- **Interactive Visualizations**: Geographic maps, response time distributions, and more
|
- **Interactive Visualizations**: Geographic maps, response time distributions, and more
|
||||||
- **Advanced Analytics**: Detailed metrics and insights about user behavior
|
- **Advanced Analytics**: Detailed metrics and insights about user behavior
|
||||||
- **User Management**: Secure authentication with role-based access control
|
- **User Management**: Secure authentication with role-based access control
|
||||||
- **Customizable Dashboard**: Filter and sort data based on your specific needs
|
- **Customizable Dashboard**: Filter and sort data based on your specific needs
|
||||||
- **Session Details**: In-depth analysis of individual user sessions
|
- **Session Details**: In-depth analysis of individual user sessions
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- **Frontend**: React 19, Next.js 15, TailwindCSS 4
|
- **Frontend**: React 19, Next.js 15, TailwindCSS 4
|
||||||
- **Backend**: Next.js API Routes, Node.js
|
- **Backend**: Next.js API Routes, Node.js
|
||||||
- **Database**: Prisma ORM with SQLite (default), compatible with PostgreSQL
|
- **Database**: Prisma ORM with SQLite (default), compatible with PostgreSQL
|
||||||
- **Authentication**: NextAuth.js
|
- **Authentication**: NextAuth.js
|
||||||
- **Visualization**: Chart.js, D3.js, React Leaflet
|
- **Visualization**: Chart.js, D3.js, React Leaflet
|
||||||
- **Data Processing**: Node-cron for scheduled tasks
|
- **Data Processing**: Node-cron for scheduled tasks
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Node.js (LTS version recommended)
|
- Node.js (LTS version recommended)
|
||||||
- npm or yarn
|
- npm or yarn
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
1. Clone this repository:
|
1. Clone this repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/kjanat/livedash-node.git
|
git clone https://github.com/kjanat/livedash-node.git
|
||||||
cd livedash-node
|
cd livedash-node
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Install dependencies:
|
2. Install dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Set up the database:
|
3. Set up the database:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run prisma:generate
|
npm run prisma:generate
|
||||||
npm run prisma:migrate
|
npm run prisma:migrate
|
||||||
npm run prisma:seed
|
npm run prisma:seed
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Start the development server:
|
4. Start the development server:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Open your browser and navigate to <http://localhost:3000>
|
5. Open your browser and navigate to <http://localhost:3000>
|
||||||
|
|
||||||
@ -76,29 +76,29 @@ NEXTAUTH_SECRET=your-secret-here
|
|||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
- `app/`: Next.js App Router components and pages
|
- `app/`: Next.js App Router components and pages
|
||||||
- `components/`: Reusable React components
|
- `components/`: Reusable React components
|
||||||
- `lib/`: Utility functions and shared code
|
- `lib/`: Utility functions and shared code
|
||||||
- `pages/`: API routes and server-side code
|
- `pages/`: API routes and server-side code
|
||||||
- `prisma/`: Database schema and migrations
|
- `prisma/`: Database schema and migrations
|
||||||
- `public/`: Static assets
|
- `public/`: Static assets
|
||||||
- `docs/`: Project documentation
|
- `docs/`: Project documentation
|
||||||
|
|
||||||
## Available Scripts
|
## Available Scripts
|
||||||
|
|
||||||
- `npm run dev`: Start the development server
|
- `npm run dev`: Start the development server
|
||||||
- `npm run build`: Build the application for production
|
- `npm run build`: Build the application for production
|
||||||
- `npm run start`: Run the production build
|
- `npm run start`: Run the production build
|
||||||
- `npm run lint`: Run ESLint
|
- `npm run lint`: Run ESLint
|
||||||
- `npm run format`: Format code with Prettier
|
- `npm run format`: Format code with Prettier
|
||||||
- `npm run prisma:studio`: Open Prisma Studio to view database
|
- `npm run prisma:studio`: Open Prisma Studio to view database
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
1. Fork the repository
|
1. Fork the repository
|
||||||
2. Create your feature branch: `git checkout -b feature/my-new-feature`
|
2. Create your feature branch: `git checkout -b feature/my-new-feature`
|
||||||
3. Commit your changes: `git commit -am 'Add some feature'`
|
3. Commit your changes: `git commit -am 'Add some feature'`
|
||||||
4. Push to the branch: `git push origin feature/my-new-feature`
|
4. Push to the branch: `git push origin feature/my-new-feature`
|
||||||
5. Submit a pull request
|
5. Submit a pull request
|
||||||
|
|
||||||
## License
|
## License
|
||||||
@ -107,9 +107,9 @@ This project is not licensed for commercial use without explicit permission. Fre
|
|||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
- [Next.js](https://nextjs.org/)
|
- [Next.js](https://nextjs.org/)
|
||||||
- [Prisma](https://prisma.io/)
|
- [Prisma](https://prisma.io/)
|
||||||
- [TailwindCSS](https://tailwindcss.com/)
|
- [TailwindCSS](https://tailwindcss.com/)
|
||||||
- [Chart.js](https://www.chartjs.org/)
|
- [Chart.js](https://www.chartjs.org/)
|
||||||
- [D3.js](https://d3js.org/)
|
- [D3.js](https://d3js.org/)
|
||||||
- [React Leaflet](https://react-leaflet.js.org/)
|
- [React Leaflet](https://react-leaflet.js.org/)
|
||||||
|
|||||||
130
TODO.md
130
TODO.md
@ -2,95 +2,107 @@
|
|||||||
|
|
||||||
## Dashboard Integration
|
## Dashboard Integration
|
||||||
|
|
||||||
- [ ] **Resolve `GeographicMap.tsx` and `ResponseTimeDistribution.tsx` data simulation**
|
- [ ] **Resolve `GeographicMap.tsx` and `ResponseTimeDistribution.tsx` data simulation**
|
||||||
- Investigate integrating real data sources with server-side analytics
|
- Investigate integrating real data sources with server-side analytics
|
||||||
- Replace simulated data mentioned in `docs/dashboard-components.md`
|
- Replace simulated data mentioned in `docs/dashboard-components.md`
|
||||||
|
|
||||||
## Component Specific
|
## Component Specific
|
||||||
|
|
||||||
- [ ] **Implement robust emailing of temporary passwords**
|
- [ ] **Implement robust emailing of temporary passwords**
|
||||||
- File: `pages/api/dashboard/users.ts`
|
|
||||||
- Set up proper email service integration
|
|
||||||
|
|
||||||
- [x] **Session page improvements** ✅
|
- File: `pages/api/dashboard/users.ts`
|
||||||
- File: `app/dashboard/sessions/page.tsx`
|
- Set up proper email service integration
|
||||||
- Implemented pagination, advanced filtering, and sorting
|
|
||||||
|
- [x] **Session page improvements** ✅
|
||||||
|
- File: `app/dashboard/sessions/page.tsx`
|
||||||
|
- Implemented pagination, advanced filtering, and sorting
|
||||||
|
|
||||||
## File Cleanup
|
## File Cleanup
|
||||||
|
|
||||||
- [x] **Remove backup files** ✅
|
- [x] **Remove backup files** ✅
|
||||||
- Reviewed and removed `.bak` and `.new` files after integration
|
- Reviewed and removed `.bak` and `.new` files after integration
|
||||||
- Cleaned up `GeographicMap.tsx.bak`, `SessionDetails.tsx.bak`, `SessionDetails.tsx.new`
|
- Cleaned up `GeographicMap.tsx.bak`, `SessionDetails.tsx.bak`, `SessionDetails.tsx.new`
|
||||||
|
|
||||||
## Database Schema Improvements
|
## Database Schema Improvements
|
||||||
|
|
||||||
- [ ] **Update EndTime field**
|
- [ ] **Update EndTime field**
|
||||||
- Make `endTime` field nullable in Prisma schema to match TypeScript interfaces
|
|
||||||
|
|
||||||
- [ ] **Add database indices**
|
- Make `endTime` field nullable in Prisma schema to match TypeScript interfaces
|
||||||
- Add appropriate indices to improve query performance
|
|
||||||
- Focus on dashboard metrics and session listing queries
|
|
||||||
|
|
||||||
- [ ] **Implement production email service**
|
- [ ] **Add database indices**
|
||||||
- Replace console logging in `lib/sendEmail.ts`
|
|
||||||
- Consider providers: Nodemailer, SendGrid, AWS SES
|
- Add appropriate indices to improve query performance
|
||||||
|
- Focus on dashboard metrics and session listing queries
|
||||||
|
|
||||||
|
- [ ] **Implement production email service**
|
||||||
|
- Replace console logging in `lib/sendEmail.ts`
|
||||||
|
- Consider providers: Nodemailer, SendGrid, AWS SES
|
||||||
|
|
||||||
## General Enhancements & Features
|
## General Enhancements & Features
|
||||||
|
|
||||||
- [ ] **Real-time updates**
|
- [ ] **Real-time updates**
|
||||||
- Implement for dashboard and session list
|
|
||||||
- Consider WebSockets or Server-Sent Events
|
|
||||||
|
|
||||||
- [ ] **Data export functionality**
|
- Implement for dashboard and session list
|
||||||
- Allow users (especially admins) to export session data
|
- Consider WebSockets or Server-Sent Events
|
||||||
- Support CSV format initially
|
|
||||||
|
|
||||||
- [ ] **Customizable dashboard**
|
- [ ] **Data export functionality**
|
||||||
- Allow users to customize dashboard view
|
|
||||||
- Let users choose which metrics/charts are most important
|
- Allow users (especially admins) to export session data
|
||||||
|
- Support CSV format initially
|
||||||
|
|
||||||
|
- [ ] **Customizable dashboard**
|
||||||
|
- Allow users to customize dashboard view
|
||||||
|
- Let users choose which metrics/charts are most important
|
||||||
|
|
||||||
## Testing & Quality Assurance
|
## Testing & Quality Assurance
|
||||||
|
|
||||||
- [ ] **Comprehensive testing suite**
|
- [ ] **Comprehensive testing suite**
|
||||||
- [ ] Unit tests for utility functions and API logic
|
|
||||||
- [ ] Integration tests for API endpoints with database
|
|
||||||
- [ ] End-to-end tests for user flows (Playwright or Cypress)
|
|
||||||
|
|
||||||
- [ ] **Error monitoring and logging**
|
- [ ] Unit tests for utility functions and API logic
|
||||||
- Integrate robust error monitoring service (Sentry)
|
- [ ] Integration tests for API endpoints with database
|
||||||
- Enhance server-side logging
|
- [ ] End-to-end tests for user flows (Playwright or Cypress)
|
||||||
|
|
||||||
- [ ] **Accessibility improvements**
|
- [ ] **Error monitoring and logging**
|
||||||
- Review application against WCAG guidelines
|
|
||||||
- Improve keyboard navigation and screen reader compatibility
|
- Integrate robust error monitoring service (Sentry)
|
||||||
- Check color contrast ratios
|
- Enhance server-side logging
|
||||||
|
|
||||||
|
- [ ] **Accessibility improvements**
|
||||||
|
- Review application against WCAG guidelines
|
||||||
|
- Improve keyboard navigation and screen reader compatibility
|
||||||
|
- Check color contrast ratios
|
||||||
|
|
||||||
## Security Enhancements
|
## Security Enhancements
|
||||||
|
|
||||||
- [x] **Password reset functionality** ✅
|
- [x] **Password reset functionality** ✅
|
||||||
- Implemented secure password reset mechanism
|
|
||||||
- Files: `app/forgot-password/page.tsx`, `app/reset-password/page.tsx`, `pages/api/forgot-password.ts`, `pages/api/reset-password.ts`
|
|
||||||
|
|
||||||
- [ ] **Two-Factor Authentication (2FA)**
|
- Implemented secure password reset mechanism
|
||||||
- Consider adding 2FA, especially for admin accounts
|
- Files: `app/forgot-password/page.tsx`, `app/reset-password/page.tsx`, `pages/api/forgot-password.ts`, `pages/api/reset-password.ts`
|
||||||
|
|
||||||
- [ ] **Input validation and sanitization**
|
- [ ] **Two-Factor Authentication (2FA)**
|
||||||
- Review all user inputs (API request bodies, query parameters)
|
|
||||||
- Ensure proper validation and sanitization
|
- Consider adding 2FA, especially for admin accounts
|
||||||
|
|
||||||
|
- [ ] **Input validation and sanitization**
|
||||||
|
- Review all user inputs (API request bodies, query parameters)
|
||||||
|
- Ensure proper validation and sanitization
|
||||||
|
|
||||||
## Code Quality & Development
|
## Code Quality & Development
|
||||||
|
|
||||||
- [ ] **Code review process**
|
- [ ] **Code review process**
|
||||||
- Enforce code reviews for all changes
|
|
||||||
|
|
||||||
- [ ] **Environment configuration**
|
- Enforce code reviews for all changes
|
||||||
- Ensure secure management of environment-specific configurations
|
|
||||||
|
|
||||||
- [ ] **Dependency management**
|
- [ ] **Environment configuration**
|
||||||
- Periodically review dependencies for vulnerabilities
|
|
||||||
- Keep dependencies updated
|
|
||||||
|
|
||||||
- [ ] **Documentation updates**
|
- Ensure secure management of environment-specific configurations
|
||||||
- [ ] Ensure `docs/dashboard-components.md` reflects actual implementations
|
|
||||||
- [ ] Verify "Dashboard Enhancements" are consistently applied
|
- [ ] **Dependency management**
|
||||||
- [ ] Update documentation for improved layout and visual hierarchies
|
|
||||||
|
- Periodically review dependencies for vulnerabilities
|
||||||
|
- Keep dependencies updated
|
||||||
|
|
||||||
|
- [ ] **Documentation updates**
|
||||||
|
- [ ] Ensure `docs/dashboard-components.md` reflects actual implementations
|
||||||
|
- [ ] Verify "Dashboard Enhancements" are consistently applied
|
||||||
|
- [ ] Update documentation for improved layout and visual hierarchies
|
||||||
|
|||||||
115
app/api/auth/[...nextauth]/route.ts
Normal file
115
app/api/auth/[...nextauth]/route.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import NextAuth, { NextAuthConfig } from "next-auth";
|
||||||
|
import { D1Adapter } from "@auth/d1-adapter";
|
||||||
|
import Credentials from "next-auth/providers/credentials";
|
||||||
|
import * as bcrypt from "bcryptjs";
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import { PrismaD1 } from "@prisma/adapter-d1";
|
||||||
|
|
||||||
|
// Check if we're in a Cloudflare Workers environment
|
||||||
|
const isCloudflareWorker =
|
||||||
|
typeof globalThis.caches !== "undefined" &&
|
||||||
|
typeof (globalThis as any).WebSocketPair !== "undefined";
|
||||||
|
|
||||||
|
const config: NextAuthConfig = {
|
||||||
|
providers: [
|
||||||
|
Credentials({
|
||||||
|
name: "credentials",
|
||||||
|
credentials: {
|
||||||
|
email: { label: "Email", type: "email" },
|
||||||
|
password: { label: "Password", type: "password" },
|
||||||
|
},
|
||||||
|
authorize: async (credentials) => {
|
||||||
|
if (!credentials?.email || !credentials?.password) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let prisma: PrismaClient;
|
||||||
|
|
||||||
|
// Initialize Prisma based on environment
|
||||||
|
if (isCloudflareWorker) {
|
||||||
|
// In Cloudflare Workers, get DB from bindings
|
||||||
|
const adapter = new PrismaD1((globalThis as any).DB);
|
||||||
|
prisma = new PrismaClient({ adapter });
|
||||||
|
} else {
|
||||||
|
// In local development, use standard Prisma
|
||||||
|
prisma = new PrismaClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email: credentials.email as string },
|
||||||
|
include: { company: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid = await bcrypt.compare(
|
||||||
|
credentials.password as string,
|
||||||
|
user.password
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.email, // Use email as name
|
||||||
|
role: user.role,
|
||||||
|
companyId: user.companyId,
|
||||||
|
company: user.company.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
await prisma.$disconnect();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Authentication error:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
callbacks: {
|
||||||
|
jwt: async ({ token, user }: any) => {
|
||||||
|
if (user) {
|
||||||
|
token.role = user.role;
|
||||||
|
token.companyId = user.companyId;
|
||||||
|
token.company = user.company;
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
},
|
||||||
|
session: async ({ session, token }: any) => {
|
||||||
|
if (token && session.user) {
|
||||||
|
session.user.id = token.sub;
|
||||||
|
session.user.role = token.role;
|
||||||
|
session.user.companyId = token.companyId;
|
||||||
|
session.user.company = token.company;
|
||||||
|
}
|
||||||
|
return session;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
signIn: "/login",
|
||||||
|
error: "/login",
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
strategy: "jwt",
|
||||||
|
maxAge: 30 * 24 * 60 * 60, // 30 days
|
||||||
|
},
|
||||||
|
secret: process.env.AUTH_SECRET,
|
||||||
|
trustHost: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add D1 adapter only in Cloudflare Workers environment
|
||||||
|
if (isCloudflareWorker && (globalThis as any).DB) {
|
||||||
|
(config as any).adapter = D1Adapter((globalThis as any).DB);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { handlers } = NextAuth(config);
|
||||||
|
|
||||||
|
export const { GET, POST } = handlers;
|
||||||
@ -4,6 +4,10 @@ import { useState, useEffect } from "react";
|
|||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { Company } from "../../../lib/types";
|
import { Company } from "../../../lib/types";
|
||||||
|
|
||||||
|
interface CompanyConfigResponse {
|
||||||
|
company: Company;
|
||||||
|
}
|
||||||
|
|
||||||
export default function CompanySettingsPage() {
|
export default function CompanySettingsPage() {
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
// We store the full company object for future use and updates after save operations
|
// We store the full company object for future use and updates after save operations
|
||||||
@ -22,7 +26,7 @@ export default function CompanySettingsPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/dashboard/config");
|
const res = await fetch("/api/dashboard/config");
|
||||||
const data = await res.json();
|
const data = (await res.json()) as CompanyConfigResponse;
|
||||||
setCompany(data.company);
|
setCompany(data.company);
|
||||||
setCsvUrl(data.company.csvUrl || "");
|
setCsvUrl(data.company.csvUrl || "");
|
||||||
setCsvUsername(data.company.csvUsername || "");
|
setCsvUsername(data.company.csvUsername || "");
|
||||||
@ -58,10 +62,10 @@ export default function CompanySettingsPage() {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setMessage("Settings saved successfully!");
|
setMessage("Settings saved successfully!");
|
||||||
// Update local state if needed
|
// Update local state if needed
|
||||||
const data = await res.json();
|
const data = (await res.json()) as CompanyConfigResponse;
|
||||||
setCompany(data.company);
|
setCompany(data.company);
|
||||||
} else {
|
} else {
|
||||||
const error = await res.json();
|
const error = (await res.json()) as { message?: string };
|
||||||
setMessage(
|
setMessage(
|
||||||
`Failed to save settings: ${error.message || "Unknown error"}`
|
`Failed to save settings: ${error.message || "Unknown error"}`
|
||||||
);
|
);
|
||||||
|
|||||||
@ -17,6 +17,11 @@ import GeographicMap from "../../../components/GeographicMap";
|
|||||||
import ResponseTimeDistribution from "../../../components/ResponseTimeDistribution";
|
import ResponseTimeDistribution from "../../../components/ResponseTimeDistribution";
|
||||||
import WelcomeBanner from "../../../components/WelcomeBanner";
|
import WelcomeBanner from "../../../components/WelcomeBanner";
|
||||||
|
|
||||||
|
interface MetricsApiResponse {
|
||||||
|
metrics: MetricsResult;
|
||||||
|
company: Company;
|
||||||
|
}
|
||||||
|
|
||||||
// Safely wrapped component with useSession
|
// Safely wrapped component with useSession
|
||||||
function DashboardContent() {
|
function DashboardContent() {
|
||||||
const { data: session, status } = useSession(); // Add status from useSession
|
const { data: session, status } = useSession(); // Add status from useSession
|
||||||
@ -40,7 +45,7 @@ function DashboardContent() {
|
|||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await fetch("/api/dashboard/metrics");
|
const res = await fetch("/api/dashboard/metrics");
|
||||||
const data = await res.json();
|
const data = (await res.json()) as MetricsApiResponse;
|
||||||
console.log("Metrics from API:", {
|
console.log("Metrics from API:", {
|
||||||
avgSessionLength: data.metrics.avgSessionLength,
|
avgSessionLength: data.metrics.avgSessionLength,
|
||||||
avgSessionTimeTrend: data.metrics.avgSessionTimeTrend,
|
avgSessionTimeTrend: data.metrics.avgSessionTimeTrend,
|
||||||
@ -76,10 +81,10 @@ function DashboardContent() {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
// Refetch metrics
|
// Refetch metrics
|
||||||
const metricsRes = await fetch("/api/dashboard/metrics");
|
const metricsRes = await fetch("/api/dashboard/metrics");
|
||||||
const data = await metricsRes.json();
|
const data = (await metricsRes.json()) as MetricsApiResponse;
|
||||||
setMetrics(data.metrics);
|
setMetrics(data.metrics);
|
||||||
} else {
|
} else {
|
||||||
const errorData = await res.json();
|
const errorData = (await res.json()) as { error: string };
|
||||||
alert(`Failed to refresh sessions: ${errorData.error}`);
|
alert(`Failed to refresh sessions: ${errorData.error}`);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -8,6 +8,10 @@ import TranscriptViewer from "../../../../components/TranscriptViewer";
|
|||||||
import { ChatSession } from "../../../../lib/types";
|
import { ChatSession } from "../../../../lib/types";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
interface SessionApiResponse {
|
||||||
|
session: ChatSession;
|
||||||
|
}
|
||||||
|
|
||||||
export default function SessionViewPage() {
|
export default function SessionViewPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const router = useRouter(); // Initialize useRouter
|
const router = useRouter(); // Initialize useRouter
|
||||||
@ -30,13 +34,13 @@ export default function SessionViewPage() {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/dashboard/session/${id}`);
|
const response = await fetch(`/api/dashboard/session/${id}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json();
|
const errorData = (await response.json()) as { error: string };
|
||||||
throw new Error(
|
throw new Error(
|
||||||
errorData.error ||
|
errorData.error ||
|
||||||
`Failed to fetch session: ${response.statusText}`
|
`Failed to fetch session: ${response.statusText}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = (await response.json()) as SessionApiResponse;
|
||||||
setSession(data.session);
|
setSession(data.session);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(
|
setError(
|
||||||
@ -150,16 +154,17 @@ export default function SessionViewPage() {
|
|||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
No transcript content available for this session.
|
No transcript content available for this session.
|
||||||
</p>
|
</p>
|
||||||
{session.fullTranscriptUrl && (
|
{session.fullTranscriptUrl &&
|
||||||
<a
|
process.env.NODE_ENV !== "production" && (
|
||||||
href={session.fullTranscriptUrl}
|
<a
|
||||||
target="_blank"
|
href={session.fullTranscriptUrl}
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
className="text-sky-600 hover:underline mt-2 inline-block"
|
rel="noopener noreferrer"
|
||||||
>
|
className="text-sky-600 hover:underline mt-2 inline-block"
|
||||||
View Source Transcript URL
|
>
|
||||||
</a>
|
View Source Transcript URL
|
||||||
)}
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -14,6 +14,11 @@ interface FilterOptions {
|
|||||||
languages: string[];
|
languages: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SessionsApiResponse {
|
||||||
|
sessions: ChatSession[];
|
||||||
|
totalSessions: number;
|
||||||
|
}
|
||||||
|
|
||||||
export default function SessionsPage() {
|
export default function SessionsPage() {
|
||||||
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@ -58,7 +63,7 @@ export default function SessionsPage() {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Failed to fetch filter options");
|
throw new Error("Failed to fetch filter options");
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = (await response.json()) as FilterOptions;
|
||||||
setFilterOptions(data);
|
setFilterOptions(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(
|
setError(
|
||||||
@ -88,7 +93,7 @@ export default function SessionsPage() {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch sessions: ${response.statusText}`);
|
throw new Error(`Failed to fetch sessions: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = (await response.json()) as SessionsApiResponse;
|
||||||
setSessions(data.sessions || []);
|
setSessions(data.sessions || []);
|
||||||
setTotalPages(Math.ceil((data.totalSessions || 0) / pageSize));
|
setTotalPages(Math.ceil((data.totalSessions || 0) / pageSize));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -12,6 +12,10 @@ interface UserManagementProps {
|
|||||||
session: UserSession;
|
session: UserSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UsersApiResponse {
|
||||||
|
users: UserItem[];
|
||||||
|
}
|
||||||
|
|
||||||
export default function UserManagement({ session }: UserManagementProps) {
|
export default function UserManagement({ session }: UserManagementProps) {
|
||||||
const [users, setUsers] = useState<UserItem[]>([]);
|
const [users, setUsers] = useState<UserItem[]>([]);
|
||||||
const [email, setEmail] = useState<string>("");
|
const [email, setEmail] = useState<string>("");
|
||||||
@ -21,7 +25,7 @@ export default function UserManagement({ session }: UserManagementProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/dashboard/users")
|
fetch("/api/dashboard/users")
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((data) => setUsers(data.users));
|
.then((data) => setUsers((data as UsersApiResponse).users));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function inviteUser() {
|
async function inviteUser() {
|
||||||
|
|||||||
@ -9,6 +9,10 @@ interface UserItem {
|
|||||||
role: string;
|
role: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UsersApiResponse {
|
||||||
|
users: UserItem[];
|
||||||
|
}
|
||||||
|
|
||||||
export default function UserManagementPage() {
|
export default function UserManagementPage() {
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
const [users, setUsers] = useState<UserItem[]>([]);
|
const [users, setUsers] = useState<UserItem[]>([]);
|
||||||
@ -27,11 +31,28 @@ export default function UserManagementPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/dashboard/users");
|
const res = await fetch("/api/dashboard/users");
|
||||||
const data = await res.json();
|
const data = await res.json() as UsersApiResponse | { error: string; };
|
||||||
setUsers(data.users);
|
|
||||||
|
if (res.ok && 'users' in data) {
|
||||||
|
setUsers(data.users);
|
||||||
|
} else {
|
||||||
|
const errorMessage = 'error' in data ? data.error : "Unknown error";
|
||||||
|
console.error("Failed to fetch users:", errorMessage);
|
||||||
|
|
||||||
|
if (errorMessage === "Admin access required") {
|
||||||
|
setMessage("You need admin privileges to manage users.");
|
||||||
|
} else if (errorMessage === "Not logged in") {
|
||||||
|
setMessage("Please log in to access this page.");
|
||||||
|
} else {
|
||||||
|
setMessage(`Failed to load users: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setUsers([]); // Set empty array to prevent undefined errors
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch users:", error);
|
console.error("Failed to fetch users:", error);
|
||||||
setMessage("Failed to load users.");
|
setMessage("Failed to load users.");
|
||||||
|
setUsers([]); // Set empty array to prevent undefined errors
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -52,7 +73,7 @@ export default function UserManagementPage() {
|
|||||||
// Refresh the user list
|
// Refresh the user list
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
} else {
|
} else {
|
||||||
const error = await res.json();
|
const error = (await res.json()) as { message?: string; };
|
||||||
setMessage(
|
setMessage(
|
||||||
`Failed to invite user: ${error.message || "Unknown error"}`
|
`Failed to invite user: ${error.message || "Unknown error"}`
|
||||||
);
|
);
|
||||||
@ -165,13 +186,22 @@ export default function UserManagementPage() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
{users.length === 0 ? (
|
{loading ? (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={3}
|
||||||
|
className="px-6 py-4 text-center text-sm text-gray-500"
|
||||||
|
>
|
||||||
|
Loading users...
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : users.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
colSpan={3}
|
colSpan={3}
|
||||||
className="px-6 py-4 text-center text-sm text-gray-500"
|
className="px-6 py-4 text-center text-sm text-gray-500"
|
||||||
>
|
>
|
||||||
No users found
|
{message || "No users found"}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import { getServerSession } from "next-auth";
|
import { auth } from "../auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { authOptions } from "../pages/api/auth/[...nextauth]";
|
|
||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await auth();
|
||||||
if (session?.user) redirect("/dashboard");
|
if (session?.user) redirect("/dashboard");
|
||||||
else redirect("/login");
|
else redirect("/login");
|
||||||
}
|
}
|
||||||
|
|||||||
117
auth.ts
Normal file
117
auth.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import NextAuth, { NextAuthConfig } from "next-auth";
|
||||||
|
import { D1Adapter } from "@auth/d1-adapter";
|
||||||
|
import Credentials from "next-auth/providers/credentials";
|
||||||
|
import bcrypt from "bcryptjs";
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import { PrismaD1 } from "@prisma/adapter-d1";
|
||||||
|
|
||||||
|
// Check if we're in a Cloudflare Workers environment
|
||||||
|
const isCloudflareWorker =
|
||||||
|
typeof globalThis.caches !== "undefined" &&
|
||||||
|
typeof (globalThis as any).WebSocketPair !== "undefined";
|
||||||
|
|
||||||
|
// For local development, we'll use the same D1 database that wrangler creates
|
||||||
|
const isDevelopment = process.env.NODE_ENV === "development";
|
||||||
|
|
||||||
|
const config: NextAuthConfig = {
|
||||||
|
providers: [
|
||||||
|
Credentials({
|
||||||
|
name: "credentials",
|
||||||
|
credentials: {
|
||||||
|
email: { label: "Email", type: "email" },
|
||||||
|
password: { label: "Password", type: "password" },
|
||||||
|
},
|
||||||
|
authorize: async (credentials) => {
|
||||||
|
if (!credentials?.email || !credentials?.password) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let prisma: PrismaClient;
|
||||||
|
|
||||||
|
// Initialize Prisma based on environment
|
||||||
|
if (isCloudflareWorker) {
|
||||||
|
// In Cloudflare Workers (production), get DB from bindings
|
||||||
|
const adapter = new PrismaD1((globalThis as any).DB);
|
||||||
|
prisma = new PrismaClient({ adapter });
|
||||||
|
} else {
|
||||||
|
// In local development (Next.js), use the local D1 database
|
||||||
|
// This uses the same database that wrangler creates locally
|
||||||
|
prisma = new PrismaClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email: credentials.email as string },
|
||||||
|
include: { company: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid = await bcrypt.compare(
|
||||||
|
credentials.password as string,
|
||||||
|
user.password
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.email, // Use email as name
|
||||||
|
role: user.role,
|
||||||
|
companyId: user.companyId,
|
||||||
|
company: user.company.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
await prisma.$disconnect();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Authentication error:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
callbacks: {
|
||||||
|
jwt: async ({ token, user }: any) => {
|
||||||
|
if (user) {
|
||||||
|
token.role = user.role;
|
||||||
|
token.companyId = user.companyId;
|
||||||
|
token.company = user.company;
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
},
|
||||||
|
session: async ({ session, token }: any) => {
|
||||||
|
if (token && session.user) {
|
||||||
|
session.user.id = token.sub;
|
||||||
|
session.user.role = token.role;
|
||||||
|
session.user.companyId = token.companyId;
|
||||||
|
session.user.company = token.company;
|
||||||
|
}
|
||||||
|
return session;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
signIn: "/login",
|
||||||
|
error: "/login",
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
strategy: "jwt",
|
||||||
|
maxAge: 30 * 24 * 60 * 60, // 30 days
|
||||||
|
},
|
||||||
|
secret: process.env.AUTH_SECRET,
|
||||||
|
trustHost: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add D1 adapter only in Cloudflare Workers environment
|
||||||
|
if (isCloudflareWorker && (globalThis as any).DB) {
|
||||||
|
(config as any).adapter = D1Adapter((globalThis as any).DB);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { auth, signIn, signOut } = NextAuth(config);
|
||||||
5765
cloudflare-env.d.ts
vendored
Normal file
5765
cloudflare-env.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@ -146,7 +146,8 @@ export default function SessionDetails({ session }: SessionDetailsProps) {
|
|||||||
{/* Fallback to link only if we only have the URL but no content - this might also be redundant if parent handles all transcript display */}
|
{/* Fallback to link only if we only have the URL but no content - this might also be redundant if parent handles all transcript display */}
|
||||||
{(!session.transcriptContent ||
|
{(!session.transcriptContent ||
|
||||||
session.transcriptContent.length === 0) &&
|
session.transcriptContent.length === 0) &&
|
||||||
session.fullTranscriptUrl && (
|
session.fullTranscriptUrl &&
|
||||||
|
process.env.NODE_ENV !== "production" && (
|
||||||
<div className="flex justify-between pt-2">
|
<div className="flex justify-between pt-2">
|
||||||
<span className="text-gray-600">Transcript:</span>
|
<span className="text-gray-600">Transcript:</span>
|
||||||
<a
|
<a
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import rehypeRaw from "rehype-raw"; // Import rehype-raw
|
import rehypeRaw from "rehype-raw";
|
||||||
|
|
||||||
interface TranscriptViewerProps {
|
interface TranscriptViewerProps {
|
||||||
transcriptContent: string;
|
transcriptContent: string;
|
||||||
@ -23,6 +23,7 @@ function formatTranscript(content: string): React.ReactNode[] {
|
|||||||
const elements: React.ReactNode[] = [];
|
const elements: React.ReactNode[] = [];
|
||||||
let currentSpeaker: string | null = null;
|
let currentSpeaker: string | null = null;
|
||||||
let currentMessages: string[] = [];
|
let currentMessages: string[] = [];
|
||||||
|
let currentTimestamp: string | null = null;
|
||||||
|
|
||||||
// Process each line
|
// Process each line
|
||||||
lines.forEach((line) => {
|
lines.forEach((line) => {
|
||||||
@ -32,8 +33,15 @@ function formatTranscript(content: string): React.ReactNode[] {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a new speaker line
|
// Check if this is a new speaker line with or without datetime
|
||||||
if (line.startsWith("User:") || line.startsWith("Assistant:")) {
|
// Format 1: [29.05.2025 21:26:44] User: message
|
||||||
|
// Format 2: User: message
|
||||||
|
const datetimeMatch = line.match(
|
||||||
|
/^\[([^\]]+)\]\s*(User|Assistant):\s*(.*)$/
|
||||||
|
);
|
||||||
|
const simpleMatch = line.match(/^(User|Assistant):\s*(.*)$/);
|
||||||
|
|
||||||
|
if (datetimeMatch || simpleMatch) {
|
||||||
// If we have accumulated messages for a previous speaker, add them
|
// If we have accumulated messages for a previous speaker, add them
|
||||||
if (currentSpeaker && currentMessages.length > 0) {
|
if (currentSpeaker && currentMessages.length > 0) {
|
||||||
elements.push(
|
elements.push(
|
||||||
@ -48,6 +56,11 @@ function formatTranscript(content: string): React.ReactNode[] {
|
|||||||
: "bg-gray-100 text-gray-800"
|
: "bg-gray-100 text-gray-800"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
{currentTimestamp && (
|
||||||
|
<div className="text-xs opacity-60 mb-1">
|
||||||
|
{currentTimestamp}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{currentMessages.map((msg, i) => (
|
{currentMessages.map((msg, i) => (
|
||||||
// Use ReactMarkdown to render each message part
|
// Use ReactMarkdown to render each message part
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
@ -73,12 +86,22 @@ function formatTranscript(content: string): React.ReactNode[] {
|
|||||||
currentMessages = [];
|
currentMessages = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the new current speaker
|
if (datetimeMatch) {
|
||||||
currentSpeaker = line.startsWith("User:") ? "User" : "Assistant";
|
// Format with datetime: [29.05.2025 21:26:44] User: message
|
||||||
// Add the content after "User:" or "Assistant:"
|
currentTimestamp = datetimeMatch[1];
|
||||||
const messageContent = line.substring(line.indexOf(":") + 1).trim();
|
currentSpeaker = datetimeMatch[2];
|
||||||
if (messageContent) {
|
const messageContent = datetimeMatch[3].trim();
|
||||||
currentMessages.push(messageContent);
|
if (messageContent) {
|
||||||
|
currentMessages.push(messageContent);
|
||||||
|
}
|
||||||
|
} else if (simpleMatch) {
|
||||||
|
// Format without datetime: User: message
|
||||||
|
currentTimestamp = null;
|
||||||
|
currentSpeaker = simpleMatch[1];
|
||||||
|
const messageContent = simpleMatch[2].trim();
|
||||||
|
if (messageContent) {
|
||||||
|
currentMessages.push(messageContent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (currentSpeaker) {
|
} else if (currentSpeaker) {
|
||||||
// This is a continuation of the current speaker's message
|
// This is a continuation of the current speaker's message
|
||||||
@ -100,6 +123,9 @@ function formatTranscript(content: string): React.ReactNode[] {
|
|||||||
: "bg-gray-100 text-gray-800"
|
: "bg-gray-100 text-gray-800"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
{currentTimestamp && (
|
||||||
|
<div className="text-xs opacity-60 mb-1">{currentTimestamp}</div>
|
||||||
|
)}
|
||||||
{currentMessages.map((msg, i) => (
|
{currentMessages.map((msg, i) => (
|
||||||
// Use ReactMarkdown to render each message part
|
// Use ReactMarkdown to render each message part
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
@ -138,6 +164,9 @@ export default function TranscriptViewer({
|
|||||||
|
|
||||||
const formattedElements = formatTranscript(transcriptContent);
|
const formattedElements = formatTranscript(transcriptContent);
|
||||||
|
|
||||||
|
// Hide "View Full Raw" button in production environment
|
||||||
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white shadow-lg rounded-lg p-4 md:p-6 mt-6">
|
<div className="bg-white shadow-lg rounded-lg p-4 md:p-6 mt-6">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
@ -145,7 +174,7 @@ export default function TranscriptViewer({
|
|||||||
Session Transcript
|
Session Transcript
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
{transcriptUrl && (
|
{transcriptUrl && !isProduction && (
|
||||||
<a
|
<a
|
||||||
href={transcriptUrl}
|
href={transcriptUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
227
docs/D1_CLI_ACCESS.md
Normal file
227
docs/D1_CLI_ACCESS.md
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
# D1 Database Command Line Access
|
||||||
|
|
||||||
|
This guide shows you how to access and manage your Cloudflare D1 database `d1-notso-livedash` from the command line.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Using the Custom D1 CLI Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Simple and fast commands
|
||||||
|
pnpm d1 tables # List all tables
|
||||||
|
pnpm d1 info # Database information
|
||||||
|
pnpm d1 schema User # Show table schema
|
||||||
|
pnpm d1 query "SELECT COUNT(*) FROM User" # Execute SQL
|
||||||
|
pnpm d1 export backup.sql # Export database
|
||||||
|
|
||||||
|
# Remote (production) commands
|
||||||
|
pnpm d1 --remote info # Production database info
|
||||||
|
pnpm d1 --remote query "SELECT * FROM Company LIMIT 5"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Package.json Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database information
|
||||||
|
pnpm d1:list # List all D1 databases
|
||||||
|
pnpm d1:info # Local database info
|
||||||
|
pnpm d1:info:remote # Remote database info
|
||||||
|
|
||||||
|
# Backup and export
|
||||||
|
pnpm d1:export # Export local database
|
||||||
|
pnpm d1:export:remote # Export remote database
|
||||||
|
pnpm d1:schema # Export schema only
|
||||||
|
```
|
||||||
|
|
||||||
|
### Direct Wrangler Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic operations
|
||||||
|
npx wrangler d1 list
|
||||||
|
npx wrangler d1 info d1-notso-livedash
|
||||||
|
npx wrangler d1 execute d1-notso-livedash --command "SELECT * FROM User"
|
||||||
|
|
||||||
|
# Remote operations (add --remote flag)
|
||||||
|
npx wrangler d1 info d1-notso-livedash --remote
|
||||||
|
npx wrangler d1 execute d1-notso-livedash --remote --command "SELECT COUNT(*) FROM Company"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
Your D1 database contains these tables:
|
||||||
|
|
||||||
|
### Company Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
- id (TEXT, PRIMARY KEY)
|
||||||
|
- name (TEXT, NOT NULL)
|
||||||
|
- csvUrl (TEXT, NOT NULL)
|
||||||
|
- csvUsername (TEXT)
|
||||||
|
- csvPassword (TEXT)
|
||||||
|
- sentimentAlert (REAL)
|
||||||
|
- dashboardOpts (TEXT)
|
||||||
|
- createdAt (DATETIME, NOT NULL, DEFAULT CURRENT_TIMESTAMP)
|
||||||
|
- updatedAt (DATETIME, NOT NULL)
|
||||||
|
```
|
||||||
|
|
||||||
|
### User Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
- id (TEXT, PRIMARY KEY)
|
||||||
|
- email (TEXT, NOT NULL)
|
||||||
|
- password (TEXT, NOT NULL)
|
||||||
|
- companyId (TEXT, NOT NULL)
|
||||||
|
- role (TEXT, NOT NULL)
|
||||||
|
- resetToken (TEXT)
|
||||||
|
- resetTokenExpiry (DATETIME)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Session Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
- id (TEXT, PRIMARY KEY)
|
||||||
|
- userId (TEXT, NOT NULL)
|
||||||
|
- expiresAt (DATETIME, NOT NULL)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common SQL Queries
|
||||||
|
|
||||||
|
### Data Exploration
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Check table sizes
|
||||||
|
SELECT 'Company' as table_name, COUNT(*) as count FROM Company
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'User' as table_name, COUNT(*) as count FROM User
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'Session' as table_name, COUNT(*) as count FROM Session;
|
||||||
|
|
||||||
|
-- Show all table names
|
||||||
|
SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;
|
||||||
|
|
||||||
|
-- Get table schema
|
||||||
|
PRAGMA table_info(User);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Business Queries
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- List companies with user counts
|
||||||
|
SELECT c.name, c.id, COUNT(u.id) as user_count
|
||||||
|
FROM Company c
|
||||||
|
LEFT JOIN User u ON c.id = u.companyId
|
||||||
|
GROUP BY c.id, c.name;
|
||||||
|
|
||||||
|
-- Find admin users
|
||||||
|
SELECT u.email, c.name as company
|
||||||
|
FROM User u
|
||||||
|
JOIN Company c ON u.companyId = c.id
|
||||||
|
WHERE u.role = 'admin';
|
||||||
|
|
||||||
|
-- Active sessions
|
||||||
|
SELECT COUNT(*) as active_sessions
|
||||||
|
FROM Session
|
||||||
|
WHERE expiresAt > datetime('now');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local vs Remote Databases
|
||||||
|
|
||||||
|
- **Local Database**: Located at `.wrangler/state/v3/d1/` (for development)
|
||||||
|
- **Remote Database**: Cloudflare's production D1 database
|
||||||
|
|
||||||
|
### When to Use Each:
|
||||||
|
|
||||||
|
- **Local**: Development, testing, safe experimentation
|
||||||
|
- **Remote**: Production data, deployment verification
|
||||||
|
|
||||||
|
## Database Statistics
|
||||||
|
|
||||||
|
Current database info:
|
||||||
|
|
||||||
|
- **Database ID**: d4ee7efe-d37a-48e4-bed7-fdfaa5108131
|
||||||
|
- **Region**: WEUR (Western Europe)
|
||||||
|
- **Size**: ~53.2 kB
|
||||||
|
- **Tables**: 6 (including system tables)
|
||||||
|
- **Read Queries (24h)**: 65
|
||||||
|
- **Write Queries (24h)**: 8
|
||||||
|
|
||||||
|
## Scripts Available
|
||||||
|
|
||||||
|
### `/scripts/d1.js` (Recommended)
|
||||||
|
|
||||||
|
Simple, fast CLI for common operations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/d1.js tables
|
||||||
|
node scripts/d1.js schema User
|
||||||
|
node scripts/d1.js query "SELECT * FROM Company"
|
||||||
|
node scripts/d1.js --remote info
|
||||||
|
```
|
||||||
|
|
||||||
|
### `/scripts/d1-query.js`
|
||||||
|
|
||||||
|
Simple query executor:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/d1-query.js "SELECT COUNT(*) FROM User"
|
||||||
|
node scripts/d1-query.js --remote "SELECT * FROM Company"
|
||||||
|
```
|
||||||
|
|
||||||
|
### `/scripts/d1-manager.js`
|
||||||
|
|
||||||
|
Comprehensive database management (if needed for advanced operations):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/d1-manager.js info
|
||||||
|
node scripts/d1-manager.js backup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backup and Recovery
|
||||||
|
|
||||||
|
### Create Backups
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Quick backup
|
||||||
|
pnpm d1 export backup_$(date +%Y%m%d).sql
|
||||||
|
|
||||||
|
# Automated backup with timestamp
|
||||||
|
npx wrangler d1 export d1-notso-livedash --output backups/backup_$(date +%Y%m%d_%H%M%S).sql
|
||||||
|
|
||||||
|
# Schema only backup
|
||||||
|
npx wrangler d1 export d1-notso-livedash --no-data --output schema.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore from Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Apply SQL file to database
|
||||||
|
npx wrangler d1 execute d1-notso-livedash --file backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **"wrangler not found"**: Use `npx wrangler` instead of `wrangler`
|
||||||
|
2. **Permission denied**: Ensure you're logged into Cloudflare: `npx wrangler login`
|
||||||
|
3. **Database not found**: Check `wrangler.json` for correct binding name
|
||||||
|
|
||||||
|
### Debug Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Wrangler authentication
|
||||||
|
npx wrangler whoami
|
||||||
|
|
||||||
|
# Verify database configuration
|
||||||
|
npx wrangler d1 list
|
||||||
|
|
||||||
|
# Test database connectivity
|
||||||
|
npx wrangler d1 execute d1-notso-livedash --command "SELECT 1"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- Local database is for development only
|
||||||
|
- Never expose production database credentials
|
||||||
|
- Use `--remote` flag carefully in production
|
||||||
|
- Regular backups are recommended for production data
|
||||||
@ -1,40 +0,0 @@
|
|||||||
import js from "@eslint/js";
|
|
||||||
import { FlatCompat } from "@eslint/eslintrc";
|
|
||||||
import path from "path";
|
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
const compat = new FlatCompat({
|
|
||||||
baseDirectory: __dirname,
|
|
||||||
});
|
|
||||||
|
|
||||||
const eslintConfig = [
|
|
||||||
js.configs.recommended,
|
|
||||||
...compat.extends(
|
|
||||||
"next/core-web-vitals",
|
|
||||||
"plugin:@typescript-eslint/recommended"
|
|
||||||
),
|
|
||||||
{
|
|
||||||
ignores: [
|
|
||||||
"node_modules/",
|
|
||||||
".next/",
|
|
||||||
".vscode/",
|
|
||||||
"out/",
|
|
||||||
"build/",
|
|
||||||
"dist/",
|
|
||||||
"coverage/",
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
|
||||||
"@typescript-eslint/no-unused-vars": "warn",
|
|
||||||
"react/no-unescaped-entities": "warn",
|
|
||||||
"no-console": "off",
|
|
||||||
"no-trailing-spaces": "warn",
|
|
||||||
"prefer-const": "error",
|
|
||||||
"no-unused-vars": "warn",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default eslintConfig;
|
|
||||||
57
eslint.config.mjs
Normal file
57
eslint.config.mjs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { dirname } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { FlatCompat } from "@eslint/eslintrc";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
});
|
||||||
|
|
||||||
|
const eslintConfig = [
|
||||||
|
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||||
|
];
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
|
|
||||||
|
// import js from "@eslint/js";
|
||||||
|
// import { FlatCompat } from "@eslint/eslintrc";
|
||||||
|
// import path from "path";
|
||||||
|
// import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
// const __filename = fileURLToPath(import.meta.url);
|
||||||
|
// const __dirname = path.dirname(__filename);
|
||||||
|
// const compat = new FlatCompat({
|
||||||
|
// baseDirectory: __dirname,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const eslintConfig = [
|
||||||
|
// js.configs.recommended,
|
||||||
|
// ...compat.extends(
|
||||||
|
// "next/core-web-vitals",
|
||||||
|
// "plugin:@typescript-eslint/recommended"
|
||||||
|
// ),
|
||||||
|
// {
|
||||||
|
// ignores: [
|
||||||
|
// "node_modules/",
|
||||||
|
// ".next/",
|
||||||
|
// ".vscode/",
|
||||||
|
// "out/",
|
||||||
|
// "build/",
|
||||||
|
// "dist/",
|
||||||
|
// "coverage/",
|
||||||
|
// ],
|
||||||
|
// rules: {
|
||||||
|
// "@typescript-eslint/no-explicit-any": "warn",
|
||||||
|
// "@typescript-eslint/no-unused-vars": "warn",
|
||||||
|
// "react/no-unescaped-entities": "warn",
|
||||||
|
// "no-console": "off",
|
||||||
|
// "no-trailing-spaces": "warn",
|
||||||
|
// "prefer-const": "error",
|
||||||
|
// "no-unused-vars": "warn",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// export default eslintConfig;
|
||||||
88
lib/api-auth.ts
Normal file
88
lib/api-auth.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export interface ApiSession {
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
companyId: string;
|
||||||
|
company: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getApiSession(req: NextApiRequest, res: NextApiResponse): Promise<ApiSession | null> {
|
||||||
|
try {
|
||||||
|
// Get session by making internal request to session endpoint
|
||||||
|
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
|
||||||
|
const host = req.headers.host || 'localhost:3000';
|
||||||
|
const sessionUrl = `${protocol}://${host}/api/auth/session`;
|
||||||
|
|
||||||
|
// Forward all relevant headers including cookies
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
if (req.headers.cookie) {
|
||||||
|
headers['Cookie'] = Array.isArray(req.headers.cookie) ? req.headers.cookie.join('; ') : req.headers.cookie;
|
||||||
|
}
|
||||||
|
if (req.headers['user-agent']) {
|
||||||
|
headers['User-Agent'] = Array.isArray(req.headers['user-agent']) ? req.headers['user-agent'][0] : req.headers['user-agent'];
|
||||||
|
}
|
||||||
|
if (req.headers['x-forwarded-for']) {
|
||||||
|
headers['X-Forwarded-For'] = Array.isArray(req.headers['x-forwarded-for']) ? req.headers['x-forwarded-for'][0] : req.headers['x-forwarded-for'];
|
||||||
|
}
|
||||||
|
if (req.headers['x-forwarded-proto']) {
|
||||||
|
headers['X-Forwarded-Proto'] = Array.isArray(req.headers['x-forwarded-proto']) ? req.headers['x-forwarded-proto'][0] : req.headers['x-forwarded-proto'];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Requesting session from:', sessionUrl);
|
||||||
|
console.log('With headers:', Object.keys(headers));
|
||||||
|
|
||||||
|
const sessionResponse = await fetch(sessionUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
// Use agent to handle localhost properly
|
||||||
|
...(host.includes('localhost') && {
|
||||||
|
// No special agent needed for localhost in Node.js
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sessionResponse.ok) {
|
||||||
|
console.log('Session response not ok:', sessionResponse.status, sessionResponse.statusText);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionData: any = await sessionResponse.json();
|
||||||
|
console.log('Session data received:', sessionData);
|
||||||
|
|
||||||
|
if (!sessionData?.user?.email) {
|
||||||
|
console.log('No user email in session data');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user data from database
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email: sessionData.user.email },
|
||||||
|
include: { company: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
console.log('User not found in database:', sessionData.user.email);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Successfully got user:', user.email);
|
||||||
|
return {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role,
|
||||||
|
companyId: user.companyId,
|
||||||
|
company: user.company.name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting API session:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
88
lib/auth-options.ts
Normal file
88
lib/auth-options.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Auth.js v5 compatibility layer for Pages Router API routes
|
||||||
|
* This provides the authOptions object for backward compatibility
|
||||||
|
* with getServerSession from next-auth/next
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextAuthOptions } from "next-auth";
|
||||||
|
import Credentials from "next-auth/providers/credentials";
|
||||||
|
import bcrypt from "bcryptjs";
|
||||||
|
import { prisma } from "../../../lib/prisma";
|
||||||
|
|
||||||
|
export const authOptions: NextAuthOptions = {
|
||||||
|
providers: [
|
||||||
|
Credentials({
|
||||||
|
name: "credentials",
|
||||||
|
credentials: {
|
||||||
|
email: { label: "Email", type: "email" },
|
||||||
|
password: { label: "Password", type: "password" },
|
||||||
|
},
|
||||||
|
authorize: async (credentials) => {
|
||||||
|
if (!credentials?.email || !credentials?.password) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email: credentials.email as string },
|
||||||
|
include: { company: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPasswordValid = await bcrypt.compare(
|
||||||
|
credentials.password as string,
|
||||||
|
user.password
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.email,
|
||||||
|
role: user.role,
|
||||||
|
companyId: user.companyId,
|
||||||
|
company: user.company.name,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Authentication error:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
callbacks: {
|
||||||
|
jwt: async ({ token, user }: any) => {
|
||||||
|
if (user) {
|
||||||
|
token.role = user.role;
|
||||||
|
token.companyId = user.companyId;
|
||||||
|
token.company = user.company;
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
},
|
||||||
|
session: async ({ session, token }: any) => {
|
||||||
|
if (token && session.user) {
|
||||||
|
session.user.id = token.sub;
|
||||||
|
session.user.role = token.role;
|
||||||
|
session.user.companyId = token.companyId;
|
||||||
|
session.user.company = token.company;
|
||||||
|
}
|
||||||
|
return session;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
signIn: "/login",
|
||||||
|
error: "/login",
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
strategy: "jwt",
|
||||||
|
maxAge: 30 * 24 * 60 * 60, // 30 days
|
||||||
|
},
|
||||||
|
secret: process.env.AUTH_SECRET,
|
||||||
|
trustHost: true,
|
||||||
|
};
|
||||||
32
lib/fetchTranscript.ts
Normal file
32
lib/fetchTranscript.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Fetches transcript content from a URL with optional authentication
|
||||||
|
* @param url The URL to fetch the transcript from
|
||||||
|
* @param username Optional username for Basic Auth
|
||||||
|
* @param password Optional password for Basic Auth
|
||||||
|
* @returns The transcript content or null if fetching fails
|
||||||
|
*/
|
||||||
|
export async function fetchTranscriptContent(
|
||||||
|
url: string,
|
||||||
|
username?: string,
|
||||||
|
password?: string
|
||||||
|
): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const authHeader =
|
||||||
|
username && password
|
||||||
|
? "Basic " + Buffer.from(`${username}:${password}`).toString("base64")
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: authHeader ? { Authorization: authHeader } : {},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
process.stderr.write(`Error fetching transcript from ${url}: ${response.statusText}\n`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await response.text();
|
||||||
|
} catch (error) {
|
||||||
|
process.stderr.write(`Failed to fetch transcript from ${url}: ${error}\n`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -512,7 +512,7 @@ export function sessionMetrics(
|
|||||||
const uniqueUsers = uniqueUserIds.size;
|
const uniqueUsers = uniqueUserIds.size;
|
||||||
const avgSessionLength =
|
const avgSessionLength =
|
||||||
validSessionsForDuration > 0
|
validSessionsForDuration > 0
|
||||||
? totalSessionDuration / validSessionsForDuration / 1000 // Convert ms to minutes
|
? totalSessionDuration / validSessionsForDuration / 1000 // Convert ms to seconds
|
||||||
: 0;
|
: 0;
|
||||||
const avgResponseTime =
|
const avgResponseTime =
|
||||||
validSessionsForResponseTime > 0
|
validSessionsForResponseTime > 0
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
// Simple Prisma client setup
|
// Prisma client setup with support for Cloudflare D1
|
||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import { PrismaD1 } from "@prisma/adapter-d1";
|
||||||
|
|
||||||
// Add prisma to the NodeJS global type
|
// Add prisma to the NodeJS global type
|
||||||
// This approach avoids NodeJS.Global which is not available
|
// This approach avoids NodeJS.Global which is not available
|
||||||
@ -9,12 +10,24 @@ declare const global: {
|
|||||||
prisma: PrismaClient | undefined;
|
prisma: PrismaClient | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize Prisma Client
|
// Check if we're running in Cloudflare Workers environment
|
||||||
const prisma = global.prisma || new PrismaClient();
|
const isCloudflareWorker = typeof globalThis.DB !== "undefined";
|
||||||
|
|
||||||
// Save in global if we're in development
|
// Initialize Prisma Client
|
||||||
if (process.env.NODE_ENV !== "production") {
|
let prisma: PrismaClient;
|
||||||
global.prisma = prisma;
|
|
||||||
|
if (isCloudflareWorker) {
|
||||||
|
// In Cloudflare Workers, use D1 adapter
|
||||||
|
const adapter = new PrismaD1(globalThis.DB);
|
||||||
|
prisma = new PrismaClient({ adapter });
|
||||||
|
} else {
|
||||||
|
// In Next.js/Node.js, use regular SQLite
|
||||||
|
prisma = global.prisma || new PrismaClient();
|
||||||
|
|
||||||
|
// Save in global if we're in development
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
global.prisma = prisma;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { prisma };
|
export { prisma };
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import cron from "node-cron";
|
import cron from "node-cron";
|
||||||
import { prisma } from "./prisma";
|
import { prisma } from "./prisma";
|
||||||
import { fetchAndParseCsv } from "./csvFetcher";
|
import { fetchAndParseCsv } from "./csvFetcher";
|
||||||
|
import { fetchTranscriptContent } from "./fetchTranscript";
|
||||||
|
|
||||||
interface SessionCreateData {
|
interface SessionCreateData {
|
||||||
id: string;
|
id: string;
|
||||||
@ -23,6 +24,16 @@ export function startScheduler() {
|
|||||||
await prisma.session.deleteMany({ where: { companyId: company.id } });
|
await prisma.session.deleteMany({ where: { companyId: company.id } });
|
||||||
|
|
||||||
for (const session of sessions) {
|
for (const session of sessions) {
|
||||||
|
// Fetch transcript content if URL is available
|
||||||
|
let transcriptContent: string | null = null;
|
||||||
|
if (session.fullTranscriptUrl) {
|
||||||
|
transcriptContent = await fetchTranscriptContent(
|
||||||
|
session.fullTranscriptUrl,
|
||||||
|
company.csvUsername as string | undefined,
|
||||||
|
company.csvPassword as string | undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const sessionData: SessionCreateData = {
|
const sessionData: SessionCreateData = {
|
||||||
...session,
|
...session,
|
||||||
companyId: company.id,
|
companyId: company.id,
|
||||||
@ -51,6 +62,8 @@ export function startScheduler() {
|
|||||||
? session.messagesSent
|
? session.messagesSent
|
||||||
: 0,
|
: 0,
|
||||||
category: session.category || null,
|
category: session.category || null,
|
||||||
|
fullTranscriptUrl: session.fullTranscriptUrl || null,
|
||||||
|
transcriptContent: transcriptContent, // Add the transcript content
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
12
lib/types.ts
12
lib/types.ts
@ -1,15 +1,7 @@
|
|||||||
import { Session as NextAuthSession } from "next-auth";
|
import { Session as NextAuthSession } from "next-auth";
|
||||||
|
|
||||||
export interface UserSession extends NextAuthSession {
|
// Use the NextAuth Session directly as it now includes our extended types
|
||||||
user: {
|
export type UserSession = NextAuthSession;
|
||||||
id?: string;
|
|
||||||
name?: string;
|
|
||||||
email?: string;
|
|
||||||
image?: string;
|
|
||||||
companyId: string;
|
|
||||||
role: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Company {
|
export interface Company {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
54
migrations/0001_initial_schema.sql
Normal file
54
migrations/0001_initial_schema.sql
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
-- Initial database schema for LiveDash-Node
|
||||||
|
-- This combines the init migration and transcript_content addition
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Company" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"csvUrl" TEXT NOT NULL,
|
||||||
|
"csvUsername" TEXT,
|
||||||
|
"csvPassword" TEXT,
|
||||||
|
"sentimentAlert" REAL,
|
||||||
|
"dashboardOpts" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "User" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"password" TEXT NOT NULL,
|
||||||
|
"companyId" TEXT NOT NULL,
|
||||||
|
"role" TEXT NOT NULL,
|
||||||
|
"resetToken" TEXT,
|
||||||
|
"resetTokenExpiry" DATETIME,
|
||||||
|
CONSTRAINT "User_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Session" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"companyId" TEXT NOT NULL,
|
||||||
|
"startTime" DATETIME NOT NULL,
|
||||||
|
"endTime" DATETIME NOT NULL,
|
||||||
|
"ipAddress" TEXT,
|
||||||
|
"country" TEXT,
|
||||||
|
"language" TEXT,
|
||||||
|
"messagesSent" INTEGER,
|
||||||
|
"sentiment" REAL,
|
||||||
|
"escalated" BOOLEAN,
|
||||||
|
"forwardedHr" BOOLEAN,
|
||||||
|
"fullTranscriptUrl" TEXT,
|
||||||
|
"transcriptContent" TEXT,
|
||||||
|
"avgResponseTime" REAL,
|
||||||
|
"tokens" INTEGER,
|
||||||
|
"tokensEur" REAL,
|
||||||
|
"category" TEXT,
|
||||||
|
"initialMsg" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "Session_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||||
61
migrations/0002_create_auth_tables.sql
Normal file
61
migrations/0002_create_auth_tables.sql
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
-- Migration: Create Auth.js v5 tables for D1 adapter
|
||||||
|
-- Auth.js v5 requires these specific table names and schemas
|
||||||
|
-- Users table for Auth.js
|
||||||
|
-- Note: This is separate from our existing User table
|
||||||
|
CREATE TABLE
|
||||||
|
IF NOT EXISTS users (
|
||||||
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
name TEXT,
|
||||||
|
email TEXT UNIQUE,
|
||||||
|
email_verified INTEGER,
|
||||||
|
image TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Accounts table for OAuth providers
|
||||||
|
CREATE TABLE
|
||||||
|
IF NOT EXISTS accounts (
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
provider TEXT NOT NULL,
|
||||||
|
provider_account_id TEXT NOT NULL,
|
||||||
|
refresh_token TEXT,
|
||||||
|
access_token TEXT,
|
||||||
|
expires_at INTEGER,
|
||||||
|
token_type TEXT,
|
||||||
|
scope TEXT,
|
||||||
|
id_token TEXT,
|
||||||
|
session_state TEXT,
|
||||||
|
PRIMARY KEY (provider, provider_account_id),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Sessions table for session management
|
||||||
|
CREATE TABLE
|
||||||
|
IF NOT EXISTS sessions (
|
||||||
|
session_token TEXT PRIMARY KEY NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
expires INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Verification tokens for email verification and magic links
|
||||||
|
CREATE TABLE
|
||||||
|
IF NOT EXISTS verification_tokens (
|
||||||
|
identifier TEXT NOT NULL,
|
||||||
|
token TEXT NOT NULL,
|
||||||
|
expires INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (identifier, token)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes for better performance
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_email ON users (email);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_accounts_user_id ON accounts (user_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions (user_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions (expires);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_verification_tokens_identifier ON verification_tokens (identifier);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_verification_tokens_token ON verification_tokens (token);
|
||||||
@ -1,15 +0,0 @@
|
|||||||
/**
|
|
||||||
* @type {import('next').NextConfig}
|
|
||||||
**/
|
|
||||||
const nextConfig = {
|
|
||||||
reactStrictMode: true,
|
|
||||||
// Allow cross-origin requests from specific origins in development
|
|
||||||
allowedDevOrigins: [
|
|
||||||
"192.168.1.2",
|
|
||||||
"localhost",
|
|
||||||
"propc",
|
|
||||||
"test123.kjanat.com",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default nextConfig;
|
|
||||||
46
next.config.ts
Normal file
46
next.config.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
|
|
||||||
|
// added by create cloudflare to enable calling `getCloudflareContext()` in `next dev`
|
||||||
|
import { initOpenNextCloudflareForDev } from '@opennextjs/cloudflare';
|
||||||
|
initOpenNextCloudflareForDev();
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * @type {import('next').NextConfig}
|
||||||
|
// **/
|
||||||
|
// const nextConfig = {
|
||||||
|
// reactStrictMode: true,
|
||||||
|
|
||||||
|
// // Allow cross-origin requests from specific origins in development
|
||||||
|
// allowedDevOrigins: [
|
||||||
|
// "192.168.1.2",
|
||||||
|
// "localhost",
|
||||||
|
// "propc",
|
||||||
|
// "test123.kjanat.com",
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// // Cloudflare Pages optimization
|
||||||
|
// trailingSlash: false,
|
||||||
|
|
||||||
|
// // Environment variables that should be available to the client
|
||||||
|
// env: {
|
||||||
|
// AUTH_URL: process.env.AUTH_URL,
|
||||||
|
// },
|
||||||
|
|
||||||
|
// // Experimental features for Cloudflare compatibility
|
||||||
|
// experimental: {
|
||||||
|
// // Future experimental features can be added here
|
||||||
|
// },
|
||||||
|
|
||||||
|
// // Image optimization - Cloudflare has its own image optimization
|
||||||
|
// images: {
|
||||||
|
// unoptimized: true,
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
|
||||||
|
// export default nextConfig;
|
||||||
9
open-next.config.ts
Normal file
9
open-next.config.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
|
||||||
|
|
||||||
|
export default defineCloudflareConfig({
|
||||||
|
// Uncomment to enable R2 cache,
|
||||||
|
// It should be imported as:
|
||||||
|
// `import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";`
|
||||||
|
// See https://opennext.js.org/cloudflare/caching for more details
|
||||||
|
// incrementalCache: r2IncrementalCache,
|
||||||
|
});
|
||||||
9506
package-lock.json
generated
9506
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
172
package.json
172
package.json
@ -3,72 +3,109 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
|
||||||
"@prisma/client": "^6.8.2",
|
|
||||||
"@rapideditor/country-coder": "^5.4.0",
|
|
||||||
"@types/d3": "^7.4.3",
|
|
||||||
"@types/d3-cloud": "^1.2.9",
|
|
||||||
"@types/geojson": "^7946.0.16",
|
|
||||||
"@types/leaflet": "^1.9.18",
|
|
||||||
"@types/node-fetch": "^2.6.12",
|
|
||||||
"bcryptjs": "^3.0.2",
|
|
||||||
"chart.js": "^4.0.0",
|
|
||||||
"chartjs-plugin-annotation": "^3.1.0",
|
|
||||||
"csv-parse": "^5.5.0",
|
|
||||||
"d3": "^7.9.0",
|
|
||||||
"d3-cloud": "^1.2.7",
|
|
||||||
"i18n-iso-countries": "^7.14.0",
|
|
||||||
"iso-639-1": "^3.1.5",
|
|
||||||
"leaflet": "^1.9.4",
|
|
||||||
"next": "^15.3.2",
|
|
||||||
"next-auth": "^4.24.11",
|
|
||||||
"node-cron": "^4.0.7",
|
|
||||||
"node-fetch": "^3.3.2",
|
|
||||||
"react": "^19.1.0",
|
|
||||||
"react-chartjs-2": "^5.0.0",
|
|
||||||
"react-dom": "^19.1.0",
|
|
||||||
"react-leaflet": "^5.0.0",
|
|
||||||
"react-markdown": "^10.1.0",
|
|
||||||
"rehype-raw": "^7.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
|
||||||
"@eslint/js": "^9.27.0",
|
|
||||||
"@playwright/test": "^1.52.0",
|
|
||||||
"@tailwindcss/postcss": "^4.1.7",
|
|
||||||
"@types/bcryptjs": "^2.4.2",
|
|
||||||
"@types/node": "^22.15.21",
|
|
||||||
"@types/node-cron": "^3.0.8",
|
|
||||||
"@types/react": "^19.1.5",
|
|
||||||
"@types/react-dom": "^19.1.5",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
|
||||||
"@typescript-eslint/parser": "^8.32.1",
|
|
||||||
"eslint": "^9.27.0",
|
|
||||||
"eslint-config-next": "^15.3.2",
|
|
||||||
"eslint-plugin-prettier": "^5.4.0",
|
|
||||||
"markdownlint-cli2": "^0.18.1",
|
|
||||||
"postcss": "^8.5.3",
|
|
||||||
"prettier": "^3.5.3",
|
|
||||||
"prettier-plugin-jinja-template": "^2.1.0",
|
|
||||||
"prisma": "^6.8.2",
|
|
||||||
"tailwindcss": "^4.1.7",
|
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"typescript": "^5.0.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "next build",
|
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"format": "npx prettier --write .",
|
"build": "next build",
|
||||||
"format:check": "npx prettier --check .",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"lint:fix": "npx eslint --fix",
|
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
||||||
|
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
||||||
|
"cf-typegen": "wrangler types --env-interface CloudflareEnv ./cloudflare-env.d.ts",
|
||||||
|
|
||||||
|
"deploy:worker": "pnpm deploy",
|
||||||
|
"deploy:pages": "pnpm build && echo 'Upload the out/ directory to Cloudflare Pages'",
|
||||||
|
|
||||||
|
"format": "pnpm run format:prettier",
|
||||||
|
"format:check": "pnpm dlx prettier --check .",
|
||||||
|
"format:prettier": "pnpm dlx prettier --write .",
|
||||||
|
"format:standard": "pnpm dlx standard . --fix",
|
||||||
|
|
||||||
|
"lint:fix": "pnpm dlx eslint --fix",
|
||||||
|
"lint:md": "markdownlint-cli2 \"**/*.md\" \"!.trunk/**\" \"!.venv/**\" \"!node_modules/**\"",
|
||||||
|
"lint:md:fix": "markdownlint-cli2 --fix \"**/*.md\" \"!.trunk/**\" \"!.venv/**\" \"!node_modules/**\"",
|
||||||
|
|
||||||
"prisma:generate": "prisma generate",
|
"prisma:generate": "prisma generate",
|
||||||
"prisma:migrate": "prisma migrate dev",
|
"prisma:migrate": "prisma migrate dev",
|
||||||
"prisma:seed": "node prisma/seed.mjs",
|
"prisma:seed": "node prisma/seed.mjs",
|
||||||
"prisma:studio": "prisma studio",
|
"prisma:studio": "prisma studio",
|
||||||
"start": "next start",
|
|
||||||
"lint:md": "markdownlint-cli2 \"**/*.md\" \"!.trunk/**\" \"!.venv/**\" \"!node_modules/**\"",
|
"check": "pnpm build && wrangler deploy --dry-run",
|
||||||
"lint:md:fix": "markdownlint-cli2 --fix \"**/*.md\" \"!.trunk/**\" \"!.venv/**\" \"!node_modules/**\""
|
"check:backup": "tsc && wrangler deploy --dry-run",
|
||||||
|
|
||||||
|
"predeploy": "wrangler d1 migrations apply DB --remote",
|
||||||
|
"predeploy:worker": "pnpm predeploy",
|
||||||
|
"seedLocalD1": "wrangler d1 migrations apply DB --local",
|
||||||
|
|
||||||
|
"d1:list": "wrangler d1 list",
|
||||||
|
"d1:info": "wrangler d1 info d1-notso-livedash",
|
||||||
|
"d1:info:remote": "wrangler d1 info d1-notso-livedash --remote",
|
||||||
|
"d1:query": "node scripts/d1-query.js",
|
||||||
|
"d1:export": "wrangler d1 export d1-notso-livedash",
|
||||||
|
"d1:export:remote": "wrangler d1 export d1-notso-livedash --remote",
|
||||||
|
"d1:backup": "wrangler d1 export d1-notso-livedash --output backups/$(date +%Y%m%d_%H%M%S)_backup.sql",
|
||||||
|
"d1:schema": "wrangler d1 export d1-notso-livedash --no-data --output schema.sql",
|
||||||
|
"d1": "node scripts/d1.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@auth/d1-adapter": "^1.9.1",
|
||||||
|
"@opennextjs/cloudflare": "^1.1.0",
|
||||||
|
"@prisma/adapter-d1": "^6.8.2",
|
||||||
|
"@prisma/client": "^6.8.2",
|
||||||
|
"@rapideditor/country-coder": "^5.4.0",
|
||||||
|
"@types/d3": "^7.4.3",
|
||||||
|
"@types/d3-cloud": "^1.2.9",
|
||||||
|
"@types/d3-selection": "^3.0.11",
|
||||||
|
"@types/geojson": "^7946.0.16",
|
||||||
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
|
"@types/leaflet": "^1.9.18",
|
||||||
|
"@types/node-fetch": "^2.6.12",
|
||||||
|
"bcryptjs": "^3.0.2",
|
||||||
|
"chart.js": "^4.4.9",
|
||||||
|
"chartjs-plugin-annotation": "^3.1.0",
|
||||||
|
"csv-parse": "^5.6.0",
|
||||||
|
"d3": "^7.9.0",
|
||||||
|
"d3-cloud": "^1.2.7",
|
||||||
|
"d3-selection": "^3.0.0",
|
||||||
|
"i18n-iso-countries": "^7.14.0",
|
||||||
|
"iso-639-1": "^3.1.5",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
|
"next": "^15.3.3",
|
||||||
|
"next-auth": "5.0.0-beta.28",
|
||||||
|
"node-cron": "^4.1.0",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-chartjs-2": "^5.3.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"react-leaflet": "^5.0.0",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"rehype-raw": "^7.0.0",
|
||||||
|
"@tailwindcss/postcss": "^4.1.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
|
"@eslint/js": "^9.28.0",
|
||||||
|
"@playwright/test": "^1.52.0",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
|
"@types/node": "^22.15.29",
|
||||||
|
"@types/node-cron": "^3.0.11",
|
||||||
|
"@types/react": "^19.1.6",
|
||||||
|
"@types/react-dom": "^19.1.5",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.33.0",
|
||||||
|
"@typescript-eslint/parser": "^8.33.0",
|
||||||
|
"concurrently": "^9.1.2",
|
||||||
|
"eslint": "^9.28.0",
|
||||||
|
"eslint-config-next": "^15.3.3",
|
||||||
|
"eslint-plugin-prettier": "^5.4.1",
|
||||||
|
"markdownlint-cli2": "^0.18.1",
|
||||||
|
"postcss": "^8.5.4",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"prettier-plugin-jinja-template": "^2.1.0",
|
||||||
|
"prisma": "^6.8.2",
|
||||||
|
"tailwindcss": "^4.1.8",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"wrangler": "4.18.0"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
@ -118,5 +155,22 @@
|
|||||||
".git",
|
".git",
|
||||||
"*.json"
|
"*.json"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"cloudflare": {
|
||||||
|
"label": "Worker + D1 Database",
|
||||||
|
"products": [
|
||||||
|
"Workers",
|
||||||
|
"D1"
|
||||||
|
],
|
||||||
|
"categories": [
|
||||||
|
"storage"
|
||||||
|
],
|
||||||
|
"icon_urls": [
|
||||||
|
"https://imagedelivery.net/wSMYJvS3Xw-n339CbDyDIA/c6fc5da3-1e0a-4608-b2f1-9628577ec800/public",
|
||||||
|
"https://imagedelivery.net/wSMYJvS3Xw-n339CbDyDIA/5ca0ca32-e897-4699-d4c1-6b680512f000/public"
|
||||||
|
],
|
||||||
|
"docs_url": "https://developers.cloudflare.com/d1/",
|
||||||
|
"preview_image_url": "https://imagedelivery.net/wSMYJvS3Xw-n339CbDyDIA/cb7cb0a9-6102-4822-633c-b76b7bb25900/public",
|
||||||
|
"publish": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { fetchAndParseCsv } from "../../../lib/csvFetcher";
|
import { fetchAndParseCsv } from "../../../lib/csvFetcher";
|
||||||
import { prisma } from "../../../lib/prisma";
|
import { prisma } from "../../../lib/prisma";
|
||||||
|
import { fetchTranscriptContent } from "../../../lib/fetchTranscript";
|
||||||
|
|
||||||
interface SessionCreateData {
|
interface SessionCreateData {
|
||||||
id: string;
|
id: string;
|
||||||
@ -11,27 +12,6 @@ interface SessionCreateData {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches transcript content from a URL
|
|
||||||
* @param url The URL to fetch the transcript from
|
|
||||||
* @returns The transcript content or null if fetching fails
|
|
||||||
*/
|
|
||||||
async function fetchTranscriptContent(url: string): Promise<string | null> {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) {
|
|
||||||
process.stderr.write(
|
|
||||||
`Error fetching transcript: ${response.statusText}\n`
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return await response.text();
|
|
||||||
} catch (error) {
|
|
||||||
process.stderr.write(`Failed to fetch transcript: ${error}\n`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse
|
res: NextApiResponse
|
||||||
@ -111,7 +91,9 @@ export default async function handler(
|
|||||||
let transcriptContent: string | null = null;
|
let transcriptContent: string | null = null;
|
||||||
if (session.fullTranscriptUrl) {
|
if (session.fullTranscriptUrl) {
|
||||||
transcriptContent = await fetchTranscriptContent(
|
transcriptContent = await fetchTranscriptContent(
|
||||||
session.fullTranscriptUrl
|
session.fullTranscriptUrl,
|
||||||
|
company.csvUsername as string | undefined,
|
||||||
|
company.csvPassword as string | undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,104 +0,0 @@
|
|||||||
import NextAuth, { NextAuthOptions } from "next-auth";
|
|
||||||
import CredentialsProvider from "next-auth/providers/credentials";
|
|
||||||
import { prisma } from "../../../lib/prisma";
|
|
||||||
import bcrypt from "bcryptjs";
|
|
||||||
|
|
||||||
// Define the shape of the JWT token
|
|
||||||
declare module "next-auth/jwt" {
|
|
||||||
interface JWT {
|
|
||||||
companyId: string;
|
|
||||||
role: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define the shape of the session object
|
|
||||||
declare module "next-auth" {
|
|
||||||
interface Session {
|
|
||||||
user: {
|
|
||||||
id?: string;
|
|
||||||
name?: string;
|
|
||||||
email?: string;
|
|
||||||
image?: string;
|
|
||||||
companyId: string;
|
|
||||||
role: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface User {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
companyId: string;
|
|
||||||
role: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const authOptions: NextAuthOptions = {
|
|
||||||
providers: [
|
|
||||||
CredentialsProvider({
|
|
||||||
name: "Credentials",
|
|
||||||
credentials: {
|
|
||||||
email: { label: "Email", type: "text" },
|
|
||||||
password: { label: "Password", type: "password" },
|
|
||||||
},
|
|
||||||
async authorize(credentials) {
|
|
||||||
if (!credentials?.email || !credentials?.password) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: { email: credentials.email },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) return null;
|
|
||||||
|
|
||||||
const valid = await bcrypt.compare(credentials.password, user.password);
|
|
||||||
if (!valid) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: user.id,
|
|
||||||
email: user.email,
|
|
||||||
companyId: user.companyId,
|
|
||||||
role: user.role,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
session: {
|
|
||||||
strategy: "jwt",
|
|
||||||
maxAge: 30 * 24 * 60 * 60, // 30 days
|
|
||||||
},
|
|
||||||
cookies: {
|
|
||||||
sessionToken: {
|
|
||||||
name: `next-auth.session-token`,
|
|
||||||
options: {
|
|
||||||
httpOnly: true,
|
|
||||||
sameSite: "lax",
|
|
||||||
path: "/",
|
|
||||||
secure: process.env.NODE_ENV === "production",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
callbacks: {
|
|
||||||
async jwt({ token, user }) {
|
|
||||||
if (user) {
|
|
||||||
token.companyId = user.companyId;
|
|
||||||
token.role = user.role;
|
|
||||||
}
|
|
||||||
return token;
|
|
||||||
},
|
|
||||||
async session({ session, token }) {
|
|
||||||
if (token && session.user) {
|
|
||||||
session.user.companyId = token.companyId;
|
|
||||||
session.user.role = token.role;
|
|
||||||
}
|
|
||||||
return session;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pages: {
|
|
||||||
signIn: "/login",
|
|
||||||
},
|
|
||||||
secret: process.env.NEXTAUTH_SECRET,
|
|
||||||
debug: process.env.NODE_ENV === "development",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NextAuth(authOptions);
|
|
||||||
@ -1,14 +1,13 @@
|
|||||||
// API endpoint: update company CSV URL config
|
// API endpoint: update company CSV URL config
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { getServerSession } from "next-auth";
|
import { getApiSession } from "../../../lib/api-auth";
|
||||||
import { prisma } from "../../../lib/prisma";
|
import { prisma } from "../../../lib/prisma";
|
||||||
import { authOptions } from "../auth/[...nextauth]";
|
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse
|
res: NextApiResponse
|
||||||
) {
|
) {
|
||||||
const session = await getServerSession(req, res, authOptions);
|
const session = await getApiSession(req, res);
|
||||||
if (!session?.user) return res.status(401).json({ error: "Not logged in" });
|
if (!session?.user) return res.status(401).json({ error: "Not logged in" });
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
// API endpoint: return metrics for current company
|
// API endpoint: return metrics for current company
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { getServerSession } from "next-auth";
|
import { getApiSession } from "../../../lib/api-auth";
|
||||||
import { prisma } from "../../../lib/prisma";
|
import { prisma } from "../../../lib/prisma";
|
||||||
import { sessionMetrics } from "../../../lib/metrics";
|
import { sessionMetrics } from "../../../lib/metrics";
|
||||||
import { authOptions } from "../auth/[...nextauth]";
|
|
||||||
import { ChatSession } from "../../../lib/types"; // Import ChatSession
|
import { ChatSession } from "../../../lib/types"; // Import ChatSession
|
||||||
|
|
||||||
interface SessionUser {
|
interface SessionUser {
|
||||||
@ -19,11 +18,7 @@ export default async function handler(
|
|||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse
|
res: NextApiResponse
|
||||||
) {
|
) {
|
||||||
const session = (await getServerSession(
|
const session = await getApiSession(req, res);
|
||||||
req,
|
|
||||||
res,
|
|
||||||
authOptions
|
|
||||||
)) as SessionData | null;
|
|
||||||
if (!session?.user) return res.status(401).json({ error: "Not logged in" });
|
if (!session?.user) return res.status(401).json({ error: "Not logged in" });
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { getServerSession } from "next-auth/next";
|
import { getApiSession } from "../../../lib/api-auth";
|
||||||
import { authOptions } from "../auth/[...nextauth]";
|
|
||||||
import { prisma } from "../../../lib/prisma";
|
import { prisma } from "../../../lib/prisma";
|
||||||
import { SessionFilterOptions } from "../../../lib/types";
|
import { SessionFilterOptions } from "../../../lib/types";
|
||||||
|
|
||||||
@ -14,7 +13,7 @@ export default async function handler(
|
|||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const authSession = await getServerSession(req, res, authOptions);
|
const authSession = await getApiSession(req, res);
|
||||||
|
|
||||||
if (!authSession || !authSession.user?.companyId) {
|
if (!authSession || !authSession.user?.companyId) {
|
||||||
return res.status(401).json({ error: "Unauthorized" });
|
return res.status(401).json({ error: "Unauthorized" });
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { getServerSession } from "next-auth/next";
|
import { getApiSession } from "../../../lib/api-auth";
|
||||||
import { authOptions } from "../auth/[...nextauth]";
|
|
||||||
import { prisma } from "../../../lib/prisma";
|
import { prisma } from "../../../lib/prisma";
|
||||||
import {
|
import {
|
||||||
ChatSession,
|
ChatSession,
|
||||||
@ -17,7 +16,7 @@ export default async function handler(
|
|||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const authSession = await getServerSession(req, res, authOptions);
|
const authSession = await getApiSession(req, res);
|
||||||
|
|
||||||
if (!authSession || !authSession.user?.companyId) {
|
if (!authSession || !authSession.user?.companyId) {
|
||||||
return res.status(401).json({ error: "Unauthorized" });
|
return res.status(401).json({ error: "Unauthorized" });
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { getServerSession } from "next-auth";
|
import { getApiSession } from "../../../lib/api-auth";
|
||||||
import { prisma } from "../../../lib/prisma";
|
import { prisma } from "../../../lib/prisma";
|
||||||
import { authOptions } from "../auth/[...nextauth]";
|
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse
|
res: NextApiResponse
|
||||||
) {
|
) {
|
||||||
const session = await getServerSession(req, res, authOptions);
|
const session = await getApiSession(req, res);
|
||||||
if (!session?.user || session.user.role !== "admin")
|
if (!session?.user || session.user.role !== "admin")
|
||||||
return res.status(403).json({ error: "Forbidden" });
|
return res.status(403).json({ error: "Forbidden" });
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { getServerSession } from "next-auth";
|
|
||||||
import { prisma } from "../../../lib/prisma";
|
import { prisma } from "../../../lib/prisma";
|
||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
import { authOptions } from "../auth/[...nextauth]";
|
import { getApiSession } from "../../../lib/api-auth";
|
||||||
// User type from prisma is used instead of the one in lib/types
|
// User type from prisma is used instead of the one in lib/types
|
||||||
|
|
||||||
interface UserBasicInfo {
|
interface UserBasicInfo {
|
||||||
@ -16,9 +15,18 @@ export default async function handler(
|
|||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse
|
res: NextApiResponse
|
||||||
) {
|
) {
|
||||||
const session = await getServerSession(req, res, authOptions);
|
const session = await getApiSession(req, res);
|
||||||
if (!session?.user || session.user.role !== "admin")
|
console.log("Session in users API:", session);
|
||||||
return res.status(403).json({ error: "Forbidden" });
|
|
||||||
|
if (!session?.user) {
|
||||||
|
console.log("No session or user found");
|
||||||
|
return res.status(401).json({ error: "Not logged in" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.user.role !== "admin") {
|
||||||
|
console.log("User is not admin:", session.user.role);
|
||||||
|
return res.status(403).json({ error: "Admin access required" });
|
||||||
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { email: session.user.email as string },
|
where: { email: session.user.email as string },
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export default async function handler(
|
|||||||
data: { resetToken: token, resetTokenExpiry: expiry },
|
data: { resetToken: token, resetTokenExpiry: expiry },
|
||||||
});
|
});
|
||||||
|
|
||||||
const resetUrl = `${process.env.NEXTAUTH_URL || "http://localhost:3000"}/reset-password?token=${token}`;
|
const resetUrl = `${process.env.AUTH_URL || "http://localhost:3000"}/reset-password?token=${token}`;
|
||||||
await sendEmail(email, "Password Reset", `Reset your password: ${resetUrl}`);
|
await sendEmail(email, "Password Reset", `Reset your password: ${resetUrl}`);
|
||||||
res.status(200).end();
|
res.status(200).end();
|
||||||
}
|
}
|
||||||
|
|||||||
10332
pnpm-lock.yaml
generated
Normal file
10332
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,5 @@
|
|||||||
const config = {
|
const config = {
|
||||||
plugins: {
|
plugins: ["@tailwindcss/postcss"],
|
||||||
"@tailwindcss/postcss": {},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@ -0,0 +1,48 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "emailVerified" DATETIME;
|
||||||
|
ALTER TABLE "User" ADD COLUMN "image" TEXT;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Account" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"provider" TEXT NOT NULL,
|
||||||
|
"providerAccountId" TEXT NOT NULL,
|
||||||
|
"refresh_token" TEXT,
|
||||||
|
"access_token" TEXT,
|
||||||
|
"expires_at" INTEGER,
|
||||||
|
"token_type" TEXT,
|
||||||
|
"scope" TEXT,
|
||||||
|
"id_token" TEXT,
|
||||||
|
"session_state" TEXT,
|
||||||
|
CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "UserSession" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"sessionToken" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"expires" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "UserSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "VerificationToken" (
|
||||||
|
"identifier" TEXT NOT NULL,
|
||||||
|
"token" TEXT NOT NULL,
|
||||||
|
"expires" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "UserSession_sessionToken_key" ON "UserSession"("sessionToken");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
|
||||||
@ -1,11 +1,12 @@
|
|||||||
// Database schema, one company = one org, linked to users and CSV config
|
// Database schema, one company = one org, linked to users and CSV config
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
|
previewFeatures = ["driverAdapters"]
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "sqlite"
|
||||||
url = "file:./dev.db"
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Company {
|
model Company {
|
||||||
@ -31,6 +32,48 @@ model User {
|
|||||||
role String // 'admin' | 'user' | 'auditor'
|
role String // 'admin' | 'user' | 'auditor'
|
||||||
resetToken String?
|
resetToken String?
|
||||||
resetTokenExpiry DateTime?
|
resetTokenExpiry DateTime?
|
||||||
|
|
||||||
|
// NextAuth fields
|
||||||
|
accounts Account[]
|
||||||
|
sessions UserSession[]
|
||||||
|
emailVerified DateTime?
|
||||||
|
image String?
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextAuth models
|
||||||
|
model Account {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
type String
|
||||||
|
provider String
|
||||||
|
providerAccountId String
|
||||||
|
refresh_token String?
|
||||||
|
access_token String?
|
||||||
|
expires_at Int?
|
||||||
|
token_type String?
|
||||||
|
scope String?
|
||||||
|
id_token String?
|
||||||
|
session_state String?
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([provider, providerAccountId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserSession {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
sessionToken String @unique
|
||||||
|
userId String
|
||||||
|
expires DateTime
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
|
model VerificationToken {
|
||||||
|
identifier String
|
||||||
|
token String @unique
|
||||||
|
expires DateTime
|
||||||
|
|
||||||
|
@@unique([identifier, token])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Session {
|
model Session {
|
||||||
|
|||||||
@ -1,39 +0,0 @@
|
|||||||
// seed.js - Create initial admin user and company
|
|
||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
import bcrypt from "bcryptjs";
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
// Create a company
|
|
||||||
const company = await prisma.company.create({
|
|
||||||
data: {
|
|
||||||
name: "Demo Company",
|
|
||||||
csvUrl: "https://example.com/data.csv", // Replace with a real URL if available
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create an admin user
|
|
||||||
const hashedPassword = await bcrypt.hash("admin123", 10);
|
|
||||||
await prisma.user.create({
|
|
||||||
data: {
|
|
||||||
email: "admin@demo.com",
|
|
||||||
password: hashedPassword,
|
|
||||||
role: "admin",
|
|
||||||
companyId: company.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Seed data created successfully:");
|
|
||||||
console.log("Company: Demo Company");
|
|
||||||
console.log("Admin user: admin@demo.com (password: admin123)");
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
.catch((e) => {
|
|
||||||
console.error("Error seeding database:", e);
|
|
||||||
process.exit(1);
|
|
||||||
})
|
|
||||||
.finally(async () => {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
});
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
// seed.ts - Create initial admin user and company
|
|
||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
import bcrypt from "bcryptjs";
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
// Create a company
|
|
||||||
const company = await prisma.company.create({
|
|
||||||
data: {
|
|
||||||
name: "Demo Company",
|
|
||||||
csvUrl: "https://example.com/data.csv", // Replace with a real URL if available
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create an admin user
|
|
||||||
const hashedPassword = await bcrypt.hash("admin123", 10);
|
|
||||||
await prisma.user.create({
|
|
||||||
data: {
|
|
||||||
email: "admin@demo.com",
|
|
||||||
password: hashedPassword,
|
|
||||||
role: "admin",
|
|
||||||
companyId: company.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Seed data created successfully:");
|
|
||||||
console.log("Company: Demo Company");
|
|
||||||
console.log("Admin user: admin@demo.com (password: admin123)");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error seeding database:", error);
|
|
||||||
process.exit(1);
|
|
||||||
} finally {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
193
scripts/d1-manager.js
Normal file
193
scripts/d1-manager.js
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Comprehensive D1 Database Management Script
|
||||||
|
*
|
||||||
|
* Usage Examples:
|
||||||
|
* node scripts/d1-manager.js tables
|
||||||
|
* node scripts/d1-manager.js schema Company
|
||||||
|
* node scripts/d1-manager.js count User
|
||||||
|
* node scripts/d1-manager.js query "SELECT * FROM User LIMIT 5"
|
||||||
|
* node scripts/d1-manager.js backup
|
||||||
|
* node scripts/d1-manager.js --remote query "SELECT COUNT(*) FROM Session"
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import { writeFileSync, mkdirSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
const DB_NAME = "d1-notso-livedash";
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
// Parse flags
|
||||||
|
const isRemote = args.includes("--remote");
|
||||||
|
const filteredArgs = args.filter((arg) => !arg.startsWith("--"));
|
||||||
|
|
||||||
|
if (filteredArgs.length === 0) {
|
||||||
|
showHelp();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = filteredArgs[0];
|
||||||
|
const params = filteredArgs.slice(1);
|
||||||
|
|
||||||
|
function showHelp() {
|
||||||
|
console.log(`
|
||||||
|
🗄️ D1 Database Manager for ${DB_NAME}
|
||||||
|
|
||||||
|
Usage: node scripts/d1-manager.js [--remote] <command> [params...]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
info Show database information
|
||||||
|
tables List all tables
|
||||||
|
schema <table> Show table schema
|
||||||
|
count <table> Count rows in table
|
||||||
|
query "<sql>" Execute custom SQL query
|
||||||
|
backup [filename] Export database to SQL file
|
||||||
|
backup-schema Export just the schema
|
||||||
|
recent-logs Show recent query activity
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--remote Execute against remote D1 (production)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
node scripts/d1-manager.js tables
|
||||||
|
node scripts/d1-manager.js schema User
|
||||||
|
node scripts/d1-manager.js count Company
|
||||||
|
node scripts/d1-manager.js query "SELECT * FROM User WHERE role = 'admin'"
|
||||||
|
node scripts/d1-manager.js backup
|
||||||
|
node scripts/d1-manager.js --remote info
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function execute(sql, silent = false) {
|
||||||
|
const remoteFlag = isRemote ? "--remote" : "";
|
||||||
|
const cmd = `npx wrangler d1 execute ${DB_NAME} ${remoteFlag} --command "${sql}"`;
|
||||||
|
|
||||||
|
if (!silent) {
|
||||||
|
console.log(
|
||||||
|
`🔍 Executing${isRemote ? " (remote)" : " (local)"}: ${sql}\\n`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return execSync(cmd, { encoding: "utf8" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Query failed:", error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function wranglerCommand(subcommand, silent = false) {
|
||||||
|
const remoteFlag = isRemote ? "--remote" : "";
|
||||||
|
const cmd = `npx wrangler d1 ${subcommand} ${DB_NAME} ${remoteFlag}`;
|
||||||
|
|
||||||
|
if (!silent) {
|
||||||
|
console.log(`📊 Running: ${cmd}\\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return execSync(cmd, { stdio: "inherit" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Command failed:", error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case "info":
|
||||||
|
wranglerCommand("info");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "tables":
|
||||||
|
console.log("📋 Listing all tables:\\n");
|
||||||
|
execute(
|
||||||
|
"SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY name;"
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "schema":
|
||||||
|
if (!params[0]) {
|
||||||
|
console.error("❌ Please specify a table name");
|
||||||
|
console.log("Usage: node scripts/d1-manager.js schema <table_name>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(`🏗️ Schema for table '${params[0]}':\\n`);
|
||||||
|
execute(`PRAGMA table_info(${params[0]});`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "count":
|
||||||
|
if (!params[0]) {
|
||||||
|
console.error("❌ Please specify a table name");
|
||||||
|
console.log("Usage: node scripts/d1-manager.js count <table_name>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(`🔢 Row count for table '${params[0]}':\\n`);
|
||||||
|
execute(`SELECT COUNT(*) as row_count FROM ${params[0]};`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "query":
|
||||||
|
if (!params[0]) {
|
||||||
|
console.error("❌ Please specify a SQL query");
|
||||||
|
console.log(
|
||||||
|
'Usage: node scripts/d1-manager.js query "SELECT * FROM table"'
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
execute(params[0]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "backup":
|
||||||
|
const timestamp = new Date()
|
||||||
|
.toISOString()
|
||||||
|
.replace(/[:.]/g, "-")
|
||||||
|
.slice(0, 19);
|
||||||
|
const filename = params[0] || `backup_${timestamp}.sql`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
mkdirSync("backups", { recursive: true });
|
||||||
|
} catch (e) {
|
||||||
|
// Directory might already exist
|
||||||
|
}
|
||||||
|
|
||||||
|
const backupPath = join("backups", filename);
|
||||||
|
console.log(`💾 Creating backup: ${backupPath}\\n`);
|
||||||
|
wranglerCommand(`export --output ${backupPath}`);
|
||||||
|
console.log(`\\n✅ Backup created successfully: ${backupPath}`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "backup-schema":
|
||||||
|
try {
|
||||||
|
mkdirSync("backups", { recursive: true });
|
||||||
|
} catch (e) {
|
||||||
|
// Directory might already exist
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("📜 Exporting schema only...\\n");
|
||||||
|
wranglerCommand("export --no-data --output backups/schema.sql");
|
||||||
|
console.log("\\n✅ Schema exported to backups/schema.sql");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "recent-logs":
|
||||||
|
console.log("📊 Recent database activity:\\n");
|
||||||
|
try {
|
||||||
|
wranglerCommand("insights");
|
||||||
|
} catch (error) {
|
||||||
|
console.log("ℹ️ Insights not available for this database");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "all-tables-info":
|
||||||
|
console.log("📊 Information about all tables:\\n");
|
||||||
|
const tables = ["Company", "User", "Session"];
|
||||||
|
for (const table of tables) {
|
||||||
|
console.log(`\\n🏷️ Table: ${table}`);
|
||||||
|
console.log("─".repeat(50));
|
||||||
|
execute(`SELECT COUNT(*) as row_count FROM ${table};`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.error(`❌ Unknown command: ${command}`);
|
||||||
|
showHelp();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
38
scripts/d1-query.js
Normal file
38
scripts/d1-query.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Simple D1 query helper script
|
||||||
|
* Usage: node scripts/d1-query.js "SELECT * FROM User LIMIT 5"
|
||||||
|
* Usage: node scripts/d1-query.js --remote "SELECT COUNT(*) FROM Company"
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length === 0) {
|
||||||
|
console.log('Usage: node scripts/d1-query.js [--remote] "SQL_QUERY"');
|
||||||
|
console.log("Examples:");
|
||||||
|
console.log(' node scripts/d1-query.js "SELECT * FROM User LIMIT 5"');
|
||||||
|
console.log(
|
||||||
|
' node scripts/d1-query.js --remote "SELECT COUNT(*) FROM Company"'
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRemote = args.includes("--remote");
|
||||||
|
const query = args[args.length - 1];
|
||||||
|
|
||||||
|
if (!query || query.startsWith("--")) {
|
||||||
|
console.error("Error: Please provide a SQL query");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteFlag = isRemote ? "--remote" : "";
|
||||||
|
const command = `npx wrangler d1 execute d1-notso-livedash ${remoteFlag} --command "${query}"`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`🔍 Executing${isRemote ? " (remote)" : " (local)"}: ${query}\n`);
|
||||||
|
execSync(command, { stdio: "inherit" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Query failed:", error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
96
scripts/d1.js
Normal file
96
scripts/d1.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Simple D1 Database CLI
|
||||||
|
* Usage: node scripts/d1.js <command> [args...]
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
|
const DB_NAME = "d1-notso-livedash";
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length === 0) {
|
||||||
|
console.log(`
|
||||||
|
🗄️ Simple D1 CLI for ${DB_NAME}
|
||||||
|
|
||||||
|
Usage: node scripts/d1.js <command> [args...]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
list List databases
|
||||||
|
info Show database info
|
||||||
|
tables List all tables
|
||||||
|
schema <table> Show table schema
|
||||||
|
query "<sql>" Execute SQL query
|
||||||
|
export [file] Export database
|
||||||
|
|
||||||
|
Add --remote flag for production database
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
node scripts/d1.js tables
|
||||||
|
node scripts/d1.js schema User
|
||||||
|
node scripts/d1.js query "SELECT COUNT(*) FROM Company"
|
||||||
|
node scripts/d1.js --remote info
|
||||||
|
`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRemote = args.includes("--remote");
|
||||||
|
const filteredArgs = args.filter((arg) => !arg.startsWith("--"));
|
||||||
|
const [command, ...params] = filteredArgs;
|
||||||
|
const remoteFlag = isRemote ? "--remote" : "";
|
||||||
|
|
||||||
|
function run(cmd) {
|
||||||
|
try {
|
||||||
|
console.log(`💫 ${cmd}`);
|
||||||
|
execSync(cmd, { stdio: "inherit" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Command failed");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case "list":
|
||||||
|
run("npx wrangler d1 list");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "info":
|
||||||
|
run(`npx wrangler d1 info ${DB_NAME} ${remoteFlag}`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "tables":
|
||||||
|
run(
|
||||||
|
`npx wrangler d1 execute ${DB_NAME} ${remoteFlag} --command "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "schema":
|
||||||
|
if (!params[0]) {
|
||||||
|
console.error("❌ Please specify table name");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
run(
|
||||||
|
`npx wrangler d1 execute ${DB_NAME} ${remoteFlag} --command "PRAGMA table_info(${params[0]})"`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "query":
|
||||||
|
if (!params[0]) {
|
||||||
|
console.error("❌ Please specify SQL query");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
run(
|
||||||
|
`npx wrangler d1 execute ${DB_NAME} ${remoteFlag} --command "${params[0]}"`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "export":
|
||||||
|
const filename =
|
||||||
|
params[0] || `backup_${new Date().toISOString().slice(0, 10)}.sql`;
|
||||||
|
run(`npx wrangler d1 export ${DB_NAME} ${remoteFlag} --output ${filename}`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.error(`❌ Unknown command: ${command}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@ -16,6 +16,7 @@ async function main() {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
fullTranscriptUrl: true,
|
fullTranscriptUrl: true,
|
||||||
|
companyId: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -28,47 +29,94 @@ async function main() {
|
|||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
let errorCount = 0;
|
let errorCount = 0;
|
||||||
|
|
||||||
|
// Group sessions by company to fetch credentials once per company
|
||||||
|
const sessionsByCompany = new Map<string, typeof sessionsToUpdate>();
|
||||||
for (const session of sessionsToUpdate) {
|
for (const session of sessionsToUpdate) {
|
||||||
if (!session.fullTranscriptUrl) {
|
if (!sessionsByCompany.has(session.companyId)) {
|
||||||
// Should not happen due to query, but good for type safety
|
sessionsByCompany.set(session.companyId, []);
|
||||||
console.warn(`Session ${session.id} has no fullTranscriptUrl, skipping.`);
|
}
|
||||||
|
sessionsByCompany.get(session.companyId)!.push(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [companyId, companySessions] of Array.from(
|
||||||
|
sessionsByCompany.entries()
|
||||||
|
)) {
|
||||||
|
// Fetch company credentials once per company
|
||||||
|
const company = await prisma.company.findUnique({
|
||||||
|
where: { id: companyId },
|
||||||
|
select: {
|
||||||
|
csvUsername: true,
|
||||||
|
csvPassword: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!company) {
|
||||||
|
console.warn(`Company ${companyId} not found, skipping sessions.`);
|
||||||
|
errorCount += companySessions.length;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Fetching transcript for session ${session.id} from ${session.fullTranscriptUrl}...`
|
`Processing ${companySessions.length} sessions for company: ${company.name}`
|
||||||
);
|
);
|
||||||
try {
|
|
||||||
const response = await fetch(session.fullTranscriptUrl);
|
for (const session of companySessions) {
|
||||||
if (!response.ok) {
|
if (!session.fullTranscriptUrl) {
|
||||||
console.error(
|
// Should not happen due to query, but good for type safety
|
||||||
`Failed to fetch transcript for session ${session.id}: ${response.status} ${response.statusText}`
|
console.warn(
|
||||||
|
`Session ${session.id} has no fullTranscriptUrl, skipping.`
|
||||||
);
|
);
|
||||||
const errorBody = await response.text();
|
|
||||||
console.error(`Error details: ${errorBody.substring(0, 500)}`); // Log first 500 chars of error
|
|
||||||
errorCount++;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const transcriptText = await response.text();
|
|
||||||
|
|
||||||
if (transcriptText.trim() === "") {
|
|
||||||
console.warn(
|
|
||||||
`Fetched empty transcript for session ${session.id}. Storing as empty string.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.session.update({
|
|
||||||
where: { id: session.id },
|
|
||||||
data: { transcriptContent: transcriptText },
|
|
||||||
});
|
|
||||||
console.log(
|
console.log(
|
||||||
`Successfully fetched and stored transcript for session ${session.id}.`
|
`Fetching transcript for session ${session.id} from ${session.fullTranscriptUrl}...`
|
||||||
);
|
);
|
||||||
successCount++;
|
try {
|
||||||
} catch (error) {
|
// Prepare authentication if credentials are available
|
||||||
console.error(`Error processing session ${session.id}:`, error);
|
const authHeader =
|
||||||
errorCount++;
|
company.csvUsername && company.csvPassword
|
||||||
|
? "Basic " +
|
||||||
|
Buffer.from(
|
||||||
|
`${company.csvUsername}:${company.csvPassword}`
|
||||||
|
).toString("base64")
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const response = await fetch(session.fullTranscriptUrl, {
|
||||||
|
headers: authHeader ? { Authorization: authHeader } : {},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(
|
||||||
|
`Failed to fetch transcript for session ${session.id}: ${response.status} ${response.statusText}`
|
||||||
|
);
|
||||||
|
const errorBody = await response.text();
|
||||||
|
console.error(`Error details: ${errorBody.substring(0, 500)}`); // Log first 500 chars of error
|
||||||
|
errorCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transcriptText = await response.text();
|
||||||
|
|
||||||
|
if (transcriptText.trim() === "") {
|
||||||
|
console.warn(
|
||||||
|
`Fetched empty transcript for session ${session.id}. Storing as empty string.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.session.update({
|
||||||
|
where: { id: session.id },
|
||||||
|
data: { transcriptContent: transcriptText },
|
||||||
|
});
|
||||||
|
console.log(
|
||||||
|
`Successfully fetched and stored transcript for session ${session.id}.`
|
||||||
|
);
|
||||||
|
successCount++;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error processing session ${session.id}:`, error);
|
||||||
|
errorCount++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
288
src/index.ts.backup
Normal file
288
src/index.ts.backup
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
// Cloudflare Worker entry point for LiveDash-Node
|
||||||
|
// This file handles requests when deployed to Cloudflare Workers
|
||||||
|
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import { PrismaD1 } from "@prisma/adapter-d1";
|
||||||
|
|
||||||
|
export interface Env {
|
||||||
|
DB: D1Database;
|
||||||
|
AUTH_SECRET?: string;
|
||||||
|
AUTH_URL?: string;
|
||||||
|
WORKER_ENV?: string; // 'development' | 'production'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch(
|
||||||
|
request: Request,
|
||||||
|
env: Env,
|
||||||
|
ctx: ExecutionContext
|
||||||
|
): Promise<Response> {
|
||||||
|
try {
|
||||||
|
// Initialize Prisma with D1 adapter
|
||||||
|
const adapter = new PrismaD1(env.DB);
|
||||||
|
const prisma = new PrismaClient({ adapter });
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
// CORS headers for all responses
|
||||||
|
const corsHeaders = {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle preflight requests
|
||||||
|
if (request.method === "OPTIONS") {
|
||||||
|
return new Response(null, { headers: corsHeaders });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle API routes
|
||||||
|
if (url.pathname.startsWith("/api/")) {
|
||||||
|
// Simple health check endpoint
|
||||||
|
if (url.pathname === "/api/health") {
|
||||||
|
const companyCount = await prisma.company.count();
|
||||||
|
const sessionCount = await prisma.session.count();
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
status: "healthy",
|
||||||
|
database: "connected",
|
||||||
|
companies: companyCount,
|
||||||
|
sessions: sessionCount,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...corsHeaders,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test metrics endpoint
|
||||||
|
if (url.pathname === "/api/test-metrics") {
|
||||||
|
const sessions = await prisma.session.findMany({
|
||||||
|
take: 10,
|
||||||
|
orderBy: { startTime: "desc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
message: "LiveDash API running on Cloudflare Workers with D1",
|
||||||
|
recentSessions: sessions.length,
|
||||||
|
sessions: sessions,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...corsHeaders,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dashboard metrics endpoint
|
||||||
|
if (url.pathname === "/api/dashboard/metrics") {
|
||||||
|
const companyCount = await prisma.company.count();
|
||||||
|
const userCount = await prisma.user.count();
|
||||||
|
const sessionCount = await prisma.session.count();
|
||||||
|
|
||||||
|
const recentSessions = await prisma.session.findMany({
|
||||||
|
take: 5,
|
||||||
|
orderBy: { startTime: "desc" },
|
||||||
|
include: {
|
||||||
|
company: {
|
||||||
|
select: { name: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
overview: {
|
||||||
|
companies: companyCount,
|
||||||
|
users: userCount,
|
||||||
|
sessions: sessionCount,
|
||||||
|
},
|
||||||
|
recentSessions: recentSessions,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...corsHeaders,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Companies endpoint
|
||||||
|
if (url.pathname === "/api/companies") {
|
||||||
|
if (request.method === "GET") {
|
||||||
|
const companies = await prisma.company.findMany({
|
||||||
|
include: {
|
||||||
|
_count: {
|
||||||
|
select: { users: true, sessions: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(companies), {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...corsHeaders,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other API routes, return a placeholder response
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
message: "API endpoint not implemented in worker yet",
|
||||||
|
path: url.pathname,
|
||||||
|
method: request.method,
|
||||||
|
note: "This endpoint needs to be migrated from Next.js API routes",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 501,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...corsHeaders,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle root path - simple test page
|
||||||
|
if (url.pathname === "/") {
|
||||||
|
try {
|
||||||
|
const companies = await prisma.company.findMany();
|
||||||
|
const recentSessions = await prisma.session.findMany({
|
||||||
|
take: 5,
|
||||||
|
orderBy: { startTime: "desc" },
|
||||||
|
include: { company: { select: { name: true } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>LiveDash-Node on Cloudflare Workers</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://static.integrations.cloudflare.com/styles.css">
|
||||||
|
<style>
|
||||||
|
.container { max-width: 1000px; margin: 0 auto; padding: 20px; }
|
||||||
|
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin: 20px 0; }
|
||||||
|
.card { background: #f8f9fa; padding: 20px; border-radius: 8px; border: 1px solid #e9ecef; }
|
||||||
|
pre { background: #f5f5f5; padding: 15px; border-radius: 5px; overflow-x: auto; font-size: 12px; }
|
||||||
|
.api-list { list-style: none; padding: 0; }
|
||||||
|
.api-list li { margin: 8px 0; }
|
||||||
|
.api-list a { color: #0066cc; text-decoration: none; }
|
||||||
|
.api-list a:hover { text-decoration: underline; }
|
||||||
|
.status { color: #28a745; font-weight: bold; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<img
|
||||||
|
src="https://imagedelivery.net/wSMYJvS3Xw-n339CbDyDIA/30e0d3f6-6076-40f8-7abb-8a7676f83c00/public"
|
||||||
|
/>
|
||||||
|
<h1>🎉 LiveDash-Node Successfully Connected to D1!</h1>
|
||||||
|
<p class="status">✓ Database Connected | ✓ Prisma Client Working | ✓ D1 Adapter Active</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card">
|
||||||
|
<h3>📊 Database Stats</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Companies:</strong> ${companies.length}</li>
|
||||||
|
<li><strong>Recent Sessions:</strong> ${recentSessions.length}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>🔗 Test API Endpoints</h3>
|
||||||
|
<ul class="api-list">
|
||||||
|
<li><a href="/api/health">/api/health</a> - Health check</li>
|
||||||
|
<li><a href="/api/test-metrics">/api/test-metrics</a> - Sample data</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>🏢 Companies in Database</h3>
|
||||||
|
<pre>${companies.length > 0 ? JSON.stringify(companies, null, 2) : "No companies found"}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>📈 Recent Sessions</h3>
|
||||||
|
<pre>${recentSessions.length > 0 ? JSON.stringify(recentSessions, null, 2) : "No sessions found"}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer style="margin-top: 40px; text-align: center; color: #666;">
|
||||||
|
<small>
|
||||||
|
<a target="_blank" href="https://developers.cloudflare.com/d1/">Learn more about Cloudflare D1</a> |
|
||||||
|
<a target="_blank" href="https://www.prisma.io/docs/guides/deployment/deployment-guides/deploying-to-cloudflare-workers">Prisma + Workers Guide</a>
|
||||||
|
</small>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/html",
|
||||||
|
...corsHeaders,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (dbError) {
|
||||||
|
return new Response(
|
||||||
|
`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><title>LiveDash-Node - Database Error</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>❌ Database Connection Error</h1>
|
||||||
|
<p>Error: ${dbError instanceof Error ? dbError.message : "Unknown database error"}</p>
|
||||||
|
<p>Check your D1 database configuration and make sure migrations have been applied.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: { "Content-Type": "text/html" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle all other routes
|
||||||
|
return new Response(
|
||||||
|
"Not Found - This endpoint is not available in the worker deployment",
|
||||||
|
{
|
||||||
|
status: 404,
|
||||||
|
headers: corsHeaders,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Worker error:", error);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: "Internal Server Error",
|
||||||
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -8,9 +8,11 @@
|
|||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node", // bundler
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"noImplicitAny": false, // Allow implicit any types
|
"noImplicitAny": false, // Allow implicit any types
|
||||||
|
"preserveSymlinks": false,
|
||||||
|
"types": ["./worker-configuration.d.ts"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"]
|
||||||
},
|
},
|
||||||
@ -23,14 +25,14 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"target": "es5"
|
"target": "ESNext"
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules"],
|
"exclude": ["node_modules"],
|
||||||
"include": [
|
"include": [
|
||||||
|
"src",
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts"
|
||||||
"components/SessionDetails.tsx.bak"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
50
types/auth.d.ts
vendored
Normal file
50
types/auth.d.ts
vendored
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { DefaultSession } from "next-auth";
|
||||||
|
|
||||||
|
declare module "next-auth" {
|
||||||
|
/**
|
||||||
|
* Returned by `auth`, `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
|
||||||
|
*/
|
||||||
|
interface Session {
|
||||||
|
user: {
|
||||||
|
/** The user's unique id. */
|
||||||
|
id: string;
|
||||||
|
/** The user's role (admin, user, etc.) */
|
||||||
|
role: string;
|
||||||
|
/** The user's company ID */
|
||||||
|
companyId: string;
|
||||||
|
/** The user's company name */
|
||||||
|
company: string;
|
||||||
|
} & DefaultSession["user"];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The shape of the user object returned in the OAuth providers' `profile` callback,
|
||||||
|
* or the second parameter of the `session` callback, when using a database.
|
||||||
|
*/
|
||||||
|
interface User {
|
||||||
|
/** The user's unique id. */
|
||||||
|
id: string;
|
||||||
|
/** The user's email address. */
|
||||||
|
email?: string;
|
||||||
|
/** The user's name. */
|
||||||
|
name?: string;
|
||||||
|
/** The user's role (admin, user, etc.) */
|
||||||
|
role: string;
|
||||||
|
/** The user's company ID */
|
||||||
|
companyId: string;
|
||||||
|
/** The user's company name */
|
||||||
|
company: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "next-auth/jwt" {
|
||||||
|
/** Returned by the `jwt` callback and `auth`, when using JWT sessions */
|
||||||
|
interface JWT {
|
||||||
|
/** The user's role */
|
||||||
|
role: string;
|
||||||
|
/** The user's company ID */
|
||||||
|
companyId: string;
|
||||||
|
/** The user's company name */
|
||||||
|
company: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
5756
worker-configuration.d.ts
vendored
Normal file
5756
worker-configuration.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
62
wrangler.jsonc
Normal file
62
wrangler.jsonc
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* For more details on how to configure Wrangler, refer to:
|
||||||
|
* https://developers.cloudflare.com/workers/wrangler/configuration/
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
|
"name": "livedash",
|
||||||
|
"main": ".open-next/worker.js",
|
||||||
|
"compatibility_date": "2025-06-01",
|
||||||
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
|
"observability": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smart Placement
|
||||||
|
* Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
|
||||||
|
*/
|
||||||
|
// "placement": { "mode": "smart" },
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bindings
|
||||||
|
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
|
||||||
|
* databases, object storage, AI inference, real-time communication and more.
|
||||||
|
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
|
||||||
|
*/
|
||||||
|
"d1_databases": [
|
||||||
|
{
|
||||||
|
"binding": "DB",
|
||||||
|
"database_id": "d4ee7efe-d37a-48e4-bed7-fdfaa5108131",
|
||||||
|
"database_name": "d1-notso-livedash"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment Variables
|
||||||
|
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
|
||||||
|
*/
|
||||||
|
// "vars": { "MY_VARIABLE": "production_value" },
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note: Use secrets to store sensitive data.
|
||||||
|
* https://developers.cloudflare.com/workers/configuration/secrets/
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static Assets
|
||||||
|
* https://developers.cloudflare.com/workers/static-assets/binding/
|
||||||
|
*/
|
||||||
|
// "assets": { "directory": "./public/", "binding": "ASSETS" },
|
||||||
|
"assets": {
|
||||||
|
"directory": ".open-next/assets",
|
||||||
|
"binding": "ASSETS"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service Bindings (communicate between multiple Workers)
|
||||||
|
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
|
||||||
|
*/
|
||||||
|
// "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user