@insureco/cli 0.1.10 → 0.1.11
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/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +6 -52
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +10 -1
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/rollback.d.ts.map +1 -1
- package/dist/commands/rollback.js +10 -35
- package/dist/commands/rollback.js.map +1 -1
- package/dist/commands/sample.d.ts +2 -0
- package/dist/commands/sample.d.ts.map +1 -1
- package/dist/commands/sample.js +140 -12
- package/dist/commands/sample.js.map +1 -1
- package/dist/commands/versions.d.ts.map +1 -1
- package/dist/commands/versions.js +10 -56
- package/dist/commands/versions.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/builder.d.ts.map +1 -1
- package/dist/lib/builder.js +46 -3
- package/dist/lib/builder.js.map +1 -1
- package/dist/lib/watch.d.ts +26 -0
- package/dist/lib/watch.d.ts.map +1 -0
- package/dist/lib/watch.js +136 -0
- package/dist/lib/watch.js.map +1 -0
- package/dist/templates/nextjs-oauth/.env.example +14 -0
- package/dist/templates/nextjs-oauth/Dockerfile +51 -0
- package/dist/templates/nextjs-oauth/README.md +128 -0
- package/dist/templates/nextjs-oauth/catalog-info.yaml +17 -0
- package/dist/templates/nextjs-oauth/helm/{{name}}/Chart.yaml +9 -0
- package/dist/templates/nextjs-oauth/helm/{{name}}/templates/deployment.yaml +68 -0
- package/dist/templates/nextjs-oauth/helm/{{name}}/templates/service.yaml +17 -0
- package/dist/templates/nextjs-oauth/helm/{{name}}/values.yaml +51 -0
- package/dist/templates/nextjs-oauth/next.config.js +23 -0
- package/dist/templates/nextjs-oauth/package.json +30 -0
- package/dist/templates/nextjs-oauth/public/.gitkeep +0 -0
- package/dist/templates/nextjs-oauth/src/app/api/auth/callback/route.ts +64 -0
- package/dist/templates/nextjs-oauth/src/app/api/auth/login/route.ts +16 -0
- package/dist/templates/nextjs-oauth/src/app/api/auth/logout/route.ts +9 -0
- package/dist/templates/nextjs-oauth/src/app/api/auth/session/route.ts +40 -0
- package/dist/templates/nextjs-oauth/src/app/api/example/route.ts +63 -0
- package/dist/templates/nextjs-oauth/src/app/api/health/route.ts +10 -0
- package/dist/templates/nextjs-oauth/src/app/dashboard/page.tsx +92 -0
- package/dist/templates/nextjs-oauth/src/app/layout.tsx +18 -0
- package/dist/templates/nextjs-oauth/src/app/page.tsx +110 -0
- package/dist/templates/nextjs-oauth/src/lib/auth.ts +274 -0
- package/dist/templates/nextjs-oauth/src/middleware.ts +70 -0
- package/dist/templates/nextjs-oauth/tsconfig.json +26 -0
- package/package.json +1 -1
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { cookies } from 'next/headers'
|
|
2
|
+
import { jwtVerify } from 'jose'
|
|
3
|
+
import crypto from 'crypto'
|
|
4
|
+
|
|
5
|
+
// Configuration — set these in .env
|
|
6
|
+
const BIO_ID_URL = process.env.BIO_ID_URL || 'http://localhost:6100'
|
|
7
|
+
const APP_URL = process.env.APP_URL || 'http://localhost:3000'
|
|
8
|
+
const CLIENT_ID = process.env.OAUTH_CLIENT_ID || ''
|
|
9
|
+
const CLIENT_SECRET = process.env.OAUTH_CLIENT_SECRET || ''
|
|
10
|
+
|
|
11
|
+
function getJwtSecret(): Uint8Array {
|
|
12
|
+
const secret = process.env.JWT_SECRET
|
|
13
|
+
if (!secret && process.env.NODE_ENV === 'production') {
|
|
14
|
+
throw new Error('JWT_SECRET environment variable is required in production')
|
|
15
|
+
}
|
|
16
|
+
return new TextEncoder().encode(secret || 'dev-only-secret-do-not-use-in-production')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const JWT_SECRET = getJwtSecret()
|
|
20
|
+
|
|
21
|
+
if (process.env.NODE_ENV === 'production' && (!CLIENT_ID || !CLIENT_SECRET)) {
|
|
22
|
+
throw new Error('OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET are required in production')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const COOKIE_PREFIX = '{{name}}'
|
|
26
|
+
const REFRESH_TOKEN_MAX_AGE = 60 * 60 * 24 * 30 // 30 days
|
|
27
|
+
const OAUTH_FLOW_MAX_AGE = 60 * 10 // 10 minutes
|
|
28
|
+
|
|
29
|
+
export interface User {
|
|
30
|
+
id: string // Maps to bio_id from Bio-id userinfo endpoint
|
|
31
|
+
email: string
|
|
32
|
+
name: string
|
|
33
|
+
roles: string[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validate that a returnTo path is safe (relative, no open redirect)
|
|
38
|
+
*/
|
|
39
|
+
export function sanitizeReturnTo(returnTo: string | undefined, fallback = '/dashboard'): string {
|
|
40
|
+
if (!returnTo || !returnTo.startsWith('/') || returnTo.startsWith('//')) {
|
|
41
|
+
return fallback
|
|
42
|
+
}
|
|
43
|
+
return returnTo
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate PKCE code verifier and S256 challenge
|
|
48
|
+
*/
|
|
49
|
+
export function generatePKCE(): { codeVerifier: string; codeChallenge: string } {
|
|
50
|
+
const codeVerifier = crypto.randomBytes(32).toString('base64url')
|
|
51
|
+
const codeChallenge = crypto
|
|
52
|
+
.createHash('sha256')
|
|
53
|
+
.update(codeVerifier)
|
|
54
|
+
.digest('base64url')
|
|
55
|
+
|
|
56
|
+
return { codeVerifier, codeChallenge }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build Bio-id authorization URL
|
|
61
|
+
*/
|
|
62
|
+
export function getAuthorizationUrl(state: string, codeChallenge: string): string {
|
|
63
|
+
const params = new URLSearchParams({
|
|
64
|
+
client_id: CLIENT_ID,
|
|
65
|
+
redirect_uri: `${APP_URL}/api/auth/callback`,
|
|
66
|
+
response_type: 'code',
|
|
67
|
+
scope: 'openid profile email',
|
|
68
|
+
state,
|
|
69
|
+
code_challenge: codeChallenge,
|
|
70
|
+
code_challenge_method: 'S256',
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
return `${BIO_ID_URL}/oauth/authorize?${params.toString()}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function parseErrorResponse(response: Response, fallbackMessage: string): Promise<string> {
|
|
77
|
+
try {
|
|
78
|
+
const error = await response.json()
|
|
79
|
+
return error.error_description || error.error || fallbackMessage
|
|
80
|
+
} catch {
|
|
81
|
+
return fallbackMessage
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Exchange authorization code for tokens
|
|
87
|
+
*/
|
|
88
|
+
export async function exchangeCodeForTokens(
|
|
89
|
+
code: string,
|
|
90
|
+
codeVerifier: string
|
|
91
|
+
): Promise<{
|
|
92
|
+
access_token: string
|
|
93
|
+
token_type: string
|
|
94
|
+
expires_in: number
|
|
95
|
+
refresh_token: string
|
|
96
|
+
scope: string
|
|
97
|
+
id_token?: string
|
|
98
|
+
}> {
|
|
99
|
+
const response = await fetch(`${BIO_ID_URL}/api/oauth/token`, {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
102
|
+
body: new URLSearchParams({
|
|
103
|
+
grant_type: 'authorization_code',
|
|
104
|
+
code,
|
|
105
|
+
redirect_uri: `${APP_URL}/api/auth/callback`,
|
|
106
|
+
client_id: CLIENT_ID,
|
|
107
|
+
client_secret: CLIENT_SECRET,
|
|
108
|
+
code_verifier: codeVerifier,
|
|
109
|
+
}).toString(),
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
throw new Error(await parseErrorResponse(response, 'Token exchange failed'))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return response.json()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Refresh an expired access token
|
|
121
|
+
*/
|
|
122
|
+
export async function refreshAccessToken(refreshToken: string): Promise<{
|
|
123
|
+
access_token: string
|
|
124
|
+
token_type: string
|
|
125
|
+
expires_in: number
|
|
126
|
+
refresh_token: string
|
|
127
|
+
scope: string
|
|
128
|
+
}> {
|
|
129
|
+
const response = await fetch(`${BIO_ID_URL}/api/oauth/token`, {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
132
|
+
body: new URLSearchParams({
|
|
133
|
+
grant_type: 'refresh_token',
|
|
134
|
+
refresh_token: refreshToken,
|
|
135
|
+
client_id: CLIENT_ID,
|
|
136
|
+
client_secret: CLIENT_SECRET,
|
|
137
|
+
}).toString(),
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
throw new Error(await parseErrorResponse(response, 'Token refresh failed'))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return response.json()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Fetch user info from Bio-id userinfo endpoint
|
|
149
|
+
*/
|
|
150
|
+
export async function fetchUserInfo(accessToken: string): Promise<User> {
|
|
151
|
+
const response = await fetch(`${BIO_ID_URL}/api/oauth/userinfo`, {
|
|
152
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
throw new Error('Failed to fetch user info')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const data = await response.json()
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
id: data.bio_id || data.sub,
|
|
163
|
+
email: data.email,
|
|
164
|
+
name: data.name,
|
|
165
|
+
roles: data.roles || [],
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Set httpOnly auth cookies
|
|
171
|
+
*/
|
|
172
|
+
export async function setAuthCookies(
|
|
173
|
+
accessToken: string,
|
|
174
|
+
refreshToken: string,
|
|
175
|
+
expiresIn: number
|
|
176
|
+
): Promise<void> {
|
|
177
|
+
const cookieStore = await cookies()
|
|
178
|
+
const secure = process.env.NODE_ENV === 'production'
|
|
179
|
+
|
|
180
|
+
cookieStore.set(`${COOKIE_PREFIX}_access_token`, accessToken, {
|
|
181
|
+
httpOnly: true,
|
|
182
|
+
secure,
|
|
183
|
+
sameSite: 'lax',
|
|
184
|
+
maxAge: expiresIn,
|
|
185
|
+
path: '/',
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
cookieStore.set(`${COOKIE_PREFIX}_refresh_token`, refreshToken, {
|
|
189
|
+
httpOnly: true,
|
|
190
|
+
secure,
|
|
191
|
+
sameSite: 'lax',
|
|
192
|
+
maxAge: REFRESH_TOKEN_MAX_AGE,
|
|
193
|
+
path: '/',
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Clear all auth cookies
|
|
199
|
+
*/
|
|
200
|
+
export async function clearAuthCookies(): Promise<void> {
|
|
201
|
+
const cookieStore = await cookies()
|
|
202
|
+
cookieStore.delete(`${COOKIE_PREFIX}_access_token`)
|
|
203
|
+
cookieStore.delete(`${COOKIE_PREFIX}_refresh_token`)
|
|
204
|
+
cookieStore.delete('oauth_state')
|
|
205
|
+
cookieStore.delete('oauth_code_verifier')
|
|
206
|
+
cookieStore.delete('oauth_return_to')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get the current authenticated user from JWT payload (no network call)
|
|
211
|
+
*/
|
|
212
|
+
export async function getCurrentUser(): Promise<User | null> {
|
|
213
|
+
const cookieStore = await cookies()
|
|
214
|
+
const accessToken = cookieStore.get(`${COOKIE_PREFIX}_access_token`)?.value
|
|
215
|
+
|
|
216
|
+
if (!accessToken) {
|
|
217
|
+
return null
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const { payload } = await jwtVerify(accessToken, JWT_SECRET, {
|
|
222
|
+
issuer: BIO_ID_URL,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
id: (payload.bio_id as string) || payload.sub || '',
|
|
227
|
+
email: payload.email as string,
|
|
228
|
+
name: payload.name as string,
|
|
229
|
+
roles: (payload.roles as string[]) || [],
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
return null
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Generate a login URL with PKCE and state stored in cookies
|
|
238
|
+
*/
|
|
239
|
+
export async function getLoginUrl(returnTo?: string): Promise<string> {
|
|
240
|
+
const cookieStore = await cookies()
|
|
241
|
+
const secure = process.env.NODE_ENV === 'production'
|
|
242
|
+
|
|
243
|
+
const state = crypto.randomBytes(16).toString('hex')
|
|
244
|
+
const { codeVerifier, codeChallenge } = generatePKCE()
|
|
245
|
+
|
|
246
|
+
cookieStore.set('oauth_state', state, {
|
|
247
|
+
httpOnly: true,
|
|
248
|
+
secure,
|
|
249
|
+
sameSite: 'lax',
|
|
250
|
+
maxAge: OAUTH_FLOW_MAX_AGE,
|
|
251
|
+
path: '/',
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
cookieStore.set('oauth_code_verifier', codeVerifier, {
|
|
255
|
+
httpOnly: true,
|
|
256
|
+
secure,
|
|
257
|
+
sameSite: 'lax',
|
|
258
|
+
maxAge: OAUTH_FLOW_MAX_AGE,
|
|
259
|
+
path: '/',
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
const safeReturnTo = sanitizeReturnTo(returnTo)
|
|
263
|
+
cookieStore.set('oauth_return_to', safeReturnTo, {
|
|
264
|
+
httpOnly: true,
|
|
265
|
+
secure,
|
|
266
|
+
sameSite: 'lax',
|
|
267
|
+
maxAge: OAUTH_FLOW_MAX_AGE,
|
|
268
|
+
path: '/',
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
return getAuthorizationUrl(state, codeChallenge)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export { COOKIE_PREFIX, JWT_SECRET, BIO_ID_URL }
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { jwtVerify } from 'jose'
|
|
3
|
+
|
|
4
|
+
const COOKIE_PREFIX = '{{name}}'
|
|
5
|
+
|
|
6
|
+
// Routes that require authentication
|
|
7
|
+
const PROTECTED_PATHS = ['/dashboard']
|
|
8
|
+
|
|
9
|
+
// Routes that should never be blocked
|
|
10
|
+
const PUBLIC_PATHS = [
|
|
11
|
+
'/',
|
|
12
|
+
'/api/auth',
|
|
13
|
+
'/api/health',
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
function isPublicPath(pathname: string): boolean {
|
|
17
|
+
return PUBLIC_PATHS.some(
|
|
18
|
+
(path) => pathname === path || pathname.startsWith(`${path}/`)
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isProtectedPath(pathname: string): boolean {
|
|
23
|
+
return PROTECTED_PATHS.some(
|
|
24
|
+
(path) => pathname === path || pathname.startsWith(`${path}/`)
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function middleware(request: NextRequest) {
|
|
29
|
+
const { pathname } = request.nextUrl
|
|
30
|
+
|
|
31
|
+
if (isPublicPath(pathname)) {
|
|
32
|
+
return NextResponse.next()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!isProtectedPath(pathname)) {
|
|
36
|
+
return NextResponse.next()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const accessToken = request.cookies.get(`${COOKIE_PREFIX}_access_token`)?.value
|
|
40
|
+
|
|
41
|
+
if (!accessToken) {
|
|
42
|
+
const loginUrl = new URL('/api/auth/login', request.url)
|
|
43
|
+
loginUrl.searchParams.set('returnTo', pathname)
|
|
44
|
+
return NextResponse.redirect(loginUrl)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Verify JWT is valid and not expired
|
|
48
|
+
try {
|
|
49
|
+
const secret = new TextEncoder().encode(
|
|
50
|
+
process.env.JWT_SECRET || 'dev-only-secret-do-not-use-in-production'
|
|
51
|
+
)
|
|
52
|
+
await jwtVerify(accessToken, secret, {
|
|
53
|
+
issuer: process.env.BIO_ID_URL || 'http://localhost:6100',
|
|
54
|
+
})
|
|
55
|
+
} catch {
|
|
56
|
+
// Token invalid or expired — redirect to login
|
|
57
|
+
const loginUrl = new URL('/api/auth/login', request.url)
|
|
58
|
+
loginUrl.searchParams.set('returnTo', pathname)
|
|
59
|
+
return NextResponse.redirect(loginUrl)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return NextResponse.next()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const config = {
|
|
66
|
+
matcher: [
|
|
67
|
+
// Match all paths except static files and Next.js internals
|
|
68
|
+
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
|
69
|
+
],
|
|
70
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
4
|
+
"allowJs": true,
|
|
5
|
+
"skipLibCheck": true,
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"module": "esnext",
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"jsx": "preserve",
|
|
14
|
+
"incremental": true,
|
|
15
|
+
"plugins": [
|
|
16
|
+
{
|
|
17
|
+
"name": "next"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"paths": {
|
|
21
|
+
"@/*": ["./src/*"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
25
|
+
"exclude": ["node_modules"]
|
|
26
|
+
}
|