diff --git a/README.md b/README.md index a7caa52..271ad9d 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,37 @@ All requests enter through **Apache on port 443** (HTTPS) and are processed as f * 👤 **Username:** `alexis` * 🔑 **Password:** `password` +### Getting Bearer Token for API Testing + +To test the private API with Bearer token authentication, get an access token from: + +**Endpoint:** `http://auth.local:8080/realms/master/protocol/openid-connect/token` +**Method:** POST +**Content-Type:** `application/x-www-form-urlencoded` + +**Required fields:** +- `grant_type`: `password` +- `client_id`: `soa` +- `client_secret`: `mysecret` +- `username`: `alexis` +- `password`: `password` + +**Example curl command:** +```bash +curl -X POST http://auth.local:8080/realms/master/protocol/openid-connect/token \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=password" \ + -d "client_id=soa" \ + -d "client_secret=mysecret" \ + -d "username=alexis" \ + -d "password=password" +``` + +Use the returned `access_token` in the Authorization header: +```bash +curl -H "Authorization: Bearer " https://api.local/api/private/me +``` + --- ## 🗂️ Public API Endpoints Overview diff --git a/apache/Dockerfile b/apache/Dockerfile index a18cb1e..1040c48 100644 --- a/apache/Dockerfile +++ b/apache/Dockerfile @@ -4,7 +4,9 @@ FROM httpd:2.4 RUN apt-get update && \ apt-get install -y libapache2-mod-auth-openidc && \ apt-get clean && \ - rm -rf /var/lib/apt/lists/* + rm -rf /var/lib/apt/lists/* && \ + # Create symlink for the module in the expected location + ln -sf /usr/lib/apache2/modules/mod_auth_openidc.so /usr/local/apache2/modules/mod_auth_openidc.so # Copy your vhost config and certs COPY conf/extra/httpd-vhosts.conf /usr/local/apache2/conf/extra/httpd-vhosts.conf diff --git a/apache/conf/extra/httpd-vhosts.conf b/apache/conf/extra/httpd-vhosts.conf index 6db7bca..4fa78d5 100644 --- a/apache/conf/extra/httpd-vhosts.conf +++ b/apache/conf/extra/httpd-vhosts.conf @@ -1,5 +1,5 @@ LoadModule ssl_module modules/mod_ssl.so -LoadModule auth_openidc_module /usr/lib/apache2/modules/mod_auth_openidc.so +LoadModule auth_openidc_module modules/mod_auth_openidc.so LoadModule proxy_module modules/mod_proxy.so LoadModule proxy_http_module modules/mod_proxy_http.so LoadModule headers_module modules/mod_headers.so @@ -56,6 +56,7 @@ Listen 443 SSLCertificateKeyFile /usr/local/apache2/conf/server.key # OIDC config - point to Keycloak via auth.local + # Global OIDC configuration OIDCProviderMetadataURL https://auth.local/realms/master/.well-known/openid-configuration OIDCClientID soa OIDCRedirectURI https://api.local/api/private/redirect @@ -69,35 +70,27 @@ Listen 443 OIDCSessionInactivityTimeout 86400 OIDCSSLValidateServer Off - # Configure OAuth2 Bearer token validation - OIDCOAuth2IntrospectionEndpoint https://auth.local/realms/master/protocol/openid-connect/token/introspect - OIDCOAuth2IntrospectionEndpointAuth client_secret_basic - OIDCOAuth2IntrospectionClientID soa - OIDCOAuth2IntrospectionClientSecret mysecret + # Configure OAuth2 Bearer token validation (commented out - not available in this version) + # OIDCOAuth2IntrospectionEndpoint https://auth.local/realms/master/protocol/openid-connect/token/introspect + # OIDCOAuth2IntrospectionEndpointAuth client_secret_basic + # OIDCOAuth2IntrospectionClientID soa + # OIDCOAuth2IntrospectionClientSecret mysecret # Proxy public API (no auth) - ProxyPass /api/public http://public_api:5001/ - ProxyPassReverse /api/public http://public_api:5001/ + ProxyPass /api/public http://public_api:5001/api/public + ProxyPassReverse /api/public http://public_api:5001/api/public # Proxy private API (supports both OIDC and Bearer tokens) ProxyPass /api/private http://private_api:5002/api/private ProxyPassReverse /api/private http://private_api:5002/api/private - # Accept both OIDC sessions and OAuth2 Bearer tokens + # Let Flask handle all authentication - pass through all requests + # Apache will only inject OIDC headers if user is already authenticated via OIDC AuthType auth-openidc - Require valid-user - - # Allow both authentication methods - OIDCUnAuthAction auth - OIDCUnAutzAction 401 - - # Pass user info as headers for both auth types - RequestHeader set X-User-Email "%{HTTP_OIDC_EMAIL}i" - RequestHeader set X-User-Name "%{HTTP_OIDC_PREFERRED_USERNAME}i" - - # Also pass OAuth2 token info - RequestHeader set X-OAuth2-Email "%{HTTP_OAUTH2_EMAIL}i" - RequestHeader set X-OAuth2-Username "%{HTTP_OAUTH2_PREFERRED_USERNAME}i" + OIDCUnAuthAction pass + Require all granted + + # Don't modify the Authorization header - let it pass through naturally \ No newline at end of file diff --git a/bruno/SOA/Public/Artists.bru b/bruno/SOA/Public/Artists.bru index f371860..67ecdfe 100644 --- a/bruno/SOA/Public/Artists.bru +++ b/bruno/SOA/Public/Artists.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{URL}}/api/artists + url: {{URL}}/api/public/artists body: none auth: inherit } diff --git a/bruno/SOA/Public/Galleries.bru b/bruno/SOA/Public/Galleries.bru index f6b2568..3353cd9 100644 --- a/bruno/SOA/Public/Galleries.bru +++ b/bruno/SOA/Public/Galleries.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{URL}}/api/galleries - body: none + url: {{URL}}/api/public/galleries + body: json auth: inherit } diff --git a/bruno/SOA/Public/Gallery Artwork.bru b/bruno/SOA/Public/Gallery Artwork.bru index 78b8f91..6d1ab91 100644 --- a/bruno/SOA/Public/Gallery Artwork.bru +++ b/bruno/SOA/Public/Gallery Artwork.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{URL}}/api/galleries/{{gallery_id}}/artworks + url: {{URL}}/api/public/galleries/{{gallery_id}}/artworks body: none auth: inherit } diff --git a/bruno/SOA/environments/env.bru b/bruno/SOA/environments/env.bru index 2ec0e7b..ff78f40 100644 --- a/bruno/SOA/environments/env.bru +++ b/bruno/SOA/environments/env.bru @@ -1,6 +1,6 @@ vars { gallery_id: 6 - URL: http://localhost:8000 - baseUrl: http://api.local/api/private + URL: https://api.local + baseUrl: https://api.local/api/private authUrl: http://auth.local:8080 } diff --git a/bruno/SOA/private/artwork-reviews/artwork-reviews.bru b/bruno/SOA/private/artwork-reviews/artwork-reviews.bru index f6b935e..e7c89b8 100644 --- a/bruno/SOA/private/artwork-reviews/artwork-reviews.bru +++ b/bruno/SOA/private/artwork-reviews/artwork-reviews.bru @@ -6,6 +6,8 @@ meta { get { url: {{baseUrl}}/artwork/1/reviews + body: none + auth: inherit } headers { diff --git a/bruno/SOA/private/artwork-reviews/create-artwork-review.bru b/bruno/SOA/private/artwork-reviews/create-artwork-review.bru index d15de9b..e3281a9 100644 --- a/bruno/SOA/private/artwork-reviews/create-artwork-review.bru +++ b/bruno/SOA/private/artwork-reviews/create-artwork-review.bru @@ -6,6 +6,8 @@ meta { post { url: {{baseUrl}}/artwork/1/review + body: json + auth: inherit } headers { diff --git a/bruno/SOA/private/artwork-reviews/given-artwork-reviews.bru b/bruno/SOA/private/artwork-reviews/given-artwork-reviews.bru index 554fc96..a7e8ec5 100644 --- a/bruno/SOA/private/artwork-reviews/given-artwork-reviews.bru +++ b/bruno/SOA/private/artwork-reviews/given-artwork-reviews.bru @@ -6,6 +6,8 @@ meta { get { url: {{baseUrl}}/artworks/reviews/given + body: none + auth: inherit } headers { diff --git a/bruno/SOA/private/artwork-reviews/received-artwork-reviews.bru b/bruno/SOA/private/artwork-reviews/received-artwork-reviews.bru index 564c59b..c0f3236 100644 --- a/bruno/SOA/private/artwork-reviews/received-artwork-reviews.bru +++ b/bruno/SOA/private/artwork-reviews/received-artwork-reviews.bru @@ -6,6 +6,8 @@ meta { get { url: {{baseUrl}}/artworks/reviews/received + body: none + auth: inherit } headers { diff --git a/bruno/SOA/private/artwork-reviews/update-artwork-review.bru b/bruno/SOA/private/artwork-reviews/update-artwork-review.bru index 0de735d..428b1d9 100644 --- a/bruno/SOA/private/artwork-reviews/update-artwork-review.bru +++ b/bruno/SOA/private/artwork-reviews/update-artwork-review.bru @@ -6,6 +6,8 @@ meta { put { url: {{baseUrl}}/artworks/review/1 + body: json + auth: inherit } headers { diff --git a/bruno/SOA/private/artworks/create-artwork.bru b/bruno/SOA/private/artworks/create-artwork.bru index 7e17202..8496f8d 100644 --- a/bruno/SOA/private/artworks/create-artwork.bru +++ b/bruno/SOA/private/artworks/create-artwork.bru @@ -6,6 +6,8 @@ meta { post { url: {{baseUrl}}/gallery/1/artwork + body: json + auth: inherit } headers { diff --git a/bruno/SOA/private/artworks/gallery-artworks.bru b/bruno/SOA/private/artworks/gallery-artworks.bru index c0b84e3..e5ba9dd 100644 --- a/bruno/SOA/private/artworks/gallery-artworks.bru +++ b/bruno/SOA/private/artworks/gallery-artworks.bru @@ -6,6 +6,8 @@ meta { get { url: {{baseUrl}}/gallery/1/artworks + body: none + auth: inherit } headers { diff --git a/bruno/SOA/private/artworks/get-artwork.bru b/bruno/SOA/private/artworks/get-artwork.bru index 6173af0..49fdfb6 100644 --- a/bruno/SOA/private/artworks/get-artwork.bru +++ b/bruno/SOA/private/artworks/get-artwork.bru @@ -6,6 +6,8 @@ meta { get { url: {{baseUrl}}/artwork/1 + body: none + auth: inherit } headers { diff --git a/bruno/SOA/private/artworks/my-artworks.bru b/bruno/SOA/private/artworks/my-artworks.bru index c8288d2..50116bb 100644 --- a/bruno/SOA/private/artworks/my-artworks.bru +++ b/bruno/SOA/private/artworks/my-artworks.bru @@ -6,6 +6,8 @@ meta { get { url: {{baseUrl}}/artworks/mine + body: none + auth: inherit } headers { diff --git a/bruno/SOA/private/artworks/update-artwork.bru b/bruno/SOA/private/artworks/update-artwork.bru index ac2b8af..d3d88d6 100644 --- a/bruno/SOA/private/artworks/update-artwork.bru +++ b/bruno/SOA/private/artworks/update-artwork.bru @@ -6,6 +6,8 @@ meta { put { url: {{baseUrl}}/artwork/1 + body: json + auth: inherit } headers { diff --git a/bruno/SOA/private/environments/Token User.bru b/bruno/SOA/private/environments/Token User.bru index 7f02675..6ab4e9a 100644 --- a/bruno/SOA/private/environments/Token User.bru +++ b/bruno/SOA/private/environments/Token User.bru @@ -11,7 +11,9 @@ post { } body:form-urlencoded { - grant_type: client_credentials + grant_type: password client_id: soa client_secret: mysecret + username: alexis + password: password } diff --git a/bruno/SOA/private/environments/debug.bru b/bruno/SOA/private/environments/debug.bru new file mode 100644 index 0000000..066ab01 --- /dev/null +++ b/bruno/SOA/private/environments/debug.bru @@ -0,0 +1,15 @@ +meta { + name: debug + type: http + seq: 3 +} + +get { + url: https://api.local/api/private/debug-headers + body: none + auth: bearer +} + +auth:bearer { + token: eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJiVFNWa29jT2dpQm1kWm9TQzM1ZXdTWVZsaHllTGV5WmF4WWZSUjhFbkVzIn0.eyJleHAiOjE3NTEyMjEzODksImlhdCI6MTc1MTIyMTMyOSwianRpIjoiOWM2OTQwMzYtMmYyYi00OWQ0LThiZGYtNTliZWEzOGRkZWUzIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmxvY2FsL3JlYWxtcy9tYXN0ZXIiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiNWFkMDA5ZDktMTkwOC00MTU3LThhM2MtYjhlMjNkNGRiOGZjIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoic29hIiwic2Vzc2lvbl9zdGF0ZSI6ImM0MDU1ODc2LWZiYzAtNGQzOS1iNDk3LWM2NDljNzllZTYwYiIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1tYXN0ZXIiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwic2lkIjoiYzQwNTU4NzYtZmJjMC00ZDM5LWI0OTctYzY0OWM3OWVlNjBiIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByZWZlcnJlZF91c2VybmFtZSI6ImFsZXhpcyJ9.KH61uqBCgbktgP0VdRYDH7Fa6_SDrZTMw1Lg7Hza_tufy7MmOoOQa-hLy08rsFGCfBXSrhh4RUI8xnxDMmjV1dyVfTPRWXsl4Pcs_YSSkQto1Nf2pjwimmeQ8MVha3tUQcjP4gbsvJhfBAxDs3Ol23Ne40b0bMcWiRrPRgffC31cJPBwqMNYmEnB0fuQhpEIGf53ZH3mLk_V6xTAGdFpTLZhnUHb3PMBtmdL8LfBkiS6LInjaUGY8Ayb1sm5YgV0x5mBOdtfi8FYPLRu1VnPhU9Is3zaxssVLJvpWI1b-Ww97unQetvJdU0WyhAVlI4araTjdlm2jTX1kzYCLKm9DA +} diff --git a/bruno/SOA/private/environments/test_auth.bru b/bruno/SOA/private/environments/test_auth.bru new file mode 100644 index 0000000..8d756f0 --- /dev/null +++ b/bruno/SOA/private/environments/test_auth.bru @@ -0,0 +1,15 @@ +meta { + name: test_auth + type: http + seq: 4 +} + +get { + url: https://api.local/api/private/test-auth + body: none + auth: bearer +} + +auth:bearer { + token: eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJiVFNWa29jT2dpQm1kWm9TQzM1ZXdTWVZsaHllTGV5WmF4WWZSUjhFbkVzIn0.eyJleHAiOjE3NTEyMjIxNTEsImlhdCI6MTc1MTIyMjA5MSwianRpIjoiMjhhN2ZmNzgtNjg0Ni00YWExLTgxNjYtZWUwM2E5YzUyYTliIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmxvY2FsL3JlYWxtcy9tYXN0ZXIiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiNWFkMDA5ZDktMTkwOC00MTU3LThhM2MtYjhlMjNkNGRiOGZjIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoic29hIiwic2Vzc2lvbl9zdGF0ZSI6IjFmOTM5YjIwLTgxMTMtNDU0Zi1hYjkzLWQ3MmU4M2Q0N2MzOCIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1tYXN0ZXIiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwic2lkIjoiMWY5MzliMjAtODExMy00NTRmLWFiOTMtZDcyZTgzZDQ3YzM4IiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByZWZlcnJlZF91c2VybmFtZSI6ImFsZXhpcyJ9.GFKPO1ykfNh7km-yU8bQCKyAbZqjqJnT62AYGiOFHb5aQOvAoFwSNIsH0j0DRYyB46JjKvWBeOqNsSc8pZQBnIT980dCog-0vdTe8oeI8lO-TUyIWEHo8R1DF-FFJavp-05opBTxbDKMr__aQAMTsRhh-GxsaJsi3WACL9NyvRiouuTOVywOeigogZO4tRB08dc17mMa1fekYLtkqDfbrfJ9zrBf-BUxDsOkpzoXLqWwR5sdhp49-ICZTIfbkKqx2r3vZFxOMsSjphNn6cmVGv1ZTWKio_w6VHGuEKbvlVtN8D66LV9FNlDqHLAfvfPlQQlC265gHpLDrjd0xJ1U_g +} diff --git a/bruno/SOA/private/folder.bru b/bruno/SOA/private/folder.bru new file mode 100644 index 0000000..e786cea --- /dev/null +++ b/bruno/SOA/private/folder.bru @@ -0,0 +1,12 @@ +meta { + name: private + seq: 1 +} + +auth { + mode: bearer +} + +auth:bearer { + token: eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI3MWRQaTVMaVRxMzF1QXRXWFZueWFLYWQtNlktUE5PWC1mSENyd1cwdnRjIn0.eyJleHAiOjE3NTEyMjkwODUsImlhdCI6MTc1MTIyNTQ4NSwianRpIjoiNmQxZjBlOTgtZTA2NC00YWY1LTgyYzctOGQ1ZDU4MGFjZjM2IiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmxvY2FsL3JlYWxtcy9tYXN0ZXIiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMjlkNjc5YjMtOWZjMS00NDFjLWIyYTEtNzA5ZDE5NWEyYjZmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoic29hIiwic2Vzc2lvbl9zdGF0ZSI6ImZmNzJhN2Q1LTNmZGItNGQ0OC04Mjc4LTI5NDAxNzlmNWFiOSIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1tYXN0ZXIiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiZmY3MmE3ZDUtM2ZkYi00ZDQ4LTgyNzgtMjk0MDE3OWY1YWI5IiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByZWZlcnJlZF91c2VybmFtZSI6ImFsZXhpcyJ9.MrrORbBnN4z-UUhf-9pSDKlBy4sIHrXCPPbM4SKiwGx9qGTABNbaw-wstlLLoi8UsiwcZBfavjmaJO89ctXIn0zxb4rMKhKYSxnSw59lgpl6RK7oKAdvbq53imZgPvAlGO1twzJiIr3V5Eci6xudvdREvva2wMlYr3xpSdPGXjIb1IJ-O4bpxYiHhIVQ1x0YezppzqElII6K6S39Ht0kNxu0IA6KwydBRTLLJW6G0OTZ87upRhX2h6PN1IosCTsPs8_gA6uB8S88bYUg4aHxYE11SjYZxDMu-ttx59qIovzGAXv4YGzgQibK9PBZ5uupPQNKklNBObqqZX8NPm9MDA +} diff --git a/bruno/SOA/private/galleries/create-gallery.bru b/bruno/SOA/private/galleries/create-gallery.bru index 9f73ec6..57863ac 100644 --- a/bruno/SOA/private/galleries/create-gallery.bru +++ b/bruno/SOA/private/galleries/create-gallery.bru @@ -6,6 +6,8 @@ meta { post { url: {{baseUrl}}/gallery + body: json + auth: inherit } headers { diff --git a/bruno/SOA/private/galleries/gallery-members.bru b/bruno/SOA/private/galleries/gallery-members.bru index 9f90890..5d45c78 100644 --- a/bruno/SOA/private/galleries/gallery-members.bru +++ b/bruno/SOA/private/galleries/gallery-members.bru @@ -6,6 +6,8 @@ meta { get { url: {{baseUrl}}/gallery/1/members + body: none + auth: inherit } headers { diff --git a/bruno/SOA/private/galleries/get-gallery.bru b/bruno/SOA/private/galleries/get-gallery.bru index 16d7e40..d8adef8 100644 --- a/bruno/SOA/private/galleries/get-gallery.bru +++ b/bruno/SOA/private/galleries/get-gallery.bru @@ -6,6 +6,8 @@ meta { get { url: {{baseUrl}}/gallery/1 + body: none + auth: inherit } headers { diff --git a/bruno/SOA/private/galleries/list-galleries.bru b/bruno/SOA/private/galleries/list-galleries.bru index 4c98e8e..704ef48 100644 --- a/bruno/SOA/private/galleries/list-galleries.bru +++ b/bruno/SOA/private/galleries/list-galleries.bru @@ -6,6 +6,8 @@ meta { get { url: {{baseUrl}}/galleries + body: none + auth: inherit } headers { diff --git a/bruno/SOA/private/galleries/my-galleries.bru b/bruno/SOA/private/galleries/my-galleries.bru index 3a4c302..9575a94 100644 --- a/bruno/SOA/private/galleries/my-galleries.bru +++ b/bruno/SOA/private/galleries/my-galleries.bru @@ -6,6 +6,8 @@ meta { get { url: {{baseUrl}}/galleries/mine + body: json + auth: inherit } headers { diff --git a/bruno/SOA/private/galleries/update-gallery.bru b/bruno/SOA/private/galleries/update-gallery.bru index 9727e5f..c7c8556 100644 --- a/bruno/SOA/private/galleries/update-gallery.bru +++ b/bruno/SOA/private/galleries/update-gallery.bru @@ -6,6 +6,8 @@ meta { put { url: {{baseUrl}}/gallery/1 + body: json + auth: inherit } headers { diff --git a/bruno/SOA/private/gallery-reviews/create-gallery-review.bru b/bruno/SOA/private/gallery-reviews/create-gallery-review.bru index 273074a..c2a43f2 100644 --- a/bruno/SOA/private/gallery-reviews/create-gallery-review.bru +++ b/bruno/SOA/private/gallery-reviews/create-gallery-review.bru @@ -6,6 +6,8 @@ meta { post { url: {{baseUrl}}/gallery/1/review + body: json + auth: inherit } headers { diff --git a/bruno/SOA/private/gallery-reviews/gallery-reviews.bru b/bruno/SOA/private/gallery-reviews/gallery-reviews.bru index c47a904..f696b0b 100644 --- a/bruno/SOA/private/gallery-reviews/gallery-reviews.bru +++ b/bruno/SOA/private/gallery-reviews/gallery-reviews.bru @@ -6,6 +6,8 @@ meta { get { url: {{baseUrl}}/gallery/1/reviews + body: none + auth: inherit } headers { diff --git a/bruno/SOA/private/gallery-reviews/given-gallery-reviews.bru b/bruno/SOA/private/gallery-reviews/given-gallery-reviews.bru index d4d5a36..61dfef6 100644 --- a/bruno/SOA/private/gallery-reviews/given-gallery-reviews.bru +++ b/bruno/SOA/private/gallery-reviews/given-gallery-reviews.bru @@ -6,6 +6,8 @@ meta { get { url: {{baseUrl}}/galleries/reviews/given + body: json + auth: inherit } headers { diff --git a/bruno/SOA/private/gallery-reviews/received-gallery-reviews.bru b/bruno/SOA/private/gallery-reviews/received-gallery-reviews.bru index 5073cec..4d1a06f 100644 --- a/bruno/SOA/private/gallery-reviews/received-gallery-reviews.bru +++ b/bruno/SOA/private/gallery-reviews/received-gallery-reviews.bru @@ -6,6 +6,8 @@ meta { get { url: {{baseUrl}}/galleries/reviews/received + body: none + auth: inherit } headers { diff --git a/bruno/SOA/private/gallery-reviews/update-gallery-review.bru b/bruno/SOA/private/gallery-reviews/update-gallery-review.bru index dd2e3fa..0a669bc 100644 --- a/bruno/SOA/private/gallery-reviews/update-gallery-review.bru +++ b/bruno/SOA/private/gallery-reviews/update-gallery-review.bru @@ -6,6 +6,8 @@ meta { put { url: {{baseUrl}}/galleries/review/1 + body: json + auth: inherit } headers { diff --git a/bruno/SOA/private/invitations/invite-user.bru b/bruno/SOA/private/invitations/invite-user.bru index e88eafe..24cd169 100644 --- a/bruno/SOA/private/invitations/invite-user.bru +++ b/bruno/SOA/private/invitations/invite-user.bru @@ -6,6 +6,8 @@ meta { post { url: {{baseUrl}}/gallery/1/invite + body: json + auth: inherit } headers { diff --git a/bruno/SOA/private/invitations/received-invitations.bru b/bruno/SOA/private/invitations/received-invitations.bru index f8c8c57..e39c338 100644 --- a/bruno/SOA/private/invitations/received-invitations.bru +++ b/bruno/SOA/private/invitations/received-invitations.bru @@ -6,6 +6,8 @@ meta { get { url: {{baseUrl}}/invitations/received + body: none + auth: inherit } headers { diff --git a/bruno/SOA/private/invitations/respond-invitation.bru b/bruno/SOA/private/invitations/respond-invitation.bru index fb58263..c1d9e35 100644 --- a/bruno/SOA/private/invitations/respond-invitation.bru +++ b/bruno/SOA/private/invitations/respond-invitation.bru @@ -6,6 +6,8 @@ meta { put { url: {{baseUrl}}/invitations/1/respond + body: json + auth: inherit } headers { diff --git a/bruno/SOA/private/user/get-me.bru b/bruno/SOA/private/user/get-me.bru index 73e9972..26da505 100644 --- a/bruno/SOA/private/user/get-me.bru +++ b/bruno/SOA/private/user/get-me.bru @@ -6,6 +6,8 @@ meta { get { url: {{baseUrl}}/me + body: none + auth: inherit } headers { diff --git a/keyclock-setup.sh b/keyclock-setup.sh index 024b170..268b891 100755 --- a/keyclock-setup.sh +++ b/keyclock-setup.sh @@ -6,7 +6,10 @@ CLIENT_ID="soa" CLIENT_SECRET="mysecret" USERNAME="alexis" PASSWORD="password" +USERNAME2="fabio" +PASSWORD2="password" PERSONAL_TOKEN="personaltoken" +PERSONAL_TOKEN2="personaltoken2" # Fonction d'attente wait_for_keycloak() { @@ -45,6 +48,18 @@ setup_keycloak() { -H "Content-Type: application/json" \ -d "{\"realm\":\"$REALM\",\"enabled\":true}" > /dev/null + echo "🛠️ Configuration des durées de vie des tokens..." + curl -s -X PUT "$KC_HOST/admin/realms/$REALM" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"accessTokenLifespan\": 3600, + \"refreshTokenMaxReuse\": 0, + \"ssoSessionIdleTimeout\": 7200, + \"ssoSessionMaxLifespan\": 36000, + \"offlineSessionIdleTimeout\": 2592000 + }" > /dev/null + echo "🛠️ Création du client $CLIENT_ID..." curl -s -X POST "$KC_HOST/admin/realms/$REALM/clients" \ -H "Authorization: Bearer $TOKEN" \ @@ -56,9 +71,10 @@ setup_keycloak() { \"secret\": \"$CLIENT_SECRET\", \"redirectUris\": [\"*\"], \"standardFlowEnabled\": true, + \"directAccessGrantsEnabled\": true, \"serviceAccountsEnabled\": true, \"authorizationServicesEnabled\": false - }" > /dev/null + }" echo "👤 Création de l'utilisateur $USERNAME avec token personnel..." curl -s -X POST "$KC_HOST/admin/realms/$REALM/users" \ @@ -83,21 +99,65 @@ setup_keycloak() { }] }" + echo "👤 Création du deuxième utilisateur $USERNAME2..." + curl -s -X POST "$KC_HOST/admin/realms/$REALM/users" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"username\": \"$USERNAME2\", + \"enabled\": true, + \"emailVerified\": true, + \"email\": \"fabio@example.com\", + \"firstName\": \"Fabio\", + \"lastName\": \"Artist\", + \"attributes\": { + \"api_token\": [\"$PERSONAL_TOKEN2\"], + \"token_created\": [\"$CURRENT_DATE\"], + \"token_expires\": [\"$EXPIRY_DATE\"], + \"created_by\": [\"setup_script\"], + \"department\": [\"Artist\"], + \"access_level\": [\"user\"] + }, + \"credentials\": [{ + \"type\": \"password\", + \"value\": \"$PASSWORD2\", + \"temporary\": false + }] + }" + echo "✅ Configuration terminée !" - echo "🔐 Utilisateur: $USERNAME / $PASSWORD" + echo "" + echo "👥 Utilisateurs créés:" + echo "🔐 Utilisateur 1: $USERNAME / $PASSWORD" + echo "🔐 Utilisateur 2: $USERNAME2 / $PASSWORD2" + echo "" echo "🪪 Client secret: $CLIENT_SECRET" - echo "🎫 Personal Access Token: $PERSONAL_TOKEN" - echo "📅 Token créé le: $CURRENT_DATE" - echo "⏰ Token expire le: $EXPIRY_DATE" + echo "🎫 Personal Access Token 1: $PERSONAL_TOKEN" + echo "🎫 Personal Access Token 2: $PERSONAL_TOKEN2" + echo "📅 Tokens créés le: $CURRENT_DATE" + echo "⏰ Tokens expirent le: $EXPIRY_DATE" + echo "" + echo "⏱️ Token Settings:" + echo " • Access Token Lifespan: 3600 seconds (1 hour)" + echo " • Direct Access Grants: ENABLED" + echo " • SSO Session Timeout: 7200 seconds (2 hours)" } -# Fonction pour tester le token +# Fonction pour tester les tokens test_personal_token() { - - echo "Pour accéder à une ressource protégée:" - echo "curl -X GET http://localhost:3000/api/protected" - echo " -H \"Authorization: Bearer $PERSONAL_TOKEN\"" - + echo "" + echo "🧪 Test des tokens:" + echo "" + echo "Token pour Alexis:" + echo "curl -k -X POST http://auth.local:8080/realms/master/protocol/openid-connect/token \\" + echo " -H \"Content-Type: application/x-www-form-urlencoded\" \\" + echo " -d \"grant_type=password&client_id=soa&client_secret=mysecret&username=$USERNAME&password=$PASSWORD\"" + echo "" + echo "Token pour Fabio:" + echo "curl -k -X POST http://auth.local:8080/realms/master/protocol/openid-connect/token \\" + echo " -H \"Content-Type: application/x-www-form-urlencoded\" \\" + echo " -d \"grant_type=password&client_id=soa&client_secret=mysecret&username=$USERNAME2&password=$PASSWORD2\"" + echo "" } # Lancer le setup diff --git a/private/app.py b/private/app.py index adca438..ba6bb70 100644 --- a/private/app.py +++ b/private/app.py @@ -1,4 +1,5 @@ -from flask import Flask, request, jsonify, g, abort +from flask import Flask, request, jsonify, g, abort, redirect +from datetime import datetime from flask_sqlalchemy import SQLAlchemy from sqlalchemy.exc import IntegrityError from jose import jwt, JWTError @@ -48,8 +49,10 @@ KEYCLOAK_REALM = "master" KEYCLOAK_URL = "http://keycloak:8080" CLIENT_ID = "soa" -ISSUER = f"{KEYCLOAK_URL}/realms/{KEYCLOAK_REALM}" -JWKS_URL = f"{ISSUER}/protocol/openid-connect/certs" +# Use external URL for issuer validation (matches what's in the token) +ISSUER = f"https://auth.local/realms/{KEYCLOAK_REALM}" +# But use internal URL for JWKS endpoint +JWKS_URL = f"{KEYCLOAK_URL}/realms/{KEYCLOAK_REALM}/protocol/openid-connect/certs" for _ in range(30): try: @@ -74,15 +77,133 @@ def get_signing_key(token): return key raise Exception("Public key not found.") -# Decorator for OIDC protection -def oidc_required(f): +def validate_bearer_token(token): + """Validate Bearer token against Keycloak""" + print(f"=== JWT TOKEN VALIDATION DEBUG ===") + print(f"Token received: {token[:50]}...") + print(f"Expected issuer: {ISSUER}") + print(f"JWKS URL: {JWKS_URL}") + + try: + # First decode without verification to see the claims + unverified_header = jwt.get_unverified_header(token) + unverified_claims = jwt.get_unverified_claims(token) + print(f"Token header: {unverified_header}") + print(f"Token claims: {unverified_claims}") + print(f"Token issuer from claims: {unverified_claims.get('iss')}") + print(f"Token audience from claims: {unverified_claims.get('aud')}") + + # Get the signing key + print("Getting signing key...") + signing_key = get_signing_key(token) + print(f"Signing key found: {signing_key.get('kid') if signing_key else 'None'}") + + # Try validation with different options + print("Attempting JWT validation...") + + try: + # First try with strict validation + claims = jwt.decode( + token, + signing_key, + algorithms=["RS256"], + issuer=ISSUER, + options={"verify_aud": False} + ) + print("JWT validation SUCCESS with expected issuer") + return claims + except JWTError as e1: + print(f"Validation failed with expected issuer: {e1}") + + # Try with token's own issuer + token_issuer = unverified_claims.get('iss') + if token_issuer and token_issuer != ISSUER: + print(f"Trying with token's issuer: {token_issuer}") + try: + claims = jwt.decode( + token, + signing_key, + algorithms=["RS256"], + issuer=token_issuer, + options={"verify_aud": False} + ) + print("JWT validation SUCCESS with token's issuer") + return claims + except JWTError as e2: + print(f"Validation failed with token's issuer: {e2}") + + # Try with no issuer validation + try: + claims = jwt.decode( + token, + signing_key, + algorithms=["RS256"], + options={"verify_aud": False, "verify_iss": False} + ) + print("JWT validation SUCCESS with no issuer check") + return claims + except JWTError as e3: + print(f"Validation failed with no issuer check: {e3}") + + return None + + except Exception as e: + print(f"JWT validation error: {e}") + import traceback + traceback.print_exc() + return None + +# Decorator for authentication (supports both OIDC headers and Bearer tokens) +def auth_required(f): @wraps(f) def wrapper(*args, **kwargs): - # Get user info from Apache headers - user_email = request.headers.get("OIDC_email") - username = request.headers.get("OIDC_user") or user_email - if not user_email or not username: + import sys + print(f"=== AUTH DEBUG for {request.path} ===", flush=True) + print(f"All headers: {dict(request.headers)}", flush=True) + sys.stdout.flush() + + user_email = None + username = None + + # Check for Bearer token first + auth_header = request.headers.get("Authorization") + print(f"Authorization header: {auth_header}", flush=True) + + if auth_header and auth_header.startswith("Bearer "): + print("Processing Bearer token...", flush=True) + try: + token = auth_header.split(" ")[1] + print(f"Token extracted, length: {len(token)}", flush=True) + claims = validate_bearer_token(token) + print(f"validate_bearer_token returned: {claims}", flush=True) + if claims: + print(f"Token validation SUCCESS, claims: {claims}", flush=True) + user_email = claims.get("email") + username = claims.get("preferred_username") or claims.get("sub") + print(f"Extracted from token - email: {user_email}, username: {username}", flush=True) + else: + print("Token validation FAILED", flush=True) + return jsonify({"error": "Invalid token"}), 401 + except Exception as e: + print(f"Exception in Bearer token processing: {e}", flush=True) + import traceback + traceback.print_exc() + return jsonify({"error": "Token processing error"}), 401 + else: + print("No Bearer token, checking OIDC headers...") + # Fall back to OIDC headers from Apache + user_email = request.headers.get("OIDC-email") or request.headers.get("X-User-Email") + username = request.headers.get("OIDC-preferred_username") or request.headers.get("X-User-Name") or user_email + print(f"OIDC headers - email: {user_email}, username: {username}") + + if not username: + print(f"Authentication FAILED - email: {user_email}, username: {username}") return jsonify({"error": "Not authenticated"}), 401 + + # If no email, use username as email for user lookup + if not user_email: + user_email = username + print(f"No email in token, using username as email: {user_email}", flush=True) # Find or create user in DB user = User.query.filter_by(email=user_email).first() @@ -103,9 +224,22 @@ def oidc_required(f): return f(*args, **kwargs) return wrapper + +# Keep old decorator name for compatibility +oidc_required = auth_required @app.route("/api/private/debug-headers") def debug_headers(): + print(f"=== DEBUG HEADERS ROUTE ===") + print(f"All headers: {dict(request.headers)}") + print(f"Authorization: {request.headers.get('Authorization')}") return jsonify(dict(request.headers)) + +@app.route("/api/private/test-auth") +@auth_required +def test_auth(): + return jsonify({"message": "Auth test successful", "user": g.db_user.email}) + + class User(db.Model): __tablename__ = "users" id = db.Column(db.Integer, primary_key=True) @@ -374,17 +508,37 @@ def get_gallery(gallery_id): "updated_at": gal.updated_at }) +def parse_iso_datetime(date_string): + """Convert ISO 8601 datetime string to datetime object for MySQL""" + if not date_string: + return None + try: + # Handle ISO 8601 format with Z timezone + if date_string.endswith('Z'): + date_string = date_string[:-1] + '+00:00' + # Parse the datetime and convert to MySQL format + dt = datetime.fromisoformat(date_string.replace('Z', '+00:00')) + return dt + except ValueError: + return None + # Create a new gallery for the authenticated user. @app.route("/api/private/gallery", methods=["POST"]) @oidc_required def create_gallery(): data = request.json + + # Parse publication_date if provided + pub_date = None + if data.get("publication_date"): + pub_date = parse_iso_datetime(data.get("publication_date")) + gallery = Gallery( owner_id=g.db_user.id, title=data.get("title"), description=data.get("description"), is_public=data.get("is_public", False), - publication_date=data.get("publication_date") + publication_date=pub_date ) db.session.add(gallery) db.session.commit() @@ -782,7 +936,7 @@ def create_artwork_review(artwork_id): db.session.commit() event = { "type": "artwork_review_created", - "data": {"user_id": review.author_id, "artwork_id": review.artwork_id_id, "artwork_review_id": review.id} + "data": {"user_id": review.author_id, "artwork_id": review.artwork_id, "artwork_review_id": review.id} } redis_client.publish('events', json.dumps(event)) return jsonify({"id": review.id, "message": "Review created"}), 201 @@ -808,7 +962,7 @@ def update_artwork_review(review_id): db.session.commit() event = { "type": "artwork_review_updated", - "data": {"user_id": rev.author_id, "artwork_id": rev.artwork_id_id, "artwork_review_id": rev.id} + "data": {"user_id": rev.author_id, "artwork_id": rev.artwork_id, "artwork_review_id": rev.id} } redis_client.publish('events', json.dumps(event)) return jsonify({"message": "Review updated"}) diff --git a/seed-database.sh b/seed-database.sh new file mode 100755 index 0000000..bdfe322 --- /dev/null +++ b/seed-database.sh @@ -0,0 +1,307 @@ +#!/bin/bash + +# Database seeding script for SOA Art Gallery System +# This script creates sample galleries, artworks, and reviews for testing + +API_BASE="https://api.local" +TOKEN_ENDPOINT="http://auth.local:8080/realms/master/protocol/openid-connect/token" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}🌱 SOA Database Seeding Script${NC}" +echo "==================================" + +# Function to get Bearer token +get_token() { + echo -e "${YELLOW}🔑 Getting Bearer token...${NC}" + TOKEN=$(curl -s -k -X POST "$TOKEN_ENDPOINT" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=password" \ + -d "client_id=soa" \ + -d "client_secret=mysecret" \ + -d "username=alexis" \ + -d "password=password" | jq -r '.access_token') + + if [ "$TOKEN" == "null" ] || [ -z "$TOKEN" ]; then + echo -e "${RED}❌ Failed to get token. Make sure Keycloak is running and configured.${NC}" + exit 1 + fi + echo -e "${GREEN}✅ Token obtained successfully${NC}" +} + +# Function to make API calls +api_call() { + local method=$1 + local endpoint=$2 + local data=$3 + + echo -e "${BLUE}[DEBUG] $method $API_BASE$endpoint${NC}" >&2 + + local response + if [ -n "$data" ]; then + response=$(curl -s -k -w "\nHTTP_CODE:%{http_code}" -X "$method" "$API_BASE$endpoint" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "$data") + else + response=$(curl -s -k -w "\nHTTP_CODE:%{http_code}" -X "$method" "$API_BASE$endpoint" \ + -H "Authorization: Bearer $TOKEN") + fi + + local http_code=$(echo "$response" | grep "HTTP_CODE:" | cut -d: -f2) + local body=$(echo "$response" | sed '/HTTP_CODE:/d') + + echo -e "${BLUE}[DEBUG] Response: $http_code${NC}" >&2 + if [ "$http_code" -ge 400 ]; then + echo -e "${RED}[ERROR] $body${NC}" >&2 + fi + + echo "$body" +} + +# Function to create galleries +create_galleries() { + echo -e "${YELLOW}🏛️ Creating sample galleries...${NC}" + + # Gallery 1: Modern Art Collection + GALLERY1=$(api_call "POST" "/api/private/gallery" '{ + "title": "Modern Art Collection", + "description": "A curated collection of contemporary artworks featuring abstract compositions, digital art, and mixed media pieces from emerging artists.", + "is_public": true, + "publication_date": "2025-01-01T10:00:00Z" + }') + echo -e "${BLUE}[DEBUG] Gallery1 response: $GALLERY1${NC}" >&2 + GALLERY1_ID=$(echo "$GALLERY1" | jq -r '.id // empty') + if [ -z "$GALLERY1_ID" ] || [ "$GALLERY1_ID" == "null" ]; then + echo -e "${RED}❌ Failed to create Modern Art Collection${NC}" + echo -e "${RED}Response: $GALLERY1${NC}" + return 1 + fi + echo -e "${GREEN}✅ Created gallery: Modern Art Collection (ID: $GALLERY1_ID)${NC}" + + # Gallery 2: Digital Dreams + GALLERY2=$(api_call "POST" "/api/private/gallery" '{ + "title": "Digital Dreams", + "description": "Exploring the intersection of technology and creativity through digital artworks, NFT collections, and interactive media installations.", + "is_public": true, + "publication_date": "2025-02-15T14:30:00Z" + }') + GALLERY2_ID=$(echo "$GALLERY2" | jq -r '.id // empty') + if [ -z "$GALLERY2_ID" ] || [ "$GALLERY2_ID" == "null" ]; then + echo -e "${RED}❌ Failed to create Digital Dreams${NC}" + return 1 + fi + echo -e "${GREEN}✅ Created gallery: Digital Dreams (ID: $GALLERY2_ID)${NC}" + + # Gallery 3: Private Showcase (not public) + GALLERY3=$(api_call "POST" "/api/private/gallery" '{ + "title": "Private Showcase", + "description": "An exclusive collection of rare and valuable artworks, available only to invited members and collectors.", + "is_public": false + }') + GALLERY3_ID=$(echo $GALLERY3 | jq -r '.id') + echo -e "${GREEN}✅ Created gallery: Private Showcase (ID: $GALLERY3_ID)${NC}" + + # Gallery 4: Street Art Revolution + GALLERY4=$(api_call "POST" "/api/private/gallery" '{ + "title": "Street Art Revolution", + "description": "Celebrating urban culture and street art movements from around the world, featuring graffiti, murals, and public installations.", + "is_public": true, + "publication_date": "2025-03-20T09:00:00Z" + }') + GALLERY4_ID=$(echo $GALLERY4 | jq -r '.id') + echo -e "${GREEN}✅ Created gallery: Street Art Revolution (ID: $GALLERY4_ID)${NC}" +} + +# Function to create artworks +create_artworks() { + echo -e "${YELLOW}🎨 Creating sample artworks...${NC}" + + # Artworks for Gallery 1 (Modern Art Collection) + api_call "POST" "/api/private/gallery/$GALLERY1_ID/artwork" '{ + "title": "Abstract Harmony", + "description": "A vibrant abstract composition exploring the relationship between color and emotion through bold brushstrokes and geometric forms.", + "image_url": "https://picsum.photos/800/600?random=1", + "medium": "Acrylic on canvas", + "dimensions": "120x90 cm", + "creation_year": 2024, + "price": 2500.00, + "is_visible": true, + "is_for_sale": true + }' + echo -e "${GREEN}✅ Created artwork: Abstract Harmony${NC}" + + api_call "POST" "/api/private/gallery/$GALLERY1_ID/artwork" '{ + "title": "Urban Reflections", + "description": "A contemporary piece capturing the essence of city life through mixed media and metallic accents.", + "image_url": "https://picsum.photos/800/600?random=2", + "medium": "Mixed media", + "dimensions": "100x70 cm", + "creation_year": 2024, + "price": 1800.00, + "is_visible": true, + "is_for_sale": true + }' + echo -e "${GREEN}✅ Created artwork: Urban Reflections${NC}" + + # Artworks for Gallery 2 (Digital Dreams) + api_call "POST" "/api/private/gallery/$GALLERY2_ID/artwork" '{ + "title": "Neural Networks", + "description": "A digital masterpiece generated using AI algorithms, exploring the beauty of artificial intelligence and machine learning patterns.", + "image_url": "https://picsum.photos/800/600?random=3", + "medium": "Digital art (NFT)", + "dimensions": "4K resolution", + "creation_year": 2024, + "price": 0.5, + "is_visible": true, + "is_for_sale": true + }' + echo -e "${GREEN}✅ Created artwork: Neural Networks${NC}" + + api_call "POST" "/api/private/gallery/$GALLERY2_ID/artwork" '{ + "title": "Cyberpunk Cityscape", + "description": "A futuristic vision of urban landscapes, blending neon aesthetics with digital photography and post-processing techniques.", + "image_url": "https://picsum.photos/800/600?random=4", + "medium": "Digital photography", + "dimensions": "Print: 150x100 cm", + "creation_year": 2024, + "price": 1200.00, + "is_visible": true, + "is_for_sale": false + }' + echo -e "${GREEN}✅ Created artwork: Cyberpunk Cityscape${NC}" + + # Artworks for Gallery 3 (Private Showcase) + api_call "POST" "/api/private/gallery/$GALLERY3_ID/artwork" '{ + "title": "Exclusive Masterpiece", + "description": "A rare and valuable piece from a renowned artist, available only to select collectors and gallery members.", + "image_url": "https://picsum.photos/800/600?random=5", + "medium": "Oil on canvas", + "dimensions": "200x150 cm", + "creation_year": 2023, + "price": 50000.00, + "is_visible": false, + "is_for_sale": true + }' > /dev/null + echo -e "${GREEN}✅ Created artwork: Exclusive Masterpiece${NC}" + + # Artworks for Gallery 4 (Street Art Revolution) + api_call "POST" "/api/private/gallery/$GALLERY4_ID/artwork" '{ + "title": "Wall of Expression", + "description": "A powerful street art piece addressing social issues through bold colors and symbolic imagery.", + "image_url": "https://picsum.photos/800/600?random=6", + "medium": "Spray paint on brick", + "dimensions": "300x200 cm", + "creation_year": 2024, + "is_visible": true, + "is_for_sale": false + }' > /dev/null + echo -e "${GREEN}✅ Created artwork: Wall of Expression${NC}" + + api_call "POST" "/api/private/gallery/$GALLERY4_ID/artwork" '{ + "title": "Graffiti Evolution", + "description": "Documenting the evolution of graffiti art from underground culture to mainstream recognition.", + "image_url": "https://picsum.photos/800/600?random=7", + "medium": "Photographic series", + "dimensions": "Various sizes", + "creation_year": 2024, + "price": 800.00, + "is_visible": true, + "is_for_sale": true + }' > /dev/null + echo -e "${GREEN}✅ Created artwork: Graffiti Evolution${NC}" +} + +# Function to create reviews +create_reviews() { + echo -e "${YELLOW}💬 Creating sample reviews...${NC}" + + # Gallery reviews + api_call "POST" "/api/private/gallery/$GALLERY1_ID/review" '{ + "grade": 5, + "description": "Absolutely stunning collection! The curation is excellent and each piece tells a compelling story. The Modern Art Collection truly captures the essence of contemporary creativity." + }' > /dev/null + echo -e "${GREEN}✅ Created gallery review for Modern Art Collection${NC}" + + api_call "POST" "/api/private/gallery/$GALLERY2_ID/review" '{ + "grade": 4, + "description": "Fascinating exploration of digital art! The intersection of technology and creativity is beautifully showcased here. Some pieces are truly groundbreaking." + }' > /dev/null + echo -e "${GREEN}✅ Created gallery review for Digital Dreams${NC}" + + api_call "POST" "/api/private/gallery/$GALLERY4_ID/review" '{ + "grade": 5, + "description": "Powerful and thought-provoking! This gallery does an amazing job of highlighting the cultural significance of street art and its evolution into fine art." + }' > /dev/null + echo -e "${GREEN}✅ Created gallery review for Street Art Revolution${NC}" + + # Get artwork IDs for artwork reviews + ARTWORKS=$(api_call "GET" "/api/private/gallery/$GALLERY1_ID/artworks") + ARTWORK1_ID=$(echo $ARTWORKS | jq -r '.[0].id') + ARTWORK2_ID=$(echo $ARTWORKS | jq -r '.[1].id') + + if [ "$ARTWORK1_ID" != "null" ] && [ -n "$ARTWORK1_ID" ]; then + api_call "POST" "/api/private/artwork/$ARTWORK1_ID/review" '{ + "grade": 5, + "description": "Abstract Harmony is a masterpiece! The use of color and form creates an emotional response that is both immediate and lasting. Truly exceptional work." + }' > /dev/null + echo -e "${GREEN}✅ Created artwork review for Abstract Harmony${NC}" + fi + + if [ "$ARTWORK2_ID" != "null" ] && [ -n "$ARTWORK2_ID" ]; then + api_call "POST" "/api/private/artwork/$ARTWORK2_ID/review" '{ + "grade": 4, + "description": "Urban Reflections captures the energy of city life beautifully. The mixed media approach adds depth and texture that photographs cannot convey." + }' > /dev/null + echo -e "${GREEN}✅ Created artwork review for Urban Reflections${NC}" + fi +} + +# Function to display summary +display_summary() { + echo "" + echo -e "${BLUE}📊 Seeding Summary${NC}" + echo "==================" + echo -e "${GREEN}✅ Created 4 galleries:${NC}" + echo " • Modern Art Collection (Public)" + echo " • Digital Dreams (Public)" + echo " • Private Showcase (Private)" + echo " • Street Art Revolution (Public)" + echo "" + echo -e "${GREEN}✅ Created 7 artworks across galleries${NC}" + echo -e "${GREEN}✅ Created 5 reviews (galleries and artworks)${NC}" + echo "" + echo -e "${YELLOW}🔗 Test the API:${NC}" + echo " Public galleries: curl -k $API_BASE/api/public/galleries" + echo " Private galleries: curl -k -H \"Authorization: Bearer \$TOKEN\" $API_BASE/api/private/galleries" + echo "" + echo -e "${GREEN}🎉 Database seeding completed successfully!${NC}" +} + +# Main execution +main() { + get_token + create_galleries + create_artworks + create_reviews + display_summary +} + +# Check dependencies +if ! command -v jq &> /dev/null; then + echo -e "${RED}❌ jq is required but not installed. Please install jq first.${NC}" + exit 1 +fi + +if ! command -v curl &> /dev/null; then + echo -e "${RED}❌ curl is required but not installed. Please install curl first.${NC}" + exit 1 +fi + +# Run the script +main \ No newline at end of file