@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.
Files changed (42) hide show
  1. package/dist/cjs/create.cjs +139 -0
  2. package/dist/cjs/create.js.map +1 -0
  3. package/dist/esm/create.js +134 -0
  4. package/dist/esm/create.js.map +1 -0
  5. package/dist/types/create.d.ts +3 -0
  6. package/dist/types/create.d.ts.map +1 -0
  7. package/init/.env.example +8 -0
  8. package/init/realm.json +162 -0
  9. package/init/tcinit.sh +261 -0
  10. package/package.json +40 -0
  11. package/template-js-app/.env.example +0 -0
  12. package/template-js-app/app/api/protected/route.js +40 -0
  13. package/template-js-app/app/auth/redirect/page.jsx +46 -0
  14. package/template-js-app/app/home/page.jsx +101 -0
  15. package/template-js-app/app/layout.jsx +18 -0
  16. package/template-js-app/app/page.jsx +64 -0
  17. package/template-js-app/app/provider.jsx +11 -0
  18. package/template-js-app/init/.env.example +8 -0
  19. package/template-js-app/init/realm.json +162 -0
  20. package/template-js-app/init/tcinit.sh +261 -0
  21. package/template-js-app/jsconfig.json +9 -0
  22. package/template-js-app/middleware.js +31 -0
  23. package/template-js-app/next.config.js +5 -0
  24. package/template-js-app/package.json +17 -0
  25. package/template-js-app/public/silent-check-sso.html +1 -0
  26. package/template-js-app/tidecloak.json +1 -0
  27. package/template-ts-app/.env.example +0 -0
  28. package/template-ts-app/app/api/protected/route.ts +38 -0
  29. package/template-ts-app/app/auth/redirect/page.tsx +46 -0
  30. package/template-ts-app/app/home/page.tsx +101 -0
  31. package/template-ts-app/app/layout.tsx +24 -0
  32. package/template-ts-app/app/page.tsx +65 -0
  33. package/template-ts-app/app/provider.tsx +23 -0
  34. package/template-ts-app/init/.env.example +8 -0
  35. package/template-ts-app/init/realm.json +162 -0
  36. package/template-ts-app/init/tcinit.sh +261 -0
  37. package/template-ts-app/middleware.ts +44 -0
  38. package/template-ts-app/next.config.js +0 -0
  39. package/template-ts-app/package.json +22 -0
  40. package/template-ts-app/public/silent-check-sso.html +1 -0
  41. package/template-ts-app/tidecloak.json +1 -0
  42. package/template-ts-app/tsconfig.json +42 -0
