fixed readme x)
This commit is contained in:
parent
5ccd887df5
commit
9dfe50d24f
31
README.md
31
README.md
@ -141,6 +141,37 @@ All requests enter through **Apache on port 443** (HTTPS) and are processed as f
|
|||||||
* 👤 **Username:** `alexis`
|
* 👤 **Username:** `alexis`
|
||||||
* 🔑 **Password:** `password`
|
* 🔑 **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 <access_token>" https://api.local/api/private/me
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🗂️ Public API Endpoints Overview
|
## 🗂️ Public API Endpoints Overview
|
||||||
|
@ -4,7 +4,9 @@ FROM httpd:2.4
|
|||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y libapache2-mod-auth-openidc && \
|
apt-get install -y libapache2-mod-auth-openidc && \
|
||||||
apt-get clean && \
|
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 your vhost config and certs
|
||||||
COPY conf/extra/httpd-vhosts.conf /usr/local/apache2/conf/extra/httpd-vhosts.conf
|
COPY conf/extra/httpd-vhosts.conf /usr/local/apache2/conf/extra/httpd-vhosts.conf
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
LoadModule ssl_module modules/mod_ssl.so
|
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_module modules/mod_proxy.so
|
||||||
LoadModule proxy_http_module modules/mod_proxy_http.so
|
LoadModule proxy_http_module modules/mod_proxy_http.so
|
||||||
LoadModule headers_module modules/mod_headers.so
|
LoadModule headers_module modules/mod_headers.so
|
||||||
@ -56,6 +56,7 @@ Listen 443
|
|||||||
SSLCertificateKeyFile /usr/local/apache2/conf/server.key
|
SSLCertificateKeyFile /usr/local/apache2/conf/server.key
|
||||||
|
|
||||||
# OIDC config - point to Keycloak via auth.local
|
# OIDC config - point to Keycloak via auth.local
|
||||||
|
# Global OIDC configuration
|
||||||
OIDCProviderMetadataURL https://auth.local/realms/master/.well-known/openid-configuration
|
OIDCProviderMetadataURL https://auth.local/realms/master/.well-known/openid-configuration
|
||||||
OIDCClientID soa
|
OIDCClientID soa
|
||||||
OIDCRedirectURI https://api.local/api/private/redirect
|
OIDCRedirectURI https://api.local/api/private/redirect
|
||||||
@ -69,35 +70,27 @@ Listen 443
|
|||||||
OIDCSessionInactivityTimeout 86400
|
OIDCSessionInactivityTimeout 86400
|
||||||
OIDCSSLValidateServer Off
|
OIDCSSLValidateServer Off
|
||||||
|
|
||||||
# Configure OAuth2 Bearer token validation
|
# Configure OAuth2 Bearer token validation (commented out - not available in this version)
|
||||||
OIDCOAuth2IntrospectionEndpoint https://auth.local/realms/master/protocol/openid-connect/token/introspect
|
# OIDCOAuth2IntrospectionEndpoint https://auth.local/realms/master/protocol/openid-connect/token/introspect
|
||||||
OIDCOAuth2IntrospectionEndpointAuth client_secret_basic
|
# OIDCOAuth2IntrospectionEndpointAuth client_secret_basic
|
||||||
OIDCOAuth2IntrospectionClientID soa
|
# OIDCOAuth2IntrospectionClientID soa
|
||||||
OIDCOAuth2IntrospectionClientSecret mysecret
|
# OIDCOAuth2IntrospectionClientSecret mysecret
|
||||||
|
|
||||||
# Proxy public API (no auth)
|
# Proxy public API (no auth)
|
||||||
ProxyPass /api/public http://public_api:5001/
|
ProxyPass /api/public http://public_api:5001/api/public
|
||||||
ProxyPassReverse /api/public http://public_api:5001/
|
ProxyPassReverse /api/public http://public_api:5001/api/public
|
||||||
|
|
||||||
# Proxy private API (supports both OIDC and Bearer tokens)
|
# Proxy private API (supports both OIDC and Bearer tokens)
|
||||||
ProxyPass /api/private http://private_api:5002/api/private
|
ProxyPass /api/private http://private_api:5002/api/private
|
||||||
ProxyPassReverse /api/private http://private_api:5002/api/private
|
ProxyPassReverse /api/private http://private_api:5002/api/private
|
||||||
|
|
||||||
<Location /api/private>
|
<Location /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
|
AuthType auth-openidc
|
||||||
Require valid-user
|
OIDCUnAuthAction pass
|
||||||
|
Require all granted
|
||||||
# Allow both authentication methods
|
|
||||||
OIDCUnAuthAction auth
|
# Don't modify the Authorization header - let it pass through naturally
|
||||||
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"
|
|
||||||
</Location>
|
</Location>
|
||||||
</VirtualHost>
|
</VirtualHost>
|
@ -5,7 +5,7 @@ meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{URL}}/api/artists
|
url: {{URL}}/api/public/artists
|
||||||
body: none
|
body: none
|
||||||
auth: inherit
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{URL}}/api/galleries
|
url: {{URL}}/api/public/galleries
|
||||||
body: none
|
body: json
|
||||||
auth: inherit
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{URL}}/api/galleries/{{gallery_id}}/artworks
|
url: {{URL}}/api/public/galleries/{{gallery_id}}/artworks
|
||||||
body: none
|
body: none
|
||||||
auth: inherit
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
vars {
|
vars {
|
||||||
gallery_id: 6
|
gallery_id: 6
|
||||||
URL: http://localhost:8000
|
URL: https://api.local
|
||||||
baseUrl: http://api.local/api/private
|
baseUrl: https://api.local/api/private
|
||||||
authUrl: http://auth.local:8080
|
authUrl: http://auth.local:8080
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{baseUrl}}/artwork/1/reviews
|
url: {{baseUrl}}/artwork/1/reviews
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
post {
|
post {
|
||||||
url: {{baseUrl}}/artwork/1/review
|
url: {{baseUrl}}/artwork/1/review
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{baseUrl}}/artworks/reviews/given
|
url: {{baseUrl}}/artworks/reviews/given
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{baseUrl}}/artworks/reviews/received
|
url: {{baseUrl}}/artworks/reviews/received
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
put {
|
put {
|
||||||
url: {{baseUrl}}/artworks/review/1
|
url: {{baseUrl}}/artworks/review/1
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
post {
|
post {
|
||||||
url: {{baseUrl}}/gallery/1/artwork
|
url: {{baseUrl}}/gallery/1/artwork
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{baseUrl}}/gallery/1/artworks
|
url: {{baseUrl}}/gallery/1/artworks
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{baseUrl}}/artwork/1
|
url: {{baseUrl}}/artwork/1
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{baseUrl}}/artworks/mine
|
url: {{baseUrl}}/artworks/mine
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
put {
|
put {
|
||||||
url: {{baseUrl}}/artwork/1
|
url: {{baseUrl}}/artwork/1
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -11,7 +11,9 @@ post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body:form-urlencoded {
|
body:form-urlencoded {
|
||||||
grant_type: client_credentials
|
grant_type: password
|
||||||
client_id: soa
|
client_id: soa
|
||||||
client_secret: mysecret
|
client_secret: mysecret
|
||||||
|
username: alexis
|
||||||
|
password: password
|
||||||
}
|
}
|
||||||
|
15
bruno/SOA/private/environments/debug.bru
Normal file
15
bruno/SOA/private/environments/debug.bru
Normal file
@ -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
|
||||||
|
}
|
15
bruno/SOA/private/environments/test_auth.bru
Normal file
15
bruno/SOA/private/environments/test_auth.bru
Normal file
@ -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
|
||||||
|
}
|
12
bruno/SOA/private/folder.bru
Normal file
12
bruno/SOA/private/folder.bru
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
meta {
|
||||||
|
name: private
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
auth {
|
||||||
|
mode: bearer
|
||||||
|
}
|
||||||
|
|
||||||
|
auth:bearer {
|
||||||
|
token: eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI3MWRQaTVMaVRxMzF1QXRXWFZueWFLYWQtNlktUE5PWC1mSENyd1cwdnRjIn0.eyJleHAiOjE3NTEyMjkwODUsImlhdCI6MTc1MTIyNTQ4NSwianRpIjoiNmQxZjBlOTgtZTA2NC00YWY1LTgyYzctOGQ1ZDU4MGFjZjM2IiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmxvY2FsL3JlYWxtcy9tYXN0ZXIiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMjlkNjc5YjMtOWZjMS00NDFjLWIyYTEtNzA5ZDE5NWEyYjZmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoic29hIiwic2Vzc2lvbl9zdGF0ZSI6ImZmNzJhN2Q1LTNmZGItNGQ0OC04Mjc4LTI5NDAxNzlmNWFiOSIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1tYXN0ZXIiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiZmY3MmE3ZDUtM2ZkYi00ZDQ4LTgyNzgtMjk0MDE3OWY1YWI5IiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByZWZlcnJlZF91c2VybmFtZSI6ImFsZXhpcyJ9.MrrORbBnN4z-UUhf-9pSDKlBy4sIHrXCPPbM4SKiwGx9qGTABNbaw-wstlLLoi8UsiwcZBfavjmaJO89ctXIn0zxb4rMKhKYSxnSw59lgpl6RK7oKAdvbq53imZgPvAlGO1twzJiIr3V5Eci6xudvdREvva2wMlYr3xpSdPGXjIb1IJ-O4bpxYiHhIVQ1x0YezppzqElII6K6S39Ht0kNxu0IA6KwydBRTLLJW6G0OTZ87upRhX2h6PN1IosCTsPs8_gA6uB8S88bYUg4aHxYE11SjYZxDMu-ttx59qIovzGAXv4YGzgQibK9PBZ5uupPQNKklNBObqqZX8NPm9MDA
|
||||||
|
}
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
post {
|
post {
|
||||||
url: {{baseUrl}}/gallery
|
url: {{baseUrl}}/gallery
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{baseUrl}}/gallery/1/members
|
url: {{baseUrl}}/gallery/1/members
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{baseUrl}}/gallery/1
|
url: {{baseUrl}}/gallery/1
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{baseUrl}}/galleries
|
url: {{baseUrl}}/galleries
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{baseUrl}}/galleries/mine
|
url: {{baseUrl}}/galleries/mine
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
put {
|
put {
|
||||||
url: {{baseUrl}}/gallery/1
|
url: {{baseUrl}}/gallery/1
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
post {
|
post {
|
||||||
url: {{baseUrl}}/gallery/1/review
|
url: {{baseUrl}}/gallery/1/review
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{baseUrl}}/gallery/1/reviews
|
url: {{baseUrl}}/gallery/1/reviews
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{baseUrl}}/galleries/reviews/given
|
url: {{baseUrl}}/galleries/reviews/given
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{baseUrl}}/galleries/reviews/received
|
url: {{baseUrl}}/galleries/reviews/received
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
put {
|
put {
|
||||||
url: {{baseUrl}}/galleries/review/1
|
url: {{baseUrl}}/galleries/review/1
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
post {
|
post {
|
||||||
url: {{baseUrl}}/gallery/1/invite
|
url: {{baseUrl}}/gallery/1/invite
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{baseUrl}}/invitations/received
|
url: {{baseUrl}}/invitations/received
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
put {
|
put {
|
||||||
url: {{baseUrl}}/invitations/1/respond
|
url: {{baseUrl}}/invitations/1/respond
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,6 +6,8 @@ meta {
|
|||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{baseUrl}}/me
|
url: {{baseUrl}}/me
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -6,7 +6,10 @@ CLIENT_ID="soa"
|
|||||||
CLIENT_SECRET="mysecret"
|
CLIENT_SECRET="mysecret"
|
||||||
USERNAME="alexis"
|
USERNAME="alexis"
|
||||||
PASSWORD="password"
|
PASSWORD="password"
|
||||||
|
USERNAME2="fabio"
|
||||||
|
PASSWORD2="password"
|
||||||
PERSONAL_TOKEN="personaltoken"
|
PERSONAL_TOKEN="personaltoken"
|
||||||
|
PERSONAL_TOKEN2="personaltoken2"
|
||||||
|
|
||||||
# Fonction d'attente
|
# Fonction d'attente
|
||||||
wait_for_keycloak() {
|
wait_for_keycloak() {
|
||||||
@ -45,6 +48,18 @@ setup_keycloak() {
|
|||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"realm\":\"$REALM\",\"enabled\":true}" > /dev/null
|
-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..."
|
echo "🛠️ Création du client $CLIENT_ID..."
|
||||||
curl -s -X POST "$KC_HOST/admin/realms/$REALM/clients" \
|
curl -s -X POST "$KC_HOST/admin/realms/$REALM/clients" \
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
@ -56,9 +71,10 @@ setup_keycloak() {
|
|||||||
\"secret\": \"$CLIENT_SECRET\",
|
\"secret\": \"$CLIENT_SECRET\",
|
||||||
\"redirectUris\": [\"*\"],
|
\"redirectUris\": [\"*\"],
|
||||||
\"standardFlowEnabled\": true,
|
\"standardFlowEnabled\": true,
|
||||||
|
\"directAccessGrantsEnabled\": true,
|
||||||
\"serviceAccountsEnabled\": true,
|
\"serviceAccountsEnabled\": true,
|
||||||
\"authorizationServicesEnabled\": false
|
\"authorizationServicesEnabled\": false
|
||||||
}" > /dev/null
|
}"
|
||||||
|
|
||||||
echo "👤 Création de l'utilisateur $USERNAME avec token personnel..."
|
echo "👤 Création de l'utilisateur $USERNAME avec token personnel..."
|
||||||
curl -s -X POST "$KC_HOST/admin/realms/$REALM/users" \
|
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 "✅ 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 "🪪 Client secret: $CLIENT_SECRET"
|
||||||
echo "🎫 Personal Access Token: $PERSONAL_TOKEN"
|
echo "🎫 Personal Access Token 1: $PERSONAL_TOKEN"
|
||||||
echo "📅 Token créé le: $CURRENT_DATE"
|
echo "🎫 Personal Access Token 2: $PERSONAL_TOKEN2"
|
||||||
echo "⏰ Token expire le: $EXPIRY_DATE"
|
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() {
|
test_personal_token() {
|
||||||
|
echo ""
|
||||||
echo "Pour accéder à une ressource protégée:"
|
echo "🧪 Test des tokens:"
|
||||||
echo "curl -X GET http://localhost:3000/api/protected"
|
echo ""
|
||||||
echo " -H \"Authorization: Bearer $PERSONAL_TOKEN\""
|
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
|
# Lancer le setup
|
||||||
|
178
private/app.py
178
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 flask_sqlalchemy import SQLAlchemy
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from jose import jwt, JWTError
|
from jose import jwt, JWTError
|
||||||
@ -48,8 +49,10 @@ KEYCLOAK_REALM = "master"
|
|||||||
KEYCLOAK_URL = "http://keycloak:8080"
|
KEYCLOAK_URL = "http://keycloak:8080"
|
||||||
CLIENT_ID = "soa"
|
CLIENT_ID = "soa"
|
||||||
|
|
||||||
ISSUER = f"{KEYCLOAK_URL}/realms/{KEYCLOAK_REALM}"
|
# Use external URL for issuer validation (matches what's in the token)
|
||||||
JWKS_URL = f"{ISSUER}/protocol/openid-connect/certs"
|
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):
|
for _ in range(30):
|
||||||
try:
|
try:
|
||||||
@ -74,15 +77,133 @@ def get_signing_key(token):
|
|||||||
return key
|
return key
|
||||||
raise Exception("Public key not found.")
|
raise Exception("Public key not found.")
|
||||||
|
|
||||||
# Decorator for OIDC protection
|
def validate_bearer_token(token):
|
||||||
def oidc_required(f):
|
"""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)
|
@wraps(f)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
# Get user info from Apache headers
|
import sys
|
||||||
user_email = request.headers.get("OIDC_email")
|
print(f"=== AUTH DEBUG for {request.path} ===", flush=True)
|
||||||
username = request.headers.get("OIDC_user") or user_email
|
print(f"All headers: {dict(request.headers)}", flush=True)
|
||||||
if not user_email or not username:
|
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
|
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
|
# Find or create user in DB
|
||||||
user = User.query.filter_by(email=user_email).first()
|
user = User.query.filter_by(email=user_email).first()
|
||||||
@ -103,9 +224,22 @@ def oidc_required(f):
|
|||||||
|
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
# Keep old decorator name for compatibility
|
||||||
|
oidc_required = auth_required
|
||||||
@app.route("/api/private/debug-headers")
|
@app.route("/api/private/debug-headers")
|
||||||
def 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))
|
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):
|
class User(db.Model):
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@ -374,17 +508,37 @@ def get_gallery(gallery_id):
|
|||||||
"updated_at": gal.updated_at
|
"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.
|
# Create a new gallery for the authenticated user.
|
||||||
@app.route("/api/private/gallery", methods=["POST"])
|
@app.route("/api/private/gallery", methods=["POST"])
|
||||||
@oidc_required
|
@oidc_required
|
||||||
def create_gallery():
|
def create_gallery():
|
||||||
data = request.json
|
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(
|
gallery = Gallery(
|
||||||
owner_id=g.db_user.id,
|
owner_id=g.db_user.id,
|
||||||
title=data.get("title"),
|
title=data.get("title"),
|
||||||
description=data.get("description"),
|
description=data.get("description"),
|
||||||
is_public=data.get("is_public", False),
|
is_public=data.get("is_public", False),
|
||||||
publication_date=data.get("publication_date")
|
publication_date=pub_date
|
||||||
)
|
)
|
||||||
db.session.add(gallery)
|
db.session.add(gallery)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -782,7 +936,7 @@ def create_artwork_review(artwork_id):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
event = {
|
event = {
|
||||||
"type": "artwork_review_created",
|
"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))
|
redis_client.publish('events', json.dumps(event))
|
||||||
return jsonify({"id": review.id, "message": "Review created"}), 201
|
return jsonify({"id": review.id, "message": "Review created"}), 201
|
||||||
@ -808,7 +962,7 @@ def update_artwork_review(review_id):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
event = {
|
event = {
|
||||||
"type": "artwork_review_updated",
|
"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))
|
redis_client.publish('events', json.dumps(event))
|
||||||
return jsonify({"message": "Review updated"})
|
return jsonify({"message": "Review updated"})
|
||||||
|
307
seed-database.sh
Executable file
307
seed-database.sh
Executable file
@ -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
|
Loading…
x
Reference in New Issue
Block a user