@tidecloak/create-nextjs 0.0.1
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/cjs/create.cjs +139 -0
- package/dist/cjs/create.js.map +1 -0
- package/dist/esm/create.js +134 -0
- package/dist/esm/create.js.map +1 -0
- package/dist/types/create.d.ts +3 -0
- package/dist/types/create.d.ts.map +1 -0
- package/init/.env.example +8 -0
- package/init/realm.json +162 -0
- package/init/tcinit.sh +261 -0
- package/package.json +40 -0
- package/template-js-app/.env.example +0 -0
- package/template-js-app/app/api/protected/route.js +40 -0
- package/template-js-app/app/auth/redirect/page.jsx +46 -0
- package/template-js-app/app/home/page.jsx +101 -0
- package/template-js-app/app/layout.jsx +18 -0
- package/template-js-app/app/page.jsx +64 -0
- package/template-js-app/app/provider.jsx +11 -0
- package/template-js-app/init/.env.example +8 -0
- package/template-js-app/init/realm.json +162 -0
- package/template-js-app/init/tcinit.sh +261 -0
- package/template-js-app/jsconfig.json +9 -0
- package/template-js-app/middleware.js +31 -0
- package/template-js-app/next.config.js +5 -0
- package/template-js-app/package.json +17 -0
- package/template-js-app/public/silent-check-sso.html +1 -0
- package/template-js-app/tidecloak.json +1 -0
- package/template-ts-app/.env.example +0 -0
- package/template-ts-app/app/api/protected/route.ts +38 -0
- package/template-ts-app/app/auth/redirect/page.tsx +46 -0
- package/template-ts-app/app/home/page.tsx +101 -0
- package/template-ts-app/app/layout.tsx +24 -0
- package/template-ts-app/app/page.tsx +65 -0
- package/template-ts-app/app/provider.tsx +23 -0
- package/template-ts-app/init/.env.example +8 -0
- package/template-ts-app/init/realm.json +162 -0
- package/template-ts-app/init/tcinit.sh +261 -0
- package/template-ts-app/middleware.ts +44 -0
- package/template-ts-app/next.config.js +0 -0
- package/template-ts-app/package.json +22 -0
- package/template-ts-app/public/silent-check-sso.html +1 -0
- package/template-ts-app/tidecloak.json +1 -0
- package/template-ts-app/tsconfig.json +42 -0
package/init/tcinit.sh
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
# Determine paths
|
|
6
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
9
|
+
|
|
10
|
+
# Load overrides from .env in the project root
|
|
11
|
+
if [ -f "${PROJECT_ROOT}/.env.example" ]; then
|
|
12
|
+
# shellcheck disable=SC1090
|
|
13
|
+
source "${PROJECT_ROOT}/.env.example"
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
# Defaults (override via env)
|
|
18
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
TIDECLOAK_LOCAL_URL="${TIDECLOAK_LOCAL_URL:-http://localhost:8080}"
|
|
20
|
+
CLIENT_APP_URL="${CLIENT_APP_URL:-http://localhost:3000}"
|
|
21
|
+
REALM_JSON_PATH="${REALM_JSON_PATH:-${SCRIPT_DIR}/realm.json}"
|
|
22
|
+
ADAPTER_OUTPUT_PATH="${ADAPTER_OUTPUT_PATH:-${PROJECT_ROOT}/tidecloak.json}"
|
|
23
|
+
NEW_REALM_NAME="${NEW_REALM_NAME:-nextjs-test}"
|
|
24
|
+
REALM_MGMT_CLIENT_ID="realm-management"
|
|
25
|
+
ADMIN_ROLE_NAME="tide-realm-admin"
|
|
26
|
+
KC_USER="${KC_USER:-admin}"
|
|
27
|
+
KC_PASSWORD="${KC_PASSWORD:-password}"
|
|
28
|
+
CLIENT_NAME="${CLIENT_NAME:-myclient}"
|
|
29
|
+
|
|
30
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
# sed -i portability
|
|
32
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
if sed --version >/dev/null 2>&1; then
|
|
34
|
+
SED_INPLACE=(-i)
|
|
35
|
+
else
|
|
36
|
+
SED_INPLACE=(-i '')
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
40
|
+
# Helper: grab an admin token
|
|
41
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
42
|
+
get_admin_token() {
|
|
43
|
+
curl -s -X POST "${TIDECLOAK_LOCAL_URL}/realms/master/protocol/openid-connect/token" \
|
|
44
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
45
|
+
-d "username=${KC_USER}" \
|
|
46
|
+
-d "password=${KC_PASSWORD}" \
|
|
47
|
+
-d "grant_type=password" \
|
|
48
|
+
-d "client_id=admin-cli" \
|
|
49
|
+
| jq -r .access_token
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
53
|
+
# Step 1: prepare realm JSON
|
|
54
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
REALM_NAME="${NEW_REALM_NAME}"
|
|
56
|
+
echo "${REALM_NAME}" > "${PROJECT_ROOT}/.realm_name"
|
|
57
|
+
|
|
58
|
+
TMP_REALM_JSON="$(mktemp)"
|
|
59
|
+
cp "${REALM_JSON_PATH}" "${TMP_REALM_JSON}"
|
|
60
|
+
|
|
61
|
+
# replace placeholders
|
|
62
|
+
sed "${SED_INPLACE[@]}" "s|http://localhost:3000|${CLIENT_APP_URL}|g" "${TMP_REALM_JSON}"
|
|
63
|
+
sed "${SED_INPLACE[@]}" "s|nextjs-test|${REALM_NAME}|g" "${TMP_REALM_JSON}"
|
|
64
|
+
sed "${SED_INPLACE[@]}" "s|myclient|${CLIENT_NAME}|g" "${TMP_REALM_JSON}"
|
|
65
|
+
|
|
66
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
67
|
+
# Step 2: create realm (allow 409 if already exists)
|
|
68
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
69
|
+
TOKEN="$(get_admin_token)"
|
|
70
|
+
echo "🌍 Creating realm..."
|
|
71
|
+
status=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
72
|
+
-X POST "${TIDECLOAK_LOCAL_URL}/admin/realms" \
|
|
73
|
+
-H "Authorization: Bearer ${TOKEN}" \
|
|
74
|
+
-H "Content-Type: application/json" \
|
|
75
|
+
--data-binary @"${TMP_REALM_JSON}")
|
|
76
|
+
|
|
77
|
+
if [[ ${status} == 2* || ${status} -eq 409 ]]; then
|
|
78
|
+
echo "✅ Realm created (or already exists)."
|
|
79
|
+
else
|
|
80
|
+
echo "❌ Realm creation failed (HTTP ${status})" >&2
|
|
81
|
+
exit 1
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
85
|
+
# Step 3: initialize Tide realm + IGA
|
|
86
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
87
|
+
TOKEN="$(get_admin_token)"
|
|
88
|
+
echo "🔐 Initializing Tide realm + IGA..."
|
|
89
|
+
|
|
90
|
+
response=$(curl -i -X POST "${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/vendorResources/setUpTideRealm" \
|
|
91
|
+
-H "Authorization: Bearer ${TOKEN}" \
|
|
92
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
93
|
+
--data-urlencode "email=email@tide.org" 2>&1)
|
|
94
|
+
|
|
95
|
+
# parse status code from response
|
|
96
|
+
status=$(printf "%s" "${response}" | awk '/HTTP\/1\.[01]/ { code=$2 } END { print code }')
|
|
97
|
+
if [[ "${status}" != "200" && "${status}" != "201" && "${status}" != "204" ]]; then
|
|
98
|
+
echo "❌ setUpTideRealm failed with HTTP ${status}" >&2
|
|
99
|
+
exit 1
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# toggle IGA
|
|
103
|
+
curl -s -X POST "${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/tide-admin/toggle-iga" \
|
|
104
|
+
-H "Authorization: Bearer ${TOKEN}" \
|
|
105
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
106
|
+
--data-urlencode "isIGAEnabled=true" \
|
|
107
|
+
> /dev/null
|
|
108
|
+
|
|
109
|
+
echo "✅ Tide realm + IGA done."
|
|
110
|
+
|
|
111
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
112
|
+
# Approve & commit change-sets
|
|
113
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
approve_and_commit() {
|
|
115
|
+
local TYPE=$1
|
|
116
|
+
echo "🔄 Processing ${TYPE} change-sets..."
|
|
117
|
+
TOKEN="$(get_admin_token)"
|
|
118
|
+
curl -s -X GET "${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/tide-admin/change-set/${TYPE}/requests" \
|
|
119
|
+
-H "Authorization: Bearer ${TOKEN}" \
|
|
120
|
+
| jq -c '.[]' | while read -r req; do
|
|
121
|
+
payload=$(jq -n \
|
|
122
|
+
--arg id "$(jq -r .draftRecordId <<< "${req}")" \
|
|
123
|
+
--arg cst "$(jq -r .changeSetType <<< "${req}")" \
|
|
124
|
+
--arg at "$(jq -r .actionType <<< "${req}")" \
|
|
125
|
+
'{changeSetId:$id,changeSetType:$cst,actionType:$at}')
|
|
126
|
+
|
|
127
|
+
curl -s -X POST "${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/tide-admin/change-set/sign" \
|
|
128
|
+
-H "Authorization: Bearer ${TOKEN}" \
|
|
129
|
+
-H "Content-Type: application/json" \
|
|
130
|
+
-d "${payload}" \
|
|
131
|
+
> /dev/null
|
|
132
|
+
|
|
133
|
+
curl -s -X POST "${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/tide-admin/change-set/commit" \
|
|
134
|
+
-H "Authorization: Bearer ${TOKEN}" \
|
|
135
|
+
-H "Content-Type: application/json" \
|
|
136
|
+
-d "${payload}" \
|
|
137
|
+
> /dev/null
|
|
138
|
+
done
|
|
139
|
+
echo "✅ ${TYPE^} change-sets done."
|
|
140
|
+
}
|
|
141
|
+
approve_and_commit clients
|
|
142
|
+
|
|
143
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
144
|
+
# Step 4: create admin user + assign role
|
|
145
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
146
|
+
TOKEN="$(get_admin_token)"
|
|
147
|
+
echo "👤 Creating admin user..."
|
|
148
|
+
curl -s -X POST "${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/users" \
|
|
149
|
+
-H "Authorization: Bearer ${TOKEN}" \
|
|
150
|
+
-H "Content-Type: application/json" \
|
|
151
|
+
-d '{"username":"admin","email":"admin@tidecloak.com","firstName":"admin","lastName":"user","enabled":true}' \
|
|
152
|
+
> /dev/null
|
|
153
|
+
|
|
154
|
+
USER_ID=$(curl -s -X GET \
|
|
155
|
+
"${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/users?username=admin" \
|
|
156
|
+
-H "Authorization: Bearer ${TOKEN}" \
|
|
157
|
+
| jq -r '.[0].id')
|
|
158
|
+
|
|
159
|
+
CLIENT_UUID=$(curl -s -X GET \
|
|
160
|
+
"${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/clients?clientId=${REALM_MGMT_CLIENT_ID}" \
|
|
161
|
+
-H "Authorization: Bearer ${TOKEN}" \
|
|
162
|
+
| jq -r '.[0].id')
|
|
163
|
+
|
|
164
|
+
ROLE_JSON=$(curl -s -X GET \
|
|
165
|
+
"${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/clients/${CLIENT_UUID}/roles/${ADMIN_ROLE_NAME}" \
|
|
166
|
+
-H "Authorization: Bearer ${TOKEN}")
|
|
167
|
+
|
|
168
|
+
curl -s -X POST "${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/users/${USER_ID}/role-mappings/clients/${CLIENT_UUID}" \
|
|
169
|
+
-H "Authorization: Bearer ${TOKEN}" \
|
|
170
|
+
-H "Content-Type: application/json" \
|
|
171
|
+
-d "[${ROLE_JSON}]" \
|
|
172
|
+
> /dev/null
|
|
173
|
+
|
|
174
|
+
echo "✅ Admin user & role done."
|
|
175
|
+
|
|
176
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
177
|
+
# Step 5: generate invite link + wait
|
|
178
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
179
|
+
TOKEN="$(get_admin_token)"
|
|
180
|
+
echo "🔗 Generating invite link..."
|
|
181
|
+
INVITE_LINK=$(curl -s -X POST \
|
|
182
|
+
"${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/tideAdminResources/get-required-action-link?userId=${USER_ID}&lifespan=43200" \
|
|
183
|
+
-H "Authorization: Bearer ${TOKEN}" \
|
|
184
|
+
-H "Content-Type: application/json" \
|
|
185
|
+
-d '["link-tide-account-action"]')
|
|
186
|
+
|
|
187
|
+
echo "🔗 Invite link: ${INVITE_LINK}"
|
|
188
|
+
echo "→ Send this link to the user so they can link their account."
|
|
189
|
+
|
|
190
|
+
MAX_TRIES=3
|
|
191
|
+
attempt=1
|
|
192
|
+
while true; do
|
|
193
|
+
echo -n "Checking link status (attempt ${attempt}/${MAX_TRIES})… "
|
|
194
|
+
ATTRS=$(curl -s -X GET \
|
|
195
|
+
"${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/users?username=admin" \
|
|
196
|
+
-H "Authorization: Bearer ${TOKEN}")
|
|
197
|
+
|
|
198
|
+
KEY=$(jq -r '.[0].attributes.tideUserKey[0] // empty' <<< "${ATTRS}")
|
|
199
|
+
VUID=$(jq -r '.[0].attributes.vuid[0] // empty' <<< "${ATTRS}")
|
|
200
|
+
|
|
201
|
+
if [[ -n "${KEY}" && -n "${VUID}" ]]; then
|
|
202
|
+
echo "✅ Linked!"
|
|
203
|
+
break
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
if (( attempt >= MAX_TRIES )); then
|
|
207
|
+
echo "⚠️ Max retries reached (${MAX_TRIES}). Moving on."
|
|
208
|
+
break
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
read -t 30 -p "Not linked yet; press ENTER to retry or wait 30s…" || true
|
|
212
|
+
echo
|
|
213
|
+
((attempt++))
|
|
214
|
+
done
|
|
215
|
+
|
|
216
|
+
approve_and_commit users
|
|
217
|
+
|
|
218
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
219
|
+
# Step 6: update CustomAdminUIDomain
|
|
220
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
221
|
+
TOKEN="$(get_admin_token)"
|
|
222
|
+
echo "🌐 Updating CustomAdminUIDomain..."
|
|
223
|
+
|
|
224
|
+
INST_JSON=$(curl -s -X GET \
|
|
225
|
+
"${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/identity-provider/instances/tide" \
|
|
226
|
+
-H "Authorization: Bearer ${TOKEN}")
|
|
227
|
+
|
|
228
|
+
UPDATED_JSON=$(jq --arg d "${CLIENT_APP_URL}" '.config.CustomAdminUIDomain = $d' <<< "${INST_JSON}")
|
|
229
|
+
|
|
230
|
+
curl -s -X PUT "${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/identity-provider/instances/tide" \
|
|
231
|
+
-H "Authorization: Bearer ${TOKEN}" \
|
|
232
|
+
-H "Content-Type: application/json" \
|
|
233
|
+
-d "${UPDATED_JSON}" \
|
|
234
|
+
> /dev/null
|
|
235
|
+
|
|
236
|
+
curl -s -X POST "${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/vendorResources/sign-idp-settings" \
|
|
237
|
+
-H "Authorization: Bearer ${TOKEN}" \
|
|
238
|
+
> /dev/null
|
|
239
|
+
|
|
240
|
+
echo "✅ CustomAdminUIDomain updated + signed."
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
244
|
+
# Step 7: fetch adapter config + cleanup
|
|
245
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
246
|
+
TOKEN="$(get_admin_token)"
|
|
247
|
+
echo "📥 Fetching adapter config…"
|
|
248
|
+
CLIENT_UUID=$(curl -s -X GET \
|
|
249
|
+
"${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/clients?clientId=${CLIENT_NAME}" \
|
|
250
|
+
-H "Authorization: Bearer ${TOKEN}" \
|
|
251
|
+
| jq -r '.[0].id')
|
|
252
|
+
|
|
253
|
+
curl -s -X GET \
|
|
254
|
+
"${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/vendorResources/get-installations-provider?clientId=${CLIENT_UUID}&providerId=keycloak-oidc-keycloak-json" \
|
|
255
|
+
-H "Authorization: Bearer ${TOKEN}" \
|
|
256
|
+
> "${ADAPTER_OUTPUT_PATH}"
|
|
257
|
+
|
|
258
|
+
echo "✅ Adapter config saved to ${ADAPTER_OUTPUT_PATH}"
|
|
259
|
+
rm -f "${PROJECT_ROOT}/.realm_name" "${TMP_REALM_JSON}"
|
|
260
|
+
|
|
261
|
+
echo "🎉 All done!"
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tidecloak/create-nextjs",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-nextjs": "./dist/cjs/create.cjs"
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
"main": "./dist/cjs/create.cjs",
|
|
11
|
+
"module": "./dist/esm/create.js",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"require": "./dist/cjs/create.cjs",
|
|
15
|
+
"import": "./dist/esm/create.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
"files": [
|
|
20
|
+
"dist/",
|
|
21
|
+
"template-ts-app/",
|
|
22
|
+
"template-js-app/",
|
|
23
|
+
"init/"
|
|
24
|
+
],
|
|
25
|
+
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build:cjs": "tsc -p tsconfig.cjs.json && mv dist/cjs/create.js dist/cjs/create.cjs",
|
|
28
|
+
"build:esm": "tsc -p tsconfig.esm.json",
|
|
29
|
+
"build": "npm run build:cjs && npm run build:esm",
|
|
30
|
+
"prepare": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"enquirer": "^2"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^24.0.13",
|
|
38
|
+
"typescript": "^5.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { verifyTideCloakToken } from '@tidecloak/nextjs/server'
|
|
3
|
+
import tcConfig from '../../../tidecloak.json'
|
|
4
|
+
|
|
5
|
+
const ALLOWED_ROLE = 'offline_access'
|
|
6
|
+
|
|
7
|
+
export async function GET(request) {
|
|
8
|
+
const authHeader = request.headers.get('authorization')
|
|
9
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
10
|
+
return NextResponse.json(
|
|
11
|
+
{ error: 'Unauthorized: Missing or invalid token' },
|
|
12
|
+
{ status: 401 }
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const token = authHeader.split(' ')[1]
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const user = await verifyTideCloakToken(tcConfig, token, [ALLOWED_ROLE])
|
|
20
|
+
|
|
21
|
+
if (!user) {
|
|
22
|
+
return NextResponse.json(
|
|
23
|
+
{ error: 'Forbidden: Invalid token or insufficient role' },
|
|
24
|
+
{ status: 403 }
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Return whatever protected data you need here
|
|
29
|
+
return NextResponse.json(
|
|
30
|
+
{ vuid: user.vuid, userkey: user.tideuserkey },
|
|
31
|
+
{ status: 200 }
|
|
32
|
+
)
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error('Token verification failed:', err)
|
|
35
|
+
return NextResponse.json(
|
|
36
|
+
{ error: 'Internal Server Error' },
|
|
37
|
+
{ status: 500 }
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { useTideCloak } from '@tidecloak/nextjs';
|
|
6
|
+
|
|
7
|
+
export default function RedirectPage() {
|
|
8
|
+
const { authenticated, isInitializing, logout } = useTideCloak()
|
|
9
|
+
const router = useRouter()
|
|
10
|
+
|
|
11
|
+
// Handles redirect when middleware detects token expiry
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const doLogOut = async () => {
|
|
14
|
+
logout();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const params = new URLSearchParams(window.location.search);
|
|
18
|
+
const auth = params.get("auth");
|
|
19
|
+
|
|
20
|
+
if (auth === "failed") {
|
|
21
|
+
sessionStorage.setItem("tokenExpired", "true");
|
|
22
|
+
doLogOut();
|
|
23
|
+
}
|
|
24
|
+
}, [])
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!isInitializing) {
|
|
28
|
+
router.push(authenticated ? '/home' : '/')
|
|
29
|
+
}
|
|
30
|
+
}, [authenticated, isInitializing, router])
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div style={containerStyle}>
|
|
34
|
+
<p>Waiting for authentication...</p>
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const containerStyle = {
|
|
40
|
+
minHeight: '100vh',
|
|
41
|
+
display: 'flex',
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
justifyContent: 'center',
|
|
44
|
+
fontSize: '1rem',
|
|
45
|
+
color: '#555',
|
|
46
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from 'react'
|
|
4
|
+
import { useTideCloak } from '@tidecloak/nextjs'
|
|
5
|
+
import tcConfig from "../../tidecloak.json"
|
|
6
|
+
|
|
7
|
+
export default function HomePage() {
|
|
8
|
+
const { logout, getValueFromIdToken, hasRealmRole, getAccessToken } = useTideCloak()
|
|
9
|
+
const username = getValueFromIdToken('preferred_username') || '…'
|
|
10
|
+
const hasDefaultRole = hasRealmRole(`default-roles-${tcConfig["realm"]}`)
|
|
11
|
+
|
|
12
|
+
const [verifyResult, setVerifyResult] = useState(null)
|
|
13
|
+
const [verifying, setVerifying] = useState(false)
|
|
14
|
+
|
|
15
|
+
const onLogout = useCallback(() => {
|
|
16
|
+
logout()
|
|
17
|
+
}, [logout])
|
|
18
|
+
|
|
19
|
+
const onVerify = useCallback(async () => {
|
|
20
|
+
setVerifying(true)
|
|
21
|
+
setVerifyResult(null)
|
|
22
|
+
try {
|
|
23
|
+
const token = await getAccessToken()
|
|
24
|
+
const res = await fetch('/api/protected', {
|
|
25
|
+
method: 'GET',
|
|
26
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
27
|
+
})
|
|
28
|
+
const data = await res.json()
|
|
29
|
+
if (res.ok) {
|
|
30
|
+
setVerifyResult(`✅ Authorized: vuid=${data.vuid}, key=${data.userkey}`)
|
|
31
|
+
} else {
|
|
32
|
+
setVerifyResult(`❌ ${res.status} - ${data.error || res.statusText}`)
|
|
33
|
+
}
|
|
34
|
+
} catch (err) {
|
|
35
|
+
setVerifyResult(`❌ Network error: ${err.message}`)
|
|
36
|
+
} finally {
|
|
37
|
+
setVerifying(false)
|
|
38
|
+
}
|
|
39
|
+
}, [getAccessToken])
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div style={containerStyle}>
|
|
43
|
+
<div style={cardStyle}>
|
|
44
|
+
<h1 style={{ margin: 0, fontSize: '1.5rem' }}>Hello, {username}!</h1>
|
|
45
|
+
<p style={{ margin: '0.5rem 0', color: '#555' }}>
|
|
46
|
+
Has default roles? <strong>{hasDefaultRole ? 'Yes' : 'No'}</strong>
|
|
47
|
+
</p>
|
|
48
|
+
<button onClick={onLogout} style={buttonStyle}>
|
|
49
|
+
Log out
|
|
50
|
+
</button>
|
|
51
|
+
<button
|
|
52
|
+
onClick={onVerify}
|
|
53
|
+
style={{ ...buttonStyle, marginTop: '0.5rem' }}
|
|
54
|
+
disabled={verifying}
|
|
55
|
+
>
|
|
56
|
+
{verifying ? 'Verifying…' : 'Verify Token'}
|
|
57
|
+
</button>
|
|
58
|
+
{verifyResult && (
|
|
59
|
+
<p
|
|
60
|
+
style={{
|
|
61
|
+
marginTop: '1rem',
|
|
62
|
+
color: verifyResult.startsWith('✅') ? 'green' : 'red',
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
{verifyResult}
|
|
66
|
+
</p>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const containerStyle = {
|
|
74
|
+
minHeight: '100vh',
|
|
75
|
+
display: 'flex',
|
|
76
|
+
alignItems: 'center',
|
|
77
|
+
justifyContent: 'center',
|
|
78
|
+
background: '#f5f5f5',
|
|
79
|
+
margin: 0,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const cardStyle = {
|
|
83
|
+
background: '#fff',
|
|
84
|
+
padding: '2rem',
|
|
85
|
+
borderRadius: '8px',
|
|
86
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
|
87
|
+
textAlign: 'center',
|
|
88
|
+
maxWidth: '360px',
|
|
89
|
+
width: '100%',
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const buttonStyle = {
|
|
93
|
+
marginTop: '1rem',
|
|
94
|
+
padding: '0.75rem 1.5rem',
|
|
95
|
+
fontSize: '1rem',
|
|
96
|
+
borderRadius: '4px',
|
|
97
|
+
border: 'none',
|
|
98
|
+
background: '#0070f3',
|
|
99
|
+
color: '#fff',
|
|
100
|
+
cursor: 'pointer',
|
|
101
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Provider } from './provider';
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
title: 'My Tidecloak App',
|
|
5
|
+
description: 'A Next.js starter with Tidecloak',
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function RootLayout({ children }) {
|
|
9
|
+
return (
|
|
10
|
+
<html lang="en">
|
|
11
|
+
<body>
|
|
12
|
+
<Provider>
|
|
13
|
+
{children}
|
|
14
|
+
</Provider>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect } from 'react'
|
|
4
|
+
import { useTideCloak } from '@tidecloak/nextjs'
|
|
5
|
+
import { useRouter } from 'next/navigation'
|
|
6
|
+
|
|
7
|
+
const containerStyle = {
|
|
8
|
+
minHeight: '100vh',
|
|
9
|
+
display: 'flex',
|
|
10
|
+
alignItems: 'center',
|
|
11
|
+
justifyContent: 'center',
|
|
12
|
+
background: '#f5f5f5',
|
|
13
|
+
margin: 0,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const cardStyle = {
|
|
17
|
+
background: '#fff',
|
|
18
|
+
padding: '2rem',
|
|
19
|
+
borderRadius: '8px',
|
|
20
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
|
21
|
+
textAlign: 'center',
|
|
22
|
+
maxWidth: '360px',
|
|
23
|
+
width: '100%',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const buttonStyle = {
|
|
27
|
+
marginTop: '1rem',
|
|
28
|
+
padding: '0.75rem 1.5rem',
|
|
29
|
+
fontSize: '1rem',
|
|
30
|
+
borderRadius: '4px',
|
|
31
|
+
border: 'none',
|
|
32
|
+
background: '#0070f3',
|
|
33
|
+
color: '#fff',
|
|
34
|
+
cursor: 'pointer',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default function LoginPage() {
|
|
38
|
+
const { login, authenticated } = useTideCloak()
|
|
39
|
+
const router = useRouter()
|
|
40
|
+
|
|
41
|
+
const onLogin = useCallback(() => {
|
|
42
|
+
login()
|
|
43
|
+
}, [login])
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (authenticated) {
|
|
47
|
+
router.push('/home')
|
|
48
|
+
}
|
|
49
|
+
}, [authenticated, router])
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div style={containerStyle}>
|
|
53
|
+
<div style={cardStyle}>
|
|
54
|
+
<h1 style={{ margin: 0, fontSize: '1.75rem' }}>Welcome!</h1>
|
|
55
|
+
<p style={{ color: '#555', marginTop: '0.5rem' }}>
|
|
56
|
+
Please log in to continue.
|
|
57
|
+
</p>
|
|
58
|
+
<button onClick={onLogin} style={buttonStyle}>
|
|
59
|
+
Log In
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|