@odvi/create-dtt-framework 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/.env.example +106 -103
- package/template/docs/framework/deployment/environment-variables.md +3 -4
- package/template/docs/framework/environment-variables.md +13 -25
- package/template/docs/framework/health-check-system.md +1 -0
- package/template/docs/framework/snowflake-integration.md +19 -16
- package/template/docs/framework/supabase-integration.md +1 -0
- package/template/drizzle.config.ts +0 -1
- package/template/src/config/__tests__/env.test.ts +6 -8
- package/template/src/config/env.ts +8 -8
- package/template/src/features/health-check/components/health-dashboard.tsx +12 -1
- package/template/src/features/health-check/config.ts +0 -1
- package/template/src/lib/nextbank/client.ts +53 -23
- package/template/src/lib/snowflake/client.ts +64 -15
- package/template/src/server/api/routes/health/database.ts +49 -25
- package/template/src/server/api/routes/health/edge-functions.ts +58 -26
- package/template/src/server/api/routes/health/framework.ts +10 -7
- package/template/src/server/api/routes/health/nextbank.ts +12 -33
- package/template/src/server/api/routes/health/snowflake.ts +2 -2
- package/template/src/server/api/routes/health/storage.ts +14 -0
- package/template/src/server/api/routes/users.ts +14 -30
- package/template/src/server/db/schema.ts +1 -26
- package/template/tsconfig.json +1 -1
package/package.json
CHANGED
package/template/.env.example
CHANGED
|
@@ -1,103 +1,106 @@
|
|
|
1
|
-
# Since the ".env" file is gitignored, you can use the ".env.example" file to
|
|
2
|
-
# build a new ".env" file when you clone the repo. Keep this file up-to-date
|
|
3
|
-
# when you add new variables to `.env`.
|
|
4
|
-
|
|
5
|
-
# This file will be committed to version control, so make sure not to have any
|
|
6
|
-
# secrets in it. If you are cloning this repo, create a copy of this file named
|
|
7
|
-
# ".env" and populate it with your secrets.
|
|
8
|
-
|
|
9
|
-
# When adding additional environment variables, the schema in "/src/env.js"
|
|
10
|
-
# should be updated accordingly.
|
|
11
|
-
|
|
12
|
-
# ========================================
|
|
13
|
-
# App Configuration
|
|
14
|
-
# ========================================
|
|
15
|
-
# The base URL of your application. Used for redirects, webhooks, and callbacks.
|
|
16
|
-
# In development: http://localhost:3000
|
|
17
|
-
# In production: https://your-domain.com
|
|
18
|
-
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
19
|
-
|
|
20
|
-
# ========================================
|
|
21
|
-
# Clerk Authentication
|
|
22
|
-
# ========================================
|
|
23
|
-
# Clerk publishable key - used on the client side for authentication
|
|
24
|
-
# Get this from your Clerk Dashboard > API Keys > Publishable Key
|
|
25
|
-
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx
|
|
26
|
-
|
|
27
|
-
# Clerk secret key - used on the server side for authentication
|
|
28
|
-
# Get this from your Clerk Dashboard > API Keys > Secret Key
|
|
29
|
-
CLERK_SECRET_KEY=sk_test_xxx
|
|
30
|
-
|
|
31
|
-
# Clerk webhook secret - used to verify webhook events from Clerk
|
|
32
|
-
# Get this from your Clerk Dashboard > Webhooks > Add Endpoint > Signing Secret
|
|
33
|
-
CLERK_WEBHOOK_SECRET=whsec_xxx
|
|
34
|
-
|
|
35
|
-
# URL path for the sign-in page
|
|
36
|
-
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
|
|
37
|
-
|
|
38
|
-
# URL path for the sign-up page
|
|
39
|
-
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
|
|
40
|
-
|
|
41
|
-
# Where to redirect users after successful sign-in
|
|
42
|
-
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/health
|
|
43
|
-
|
|
44
|
-
# Where to redirect users after successful sign-up
|
|
45
|
-
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/health
|
|
46
|
-
|
|
47
|
-
# ========================================
|
|
48
|
-
# Supabase
|
|
49
|
-
# ========================================
|
|
50
|
-
# Supabase project URL - found in your Supabase project settings
|
|
51
|
-
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
|
|
52
|
-
|
|
53
|
-
# Supabase anonymous/public key - safe to expose on client side
|
|
54
|
-
# Found in your Supabase project settings under API keys
|
|
55
|
-
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJxxx
|
|
56
|
-
|
|
57
|
-
# Supabase service role key - full admin access, keep secret!
|
|
58
|
-
# Found in your Supabase project settings under API keys
|
|
59
|
-
SUPABASE_SERVICE_ROLE_KEY=eyJxxx
|
|
60
|
-
|
|
61
|
-
# PostgreSQL connection string for Drizzle ORM
|
|
62
|
-
# Use Supabase's Transaction mode pooler for better performance
|
|
63
|
-
# Format: postgresql://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres
|
|
64
|
-
DATABASE_URL=postgresql://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres
|
|
65
|
-
|
|
66
|
-
# ========================================
|
|
67
|
-
# Snowflake Data Warehouse
|
|
68
|
-
# ========================================
|
|
69
|
-
# Snowflake account identifier (e.g.,
|
|
70
|
-
SNOWFLAKE_ACCOUNT=
|
|
71
|
-
|
|
72
|
-
# Snowflake
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
# Snowflake
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
#
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
# Snowflake
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# API
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
1
|
+
# Since the ".env" file is gitignored, you can use the ".env.example" file to
|
|
2
|
+
# build a new ".env" file when you clone the repo. Keep this file up-to-date
|
|
3
|
+
# when you add new variables to `.env`.
|
|
4
|
+
|
|
5
|
+
# This file will be committed to version control, so make sure not to have any
|
|
6
|
+
# secrets in it. If you are cloning this repo, create a copy of this file named
|
|
7
|
+
# ".env" and populate it with your secrets.
|
|
8
|
+
|
|
9
|
+
# When adding additional environment variables, the schema in "/src/env.js"
|
|
10
|
+
# should be updated accordingly.
|
|
11
|
+
|
|
12
|
+
# ========================================
|
|
13
|
+
# App Configuration
|
|
14
|
+
# ========================================
|
|
15
|
+
# The base URL of your application. Used for redirects, webhooks, and callbacks.
|
|
16
|
+
# In development: http://localhost:3000
|
|
17
|
+
# In production: https://your-domain.com
|
|
18
|
+
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
19
|
+
|
|
20
|
+
# ========================================
|
|
21
|
+
# Clerk Authentication
|
|
22
|
+
# ========================================
|
|
23
|
+
# Clerk publishable key - used on the client side for authentication
|
|
24
|
+
# Get this from your Clerk Dashboard > API Keys > Publishable Key
|
|
25
|
+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx
|
|
26
|
+
|
|
27
|
+
# Clerk secret key - used on the server side for authentication
|
|
28
|
+
# Get this from your Clerk Dashboard > API Keys > Secret Key
|
|
29
|
+
CLERK_SECRET_KEY=sk_test_xxx
|
|
30
|
+
|
|
31
|
+
# Clerk webhook secret - used to verify webhook events from Clerk
|
|
32
|
+
# Get this from your Clerk Dashboard > Webhooks > Add Endpoint > Signing Secret
|
|
33
|
+
CLERK_WEBHOOK_SECRET=whsec_xxx
|
|
34
|
+
|
|
35
|
+
# URL path for the sign-in page
|
|
36
|
+
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
|
|
37
|
+
|
|
38
|
+
# URL path for the sign-up page
|
|
39
|
+
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
|
|
40
|
+
|
|
41
|
+
# Where to redirect users after successful sign-in
|
|
42
|
+
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/health
|
|
43
|
+
|
|
44
|
+
# Where to redirect users after successful sign-up
|
|
45
|
+
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/health
|
|
46
|
+
|
|
47
|
+
# ========================================
|
|
48
|
+
# Supabase
|
|
49
|
+
# ========================================
|
|
50
|
+
# Supabase project URL - found in your Supabase project settings
|
|
51
|
+
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
|
|
52
|
+
|
|
53
|
+
# Supabase anonymous/public key - safe to expose on client side
|
|
54
|
+
# Found in your Supabase project settings under API keys
|
|
55
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJxxx
|
|
56
|
+
|
|
57
|
+
# Supabase service role key - full admin access, keep secret!
|
|
58
|
+
# Found in your Supabase project settings under API keys
|
|
59
|
+
SUPABASE_SERVICE_ROLE_KEY=eyJxxx
|
|
60
|
+
|
|
61
|
+
# PostgreSQL connection string for Drizzle ORM
|
|
62
|
+
# Use Supabase's Transaction mode pooler for better performance
|
|
63
|
+
# Format: postgresql://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres
|
|
64
|
+
DATABASE_URL=postgresql://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres
|
|
65
|
+
|
|
66
|
+
# ========================================
|
|
67
|
+
# Snowflake Data Warehouse
|
|
68
|
+
# ========================================
|
|
69
|
+
# Snowflake account identifier (e.g., ef19411.ap-southeast-1)
|
|
70
|
+
SNOWFLAKE_ACCOUNT=ef19411.ap-southeast-1
|
|
71
|
+
|
|
72
|
+
# Snowflake warehouse to use
|
|
73
|
+
SNOWFLAKE_WAREHOUSE=COMPUTE_WH
|
|
74
|
+
|
|
75
|
+
# Snowflake role
|
|
76
|
+
SNOWFLAKE_ROLE=ACCOUNTADMIN
|
|
77
|
+
|
|
78
|
+
# Authentication method (SNOWFLAKE_JWT for Key Pair Auth)
|
|
79
|
+
SNOWFLAKE_AUTHENTICATOR=SNOWFLAKE_JWT
|
|
80
|
+
|
|
81
|
+
# Snowflake username
|
|
82
|
+
SNOWFLAKE_USERNAME=APP_USER_WITH_KEY_AUTH
|
|
83
|
+
|
|
84
|
+
# Private Key for Authentication (PEM format)
|
|
85
|
+
SNOWFLAKE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
|
|
86
|
+
...
|
|
87
|
+
-----END PRIVATE KEY-----"
|
|
88
|
+
|
|
89
|
+
# Passphrase for the Private Key (if encrypted)
|
|
90
|
+
SNOWFLAKE_PRIVATE_KEY_PASSPHRASE="<password>"
|
|
91
|
+
|
|
92
|
+
# Enable logging (optional)
|
|
93
|
+
SNOWFLAKE_LOGGING=true
|
|
94
|
+
|
|
95
|
+
# ========================================
|
|
96
|
+
# NextBank API (Placeholder)
|
|
97
|
+
# ========================================
|
|
98
|
+
# Base URL for the NextBank API endpoint
|
|
99
|
+
NEXTBANK_API=https://api.nextbank.com
|
|
100
|
+
|
|
101
|
+
# Username for NextBank Basic Authentication
|
|
102
|
+
NEXTBANK_API_USERNAME=user
|
|
103
|
+
|
|
104
|
+
# Password for NextBank Basic Authentication
|
|
105
|
+
NEXTBANK_API_PASSWORD=pass
|
|
106
|
+
|
|
@@ -63,10 +63,9 @@ This document provides a comprehensive guide to managing environment variables f
|
|
|
63
63
|
|
|
64
64
|
| Variable Name | Type | Required | Description | Example |
|
|
65
65
|
|---------------|------|----------|-------------|---------|
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `NEXTBANK_CLIENT_SECRET` | Server | No | NextBank OAuth client secret | `xxx` |
|
|
66
|
+
| `NEXTBANK_API` | Server | No | NextBank API endpoint | `https://api.nextbank.com` |
|
|
67
|
+
| `NEXTBANK_API_USERNAME` | Server | No | NextBank API username | `user` |
|
|
68
|
+
| `NEXTBANK_API_PASSWORD` | Server | No | NextBank API password | `pass` |
|
|
70
69
|
|
|
71
70
|
---
|
|
72
71
|
|
|
@@ -102,16 +102,13 @@ SNOWFLAKE_ROLE=ANALYST
|
|
|
102
102
|
# ============================================
|
|
103
103
|
|
|
104
104
|
# NextBank API URL
|
|
105
|
-
|
|
105
|
+
NEXTBANK_API=https://api.nextbank.com
|
|
106
106
|
|
|
107
|
-
# NextBank API
|
|
108
|
-
|
|
107
|
+
# NextBank API username
|
|
108
|
+
NEXTBANK_API_USERNAME=user
|
|
109
109
|
|
|
110
|
-
# NextBank
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
# NextBank OAuth client secret
|
|
114
|
-
NEXTBANK_CLIENT_SECRET=xxx
|
|
110
|
+
# NextBank API password
|
|
111
|
+
NEXTBANK_API_PASSWORD=pass
|
|
115
112
|
```
|
|
116
113
|
|
|
117
114
|
---
|
|
@@ -425,40 +422,31 @@ SNOWFLAKE_ROLE=ANALYST
|
|
|
425
422
|
|
|
426
423
|
**Note:** NextBank is a placeholder integration. These variables are optional and not currently used.
|
|
427
424
|
|
|
428
|
-
####
|
|
425
|
+
#### NEXTBANK_API
|
|
429
426
|
|
|
430
427
|
**Purpose:** NextBank API endpoint
|
|
431
428
|
|
|
432
429
|
**Example:**
|
|
433
430
|
```bash
|
|
434
|
-
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
#### NEXTBANK_API_KEY
|
|
438
|
-
|
|
439
|
-
**Purpose:** API key for NextBank
|
|
440
|
-
|
|
441
|
-
**Example:**
|
|
442
|
-
```bash
|
|
443
|
-
NEXTBANK_API_KEY=xxx
|
|
431
|
+
NEXTBANK_API=https://api.nextbank.com
|
|
444
432
|
```
|
|
445
433
|
|
|
446
|
-
####
|
|
434
|
+
#### NEXTBANK_API_USERNAME
|
|
447
435
|
|
|
448
|
-
**Purpose:**
|
|
436
|
+
**Purpose:** API username for NextBank
|
|
449
437
|
|
|
450
438
|
**Example:**
|
|
451
439
|
```bash
|
|
452
|
-
|
|
440
|
+
NEXTBANK_API_USERNAME=user
|
|
453
441
|
```
|
|
454
442
|
|
|
455
|
-
####
|
|
443
|
+
#### NEXTBANK_API_PASSWORD
|
|
456
444
|
|
|
457
|
-
**Purpose:**
|
|
445
|
+
**Purpose:** API password for NextBank
|
|
458
446
|
|
|
459
447
|
**Example:**
|
|
460
448
|
```bash
|
|
461
|
-
|
|
449
|
+
NEXTBANK_API_PASSWORD=pass
|
|
462
450
|
```
|
|
463
451
|
|
|
464
452
|
---
|
|
@@ -564,6 +564,7 @@ try {
|
|
|
564
564
|
- Check service logs for detailed error messages
|
|
565
565
|
- Verify network connectivity to the service
|
|
566
566
|
- Check if service is running and accessible
|
|
567
|
+
- For Supabase Database: Ensure migrations are applied (`pnpm db:push` or `pnpm db:migrate`) so `health_check_tests` table exists
|
|
567
568
|
|
|
568
569
|
**Issue: Health check is slow**
|
|
569
570
|
|
|
@@ -36,24 +36,27 @@ Add the following to your [`.env`](./environment-variables.md) file:
|
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
38
|
# Snowflake
|
|
39
|
-
SNOWFLAKE_ACCOUNT=
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
SNOWFLAKE_ACCOUNT=ef19411.ap-southeast-1
|
|
40
|
+
SNOWFLAKE_AUTHENTICATOR=SNOWFLAKE_JWT
|
|
41
|
+
SNOWFLAKE_USERNAME=APP_USER_WITH_KEY_AUTH
|
|
42
|
+
SNOWFLAKE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
|
|
43
|
+
-----END PRIVATE KEY-----"
|
|
44
|
+
SNOWFLAKE_PRIVATE_KEY_PASSPHRASE="<password>"
|
|
42
45
|
SNOWFLAKE_WAREHOUSE=COMPUTE_WH
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
SNOWFLAKE_ROLE=ANALYST
|
|
46
|
+
SNOWFLAKE_ROLE=ACCOUNTADMIN
|
|
47
|
+
SNOWFLAKE_LOGGING=true
|
|
46
48
|
```
|
|
47
49
|
|
|
48
50
|
**Where to find these values:**
|
|
49
51
|
|
|
50
|
-
- **Account**: Snowflake URL (e.g., `
|
|
51
|
-
- **
|
|
52
|
-
- **
|
|
53
|
-
- **
|
|
54
|
-
- **
|
|
55
|
-
- **
|
|
56
|
-
- **Role**: Role with appropriate permissions
|
|
52
|
+
- **Account**: Snowflake URL (e.g., `ef19411.ap-southeast-1` from `https://ef19411.ap-southeast-1.snowflakecomputing.com`)
|
|
53
|
+
- **Authenticator**: `SNOWFLAKE_JWT` for Key Pair Authentication
|
|
54
|
+
- **Username**: Your Snowflake username (must be configured with public key)
|
|
55
|
+
- **Private Key**: Your private key in PEM format
|
|
56
|
+
- **Passphrase**: Password for your private key (if encrypted)
|
|
57
|
+
- **Warehouse**: Compute warehouse name
|
|
58
|
+
- **Role**: Role with appropriate permissions (e.g., `ACCOUNTADMIN` or custom role)
|
|
59
|
+
- **Logging**: Enable verbose logging (optional)
|
|
57
60
|
|
|
58
61
|
### 4. Create Snowflake Client
|
|
59
62
|
|
|
@@ -67,10 +70,10 @@ import { env } from '@/config/env'
|
|
|
67
70
|
const config = {
|
|
68
71
|
account: env.SNOWFLAKE_ACCOUNT,
|
|
69
72
|
username: env.SNOWFLAKE_USERNAME,
|
|
70
|
-
|
|
73
|
+
authenticator: env.SNOWFLAKE_AUTHENTICATOR,
|
|
74
|
+
privateKey: env.SNOWFLAKE_PRIVATE_KEY,
|
|
75
|
+
privateKeyPass: env.SNOWFLAKE_PRIVATE_KEY_PASSPHRASE,
|
|
71
76
|
warehouse: env.SNOWFLAKE_WAREHOUSE,
|
|
72
|
-
database: env.SNOWFLAKE_DATABASE,
|
|
73
|
-
schema: env.SNOWFLAKE_SCHEMA,
|
|
74
77
|
role: env.SNOWFLAKE_ROLE,
|
|
75
78
|
}
|
|
76
79
|
|
|
@@ -33,10 +33,9 @@ describe('env configuration', () => {
|
|
|
33
33
|
SNOWFLAKE_DATABASE: z.string().default('ANALYTICS'),
|
|
34
34
|
SNOWFLAKE_SCHEMA: z.string().default('PUBLIC'),
|
|
35
35
|
SNOWFLAKE_ROLE: z.string().default('ANALYST'),
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
NEXTBANK_CLIENT_SECRET: z.string().optional(),
|
|
36
|
+
NEXTBANK_API: z.string().url().optional(),
|
|
37
|
+
NEXTBANK_API_USERNAME: z.string().optional(),
|
|
38
|
+
NEXTBANK_API_PASSWORD: z.string().optional(),
|
|
40
39
|
});
|
|
41
40
|
|
|
42
41
|
const result = envSchema.safeParse({});
|
|
@@ -104,10 +103,9 @@ describe('env configuration', () => {
|
|
|
104
103
|
|
|
105
104
|
it('should allow optional NextBank fields', () => {
|
|
106
105
|
const nextbankSchema = z.object({
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
NEXTBANK_CLIENT_SECRET: z.string().optional(),
|
|
106
|
+
NEXTBANK_API: z.string().url().optional(),
|
|
107
|
+
NEXTBANK_API_USERNAME: z.string().optional(),
|
|
108
|
+
NEXTBANK_API_PASSWORD: z.string().optional(),
|
|
111
109
|
});
|
|
112
110
|
|
|
113
111
|
const result = nextbankSchema.safeParse({});
|
|
@@ -19,17 +19,17 @@ const envSchema = z.object({
|
|
|
19
19
|
// Snowflake
|
|
20
20
|
SNOWFLAKE_ACCOUNT: z.string().default('placeholder'),
|
|
21
21
|
SNOWFLAKE_USERNAME: z.string().default('placeholder'),
|
|
22
|
-
|
|
22
|
+
SNOWFLAKE_AUTHENTICATOR: z.literal('SNOWFLAKE_JWT').default('SNOWFLAKE_JWT'),
|
|
23
|
+
SNOWFLAKE_PRIVATE_KEY: z.string().default('placeholder'),
|
|
24
|
+
SNOWFLAKE_PRIVATE_KEY_PASSPHRASE: z.string().optional(),
|
|
23
25
|
SNOWFLAKE_WAREHOUSE: z.string().default('COMPUTE_WH'),
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
SNOWFLAKE_ROLE: z.string().default('ANALYST'),
|
|
26
|
+
SNOWFLAKE_ROLE: z.string().default('ACCOUNTADMIN'),
|
|
27
|
+
SNOWFLAKE_LOGGING: z.string().transform((val) => val === 'true').optional(),
|
|
27
28
|
|
|
28
29
|
// NextBank (optional)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
NEXTBANK_CLIENT_SECRET: z.string().optional(),
|
|
30
|
+
NEXTBANK_API: z.string().url().optional(),
|
|
31
|
+
NEXTBANK_API_USERNAME: z.string().optional(),
|
|
32
|
+
NEXTBANK_API_PASSWORD: z.string().optional(),
|
|
33
33
|
})
|
|
34
34
|
|
|
35
35
|
export const env = envSchema.parse(process.env)
|
|
@@ -11,6 +11,7 @@ import { useState } from 'react'
|
|
|
11
11
|
import type { HealthStatus } from '@/features/health-check/types'
|
|
12
12
|
import { useHealthStore } from '../stores/health-store'
|
|
13
13
|
import { useQueryClient } from '@tanstack/react-query'
|
|
14
|
+
import { useAuth } from '@clerk/nextjs'
|
|
14
15
|
|
|
15
16
|
const statusIcons: Record<HealthStatus, React.ReactNode> = {
|
|
16
17
|
healthy: <CheckCircle className="h-5 w-5 text-green-500" />,
|
|
@@ -43,6 +44,7 @@ export function HealthDashboard() {
|
|
|
43
44
|
const [loadingChecks, setLoadingChecks] = useState<Set<string>>(new Set())
|
|
44
45
|
const healthStore = useHealthStore()
|
|
45
46
|
const queryClient = useQueryClient()
|
|
47
|
+
const { getToken } = useAuth()
|
|
46
48
|
|
|
47
49
|
const totalHealthChecks = SERVICES.reduce((total, service) => total + service.checks.length, 0)
|
|
48
50
|
|
|
@@ -85,8 +87,17 @@ export function HealthDashboard() {
|
|
|
85
87
|
throw new Error(`Unknown client endpoint: ${endpoint}`)
|
|
86
88
|
}
|
|
87
89
|
} else {
|
|
90
|
+
// Get auth token if available
|
|
91
|
+
const token = await getToken()
|
|
92
|
+
|
|
88
93
|
// Handle server-side checks
|
|
89
|
-
const response = await fetch(`/api/health${endpoint}
|
|
94
|
+
const response = await fetch(`/api/health${endpoint}`, {
|
|
95
|
+
method: ['/database/write', '/database/delete', '/storage/upload', '/storage/delete'].includes(endpoint) ? 'POST' : 'GET',
|
|
96
|
+
headers: {
|
|
97
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
98
|
+
},
|
|
99
|
+
...(endpoint === '/database/delete' || endpoint === '/storage/delete' ? { method: 'DELETE' } : {})
|
|
100
|
+
})
|
|
90
101
|
// Check content type to ensure JSON
|
|
91
102
|
const contentType = response.headers.get('content-type')
|
|
92
103
|
if (!contentType || !contentType.includes('application/json')) {
|
|
@@ -2,35 +2,65 @@ import { env } from '@/config/env'
|
|
|
2
2
|
|
|
3
3
|
class NextBankClient {
|
|
4
4
|
private apiUrl: string
|
|
5
|
-
private
|
|
6
|
-
private
|
|
5
|
+
private username: string
|
|
6
|
+
private password: string
|
|
7
7
|
|
|
8
8
|
constructor() {
|
|
9
|
-
this.apiUrl = env.
|
|
10
|
-
this.
|
|
9
|
+
this.apiUrl = env.NEXTBANK_API ?? ''
|
|
10
|
+
this.username = env.NEXTBANK_API_USERNAME ?? ''
|
|
11
|
+
this.password = env.NEXTBANK_API_PASSWORD ?? ''
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
if (!response.ok) throw new Error(`NextBank API error: ${response.status}`)
|
|
18
|
-
return response.json()
|
|
14
|
+
private getAuthHeader(): Record<string, string> {
|
|
15
|
+
if (!this.username || !this.password) return {}
|
|
16
|
+
const token = btoa(`${this.username}:${this.password}`)
|
|
17
|
+
return { Authorization: `Basic ${token}` }
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
async
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
async ping(fingerprint: string): Promise<{ status: string; timestamp: string }> {
|
|
21
|
+
const url = `${this.apiUrl}/management/status`
|
|
22
|
+
console.log(`[NextBank] Pinging ${url}`)
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(url, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: {
|
|
28
|
+
...this.getAuthHeader(),
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
'Accept': 'application/json',
|
|
31
|
+
'User-Agent': 'dtt-framework-health-check',
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
fingerprint: fingerprint,
|
|
35
|
+
}),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const contentType = response.headers.get('content-type')
|
|
39
|
+
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
if (contentType && contentType.includes('text/html')) {
|
|
42
|
+
const text = await response.text()
|
|
43
|
+
console.error('[NextBank] Received HTML error response. Possible causes:')
|
|
44
|
+
console.error('1. NEXTBANK_API is pointing to the wrong server (e.g. Next.js app instead of NextBank API)')
|
|
45
|
+
console.error('2. The endpoint /management/status does not exist or does not support POST')
|
|
46
|
+
console.error(`[NextBank] Response preview: ${text.substring(0, 150)}...`)
|
|
47
|
+
throw new Error(`NextBank API error: ${response.status} (HTML response)`)
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`NextBank API error: ${response.status} ${response.statusText}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (contentType && contentType.includes('application/json')) {
|
|
53
|
+
return await response.json()
|
|
54
|
+
} else {
|
|
55
|
+
const text = await response.text()
|
|
56
|
+
console.error('[NextBank] Invalid response content type:', contentType)
|
|
57
|
+
console.error('[NextBank] Response preview:', text.substring(0, 200))
|
|
58
|
+
throw new Error(`Invalid response format. Expected JSON, got ${contentType}`)
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error('[NextBank] Ping failed:', error)
|
|
62
|
+
throw error
|
|
63
|
+
}
|
|
34
64
|
}
|
|
35
65
|
}
|
|
36
66
|
|
|
@@ -1,33 +1,82 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type { Bind } from 'snowflake-sdk'
|
|
1
|
+
import type { Connection, Bind } from 'snowflake-sdk'
|
|
3
2
|
import { env } from '@/config/env'
|
|
4
3
|
|
|
4
|
+
// Helper to format private key correctly
|
|
5
|
+
function formatPrivateKey(key: string | undefined): string | undefined {
|
|
6
|
+
if (!key || key === 'placeholder') return undefined
|
|
7
|
+
|
|
8
|
+
let cleanKey = key
|
|
9
|
+
|
|
10
|
+
// 1. Remove surrounding quotes if they exist
|
|
11
|
+
if ((cleanKey.startsWith('"') && cleanKey.endsWith('"')) ||
|
|
12
|
+
(cleanKey.startsWith("'") && cleanKey.endsWith("'"))) {
|
|
13
|
+
cleanKey = cleanKey.slice(1, -1)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 2. Handle literal escaped newlines (common in one-line env vars)
|
|
17
|
+
if (cleanKey.includes('\\n')) {
|
|
18
|
+
cleanKey = cleanKey.replace(/\\n/g, '\n')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 3. Split by newline to handle potential indentation/whitespace issues
|
|
22
|
+
const lines = cleanKey.split('\n')
|
|
23
|
+
.map(line => line.trim()) // Remove indentation/whitespace from each line
|
|
24
|
+
.filter(line => line.length > 0) // Remove empty lines
|
|
25
|
+
|
|
26
|
+
// 4. Reconstruct the key
|
|
27
|
+
const formattedKey = lines.join('\n')
|
|
28
|
+
|
|
29
|
+
// Debug log (safe)
|
|
30
|
+
if (!formattedKey.includes('BEGIN PRIVATE KEY')) {
|
|
31
|
+
console.warn('Snowflake Private Key may be invalid or missing PKCS#8 header (expected "-----BEGIN PRIVATE KEY-----")')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (formattedKey.includes('BEGIN RSA PRIVATE KEY')) {
|
|
35
|
+
console.warn('Snowflake Private Key appears to be PKCS#1 (RSA). Snowflake requires PKCS#8. Use "openssl pkcs8 -topk8..." to convert.')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return formattedKey
|
|
39
|
+
}
|
|
40
|
+
|
|
5
41
|
const config = {
|
|
6
42
|
account: env.SNOWFLAKE_ACCOUNT,
|
|
7
43
|
username: env.SNOWFLAKE_USERNAME,
|
|
8
|
-
|
|
44
|
+
authenticator: env.SNOWFLAKE_AUTHENTICATOR,
|
|
45
|
+
privateKey: formatPrivateKey(env.SNOWFLAKE_PRIVATE_KEY),
|
|
46
|
+
privateKeyPass: env.SNOWFLAKE_PRIVATE_KEY_PASSPHRASE,
|
|
9
47
|
warehouse: env.SNOWFLAKE_WAREHOUSE,
|
|
10
|
-
database: env.SNOWFLAKE_DATABASE,
|
|
11
|
-
schema: env.SNOWFLAKE_SCHEMA,
|
|
12
48
|
role: env.SNOWFLAKE_ROLE,
|
|
13
49
|
}
|
|
14
50
|
|
|
15
|
-
|
|
51
|
+
async function getSnowflake() {
|
|
52
|
+
if (process.env.NEXT_RUNTIME === 'edge') {
|
|
53
|
+
throw new Error('Snowflake SDK is not supported in Edge Runtime')
|
|
54
|
+
}
|
|
55
|
+
// Dynamically import snowflake-sdk to avoid build errors in Edge Runtime
|
|
56
|
+
return (await import('snowflake-sdk')).default
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function createSnowflakeConnection(): Promise<Connection> {
|
|
60
|
+
const snowflake = await getSnowflake()
|
|
16
61
|
return snowflake.createConnection(config)
|
|
17
62
|
}
|
|
18
63
|
|
|
19
|
-
export async function connectSnowflake(): Promise<
|
|
20
|
-
return new Promise((resolve, reject) => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
64
|
+
export async function connectSnowflake(): Promise<Connection> {
|
|
65
|
+
return new Promise(async (resolve, reject) => {
|
|
66
|
+
try {
|
|
67
|
+
const connection = await createSnowflakeConnection()
|
|
68
|
+
connection.connect((err, conn) => {
|
|
69
|
+
if (err) reject(err)
|
|
70
|
+
else resolve(conn)
|
|
71
|
+
})
|
|
72
|
+
} catch (error) {
|
|
73
|
+
reject(error)
|
|
74
|
+
}
|
|
26
75
|
})
|
|
27
76
|
}
|
|
28
77
|
|
|
29
78
|
export async function executeQuery<T = unknown>(
|
|
30
|
-
connection:
|
|
79
|
+
connection: Connection,
|
|
31
80
|
sqlText: string,
|
|
32
81
|
binds?: Bind[]
|
|
33
82
|
): Promise<T[]> {
|
|
@@ -43,7 +92,7 @@ export async function executeQuery<T = unknown>(
|
|
|
43
92
|
})
|
|
44
93
|
}
|
|
45
94
|
|
|
46
|
-
export async function destroyConnection(connection:
|
|
95
|
+
export async function destroyConnection(connection: Connection): Promise<void> {
|
|
47
96
|
return new Promise((resolve, reject) => {
|
|
48
97
|
connection.destroy((err) => {
|
|
49
98
|
if (err) reject(err)
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { Hono } from 'hono'
|
|
2
|
-
import {
|
|
3
|
-
import { healthCheckTests } from '@/server/db/schema/health-checks'
|
|
4
|
-
import { eq } from 'drizzle-orm'
|
|
2
|
+
import { supabaseAdmin } from '@/lib/supabase/admin'
|
|
5
3
|
|
|
6
4
|
export const databaseHealthRoutes = new Hono()
|
|
7
5
|
|
|
@@ -12,24 +10,26 @@ databaseHealthRoutes.post('/write', async (c) => {
|
|
|
12
10
|
|
|
13
11
|
try {
|
|
14
12
|
// Delete any existing test row first
|
|
15
|
-
await
|
|
13
|
+
await supabaseAdmin.from('health_check_tests').delete().eq('test_key', TEST_KEY)
|
|
16
14
|
|
|
17
15
|
// Insert a new test row
|
|
18
|
-
const result = await
|
|
19
|
-
.
|
|
20
|
-
.
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
const { data: result, error } = await supabaseAdmin
|
|
17
|
+
.from('health_check_tests')
|
|
18
|
+
.insert({
|
|
19
|
+
test_key: TEST_KEY,
|
|
20
|
+
test_value: `test-${Date.now()}`,
|
|
23
21
|
})
|
|
24
|
-
.
|
|
22
|
+
.select()
|
|
23
|
+
|
|
24
|
+
if (error) throw error
|
|
25
25
|
|
|
26
26
|
return c.json({
|
|
27
27
|
status: 'healthy',
|
|
28
28
|
responseTimeMs: Math.round(performance.now() - start),
|
|
29
29
|
message: 'Successfully wrote test row to database',
|
|
30
30
|
data: {
|
|
31
|
-
id: result[0]?.id,
|
|
32
|
-
testKey: result[0]?.
|
|
31
|
+
id: result?.[0]?.id,
|
|
32
|
+
testKey: result?.[0]?.test_key,
|
|
33
33
|
},
|
|
34
34
|
})
|
|
35
35
|
} catch (error) {
|
|
@@ -48,13 +48,15 @@ databaseHealthRoutes.get('/read', async (c) => {
|
|
|
48
48
|
const start = performance.now()
|
|
49
49
|
|
|
50
50
|
try {
|
|
51
|
-
const result = await
|
|
52
|
-
.
|
|
53
|
-
.
|
|
54
|
-
.
|
|
51
|
+
const { data: result, error } = await supabaseAdmin
|
|
52
|
+
.from('health_check_tests')
|
|
53
|
+
.select('*')
|
|
54
|
+
.eq('test_key', TEST_KEY)
|
|
55
55
|
.limit(1)
|
|
56
56
|
|
|
57
|
-
if (
|
|
57
|
+
if (error) throw error
|
|
58
|
+
|
|
59
|
+
if (!result || result.length === 0) {
|
|
58
60
|
return c.json({
|
|
59
61
|
status: 'healthy',
|
|
60
62
|
responseTimeMs: Math.round(performance.now() - start),
|
|
@@ -70,8 +72,8 @@ databaseHealthRoutes.get('/read', async (c) => {
|
|
|
70
72
|
data: {
|
|
71
73
|
found: true,
|
|
72
74
|
id: result[0]?.id,
|
|
73
|
-
testKey: result[0]?.
|
|
74
|
-
createdAt: result[0]?.
|
|
75
|
+
testKey: result[0]?.test_key,
|
|
76
|
+
createdAt: result[0]?.created_at,
|
|
75
77
|
},
|
|
76
78
|
})
|
|
77
79
|
} catch (error) {
|
|
@@ -90,18 +92,39 @@ databaseHealthRoutes.delete('/delete', async (c) => {
|
|
|
90
92
|
const start = performance.now()
|
|
91
93
|
|
|
92
94
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
.
|
|
96
|
-
.
|
|
95
|
+
// Check if row exists first
|
|
96
|
+
const { data: result, error: readError } = await supabaseAdmin
|
|
97
|
+
.from('health_check_tests')
|
|
98
|
+
.select('*')
|
|
99
|
+
.eq('test_key', TEST_KEY)
|
|
100
|
+
.limit(1)
|
|
101
|
+
|
|
102
|
+
if (readError) throw readError
|
|
103
|
+
|
|
104
|
+
if (!result || result.length === 0) {
|
|
105
|
+
return c.json({
|
|
106
|
+
status: 'healthy',
|
|
107
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
108
|
+
message: 'Database connection successful, but no test row to delete',
|
|
109
|
+
data: { deleted: false },
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Delete the row
|
|
114
|
+
const { error: deleteError } = await supabaseAdmin
|
|
115
|
+
.from('health_check_tests')
|
|
116
|
+
.delete()
|
|
117
|
+
.eq('test_key', TEST_KEY)
|
|
118
|
+
|
|
119
|
+
if (deleteError) throw deleteError
|
|
97
120
|
|
|
98
121
|
return c.json({
|
|
99
122
|
status: 'healthy',
|
|
100
123
|
responseTimeMs: Math.round(performance.now() - start),
|
|
101
124
|
message: 'Successfully deleted test row from database',
|
|
102
125
|
data: {
|
|
103
|
-
deleted:
|
|
104
|
-
|
|
126
|
+
deleted: true,
|
|
127
|
+
id: result[0]?.id,
|
|
105
128
|
},
|
|
106
129
|
})
|
|
107
130
|
} catch (error) {
|
|
@@ -115,3 +138,4 @@ databaseHealthRoutes.delete('/delete', async (c) => {
|
|
|
115
138
|
)
|
|
116
139
|
}
|
|
117
140
|
})
|
|
141
|
+
|
|
@@ -8,27 +8,31 @@ edgeFunctionsHealthRoutes.get('/ping', async (c) => {
|
|
|
8
8
|
const start = performance.now()
|
|
9
9
|
|
|
10
10
|
try {
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// For now, we'll simulate a successful ping by checking the Supabase connection
|
|
16
|
-
const { data, error } = await supabaseAdmin
|
|
17
|
-
.from('_test_connection_')
|
|
18
|
-
.select('*')
|
|
19
|
-
.limit(1)
|
|
11
|
+
// Invoke the 'health-check' edge function
|
|
12
|
+
const { data, error } = await supabaseAdmin.functions.invoke('health-check', {
|
|
13
|
+
body: { message: 'ping' },
|
|
14
|
+
})
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
if (error) {
|
|
17
|
+
// Return detailed error information
|
|
18
|
+
const errorDetails = error as any
|
|
19
|
+
return c.json({
|
|
20
|
+
status: 'error',
|
|
21
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
22
|
+
message: 'Edge Function invocation failed',
|
|
23
|
+
data: {
|
|
24
|
+
error: error.message,
|
|
25
|
+
context: errorDetails.context || 'No context available',
|
|
26
|
+
hint: 'Ensure the function is deployed: "supabase functions deploy health-check"'
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
}
|
|
23
30
|
|
|
24
31
|
return c.json({
|
|
25
32
|
status: 'healthy',
|
|
26
33
|
responseTimeMs: Math.round(performance.now() - start),
|
|
27
|
-
message: 'Successfully
|
|
28
|
-
data
|
|
29
|
-
note: 'This is a placeholder - deploy an actual edge function to test real functionality',
|
|
30
|
-
supabaseUrl: env.NEXT_PUBLIC_SUPABASE_URL,
|
|
31
|
-
},
|
|
34
|
+
message: 'Successfully invoked edge function',
|
|
35
|
+
data,
|
|
32
36
|
})
|
|
33
37
|
} catch (error) {
|
|
34
38
|
return c.json(
|
|
@@ -46,28 +50,56 @@ edgeFunctionsHealthRoutes.get('/auth', async (c) => {
|
|
|
46
50
|
const start = performance.now()
|
|
47
51
|
|
|
48
52
|
try {
|
|
49
|
-
//
|
|
50
|
-
// In a real setup, you would invoke an edge function with an auth header
|
|
51
|
-
// and verify that the function can access and validate the auth token
|
|
52
|
-
|
|
53
|
-
// For now, we'll verify that we can create a valid auth header
|
|
53
|
+
// Verify that we can create a valid auth header and invoke the function
|
|
54
54
|
const authHeader = c.req.header('authorization')
|
|
55
55
|
|
|
56
|
+
// If no auth header is provided to the health check endpoint, we can't test the downstream auth
|
|
57
|
+
if (!authHeader) {
|
|
58
|
+
return c.json({
|
|
59
|
+
status: 'warning', // Changed from error to warning/skipped if user isn't logged in
|
|
60
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
61
|
+
message: 'No auth header provided - skipping auth propagation check',
|
|
62
|
+
data: {
|
|
63
|
+
skipped: true,
|
|
64
|
+
reason: 'Client request did not include Authorization header',
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { data, error } = await supabaseAdmin.functions.invoke('health-check', {
|
|
70
|
+
body: { message: 'auth-check' },
|
|
71
|
+
headers: {
|
|
72
|
+
// We send the auth header in a custom header to verify it can be passed through
|
|
73
|
+
// We avoid overriding 'Authorization' to prevent Supabase Gateway from rejecting
|
|
74
|
+
// the request if the Clerk token hasn't been configured in Supabase yet.
|
|
75
|
+
'x-test-auth': authHeader,
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
if (error) {
|
|
80
|
+
// If we get an error from the function, it might be due to auth validation failure
|
|
81
|
+
// or the function crashing. We want to expose the error message.
|
|
82
|
+
const errorDetails = error as any
|
|
83
|
+
throw new Error(`Edge function invocation failed: ${error.message || 'Unknown error'}`)
|
|
84
|
+
}
|
|
85
|
+
|
|
56
86
|
return c.json({
|
|
57
87
|
status: 'healthy',
|
|
58
88
|
responseTimeMs: Math.round(performance.now() - start),
|
|
59
|
-
message: 'Successfully verified auth header
|
|
60
|
-
data
|
|
61
|
-
hasAuthHeader: !!authHeader,
|
|
62
|
-
note: 'This is a placeholder - deploy an actual edge function to test real functionality',
|
|
63
|
-
},
|
|
89
|
+
message: 'Successfully verified auth header with edge function',
|
|
90
|
+
data,
|
|
64
91
|
})
|
|
65
92
|
} catch (error) {
|
|
93
|
+
// Check if the error is due to a 401/403 from the function itself (which would come as an error response data if handled,
|
|
94
|
+
// or thrown if using invoke with certain options, but here likely a re-throw)
|
|
66
95
|
return c.json(
|
|
67
96
|
{
|
|
68
97
|
status: 'error',
|
|
69
98
|
responseTimeMs: Math.round(performance.now() - start),
|
|
70
99
|
error: error instanceof Error ? error.message : 'Failed to test auth header',
|
|
100
|
+
data: {
|
|
101
|
+
details: error instanceof Error ? error.stack : undefined
|
|
102
|
+
}
|
|
71
103
|
},
|
|
72
104
|
500
|
|
73
105
|
)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Hono } from 'hono'
|
|
2
|
-
import {
|
|
3
|
-
import { sql } from 'drizzle-orm'
|
|
2
|
+
import { supabaseAdmin } from '@/lib/supabase/admin'
|
|
4
3
|
|
|
5
4
|
export const frameworkHealthRoutes = new Hono()
|
|
6
5
|
|
|
@@ -20,15 +19,19 @@ frameworkHealthRoutes.get('/hono', (c) => {
|
|
|
20
19
|
frameworkHealthRoutes.get('/drizzle', async (c) => {
|
|
21
20
|
const start = performance.now()
|
|
22
21
|
try {
|
|
23
|
-
//
|
|
24
|
-
|
|
22
|
+
// Note: Edge Runtime doesn't support direct Drizzle + postgres.js connection
|
|
23
|
+
// We check Supabase connection instead as a proxy for DB health
|
|
24
|
+
const { error } = await supabaseAdmin.from('health_check_tests').select('count').limit(1)
|
|
25
|
+
|
|
26
|
+
if (error) throw error
|
|
25
27
|
|
|
26
28
|
return c.json({
|
|
27
29
|
status: 'healthy',
|
|
28
30
|
responseTimeMs: Math.round(performance.now() - start),
|
|
29
|
-
message: '
|
|
31
|
+
message: 'Database access via Supabase Client is working (Edge compatible)',
|
|
30
32
|
data: {
|
|
31
|
-
|
|
33
|
+
mode: 'http-client',
|
|
34
|
+
platform: 'supabase',
|
|
32
35
|
},
|
|
33
36
|
})
|
|
34
37
|
} catch (error) {
|
|
@@ -36,7 +39,7 @@ frameworkHealthRoutes.get('/drizzle', async (c) => {
|
|
|
36
39
|
{
|
|
37
40
|
status: 'error',
|
|
38
41
|
responseTimeMs: Math.round(performance.now() - start),
|
|
39
|
-
error: error instanceof Error ? error.message : '
|
|
42
|
+
error: error instanceof Error ? error.message : 'Database check failed',
|
|
40
43
|
},
|
|
41
44
|
500
|
|
42
45
|
)
|
|
@@ -7,7 +7,7 @@ export const nextbankHealthRoutes = new Hono()
|
|
|
7
7
|
nextbankHealthRoutes.get('/ping', async (c) => {
|
|
8
8
|
const start = performance.now()
|
|
9
9
|
|
|
10
|
-
if (!env.
|
|
10
|
+
if (!env.NEXTBANK_API || env.NEXTBANK_API === 'placeholder') {
|
|
11
11
|
return c.json({
|
|
12
12
|
status: 'unconfigured',
|
|
13
13
|
responseTimeMs: Math.round(performance.now() - start),
|
|
@@ -15,43 +15,22 @@ nextbankHealthRoutes.get('/ping', async (c) => {
|
|
|
15
15
|
})
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
data: result,
|
|
25
|
-
})
|
|
26
|
-
} catch (error) {
|
|
27
|
-
return c.json(
|
|
28
|
-
{
|
|
29
|
-
status: 'error',
|
|
30
|
-
responseTimeMs: Math.round(performance.now() - start),
|
|
31
|
-
error: error instanceof Error ? error.message : 'Ping failed',
|
|
32
|
-
},
|
|
33
|
-
500
|
|
34
|
-
)
|
|
35
|
-
}
|
|
36
|
-
})
|
|
18
|
+
// Generate fingerprint based on request headers
|
|
19
|
+
const userAgent = c.req.header('user-agent') || 'unknown'
|
|
20
|
+
const ipAddress = c.req.header('x-forwarded-for')?.split(',')[0] ||
|
|
21
|
+
c.req.header('x-real-ip') ||
|
|
22
|
+
'127.0.0.1'
|
|
23
|
+
const acceptLanguage = c.req.header('accept-language') || ''
|
|
37
24
|
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
if (!env.NEXTBANK_CLIENT_ID || env.NEXTBANK_CLIENT_ID === 'placeholder') {
|
|
42
|
-
return c.json({
|
|
43
|
-
status: 'unconfigured',
|
|
44
|
-
responseTimeMs: Math.round(performance.now() - start),
|
|
45
|
-
message: 'NextBank OAuth not configured',
|
|
46
|
-
})
|
|
47
|
-
}
|
|
25
|
+
const fingerprintString = `${userAgent}-${ipAddress}-${acceptLanguage}`
|
|
26
|
+
const fingerprint = Buffer.from(fingerprintString).toString('base64')
|
|
48
27
|
|
|
49
28
|
try {
|
|
50
|
-
const result = await nextbankClient.
|
|
29
|
+
const result = await nextbankClient.ping(fingerprint)
|
|
51
30
|
return c.json({
|
|
52
31
|
status: 'healthy',
|
|
53
32
|
responseTimeMs: Math.round(performance.now() - start),
|
|
54
|
-
message: 'Successfully
|
|
33
|
+
message: 'Successfully pinged NextBank API',
|
|
55
34
|
data: result,
|
|
56
35
|
})
|
|
57
36
|
} catch (error) {
|
|
@@ -59,7 +38,7 @@ nextbankHealthRoutes.get('/auth', async (c) => {
|
|
|
59
38
|
{
|
|
60
39
|
status: 'error',
|
|
61
40
|
responseTimeMs: Math.round(performance.now() - start),
|
|
62
|
-
error: error instanceof Error ? error.message : '
|
|
41
|
+
error: error instanceof Error ? error.message : 'Ping failed',
|
|
63
42
|
},
|
|
64
43
|
500
|
|
65
44
|
)
|
|
@@ -26,8 +26,8 @@ snowflakeHealthRoutes.get('/connect', async (c) => {
|
|
|
26
26
|
data: {
|
|
27
27
|
account: env.SNOWFLAKE_ACCOUNT,
|
|
28
28
|
warehouse: env.SNOWFLAKE_WAREHOUSE,
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
authenticator: env.SNOWFLAKE_AUTHENTICATOR,
|
|
30
|
+
role: env.SNOWFLAKE_ROLE,
|
|
31
31
|
},
|
|
32
32
|
})
|
|
33
33
|
} catch (error) {
|
|
@@ -86,6 +86,20 @@ storageHealthRoutes.get('/download', async (c) => {
|
|
|
86
86
|
.from(BUCKET_NAME)
|
|
87
87
|
.createSignedUrl(TEST_FILE_PATH, 60)
|
|
88
88
|
|
|
89
|
+
// Handle "Object not found" as a success state for health check
|
|
90
|
+
// since connectivity is working, but the file just isn't there
|
|
91
|
+
if (error && error.message.includes('Object not found')) {
|
|
92
|
+
return c.json({
|
|
93
|
+
status: 'healthy',
|
|
94
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
95
|
+
message: 'Storage connection successful, but no test file found to download',
|
|
96
|
+
data: {
|
|
97
|
+
found: false,
|
|
98
|
+
path: TEST_FILE_PATH,
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
89
103
|
if (error) throw error
|
|
90
104
|
|
|
91
105
|
return c.json({
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { Hono } from 'hono'
|
|
2
|
-
import {
|
|
3
|
-
import { users } from '@/server/db/schema/users'
|
|
4
|
-
import { eq, desc } from 'drizzle-orm'
|
|
2
|
+
import { supabaseAdmin } from '@/lib/supabase/admin'
|
|
5
3
|
|
|
6
4
|
export const usersRoutes = new Hono()
|
|
7
5
|
|
|
@@ -9,21 +7,14 @@ usersRoutes.get('/', async (c) => {
|
|
|
9
7
|
const start = performance.now()
|
|
10
8
|
|
|
11
9
|
try {
|
|
12
|
-
const allUsers = await
|
|
13
|
-
.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
firstName: users.firstName,
|
|
17
|
-
lastName: users.lastName,
|
|
18
|
-
imageUrl: users.imageUrl,
|
|
19
|
-
clerkOrgId: users.clerkOrgId,
|
|
20
|
-
createdAt: users.createdAt,
|
|
21
|
-
updatedAt: users.updatedAt,
|
|
22
|
-
})
|
|
23
|
-
.from(users)
|
|
24
|
-
.orderBy(desc(users.createdAt))
|
|
10
|
+
const { data: allUsers, error } = await supabaseAdmin
|
|
11
|
+
.from('users')
|
|
12
|
+
.select('id, email, first_name, last_name, image_url, clerk_org_id, created_at, updated_at')
|
|
13
|
+
.order('created_at', { ascending: false })
|
|
25
14
|
.limit(100)
|
|
26
15
|
|
|
16
|
+
if (error) throw error
|
|
17
|
+
|
|
27
18
|
return c.json({
|
|
28
19
|
status: 'healthy',
|
|
29
20
|
responseTimeMs: Math.round(performance.now() - start),
|
|
@@ -50,22 +41,15 @@ usersRoutes.get('/:id', async (c) => {
|
|
|
50
41
|
const id = c.req.param('id')
|
|
51
42
|
|
|
52
43
|
try {
|
|
53
|
-
const user = await
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
firstName: users.firstName,
|
|
58
|
-
lastName: users.lastName,
|
|
59
|
-
imageUrl: users.imageUrl,
|
|
60
|
-
clerkOrgId: users.clerkOrgId,
|
|
61
|
-
createdAt: users.createdAt,
|
|
62
|
-
updatedAt: users.updatedAt,
|
|
63
|
-
})
|
|
64
|
-
.from(users)
|
|
65
|
-
.where(eq(users.id, id))
|
|
44
|
+
const { data: user, error } = await supabaseAdmin
|
|
45
|
+
.from('users')
|
|
46
|
+
.select('id, email, first_name, last_name, image_url, clerk_org_id, created_at, updated_at')
|
|
47
|
+
.eq('id', id)
|
|
66
48
|
.limit(1)
|
|
67
49
|
|
|
68
|
-
if (
|
|
50
|
+
if (error) throw error
|
|
51
|
+
|
|
52
|
+
if (!user || user.length === 0) {
|
|
69
53
|
return c.json(
|
|
70
54
|
{
|
|
71
55
|
status: 'error',
|
|
@@ -1,26 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// https://orm.drizzle.team/docs/sql-schema-declaration
|
|
3
|
-
|
|
4
|
-
import { index, pgTableCreator } from "drizzle-orm/pg-core";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
|
8
|
-
* database instance for multiple projects.
|
|
9
|
-
*
|
|
10
|
-
* @see https://orm.drizzle.team/docs/goodies#multi-project-schema
|
|
11
|
-
*/
|
|
12
|
-
export const createTable = pgTableCreator((name) => `dtt-framework_${name}`);
|
|
13
|
-
|
|
14
|
-
export const posts = createTable(
|
|
15
|
-
"post",
|
|
16
|
-
(d) => ({
|
|
17
|
-
id: d.integer().primaryKey().generatedByDefaultAsIdentity(),
|
|
18
|
-
name: d.varchar({ length: 256 }),
|
|
19
|
-
createdAt: d
|
|
20
|
-
.timestamp({ withTimezone: true })
|
|
21
|
-
.$defaultFn(() => /* @__PURE__ */ new Date())
|
|
22
|
-
.notNull(),
|
|
23
|
-
updatedAt: d.timestamp({ withTimezone: true }).$onUpdate(() => new Date()),
|
|
24
|
-
}),
|
|
25
|
-
(t) => [index("name_idx").on(t.name)],
|
|
26
|
-
);
|
|
1
|
+
export * from "./schema/index";
|
package/template/tsconfig.json
CHANGED
|
@@ -39,5 +39,5 @@
|
|
|
39
39
|
"**/*.js",
|
|
40
40
|
".next/types/**/*.ts"
|
|
41
41
|
],
|
|
42
|
-
"exclude": ["node_modules", "generated", "cli", "src/test", "**/*.test.ts", "**/*.test.tsx", "vitest.config.ts", "drizzle.config.ts"]
|
|
42
|
+
"exclude": ["node_modules", "generated", "cli", "src/test", "**/*.test.ts", "**/*.test.tsx", "vitest.config.ts", "drizzle.config.ts", "supabase"]
|
|
43
43
|
}
|