diff --git a/.env.example b/.env.example index b762311..53650da 100644 --- a/.env.example +++ b/.env.example @@ -12,7 +12,7 @@ POSTGRES_DB=cameleer_saas # Logto Identity Provider LOGTO_ENDPOINT=http://logto:3001 LOGTO_PUBLIC_ENDPOINT=http://localhost:3001 -LOGTO_ISSUER_URI=http://logto:3001/oidc +LOGTO_ISSUER_URI=http://localhost:3001/oidc LOGTO_JWK_SET_URI=http://logto:3001/oidc/jwks LOGTO_DB_PASSWORD=change_me_in_production LOGTO_M2M_CLIENT_ID= diff --git a/docker/logto-bootstrap.sh b/docker/logto-bootstrap.sh index 849cfbd..e863570 100644 --- a/docker/logto-bootstrap.sh +++ b/docker/logto-bootstrap.sh @@ -16,6 +16,8 @@ PG_DB="logto" SPA_APP_NAME="Cameleer SaaS" M2M_APP_NAME="Cameleer SaaS Backend" +API_RESOURCE_INDICATOR="https://api.cameleer.local" +API_RESOURCE_NAME="Cameleer SaaS API" DEFAULT_USERNAME="camel" DEFAULT_PASSWORD="camel" @@ -103,6 +105,23 @@ else log "Created SPA app: $SPA_ID" fi +# --- Find or create API resource --- +log "Checking for existing API resource..." +EXISTING_RESOURCES=$(api_get "/api/resources") +API_RESOURCE_ID=$(echo "$EXISTING_RESOURCES" | jq -r ".[] | select(.indicator == \"$API_RESOURCE_INDICATOR\") | .id") + +if [ -n "$API_RESOURCE_ID" ]; then + log "API resource already exists: $API_RESOURCE_ID" +else + log "Creating API resource..." + RESOURCE_RESPONSE=$(api_post "/api/resources" "{ + \"name\": \"$API_RESOURCE_NAME\", + \"indicator\": \"$API_RESOURCE_INDICATOR\" + }") + API_RESOURCE_ID=$(echo "$RESOURCE_RESPONSE" | jq -r '.id') + log "Created API resource: $API_RESOURCE_ID" +fi + # --- Find or create M2M app --- log "Checking for existing M2M app..." M2M_ID=$(echo "$EXISTING_APPS" | jq -r ".[] | select(.name == \"$M2M_APP_NAME\" and .type == \"MachineToMachine\") | .id") @@ -194,6 +213,7 @@ cat > "$BOOTSTRAP_FILE" < config() { - String clientId = spaClientId; + JsonNode bootstrap = readBootstrapFile(); - // Fall back to bootstrap file if env var not set + String clientId = spaClientId; if (clientId == null || clientId.isEmpty()) { - clientId = readBootstrapClientId(); + clientId = bootstrap != null && bootstrap.has("spaClientId") + ? bootstrap.get("spaClientId").asText() : ""; } + String apiResource = bootstrap != null && bootstrap.has("apiResourceIndicator") + ? bootstrap.get("apiResourceIndicator").asText() : ""; + // Use public endpoint for browser redirects (not Docker-internal URL) String endpoint = logtoPublicEndpoint; if (endpoint == null || endpoint.isEmpty()) { @@ -42,20 +46,20 @@ public class PublicConfigController { return Map.of( "logtoEndpoint", endpoint, - "logtoClientId", clientId != null ? clientId : "" + "logtoClientId", clientId != null ? clientId : "", + "logtoResource", apiResource ); } - private String readBootstrapClientId() { + private JsonNode readBootstrapFile() { try { File file = new File(BOOTSTRAP_FILE); if (file.exists()) { - JsonNode node = objectMapper.readTree(file); - return node.has("spaClientId") ? node.get("spaClientId").asText() : ""; + return objectMapper.readTree(file); } } catch (Exception e) { log.warn("Failed to read bootstrap config: {}", e.getMessage()); } - return ""; + return null; } } diff --git a/ui/src/auth/LoginPage.tsx b/ui/src/auth/LoginPage.tsx index 2173545..bc4370c 100644 --- a/ui/src/auth/LoginPage.tsx +++ b/ui/src/auth/LoginPage.tsx @@ -4,7 +4,7 @@ import { fetchConfig } from '../config'; import { generatePkce, storeCodeVerifier } from './pkce'; export function LoginPage() { - const [config, setConfig] = useState<{ logtoEndpoint: string; logtoClientId: string } | null>(null); + const [config, setConfig] = useState<{ logtoEndpoint: string; logtoClientId: string; logtoResource: string } | null>(null); const [loading, setLoading] = useState(true); useEffect(() => { @@ -35,6 +35,9 @@ export function LoginPage() { code_challenge: codeChallenge, code_challenge_method: 'S256', }); + if (config.logtoResource) { + params.set('resource', config.logtoResource); + } window.location.href = `${config.logtoEndpoint}/oidc/auth?${params}`; }; diff --git a/ui/src/config.ts b/ui/src/config.ts index 25e9d38..b60e7bf 100644 --- a/ui/src/config.ts +++ b/ui/src/config.ts @@ -1,6 +1,7 @@ interface AppConfig { logtoEndpoint: string; logtoClientId: string; + logtoResource: string; } let cached: AppConfig | null = null; @@ -22,6 +23,7 @@ export async function fetchConfig(): Promise { cached = { logtoEndpoint: import.meta.env.VITE_LOGTO_ENDPOINT || 'http://localhost:3001', logtoClientId: import.meta.env.VITE_LOGTO_CLIENT_ID || '', + logtoResource: import.meta.env.VITE_LOGTO_RESOURCE || '', }; return cached; }