@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,68 @@
|
|
|
1
|
+
apiVersion: apps/v1
|
|
2
|
+
kind: Deployment
|
|
3
|
+
metadata:
|
|
4
|
+
name: {{ .Release.Name }}
|
|
5
|
+
labels:
|
|
6
|
+
app: {{ .Release.Name }}
|
|
7
|
+
app.kubernetes.io/name: {{ .Release.Name }}
|
|
8
|
+
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
9
|
+
spec:
|
|
10
|
+
replicas: {{ .Values.replicaCount }}
|
|
11
|
+
selector:
|
|
12
|
+
matchLabels:
|
|
13
|
+
app: {{ .Release.Name }}
|
|
14
|
+
template:
|
|
15
|
+
metadata:
|
|
16
|
+
labels:
|
|
17
|
+
app: {{ .Release.Name }}
|
|
18
|
+
app.kubernetes.io/name: {{ .Release.Name }}
|
|
19
|
+
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
20
|
+
spec:
|
|
21
|
+
{{- with .Values.imagePullSecrets }}
|
|
22
|
+
imagePullSecrets:
|
|
23
|
+
{{- toYaml . | nindent 8 }}
|
|
24
|
+
{{- end }}
|
|
25
|
+
containers:
|
|
26
|
+
- name: {{ .Chart.Name }}
|
|
27
|
+
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
|
28
|
+
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
|
29
|
+
ports:
|
|
30
|
+
- name: http
|
|
31
|
+
containerPort: {{ .Values.service.port }}
|
|
32
|
+
protocol: TCP
|
|
33
|
+
envFrom:
|
|
34
|
+
- configMapRef:
|
|
35
|
+
name: {{ .Release.Name }}-config
|
|
36
|
+
optional: true
|
|
37
|
+
- secretRef:
|
|
38
|
+
name: {{ .Release.Name }}-secrets
|
|
39
|
+
optional: true
|
|
40
|
+
{{- with .Values.env }}
|
|
41
|
+
env:
|
|
42
|
+
{{- range $key, $value := . }}
|
|
43
|
+
- name: {{ $key }}
|
|
44
|
+
value: {{ $value | quote }}
|
|
45
|
+
{{- end }}
|
|
46
|
+
{{- end }}
|
|
47
|
+
{{- with .Values.livenessProbe }}
|
|
48
|
+
livenessProbe:
|
|
49
|
+
{{- toYaml . | nindent 12 }}
|
|
50
|
+
{{- end }}
|
|
51
|
+
{{- with .Values.readinessProbe }}
|
|
52
|
+
readinessProbe:
|
|
53
|
+
{{- toYaml . | nindent 12 }}
|
|
54
|
+
{{- end }}
|
|
55
|
+
resources:
|
|
56
|
+
{{- toYaml .Values.resources | nindent 12 }}
|
|
57
|
+
{{- with .Values.nodeSelector }}
|
|
58
|
+
nodeSelector:
|
|
59
|
+
{{- toYaml . | nindent 8 }}
|
|
60
|
+
{{- end }}
|
|
61
|
+
{{- with .Values.affinity }}
|
|
62
|
+
affinity:
|
|
63
|
+
{{- toYaml . | nindent 8 }}
|
|
64
|
+
{{- end }}
|
|
65
|
+
{{- with .Values.tolerations }}
|
|
66
|
+
tolerations:
|
|
67
|
+
{{- toYaml . | nindent 8 }}
|
|
68
|
+
{{- end }}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
apiVersion: v1
|
|
2
|
+
kind: Service
|
|
3
|
+
metadata:
|
|
4
|
+
name: {{ .Release.Name }}
|
|
5
|
+
labels:
|
|
6
|
+
app: {{ .Release.Name }}
|
|
7
|
+
app.kubernetes.io/name: {{ .Release.Name }}
|
|
8
|
+
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
9
|
+
spec:
|
|
10
|
+
type: {{ .Values.service.type }}
|
|
11
|
+
ports:
|
|
12
|
+
- port: {{ .Values.service.port }}
|
|
13
|
+
targetPort: http
|
|
14
|
+
protocol: TCP
|
|
15
|
+
name: http
|
|
16
|
+
selector:
|
|
17
|
+
app: {{ .Release.Name }}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
replicaCount: 1
|
|
2
|
+
|
|
3
|
+
image:
|
|
4
|
+
repository: registry.digitalocean.com/insureco/{{name}}
|
|
5
|
+
tag: latest
|
|
6
|
+
pullPolicy: IfNotPresent
|
|
7
|
+
|
|
8
|
+
imagePullSecrets:
|
|
9
|
+
- name: do-registry
|
|
10
|
+
|
|
11
|
+
service:
|
|
12
|
+
type: ClusterIP
|
|
13
|
+
port: 3000
|
|
14
|
+
|
|
15
|
+
resources:
|
|
16
|
+
limits:
|
|
17
|
+
cpu: 500m
|
|
18
|
+
memory: 512Mi
|
|
19
|
+
requests:
|
|
20
|
+
cpu: 100m
|
|
21
|
+
memory: 128Mi
|
|
22
|
+
|
|
23
|
+
autoscaling:
|
|
24
|
+
enabled: false
|
|
25
|
+
minReplicas: 1
|
|
26
|
+
maxReplicas: 5
|
|
27
|
+
targetCPUUtilizationPercentage: 80
|
|
28
|
+
|
|
29
|
+
nodeSelector: {}
|
|
30
|
+
|
|
31
|
+
tolerations: []
|
|
32
|
+
|
|
33
|
+
affinity: {}
|
|
34
|
+
|
|
35
|
+
env: {}
|
|
36
|
+
|
|
37
|
+
secrets: {}
|
|
38
|
+
|
|
39
|
+
livenessProbe:
|
|
40
|
+
httpGet:
|
|
41
|
+
path: /api/health
|
|
42
|
+
port: 3000
|
|
43
|
+
initialDelaySeconds: 15
|
|
44
|
+
periodSeconds: 10
|
|
45
|
+
|
|
46
|
+
readinessProbe:
|
|
47
|
+
httpGet:
|
|
48
|
+
path: /api/health
|
|
49
|
+
port: 3000
|
|
50
|
+
initialDelaySeconds: 5
|
|
51
|
+
periodSeconds: 5
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** @type {import('next').NextConfig} */
|
|
2
|
+
const nextConfig = {
|
|
3
|
+
output: 'standalone',
|
|
4
|
+
reactStrictMode: true,
|
|
5
|
+
poweredByHeader: false,
|
|
6
|
+
|
|
7
|
+
// Enable experimental features as needed
|
|
8
|
+
// experimental: {
|
|
9
|
+
// serverActions: true,
|
|
10
|
+
// },
|
|
11
|
+
|
|
12
|
+
// Configure allowed image domains
|
|
13
|
+
// images: {
|
|
14
|
+
// remotePatterns: [
|
|
15
|
+
// {
|
|
16
|
+
// protocol: 'https',
|
|
17
|
+
// hostname: '*.insureco.io',
|
|
18
|
+
// },
|
|
19
|
+
// ],
|
|
20
|
+
// },
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = nextConfig
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "{{description}}",
|
|
5
|
+
"private": true,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "next dev",
|
|
8
|
+
"build": "next build",
|
|
9
|
+
"start": "next start",
|
|
10
|
+
"lint": "next lint",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"next": "^14.2.0",
|
|
15
|
+
"react": "^18.3.0",
|
|
16
|
+
"react-dom": "^18.3.0",
|
|
17
|
+
"jose": "^5.9.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.0.0",
|
|
21
|
+
"@types/react": "^18.3.0",
|
|
22
|
+
"@types/react-dom": "^18.3.0",
|
|
23
|
+
"eslint": "^9.0.0",
|
|
24
|
+
"eslint-config-next": "^14.2.0",
|
|
25
|
+
"typescript": "^5.7.0"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20.0.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { cookies } from 'next/headers'
|
|
3
|
+
import { exchangeCodeForTokens, setAuthCookies, sanitizeReturnTo } from '@/lib/auth'
|
|
4
|
+
|
|
5
|
+
const APP_URL = process.env.APP_URL || 'http://localhost:3000'
|
|
6
|
+
|
|
7
|
+
export async function GET(request: NextRequest) {
|
|
8
|
+
const searchParams = request.nextUrl.searchParams
|
|
9
|
+
const code = searchParams.get('code')
|
|
10
|
+
const state = searchParams.get('state')
|
|
11
|
+
const error = searchParams.get('error')
|
|
12
|
+
const errorDescription = searchParams.get('error_description')
|
|
13
|
+
|
|
14
|
+
const cookieStore = await cookies()
|
|
15
|
+
const storedState = cookieStore.get('oauth_state')?.value
|
|
16
|
+
const codeVerifier = cookieStore.get('oauth_code_verifier')?.value
|
|
17
|
+
const returnTo = cookieStore.get('oauth_return_to')?.value || '/dashboard'
|
|
18
|
+
|
|
19
|
+
// Clear OAuth flow cookies
|
|
20
|
+
cookieStore.delete('oauth_state')
|
|
21
|
+
cookieStore.delete('oauth_code_verifier')
|
|
22
|
+
cookieStore.delete('oauth_return_to')
|
|
23
|
+
|
|
24
|
+
// Handle error from authorization server
|
|
25
|
+
if (error) {
|
|
26
|
+
const errorUrl = new URL('/', APP_URL)
|
|
27
|
+
errorUrl.searchParams.set('error', error)
|
|
28
|
+
if (errorDescription) {
|
|
29
|
+
errorUrl.searchParams.set('error_description', errorDescription)
|
|
30
|
+
}
|
|
31
|
+
return NextResponse.redirect(errorUrl)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Validate state to prevent CSRF
|
|
35
|
+
if (!state || state !== storedState) {
|
|
36
|
+
const errorUrl = new URL('/', APP_URL)
|
|
37
|
+
errorUrl.searchParams.set('error', 'invalid_state')
|
|
38
|
+
errorUrl.searchParams.set('error_description', 'State parameter mismatch')
|
|
39
|
+
return NextResponse.redirect(errorUrl)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Validate required params
|
|
43
|
+
if (!code || !codeVerifier) {
|
|
44
|
+
const errorUrl = new URL('/', APP_URL)
|
|
45
|
+
errorUrl.searchParams.set('error', 'invalid_request')
|
|
46
|
+
errorUrl.searchParams.set('error_description', 'Missing authorization code')
|
|
47
|
+
return NextResponse.redirect(errorUrl)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const tokens = await exchangeCodeForTokens(code, codeVerifier)
|
|
52
|
+
await setAuthCookies(tokens.access_token, tokens.refresh_token, tokens.expires_in)
|
|
53
|
+
const safeReturnTo = sanitizeReturnTo(returnTo)
|
|
54
|
+
return NextResponse.redirect(new URL(safeReturnTo, APP_URL))
|
|
55
|
+
} catch (err) {
|
|
56
|
+
const errorUrl = new URL('/', APP_URL)
|
|
57
|
+
errorUrl.searchParams.set('error', 'token_exchange_failed')
|
|
58
|
+
errorUrl.searchParams.set(
|
|
59
|
+
'error_description',
|
|
60
|
+
err instanceof Error ? err.message : 'Failed to exchange authorization code'
|
|
61
|
+
)
|
|
62
|
+
return NextResponse.redirect(errorUrl)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { getLoginUrl } from '@/lib/auth'
|
|
3
|
+
|
|
4
|
+
export async function GET(request: NextRequest) {
|
|
5
|
+
const returnTo = request.nextUrl.searchParams.get('returnTo') || '/dashboard'
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const loginUrl = await getLoginUrl(returnTo)
|
|
9
|
+
return NextResponse.redirect(loginUrl)
|
|
10
|
+
} catch (error) {
|
|
11
|
+
const message = error instanceof Error ? error.message : 'Login failed'
|
|
12
|
+
return NextResponse.redirect(
|
|
13
|
+
new URL(`/?error=${encodeURIComponent(message)}`, request.url)
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { clearAuthCookies } from '@/lib/auth'
|
|
3
|
+
|
|
4
|
+
const APP_URL = process.env.APP_URL || 'http://localhost:3000'
|
|
5
|
+
|
|
6
|
+
export async function POST() {
|
|
7
|
+
await clearAuthCookies()
|
|
8
|
+
return NextResponse.redirect(new URL('/', APP_URL), { status: 303 })
|
|
9
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { cookies } from 'next/headers'
|
|
3
|
+
import {
|
|
4
|
+
getCurrentUser,
|
|
5
|
+
refreshAccessToken,
|
|
6
|
+
setAuthCookies,
|
|
7
|
+
fetchUserInfo,
|
|
8
|
+
COOKIE_PREFIX,
|
|
9
|
+
} from '@/lib/auth'
|
|
10
|
+
|
|
11
|
+
const NO_CACHE_HEADERS = { 'Cache-Control': 'no-store, no-cache, must-revalidate' }
|
|
12
|
+
|
|
13
|
+
export async function GET() {
|
|
14
|
+
try {
|
|
15
|
+
const user = await getCurrentUser()
|
|
16
|
+
|
|
17
|
+
if (user) {
|
|
18
|
+
return NextResponse.json({ user }, { headers: NO_CACHE_HEADERS })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Access token expired or missing — try refresh
|
|
22
|
+
const cookieStore = await cookies()
|
|
23
|
+
const refreshToken = cookieStore.get(`${COOKIE_PREFIX}_refresh_token`)?.value
|
|
24
|
+
|
|
25
|
+
if (refreshToken) {
|
|
26
|
+
try {
|
|
27
|
+
const tokens = await refreshAccessToken(refreshToken)
|
|
28
|
+
await setAuthCookies(tokens.access_token, tokens.refresh_token, tokens.expires_in)
|
|
29
|
+
const refreshedUser = await fetchUserInfo(tokens.access_token)
|
|
30
|
+
return NextResponse.json({ user: refreshedUser }, { headers: NO_CACHE_HEADERS })
|
|
31
|
+
} catch {
|
|
32
|
+
// Refresh token also invalid — user must re-login
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return NextResponse.json({ user: null }, { headers: NO_CACHE_HEADERS })
|
|
37
|
+
} catch {
|
|
38
|
+
return NextResponse.json({ user: null }, { headers: NO_CACHE_HEADERS })
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
|
|
3
|
+
// In-memory storage for demo
|
|
4
|
+
const items: Map<string, { id: string; name: string; createdAt: string }> = new Map()
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
const itemList = Array.from(items.values())
|
|
8
|
+
return NextResponse.json({
|
|
9
|
+
success: true,
|
|
10
|
+
data: itemList,
|
|
11
|
+
meta: {
|
|
12
|
+
total: itemList.length,
|
|
13
|
+
timestamp: new Date().toISOString(),
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function POST(request: NextRequest) {
|
|
19
|
+
try {
|
|
20
|
+
const body = await request.json()
|
|
21
|
+
|
|
22
|
+
if (!body.name || typeof body.name !== 'string') {
|
|
23
|
+
return NextResponse.json(
|
|
24
|
+
{
|
|
25
|
+
success: false,
|
|
26
|
+
error: {
|
|
27
|
+
code: 'VALIDATION_ERROR',
|
|
28
|
+
message: 'Name is required',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{ status: 400 }
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const id = Date.now().toString(36) + Math.random().toString(36).substring(2)
|
|
36
|
+
const item = {
|
|
37
|
+
id,
|
|
38
|
+
name: body.name,
|
|
39
|
+
createdAt: new Date().toISOString(),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
items.set(id, item)
|
|
43
|
+
|
|
44
|
+
return NextResponse.json(
|
|
45
|
+
{
|
|
46
|
+
success: true,
|
|
47
|
+
data: item,
|
|
48
|
+
},
|
|
49
|
+
{ status: 201 }
|
|
50
|
+
)
|
|
51
|
+
} catch {
|
|
52
|
+
return NextResponse.json(
|
|
53
|
+
{
|
|
54
|
+
success: false,
|
|
55
|
+
error: {
|
|
56
|
+
code: 'PARSE_ERROR',
|
|
57
|
+
message: 'Invalid JSON body',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{ status: 400 }
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { redirect } from 'next/navigation'
|
|
2
|
+
import { getCurrentUser } from '@/lib/auth'
|
|
3
|
+
|
|
4
|
+
export default async function DashboardPage() {
|
|
5
|
+
const user = await getCurrentUser()
|
|
6
|
+
|
|
7
|
+
if (!user) {
|
|
8
|
+
redirect('/api/auth/login?returnTo=/dashboard')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<main style={{
|
|
13
|
+
maxWidth: '48rem',
|
|
14
|
+
margin: '0 auto',
|
|
15
|
+
padding: '2rem',
|
|
16
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
17
|
+
}}>
|
|
18
|
+
<div style={{
|
|
19
|
+
display: 'flex',
|
|
20
|
+
justifyContent: 'space-between',
|
|
21
|
+
alignItems: 'center',
|
|
22
|
+
marginBottom: '2rem',
|
|
23
|
+
}}>
|
|
24
|
+
<h1 style={{ fontSize: '2rem', margin: 0 }}>Dashboard</h1>
|
|
25
|
+
<form action="/api/auth/logout" method="post">
|
|
26
|
+
<button
|
|
27
|
+
type="submit"
|
|
28
|
+
style={{
|
|
29
|
+
padding: '0.5rem 1rem',
|
|
30
|
+
backgroundColor: '#dc2626',
|
|
31
|
+
color: 'white',
|
|
32
|
+
borderRadius: '0.375rem',
|
|
33
|
+
border: 'none',
|
|
34
|
+
cursor: 'pointer',
|
|
35
|
+
fontSize: '0.875rem',
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
Logout
|
|
39
|
+
</button>
|
|
40
|
+
</form>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div style={{
|
|
44
|
+
backgroundColor: '#f9fafb',
|
|
45
|
+
border: '1px solid #e5e7eb',
|
|
46
|
+
borderRadius: '0.5rem',
|
|
47
|
+
padding: '1.5rem',
|
|
48
|
+
marginBottom: '1.5rem',
|
|
49
|
+
}}>
|
|
50
|
+
<h2 style={{ fontSize: '1.25rem', marginTop: 0, marginBottom: '1rem' }}>
|
|
51
|
+
Welcome, {user.name}
|
|
52
|
+
</h2>
|
|
53
|
+
<dl style={{ margin: 0 }}>
|
|
54
|
+
<dt style={{ fontWeight: 600, color: '#6b7280', fontSize: '0.875rem' }}>Email</dt>
|
|
55
|
+
<dd style={{ margin: '0 0 0.75rem 0' }}>{user.email}</dd>
|
|
56
|
+
|
|
57
|
+
<dt style={{ fontWeight: 600, color: '#6b7280', fontSize: '0.875rem' }}>User ID</dt>
|
|
58
|
+
<dd style={{ margin: '0 0 0.75rem 0', fontFamily: 'monospace', fontSize: '0.875rem' }}>{user.id}</dd>
|
|
59
|
+
|
|
60
|
+
<dt style={{ fontWeight: 600, color: '#6b7280', fontSize: '0.875rem' }}>Roles</dt>
|
|
61
|
+
<dd style={{ margin: 0 }}>
|
|
62
|
+
{user.roles.length > 0 ? (
|
|
63
|
+
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
|
64
|
+
{user.roles.map((role) => (
|
|
65
|
+
<span
|
|
66
|
+
key={role}
|
|
67
|
+
style={{
|
|
68
|
+
backgroundColor: '#dbeafe',
|
|
69
|
+
color: '#1e40af',
|
|
70
|
+
padding: '0.125rem 0.5rem',
|
|
71
|
+
borderRadius: '9999px',
|
|
72
|
+
fontSize: '0.75rem',
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
{role}
|
|
76
|
+
</span>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
) : (
|
|
80
|
+
<span style={{ color: '#9ca3af' }}>No roles assigned</span>
|
|
81
|
+
)}
|
|
82
|
+
</dd>
|
|
83
|
+
</dl>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<p style={{ color: '#6b7280', fontSize: '0.875rem' }}>
|
|
87
|
+
This page is protected by middleware and server-side auth checks.
|
|
88
|
+
Edit <code>src/app/dashboard/page.tsx</code> to customize.
|
|
89
|
+
</p>
|
|
90
|
+
</main>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
|
|
3
|
+
export const metadata: Metadata = {
|
|
4
|
+
title: '{{name}}',
|
|
5
|
+
description: '{{description}}',
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function RootLayout({
|
|
9
|
+
children,
|
|
10
|
+
}: {
|
|
11
|
+
children: React.ReactNode
|
|
12
|
+
}) {
|
|
13
|
+
return (
|
|
14
|
+
<html lang="en">
|
|
15
|
+
<body>{children}</body>
|
|
16
|
+
</html>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { getCurrentUser } from '@/lib/auth'
|
|
2
|
+
|
|
3
|
+
export default async function Home() {
|
|
4
|
+
const user = await getCurrentUser()
|
|
5
|
+
|
|
6
|
+
return (
|
|
7
|
+
<main style={{
|
|
8
|
+
display: 'flex',
|
|
9
|
+
flexDirection: 'column',
|
|
10
|
+
alignItems: 'center',
|
|
11
|
+
justifyContent: 'center',
|
|
12
|
+
minHeight: '100vh',
|
|
13
|
+
padding: '2rem',
|
|
14
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
15
|
+
}}>
|
|
16
|
+
<h1 style={{ fontSize: '3rem', marginBottom: '1rem' }}>
|
|
17
|
+
{'{{name}}'}
|
|
18
|
+
</h1>
|
|
19
|
+
<p style={{ fontSize: '1.25rem', color: '#666', marginBottom: '2rem' }}>
|
|
20
|
+
{'{{description}}'}
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
{user ? (
|
|
24
|
+
<div style={{ textAlign: 'center', marginBottom: '2rem' }}>
|
|
25
|
+
<p style={{ marginBottom: '1rem' }}>
|
|
26
|
+
Signed in as <strong>{user.name}</strong> ({user.email})
|
|
27
|
+
</p>
|
|
28
|
+
<div style={{ display: 'flex', gap: '1rem' }}>
|
|
29
|
+
<a
|
|
30
|
+
href="/dashboard"
|
|
31
|
+
style={{
|
|
32
|
+
padding: '0.75rem 1.5rem',
|
|
33
|
+
backgroundColor: '#0070f3',
|
|
34
|
+
color: 'white',
|
|
35
|
+
borderRadius: '0.5rem',
|
|
36
|
+
textDecoration: 'none',
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
Dashboard
|
|
40
|
+
</a>
|
|
41
|
+
<form action="/api/auth/logout" method="post" style={{ display: 'inline' }}>
|
|
42
|
+
<button
|
|
43
|
+
type="submit"
|
|
44
|
+
style={{
|
|
45
|
+
padding: '0.75rem 1.5rem',
|
|
46
|
+
backgroundColor: '#dc2626',
|
|
47
|
+
color: 'white',
|
|
48
|
+
borderRadius: '0.5rem',
|
|
49
|
+
border: 'none',
|
|
50
|
+
cursor: 'pointer',
|
|
51
|
+
fontSize: 'inherit',
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
Logout
|
|
55
|
+
</button>
|
|
56
|
+
</form>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
) : (
|
|
60
|
+
<div style={{ display: 'flex', gap: '1rem', marginBottom: '2rem' }}>
|
|
61
|
+
<a
|
|
62
|
+
href="/api/auth/login"
|
|
63
|
+
style={{
|
|
64
|
+
padding: '0.75rem 1.5rem',
|
|
65
|
+
backgroundColor: '#0070f3',
|
|
66
|
+
color: 'white',
|
|
67
|
+
borderRadius: '0.5rem',
|
|
68
|
+
textDecoration: 'none',
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
Sign In
|
|
72
|
+
</a>
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
<div style={{ display: 'flex', gap: '1rem' }}>
|
|
77
|
+
<a
|
|
78
|
+
href="/api/health"
|
|
79
|
+
style={{
|
|
80
|
+
padding: '0.5rem 1rem',
|
|
81
|
+
border: '1px solid #e5e7eb',
|
|
82
|
+
borderRadius: '0.5rem',
|
|
83
|
+
textDecoration: 'none',
|
|
84
|
+
color: '#666',
|
|
85
|
+
fontSize: '0.875rem',
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
Health Check
|
|
89
|
+
</a>
|
|
90
|
+
<a
|
|
91
|
+
href="/api/example"
|
|
92
|
+
style={{
|
|
93
|
+
padding: '0.5rem 1rem',
|
|
94
|
+
border: '1px solid #e5e7eb',
|
|
95
|
+
borderRadius: '0.5rem',
|
|
96
|
+
textDecoration: 'none',
|
|
97
|
+
color: '#666',
|
|
98
|
+
fontSize: '0.875rem',
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
Example API
|
|
102
|
+
</a>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<footer style={{ marginTop: '4rem', color: '#999' }}>
|
|
106
|
+
Deployed on Tawa Platform
|
|
107
|
+
</footer>
|
|
108
|
+
</main>
|
|
109
|
+
)
|
|
110
|
+
}
|