@@ -0,0 +1,101 @@
1
+ 'use client'
2
+
3
+ import { useTideCloak } from '@tidecloak/nextjs'
4
+ import { useState, useCallback } from 'react'
5
+ import tcConfig from "../../tidecloak.json"
6
+
7
+
8
+ export default function HomePage() {
9
+ const { logout, getValueFromIdToken, hasRealmRole, token } = useTideCloak()
10
+ const username = getValueFromIdToken('preferred_username') ?? '…'
11
+ const hasDefaultRole = hasRealmRole(`default-roles-${tcConfig["realm"]}`)
12
+
13
+ const [verifyResult, setVerifyResult] = useState<string | null>(null)
14
+ const [verifying, setVerifying] = useState(false)
15
+
16
+ const onLogout = useCallback(() => {
17
+ logout()
18
+ }, [logout])
19
+
20
+ const onVerify = useCallback(async () => {
21
+ setVerifying(true)
22
+ setVerifyResult(null)
23
+ try {
24
+ const res = await fetch('/api/protected', {
25
+ method: 'GET',
26
+ headers: {
27
+ Authorization: `Bearer ${token}`,
28
+ },
29
+ })
30
+ const data = await res.json()
31
+ if (res.ok) {
32
+ setVerifyResult(`✅ Authorized: vuid=${data.vuid}, key=${data.userkey}`)
33
+ } else {
34
+ setVerifyResult(`❌ ${res.status} - ${data.error || res.statusText}`)
35
+ }
36
+ } catch (err: any) {
37
+ setVerifyResult(`❌ Network error: ${err.message}`)
38
+ } finally {
39
+ setVerifying(false)
40
+ }
41
+ }, [])
42
+
43
+ return (
44
+ <div style={containerStyle}>
45
+ <div style={cardStyle}>
46
+ <h1 style={{ margin: 0, fontSize: '1.5rem' }}>Hello, {username}!</h1>
47
+ <p style={{ margin: '0.5rem 0', color: '#555' }}>
48
+ Has default roles? <strong>{hasDefaultRole ? 'Yes' : 'No'}</strong>
49
+ </p>
50
+
51
+ <button onClick={onLogout} style={buttonStyle}>
52
+ Log out
53
+ </button>
54
+
55
+ <button
56
+ onClick={onVerify}
57
+ style={{ ...buttonStyle, marginTop: '0.5rem' }}
58
+ disabled={verifying}
59
+ >
60
+ {verifying ? 'Verifying…' : 'Verify Token'}
61
+ </button>
62
+
63
+ {verifyResult && (
64
+ <p style={{ marginTop: '1rem', color: verifyResult.startsWith('✅') ? 'green' : 'red' }}>
65
+ {verifyResult}
66
+ </p>
67
+ )}
68
+ </div>
69
+ </div>
70
+ )
71
+ }
72
+
73
+ const containerStyle: React.CSSProperties = {
74
+ minHeight: '100vh',
75
+ display: 'flex',
76
+ alignItems: 'center',
77
+ justifyContent: 'center',
78
+ background: '#f5f5f5',
79
+ margin: 0,
80
+ }
81
+
82
+ const cardStyle: React.CSSProperties = {
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: React.CSSProperties = {
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,24 @@
1
+ import type { Metadata } from 'next'
2
+ import type { ReactNode } from 'react'
3
+ import { Provider } from './provider'
4
+
5
+ export const metadata: Metadata = {
6
+ title: 'My Tidecloak App',
7
+ description: 'A Next.js starter with Tidecloak',
8
+ }
9
+
10
+ interface RootLayoutProps {
11
+ children: ReactNode
12
+ }
13
+
14
+ export default function RootLayout({ children }: RootLayoutProps): JSX.Element {
15
+ return (
16
+ <html lang="en">
17
+ <body>
18
+ <Provider>
19
+ {children}
20
+ </Provider>
21
+ </body>
22
+ </html>
23
+ )
24
+ }
@@ -0,0 +1,65 @@
1
+ 'use client'
2
+
3
+ import { useCallback, type CSSProperties } from 'react'
4
+ import { useTideCloak } from '@tidecloak/nextjs'
5
+ import { useRouter } from 'next/navigation'
6
+ import { useEffect } from 'react'
7
+
8
+ const containerStyle: CSSProperties = {
9
+ minHeight: '100vh',
10
+ display: 'flex',
11
+ alignItems: 'center',
12
+ justifyContent: 'center',
13
+ background: '#f5f5f5',
14
+ margin: 0,
15
+ }
16
+
17
+ const cardStyle: CSSProperties = {
18
+ background: '#fff',
19
+ padding: '2rem',
20
+ borderRadius: '8px',
21
+ boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
22
+ textAlign: 'center',
23
+ maxWidth: '360px',
24
+ width: '100%',
25
+ }
26
+
27
+ const buttonStyle: CSSProperties = {
28
+ marginTop: '1rem',
29
+ padding: '0.75rem 1.5rem',
30
+ fontSize: '1rem',
31
+ borderRadius: '4px',
32
+ border: 'none',
33
+ background: '#0070f3',
34
+ color: '#fff',
35
+ cursor: 'pointer',
36
+ }
37
+
38
+ export default function LoginPage(): JSX.Element {
39
+ const { login, authenticated } = useTideCloak()
40
+ const router = useRouter()
41
+
42
+ const onLogin = useCallback(() => {
43
+ login()
44
+ }, [login])
45
+
46
+ useEffect(() => {
47
+ if (authenticated) {
48
+ router.push('/home')
49
+ }
50
+ }, [authenticated])
51
+
52
+ return (
53
+ <div style={containerStyle}>
54
+ <div style={cardStyle}>
55
+ <h1 style={{ margin: 0, fontSize: '1.75rem' }}>Welcome!</h1>
56
+ <p style={{ color: '#555', marginTop: '0.5rem' }}>
57
+ Please log in to continue.
58
+ </p>
59
+ <button onClick={onLogin} style={buttonStyle}>
60
+ Log In
61
+ </button>
62
+ </div>
63
+ </div>
64
+ )
65
+ }
@@ -0,0 +1,23 @@
1
+ 'use client'
2
+
3
+ import React, { type ReactNode } from 'react'
4
+ import {
5
+ TideCloakProvider,
6
+ type TideCloakConfig,
7
+ } from '@tidecloak/nextjs'
8
+ import tcConfig from '../tidecloak.json'
9
+
10
+ interface ProviderProps {
11
+ children: ReactNode
12
+ }
13
+
14
+ export function Provider({ children }: ProviderProps): JSX.Element {
15
+ // If tidecloak.json isn’t already typed, you can cast it:
16
+ const config = tcConfig as unknown as TideCloakConfig
17
+
18
+ return (
19
+ <TideCloakProvider config={config}>
20
+ {children}
21
+ </TideCloakProvider>
22
+ )
23
+ }
@@ -0,0 +1,8 @@
1
+ TIDECLOAK_LOCAL_URL="http://localhost:8080"
2
+ CLIENT_NAME="myclient"
3
+ CLIENT_APP_URL="http://localhost:3000"
4
+ REALM_JSON_PATH="./realm.json"
5
+ NEW_REALM_NAME="nextjs-test"
6
+ KC_USER="admin"
7
+ KC_PASSWORD="password"
8
+ # ADAPTER_OUTPUT_PATH=""
@@ -0,0 +1,162 @@
1
+ {
2
+ "realm": "nextjs-test",
3
+ "accessTokenLifespan": 600,
4
+ "enabled": true,
5
+ "sslRequired": "external",
6
+ "registrationAllowed": false,
7
+ "duplicateEmailsAllowed": true,
8
+ "roles": {
9
+ "realm": [
10
+ {
11
+ "name": "appUser",
12
+ "description": "Standard application user"
13
+ },
14
+ {
15
+ "name": "_tide_dob.selfencrypt",
16
+ "description": "Tide E2EE self-encrypt DoB data"
17
+ },
18
+ {
19
+ "name": "_tide_dob.selfdecrypt",
20
+ "description": "Tide E2EE self-decrypt DoB data"
21
+ },
22
+ {
23
+ "name": "default-roles-nextjs-test",
24
+ "description": "${role_default-roles}",
25
+ "composite": true,
26
+ "composites": {
27
+ "realm": [
28
+ "_tide_dob.selfencrypt",
29
+ "_tide_dob.selfdecrypt",
30
+ "appUser"
31
+ ]
32
+ }
33
+ }
34
+ ],
35
+ "client": {
36
+ "myclient": []
37
+ }
38
+ },
39
+ "defaultRole": {
40
+ "name": "default-roles-nextjs-test",
41
+ "description": "${role_default-roles}",
42
+ "composite": true,
43
+ "clientRole": false
44
+ },
45
+ "clients": [
46
+ {
47
+ "clientId": "myclient",
48
+ "enabled": true,
49
+ "redirectUris": [
50
+ "http://localhost:3000",
51
+ "http://localhost:3000/*",
52
+ "http://localhost:3000/silent-check-sso.html",
53
+ "http://localhost:3000/auth/redirect"
54
+ ],
55
+ "webOrigins": [
56
+ "http://localhost:3000"
57
+ ],
58
+ "standardFlowEnabled": true,
59
+ "implicitFlowEnabled": false,
60
+ "publicClient": true,
61
+ "fullScopeAllowed": true,
62
+ "protocolMappers": [
63
+ {
64
+ "name": "Tide User Key",
65
+ "protocol": "openid-connect",
66
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
67
+ "consentRequired": false,
68
+ "config": {
69
+ "introspection.token.claim": "true",
70
+ "userinfo.token.claim": "true",
71
+ "user.attribute": "tideUserKey",
72
+ "lightweight.claim": "true",
73
+ "id.token.claim": "true",
74
+ "access.token.claim": "true",
75
+ "claim.name": "tideuserkey",
76
+ "jsonType.label": "String"
77
+ }
78
+ },
79
+ {
80
+ "name": "Tide IGA Role Mapper",
81
+ "protocol": "openid-connect",
82
+ "protocolMapper": "tide-roles-mapper",
83
+ "consentRequired": false,
84
+ "config": {
85
+ "lightweight.claim": "true",
86
+ "access.token.claim": "true"
87
+ }
88
+ },
89
+ {
90
+ "name": "Tide vuid",
91
+ "protocol": "openid-connect",
92
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
93
+ "consentRequired": false,
94
+ "config": {
95
+ "introspection.token.claim": "true",
96
+ "userinfo.token.claim": "true",
97
+ "user.attribute": "vuid",
98
+ "lightweight.claim": "true",
99
+ "id.token.claim": "true",
100
+ "access.token.claim": "true",
101
+ "claim.name": "vuid",
102
+ "jsonType.label": "String"
103
+ }
104
+ }
105
+ ]
106
+ }
107
+ ],
108
+ "components": {
109
+ "org.keycloak.userprofile.UserProfileProvider": [
110
+ {
111
+ "providerId": "declarative-user-profile",
112
+ "config": {
113
+ "kc.user.profile.config": [
114
+ "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}]}"
115
+ ]
116
+ }
117
+ }
118
+ ]
119
+ },
120
+ "authenticationFlows": [
121
+ {
122
+ "alias": "tidebrowser",
123
+ "providerId": "basic-flow",
124
+ "topLevel": true,
125
+ "authenticationExecutions": [
126
+ {
127
+ "authenticator": "auth-cookie",
128
+ "authenticatorFlow": false,
129
+ "requirement": "ALTERNATIVE",
130
+ "priority": 10,
131
+ "userSetupAllowed": false
132
+ },
133
+ {
134
+ "authenticatorConfig": "tide browser",
135
+ "authenticator": "identity-provider-redirector",
136
+ "authenticatorFlow": false,
137
+ "requirement": "ALTERNATIVE",
138
+ "priority": 25,
139
+ "userSetupAllowed": false
140
+ }
141
+ ]
142
+ }
143
+ ],
144
+ "authenticatorConfig": [
145
+ {
146
+ "alias": "tide browser",
147
+ "config": {
148
+ "defaultProvider": "tide"
149
+ }
150
+ }
151
+ ],
152
+ "browserFlow": "tidebrowser",
153
+ "requiredActions": [
154
+ {
155
+ "alias": "link-tide-account-action",
156
+ "name": "Link Tide Account",
157
+ "providerId": "link-tide-account-action",
158
+ "enabled": true
159
+ }
160
+ ],
161
+ "keycloakVersion": "26.1.4"
162
+ }
@@ -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!"