Compare commits

...

17 Commits
public ... main

Author SHA1 Message Date
4e07a03bd4 Update README.md 2025-06-29 19:36:26 +00:00
Alexis Bruneteau
9dfe50d24f fixed readme x) 2025-06-29 21:35:13 +02:00
5ccd887df5 Update README.md
Beauty
2025-06-29 17:43:28 +00:00
Alexis Bruneteau
254479d475 readme updated 2025-06-29 19:41:42 +02:00
Alexis Bruneteau
15e56fea50 merged 2025-06-28 12:40:55 +02:00
87a08cef9f Update README.md 2025-06-26 20:59:53 +00:00
b595cc4c77 Update README.md 2025-06-26 20:59:38 +00:00
46014f078d Update README.md 2025-06-26 19:13:19 +00:00
e6ee4ac3fd Update README.md 2025-06-26 18:43:48 +00:00
f99ba53f53 Update README.md 2025-06-26 18:18:11 +00:00
58077e0bd7 Update README.md 2025-06-26 18:17:57 +00:00
Alexis Bruneteau
05788b3d2d config 2025-06-19 00:01:53 +02:00
Alexis Bruneteau
575da0c189 tag 2025-06-12 15:18:52 +02:00
Alexis Bruneteau
d59fe93771 . 2025-06-12 14:57:12 +02:00
Alexis Bruneteau
4991525232 should work 2025-06-12 14:52:41 +02:00
Alexis Bruneteau
030ed23bc8 pouet 2025-06-12 14:29:40 +02:00
Alexis Bruneteau
31181c49db pouet 2025-06-12 14:28:57 +02:00
123 changed files with 2255 additions and 37 deletions

259
README.md
View File

@ -1,4 +1,261 @@
# SOA # 🧩 SOA Service-Oriented Architecture Project
## 🏗️ Architecture Overview
This project implements a **Service-Oriented Architecture (SOA)** for an art gallery management system using Docker containerization and microservices architecture.
### System Components
```
🌐 HTTPS (Port 443)
┌──────────────────────────▼──────────────────────────────┐
│ Apache Reverse Proxy │
│ (SSL Termination + OIDC Validation) │
│ │
│ /api/public/* ──────┐ ┌────── /api/private/* │
│ (No Auth) │ │ (OIDC Required) │
└───────────────────────┼──────┼──────────────────────────┘
│ │ |
│ │ (+ OIDC Headers) |
│ │ |
┌─────────────▼──┐ ┌─▼──────────────┐ |
│ │ │ │ |
│ Public API │ │ Private API │ |
│ (Laravel) │ │ (Flask) │ |
│ Port 5001 │ │ Port 5002 │ |
└─────────┬──────┘ └─┬──────────────┘ |
│ │ |
┌─────────▼──────────▼─────────┐ ┌───────▼──────┐
│ MySQL │ │ Keycloak │
│ (Application DB) │ │ + Postgres │
└──────────────────────────────┘ └──────────────┘
┌─────────────────▼─────────────────┐
│ Redis │
│ (Cache + Events) │
└───────────────────────────────────┘
```
### Technology Stack
| Component | Technology | Purpose |
|-----------|------------|---------|
| **Reverse Proxy** | Apache HTTP Server | SSL termination, request routing |
| **Authentication** | Keycloak | OIDC/OAuth2 identity provider |
| **Public API** | Laravel 12 (PHP) | Public galleries and artworks API |
| **Private API** | Flask (Python) | User management and private content |
| **Databases** | PostgreSQL + MySQL | Data persistence |
| **Cache/Queue** | Redis | Caching and event messaging | // NOT IMPLEMENTED YET
### Service Architecture
#### 🔓 **Public API Service** (Laravel)
- **Port**: Internal 5001 (via Apache)
- **Database**: MySQL
- **Purpose**: Public access to galleries and artworks
- **Features**:
- RESTful API for public galleries
- Artist directory
- Public artwork browsing
- Pagination and filtering
#### 🔒 **Private API Service** (Flask)
- **Port**: Internal 5002 (via Apache)
- **Database**: MySQL
- **Authentication**: OIDC via Apache mod_auth_openidc
- **Purpose**: Authenticated user operations
- **Features**:
- User profile management
- Gallery creation and management
- Artwork upload and editing
- Review system for galleries/artworks
- Invitation system for gallery members
#### 🔐 **Authentication Service** (Keycloak)
- **Port**: 8080
- **Database**: PostgreSQL
- **Purpose**: Centralized identity and access management
- **Features**:
- OIDC/OAuth2 provider
- User registration and login
- Token-based authentication
- Single Sign-On (SSO)
### Request Flow
All requests enter through **Apache on port 443** (HTTPS) and are processed as follows:
1. **SSL Termination**: Apache handles SSL/TLS encryption
2. **Request Routing**: Based on URL path:
- `/api/public/*` → Routes to **Public API** (Laravel on port 5001)
- `/api/private/*` → Routes to **Private API** (Flask on port 5002)
3. **OIDC Authentication Check**:
- **Public API**: No authentication required
- **Private API**: Apache mod_auth_openidc validates OIDC tokens with Keycloak
4. **Header Injection**: Apache injects user info headers (OIDC_email, OIDC_user) for authenticated requests
5. **API Processing**: Backend services handle business logic
6. **Data Persistence**: MySQL stores application data
7. **Event Publishing**: Redis handles inter-service communication
### Security Model
- **SSL/TLS**: All external communication encrypted
- **OIDC Authentication**: Industry standard OAuth2/OIDC flow
- **Token-based Authorization**: JWT tokens for API access
- **Network Isolation**: Services communicate via internal network
- **Database Security**: Separate databases for auth and application data
## 🚀 Quick Start
1. **Start the application stack:**
```bash
docker compose up -d --build
```
2. **Initialize Keycloak configuration:**
```bash
./keycloak-setup.sh
```
3. **Seed Database:**
```bash
./seed-database.sh
```
4. **Update your `/etc/hosts` file:**
```
127.0.0.1 api.local auth.local
```
---
## 🔐 Credentials
### Keycloak Admin Panel
* 📍 URL: [http://auth.local:8080](http://auth.local:8080)
* 👤 **Username:** `admin`
* 🔑 **Password:** `admin`
### Private API User
* 👤 **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 <access_token>" https://api.local/api/private/me
```
---
## 🗂️ Public API Endpoints Overview
All routes are prefixed with `/api/public`.
| Method | Endpoint | Description |
| ------ | ------------------------------- | ---------------------------------- |
| GET | `/artists` | List all public artists |
| GET | `/galleries` | List all public galleries |
| GET | `/galleries/{gallery}/artworks` | List artworks for a public gallery |
---
## 🗂️ Private API Endpoints Overview
All routes are prefixed with `/api/private` and require a **Bearer token**.
### 👤 User
| Method | Endpoint | Description |
| ------ | -------- | ----------------------------- |
| GET | `/me` | Get current user's profile |
| PUT | `/me` | Update current user's profile |
### 🖼️ Galleries
| Method | Endpoint | Description |
| ------ | ----------------------- | --------------------------------- |
| GET | `/galleries` | List all accessible galleries |
| GET | `/galleries/mine` | List galleries owned by the user |
| POST | `/gallery` | Create a new gallery |
| GET | `/gallery/{gallery_id}` | Get details of a specific gallery |
| PUT | `/gallery/{gallery_id}` | Update a gallery (owner only) |
### 👥 Members
| Method | Endpoint | Description |
| ------ | ------------------------------- | ------------------------- |
| GET | `/gallery/{gallery_id}/members` | List members of a gallery |
### 📩 Invitations
| Method | Endpoint | Description |
| ------ | ----------------------------------- | --------------------------- |
| POST | `/gallery/{gallery_id}/invite` | Invite user to a gallery |
| PUT | `/invitations/{gallery_id}/respond` | Accept or reject invitation |
| GET | `/invitations/received` | List received invitations |
### 🖼️ Artworks
| Method | Endpoint | Description |
| ------ | -------------------------------- | ----------------------------------- |
| GET | `/gallery/{gallery_id}/artworks` | List artworks in a gallery |
| POST | `/gallery/{gallery_id}/artwork` | Add artwork to gallery (owner only) |
| GET | `/artwork/{artwork_id}` | Get details of an artwork |
| PUT | `/artwork/{artwork_id}` | Update an artwork (creator only) |
| GET | `/artworks/mine` | List artworks created by the user |
### 📝 Gallery Reviews
| Method | Endpoint | Description |
| ------ | ------------------------------- | ------------------------------------- |
| GET | `/gallery/{gallery_id}/reviews` | List reviews for a gallery |
| POST | `/gallery/{gallery_id}/review` | Submit a review for a gallery |
| PUT | `/galleries/review/{review_id}` | Update a gallery review (author only) |
| GET | `/galleries/reviews/given` | Reviews written by the user |
| GET | `/galleries/reviews/received` | Reviews received on users galleries |
### 📝 Artwork Reviews
| Method | Endpoint | Description |
| ------ | ------------------------------- | -------------------------------------- |
| GET | `/artwork/{artwork_id}/reviews` | List reviews for an artwork |
| POST | `/artwork/{artwork_id}/review` | Submit a review for an artwork |
| PUT | `/artworks/review/{review_id}` | Update an artwork review (author only) |
| GET | `/artworks/reviews/given` | Reviews written by the user |
| GET | `/artworks/reviews/received` | Reviews received on users artworks |
---
# Public API: # Public API:

View File

@ -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

View File

@ -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
@ -50,11 +50,13 @@ Listen 443
ServerName api.local ServerName api.local
ErrorLog ${APACHE_LOG_DIR}/api_error.log ErrorLog ${APACHE_LOG_DIR}/api_error.log
CustomLog ${APACHE_LOG_DIR}/api_access.log combined CustomLog ${APACHE_LOG_DIR}/api_access.log combined
SSLEngine on SSLEngine on
SSLCertificateFile /usr/local/apache2/conf/server.crt SSLCertificateFile /usr/local/apache2/conf/server.crt
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
@ -67,18 +69,28 @@ Listen 443
OIDCScope "openid email profile" OIDCScope "openid email profile"
OIDCSessionInactivityTimeout 86400 OIDCSessionInactivityTimeout 86400
OIDCSSLValidateServer Off OIDCSSLValidateServer Off
# Proxy public API (no auth)
ProxyPass /api/public http://public_api:5001/
ProxyPassReverse /api/public http://public_api:5001/
# Proxy private API (OIDC protected) # 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/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 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>
AuthType openid-connect # Let Flask handle all authentication - pass through all requests
Require valid-user # Apache will only inject OIDC headers if user is already authenticated via OIDC
RequestHeader set X-User-Email "%{HTTP_OIDC_EMAIL}i" AuthType auth-openidc
RequestHeader set X-User-Name "%{HTTP_OIDC_PREFERRED_USERNAME}i" OIDCUnAuthAction pass
Require all granted
# Don't modify the Authorization header - let it pass through naturally
</Location> </Location>
</VirtualHost> </VirtualHost>

View File

@ -5,7 +5,7 @@ meta {
} }
get { get {
url: {{URL}}/api/artists url: {{URL}}/api/public/artists
body: none body: none
auth: inherit auth: inherit
} }

View File

@ -5,7 +5,7 @@ meta {
} }
get { get {
url: {{URL}}/api/galleries url: {{URL}}/api/public/galleries
body: none body: json
auth: inherit auth: inherit
} }

View File

@ -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
} }

View File

@ -1,4 +1,6 @@
vars { vars {
gallery_id: 6 gallery_id: 6
URL: http://localhost:8000 URL: https://api.local
baseUrl: https://api.local/api/private
authUrl: http://auth.local:8080
} }

View File

@ -0,0 +1,16 @@
meta {
name: Get Artwork Reviews
type: http
seq: 1
}
get {
url: {{baseUrl}}/artwork/1/reviews
body: none
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}

View File

@ -0,0 +1,25 @@
meta {
name: Create Artwork Review
type: http
seq: 2
}
post {
url: {{baseUrl}}/artwork/1/review
body: json
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"grade": 5,
"description": "Absolutely stunning artwork! The colors and technique are magnificent.",
"parent_ar_id": null
}
}

View File

@ -0,0 +1,16 @@
meta {
name: Get Given Artwork Reviews
type: http
seq: 4
}
get {
url: {{baseUrl}}/artworks/reviews/given
body: none
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}

View File

@ -0,0 +1,16 @@
meta {
name: Get Received Artwork Reviews
type: http
seq: 5
}
get {
url: {{baseUrl}}/artworks/reviews/received
body: none
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}

View File

@ -0,0 +1,24 @@
meta {
name: Update Artwork Review
type: http
seq: 3
}
put {
url: {{baseUrl}}/artworks/review/1
body: json
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"grade": 4,
"description": "Updated: Very beautiful artwork! Great technique and composition."
}
}

View File

@ -0,0 +1,31 @@
meta {
name: Create Artwork
type: http
seq: 3
}
post {
url: {{baseUrl}}/gallery/1/artwork
body: json
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"title": "Sunset Over Mountains",
"description": "A beautiful landscape painting capturing the golden hour",
"image_url": "https://example.com/artwork1.jpg",
"medium": "Oil on Canvas",
"dimensions": "24x36 inches",
"creation_year": 2024,
"price": 1500.00,
"is_visible": true,
"is_for_sale": true
}
}

View File

@ -0,0 +1,16 @@
meta {
name: Get Gallery Artworks
type: http
seq: 1
}
get {
url: {{baseUrl}}/gallery/1/artworks
body: none
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}

View File

@ -0,0 +1,16 @@
meta {
name: Get Artwork Details
type: http
seq: 2
}
get {
url: {{baseUrl}}/artwork/1
body: none
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}

View File

@ -0,0 +1,16 @@
meta {
name: Get My Artworks
type: http
seq: 5
}
get {
url: {{baseUrl}}/artworks/mine
body: none
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}

View File

@ -0,0 +1,26 @@
meta {
name: Update Artwork
type: http
seq: 4
}
put {
url: {{baseUrl}}/artwork/1
body: json
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"title": "Updated Sunset Over Mountains",
"description": "An updated beautiful landscape painting",
"price": 1800.00,
"is_for_sale": false
}
}

View File

@ -0,0 +1,14 @@
meta {
name: Debug Headers
type: http
seq: 1
}
get {
url: {{baseUrl}}/debug-headers
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}

View File

@ -0,0 +1,13 @@
meta {
name: OIDC Redirect
type: http
seq: 2
}
get {
url: {{baseUrl}}/redirect
}
params:query {
code: authorization_code_here
}

View File

@ -0,0 +1,19 @@
meta {
name: Token User
type: http
seq: 2
}
post {
url: {{authUrl}}/realms/master/protocol/openid-connect/token
body: formUrlEncoded
auth: inherit
}
body:form-urlencoded {
grant_type: password
client_id: soa
client_secret: mysecret
username: alexis
password: password
}

View File

@ -0,0 +1,11 @@
meta {
name: Token
type: http
seq: 1
}
get {
url: /realms/master/protocol/openid-connect/token
body: none
auth: inherit
}

View 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
}

View File

@ -0,0 +1,5 @@
vars {
baseUrl: https://api.local/api/private
email: alexis@example.com
username: alexis
}

View 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
}

View 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
}

View File

@ -0,0 +1,26 @@
meta {
name: Create Gallery
type: http
seq: 3
}
post {
url: {{baseUrl}}/gallery
body: json
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"title": "Modern Art Collection",
"description": "A curated collection of contemporary artworks",
"is_public": true,
"publication_date": "2025-06-28T10:00:00Z"
}
}

View File

@ -0,0 +1,16 @@
meta {
name: Get Gallery Members
type: http
seq: 6
}
get {
url: {{baseUrl}}/gallery/1/members
body: none
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}

View File

@ -0,0 +1,16 @@
meta {
name: Get Gallery Details
type: http
seq: 2
}
get {
url: {{baseUrl}}/gallery/1
body: none
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}

View File

@ -0,0 +1,16 @@
meta {
name: List All Galleries
type: http
seq: 1
}
get {
url: {{baseUrl}}/galleries
body: none
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}

View File

@ -0,0 +1,16 @@
meta {
name: Get My Galleries
type: http
seq: 5
}
get {
url: {{baseUrl}}/galleries/mine
body: json
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}

View File

@ -0,0 +1,25 @@
meta {
name: Update Gallery
type: http
seq: 4
}
put {
url: {{baseUrl}}/gallery/1
body: json
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"title": "Updated Modern Art Collection",
"description": "An updated curated collection of contemporary artworks",
"is_public": false
}
}

View File

@ -0,0 +1,25 @@
meta {
name: Create Gallery Review
type: http
seq: 2
}
post {
url: {{baseUrl}}/gallery/1/review
body: json
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"grade": 5,
"description": "Excellent gallery with amazing artwork collection!",
"parent_gr_id": null
}
}

View File

@ -0,0 +1,16 @@
meta {
name: Get Gallery Reviews
type: http
seq: 1
}
get {
url: {{baseUrl}}/gallery/1/reviews
body: none
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}

View File

@ -0,0 +1,16 @@
meta {
name: Get Given Gallery Reviews
type: http
seq: 4
}
get {
url: {{baseUrl}}/galleries/reviews/given
body: json
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}

View File

@ -0,0 +1,16 @@
meta {
name: Get Received Gallery Reviews
type: http
seq: 5
}
get {
url: {{baseUrl}}/galleries/reviews/received
body: none
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}

View File

@ -0,0 +1,24 @@
meta {
name: Update Gallery Review
type: http
seq: 3
}
put {
url: {{baseUrl}}/galleries/review/1
body: json
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"grade": 4,
"description": "Updated: Very good gallery with great artwork collection!"
}
}

View File

@ -0,0 +1,24 @@
meta {
name: Invite User to Gallery
type: http
seq: 1
}
post {
url: {{baseUrl}}/gallery/1/invite
body: json
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"user_id": 2,
"role": "viewer"
}
}

View File

@ -0,0 +1,16 @@
meta {
name: Get Received Invitations
type: http
seq: 3
}
get {
url: {{baseUrl}}/invitations/received
body: none
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}

View File

@ -0,0 +1,23 @@
meta {
name: Respond to Invitation
type: http
seq: 2
}
put {
url: {{baseUrl}}/invitations/1/respond
body: json
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"status": "accepted"
}
}

View File

@ -0,0 +1,16 @@
meta {
name: Get My Profile
type: http
seq: 1
}
get {
url: {{baseUrl}}/me
body: none
auth: inherit
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}

View File

@ -0,0 +1,25 @@
meta {
name: Update My Profile
type: http
seq: 2
}
put {
url: {{baseUrl}}/me
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"alias": "Alexis Updated",
"first_name": "Alexis",
"last_name": "Doe",
"bio": "Art enthusiast and gallery curator",
"profile_picture_url": "https://example.com/avatar.jpg"
}
}

765
bruno/bruno.sh Executable file
View File

@ -0,0 +1,765 @@
#!/bin/bash
# Bruno API Collection Generator for Private API
# Creates a complete Bruno collection structure for testing the private API endpoints
set -e
# Configuration
COLLECTION_NAME="private"
BASE_URL="https://api.local/api/private"
DEFAULT_EMAIL="alexis@example.com"
DEFAULT_USERNAME="alexis"
# 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_info() {
echo -e "${BLUE} $1${NC}"
}
echo_success() {
echo -e "${GREEN}$1${NC}"
}
echo_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
echo_error() {
echo -e "${RED}$1${NC}"
}
# Function to create directory structure
create_directories() {
local base_dir="$1"
echo_info "Creating directory structure..."
mkdir -p "${base_dir}"/{environments,auth,user,invitations,galleries,artworks,gallery-reviews,artwork-reviews}
echo_success "Directory structure created"
}
# Function to create a file with content
create_file() {
local filepath="$1"
local content="$2"
echo "$content" > "$filepath"
echo_success "Created: $filepath"
}
# Generate collection files
generate_files() {
local base_dir="$1"
echo_info "Generating Bruno collection files..."
# Main collection file
cat > "${base_dir}/bruno.json" << EOF
{
"version": "1",
"name": "Private API",
"type": "collection"
}
EOF
echo_success "Created: ${base_dir}/bruno.json"
# Environment file
cat > "${base_dir}/environments/local.bru" << EOF
vars {
baseUrl: ${BASE_URL}
email: ${DEFAULT_EMAIL}
username: ${DEFAULT_USERNAME}
}
EOF
echo_success "Created: ${base_dir}/environments/local.bru"
# Auth files
cat > "${base_dir}/auth/debug-headers.bru" << 'EOF'
meta {
name: Debug Headers
type: http
seq: 1
}
get {
url: {{baseUrl}}/debug-headers
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}
EOF
echo_success "Created: ${base_dir}/auth/debug-headers.bru"
cat > "${base_dir}/auth/redirect.bru" << 'EOF'
meta {
name: OIDC Redirect
type: http
seq: 2
}
get {
url: {{baseUrl}}/redirect
}
params:query {
code: authorization_code_here
}
EOF
echo_success "Created: ${base_dir}/auth/redirect.bru"
# User files
cat > "${base_dir}/user/get-me.bru" << 'EOF'
meta {
name: Get My Profile
type: http
seq: 1
}
get {
url: {{baseUrl}}/me
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}
EOF
echo_success "Created: ${base_dir}/user/get-me.bru"
cat > "${base_dir}/user/update-me.bru" << 'EOF'
meta {
name: Update My Profile
type: http
seq: 2
}
put {
url: {{baseUrl}}/me
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"alias": "Alexis Updated",
"first_name": "Alexis",
"last_name": "Doe",
"bio": "Art enthusiast and gallery curator",
"profile_picture_url": "https://example.com/avatar.jpg"
}
}
EOF
echo_success "Created: ${base_dir}/user/update-me.bru"
# Invitation files
cat > "${base_dir}/invitations/invite-user.bru" << 'EOF'
meta {
name: Invite User to Gallery
type: http
seq: 1
}
post {
url: {{baseUrl}}/gallery/1/invite
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"user_id": 2,
"role": "viewer"
}
}
EOF
echo_success "Created: ${base_dir}/invitations/invite-user.bru"
cat > "${base_dir}/invitations/respond-invitation.bru" << 'EOF'
meta {
name: Respond to Invitation
type: http
seq: 2
}
put {
url: {{baseUrl}}/invitations/1/respond
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"status": "accepted"
}
}
EOF
echo_success "Created: ${base_dir}/invitations/respond-invitation.bru"
cat > "${base_dir}/invitations/received-invitations.bru" << 'EOF'
meta {
name: Get Received Invitations
type: http
seq: 3
}
get {
url: {{baseUrl}}/invitations/received
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}
EOF
echo_success "Created: ${base_dir}/invitations/received-invitations.bru"
# Gallery files
cat > "${base_dir}/galleries/list-galleries.bru" << 'EOF'
meta {
name: List All Galleries
type: http
seq: 1
}
get {
url: {{baseUrl}}/galleries
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}
EOF
echo_success "Created: ${base_dir}/galleries/list-galleries.bru"
cat > "${base_dir}/galleries/get-gallery.bru" << 'EOF'
meta {
name: Get Gallery Details
type: http
seq: 2
}
get {
url: {{baseUrl}}/gallery/1
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}
EOF
echo_success "Created: ${base_dir}/galleries/get-gallery.bru"
cat > "${base_dir}/galleries/create-gallery.bru" << 'EOF'
meta {
name: Create Gallery
type: http
seq: 3
}
post {
url: {{baseUrl}}/gallery
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"title": "Modern Art Collection",
"description": "A curated collection of contemporary artworks",
"is_public": true,
"publication_date": "2025-06-28T10:00:00Z"
}
}
EOF
echo_success "Created: ${base_dir}/galleries/create-gallery.bru"
cat > "${base_dir}/galleries/update-gallery.bru" << 'EOF'
meta {
name: Update Gallery
type: http
seq: 4
}
put {
url: {{baseUrl}}/gallery/1
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"title": "Updated Modern Art Collection",
"description": "An updated curated collection of contemporary artworks",
"is_public": false
}
}
EOF
echo_success "Created: ${base_dir}/galleries/update-gallery.bru"
cat > "${base_dir}/galleries/my-galleries.bru" << 'EOF'
meta {
name: Get My Galleries
type: http
seq: 5
}
get {
url: {{baseUrl}}/galleries/mine
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}
EOF
echo_success "Created: ${base_dir}/galleries/my-galleries.bru"
cat > "${base_dir}/galleries/gallery-members.bru" << 'EOF'
meta {
name: Get Gallery Members
type: http
seq: 6
}
get {
url: {{baseUrl}}/gallery/1/members
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}
EOF
echo_success "Created: ${base_dir}/galleries/gallery-members.bru"
# Artwork files
cat > "${base_dir}/artworks/gallery-artworks.bru" << 'EOF'
meta {
name: Get Gallery Artworks
type: http
seq: 1
}
get {
url: {{baseUrl}}/gallery/1/artworks
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}
EOF
echo_success "Created: ${base_dir}/artworks/gallery-artworks.bru"
cat > "${base_dir}/artworks/get-artwork.bru" << 'EOF'
meta {
name: Get Artwork Details
type: http
seq: 2
}
get {
url: {{baseUrl}}/artwork/1
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}
EOF
echo_success "Created: ${base_dir}/artworks/get-artwork.bru"
cat > "${base_dir}/artworks/create-artwork.bru" << 'EOF'
meta {
name: Create Artwork
type: http
seq: 3
}
post {
url: {{baseUrl}}/gallery/1/artwork
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"title": "Sunset Over Mountains",
"description": "A beautiful landscape painting capturing the golden hour",
"image_url": "https://example.com/artwork1.jpg",
"medium": "Oil on Canvas",
"dimensions": "24x36 inches",
"creation_year": 2024,
"price": 1500.00,
"is_visible": true,
"is_for_sale": true
}
}
EOF
echo_success "Created: ${base_dir}/artworks/create-artwork.bru"
cat > "${base_dir}/artworks/update-artwork.bru" << 'EOF'
meta {
name: Update Artwork
type: http
seq: 4
}
put {
url: {{baseUrl}}/artwork/1
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"title": "Updated Sunset Over Mountains",
"description": "An updated beautiful landscape painting",
"price": 1800.00,
"is_for_sale": false
}
}
EOF
echo_success "Created: ${base_dir}/artworks/update-artwork.bru"
cat > "${base_dir}/artworks/my-artworks.bru" << 'EOF'
meta {
name: Get My Artworks
type: http
seq: 5
}
get {
url: {{baseUrl}}/artworks/mine
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}
EOF
echo_success "Created: ${base_dir}/artworks/my-artworks.bru"
# Gallery review files
cat > "${base_dir}/gallery-reviews/gallery-reviews.bru" << 'EOF'
meta {
name: Get Gallery Reviews
type: http
seq: 1
}
get {
url: {{baseUrl}}/gallery/1/reviews
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}
EOF
echo_success "Created: ${base_dir}/gallery-reviews/gallery-reviews.bru"
cat > "${base_dir}/gallery-reviews/create-gallery-review.bru" << 'EOF'
meta {
name: Create Gallery Review
type: http
seq: 2
}
post {
url: {{baseUrl}}/gallery/1/review
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"grade": 5,
"description": "Excellent gallery with amazing artwork collection!",
"parent_gr_id": null
}
}
EOF
echo_success "Created: ${base_dir}/gallery-reviews/create-gallery-review.bru"
cat > "${base_dir}/gallery-reviews/update-gallery-review.bru" << 'EOF'
meta {
name: Update Gallery Review
type: http
seq: 3
}
put {
url: {{baseUrl}}/galleries/review/1
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"grade": 4,
"description": "Updated: Very good gallery with great artwork collection!"
}
}
EOF
echo_success "Created: ${base_dir}/gallery-reviews/update-gallery-review.bru"
cat > "${base_dir}/gallery-reviews/given-gallery-reviews.bru" << 'EOF'
meta {
name: Get Given Gallery Reviews
type: http
seq: 4
}
get {
url: {{baseUrl}}/galleries/reviews/given
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}
EOF
echo_success "Created: ${base_dir}/gallery-reviews/given-gallery-reviews.bru"
cat > "${base_dir}/gallery-reviews/received-gallery-reviews.bru" << 'EOF'
meta {
name: Get Received Gallery Reviews
type: http
seq: 5
}
get {
url: {{baseUrl}}/galleries/reviews/received
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}
EOF
echo_success "Created: ${base_dir}/gallery-reviews/received-gallery-reviews.bru"
# Artwork review files
cat > "${base_dir}/artwork-reviews/artwork-reviews.bru" << 'EOF'
meta {
name: Get Artwork Reviews
type: http
seq: 1
}
get {
url: {{baseUrl}}/artwork/1/reviews
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}
EOF
echo_success "Created: ${base_dir}/artwork-reviews/artwork-reviews.bru"
cat > "${base_dir}/artwork-reviews/create-artwork-review.bru" << 'EOF'
meta {
name: Create Artwork Review
type: http
seq: 2
}
post {
url: {{baseUrl}}/artwork/1/review
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"grade": 5,
"description": "Absolutely stunning artwork! The colors and technique are magnificent.",
"parent_ar_id": null
}
}
EOF
echo_success "Created: ${base_dir}/artwork-reviews/create-artwork-review.bru"
cat > "${base_dir}/artwork-reviews/update-artwork-review.bru" << 'EOF'
meta {
name: Update Artwork Review
type: http
seq: 3
}
put {
url: {{baseUrl}}/artworks/review/1
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
Content-Type: application/json
}
body:json {
{
"grade": 4,
"description": "Updated: Very beautiful artwork! Great technique and composition."
}
}
EOF
echo_success "Created: ${base_dir}/artwork-reviews/update-artwork-review.bru"
cat > "${base_dir}/artwork-reviews/given-artwork-reviews.bru" << 'EOF'
meta {
name: Get Given Artwork Reviews
type: http
seq: 4
}
get {
url: {{baseUrl}}/artworks/reviews/given
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}
EOF
echo_success "Created: ${base_dir}/artwork-reviews/given-artwork-reviews.bru"
cat > "${base_dir}/artwork-reviews/received-artwork-reviews.bru" << 'EOF'
meta {
name: Get Received Artwork Reviews
type: http
seq: 5
}
get {
url: {{baseUrl}}/artworks/reviews/received
}
headers {
OIDC_email: {{email}}
OIDC_user: {{username}}
}
EOF
echo_success "Created: ${base_dir}/artwork-reviews/received-artwork-reviews.bru"
}
# Main function
main() {
echo_info "🚀 Generating Bruno API Collection for Private API..."
echo_info "📁 Collection name: ${COLLECTION_NAME}"
echo_info "🌐 Base URL: ${BASE_URL}"
echo_info "👤 Default user: ${DEFAULT_USERNAME} (${DEFAULT_EMAIL})"
echo ""
# Get current directory or use provided path
local target_dir="${1:-$(pwd)}"
local base_dir="${target_dir}/${COLLECTION_NAME}"
echo_info "📍 Creating collection in: ${base_dir}"
echo ""
# Create directory structure
create_directories "$base_dir"
echo ""
# Generate all files
generate_files "$base_dir"
echo ""
echo_success "Bruno collection generated successfully!"
echo ""
echo_info "📋 Next steps:"
echo " 1. Open Bruno and import the collection"
echo " 2. Select the 'local' environment"
echo " 3. Update variables in environments/local.bru if needed"
echo " 4. Start testing with auth/debug-headers.bru"
echo ""
echo_info "🔧 To customize:"
echo " - Edit environments/local.bru to change baseUrl, email, username"
echo " - Update gallery/artwork IDs in individual requests"
echo " - Modify request bodies to match your test data"
echo ""
echo_warning "💡 Don't forget to update the OIDC headers if your user changes!"
}
# Help function
show_help() {
echo "Bruno API Collection Generator"
echo ""
echo "Usage: $0 [target_directory]"
echo ""
echo "Arguments:"
echo " target_directory Directory where to create the collection (default: current directory)"
echo ""
echo "Examples:"
echo " $0 # Create in current directory"
echo " $0 ~/bruno # Create in ~/bruno directory"
echo " $0 /path/to/bruno # Create in specific path"
echo ""
echo "Configuration (edit script to change):"
echo " Collection name: ${COLLECTION_NAME}"
echo " Base URL: ${BASE_URL}"
echo " Default email: ${DEFAULT_EMAIL}"
echo " Default username: ${DEFAULT_USERNAME}"
}
# Check for help flag
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
show_help
exit 0
fi
# Run main function
main "$@"}

View File

@ -9,7 +9,7 @@ services:
POSTGRES_USER: keycloak # CHANGE THIS IN PROD POSTGRES_USER: keycloak # CHANGE THIS IN PROD
POSTGRES_PASSWORD: password # CHANGE THIS IN PROD POSTGRES_PASSWORD: password # CHANGE THIS IN PROD
volumes: volumes:
- ./data/keycloak-db:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
networks: networks:
- soa - soa
@ -33,9 +33,10 @@ services:
networks: networks:
- soa - soa
public_api: public_api:
build: build:
context: ./laravel context: ./public
depends_on: depends_on:
- keycloak - keycloak
- mysql - mysql
@ -83,7 +84,7 @@ services:
ports: ports:
- "3306:3306" - "3306:3306"
volumes: volumes:
- ./data/mysql-data:/var/lib/mysql - mysql_data:/var/lib/mysql
networks: networks:
- soa - soa
@ -99,3 +100,8 @@ networks:
soa: soa:
driver: bridge driver: bridge
volumes:
postgres_data:
mysql_data:

View File

@ -1,5 +1,4 @@
#!/bin/bash #!/bin/bash
# Variables # Variables
KC_HOST="http://localhost:8080" KC_HOST="http://localhost:8080"
REALM="master" REALM="master"
@ -7,6 +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_TOKEN2="personaltoken2"
# Fonction d'attente # Fonction d'attente
wait_for_keycloak() { wait_for_keycloak() {
@ -28,9 +31,16 @@ get_admin_token() {
jq -r .access_token jq -r .access_token
} }
# Générer une date d'expiration (1 an à partir de maintenant)
generate_expiry_date() {
date -d "+1 year" --iso-8601=seconds
}
# Créer un realm, client et utilisateur # Créer un realm, client et utilisateur
setup_keycloak() { setup_keycloak() {
TOKEN=$(get_admin_token) TOKEN=$(get_admin_token)
CURRENT_DATE=$(date --iso-8601=seconds)
EXPIRY_DATE=$(generate_expiry_date)
echo "🛠️ Création du realm $REALM..." echo "🛠️ Création du realm $REALM..."
curl -s -X POST "$KC_HOST/admin/realms" \ curl -s -X POST "$KC_HOST/admin/realms" \
@ -38,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" \
@ -48,28 +70,97 @@ setup_keycloak() {
\"publicClient\": false, \"publicClient\": false,
\"secret\": \"$CLIENT_SECRET\", \"secret\": \"$CLIENT_SECRET\",
\"redirectUris\": [\"*\"], \"redirectUris\": [\"*\"],
\"standardFlowEnabled\": true \"standardFlowEnabled\": true,
}" > /dev/null \"directAccessGrantsEnabled\": true,
\"serviceAccountsEnabled\": true,
\"authorizationServicesEnabled\": false
}"
echo "👤 Création de l'utilisateur $USERNAME..." 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" \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{ -d "{
\"username\": \"$USERNAME\", \"username\": \"$USERNAME\",
\"enabled\": true, \"enabled\": true,
\"emailVerified\": true,
\"attributes\": {
\"api_token\": [\"$PERSONAL_TOKEN\"],
\"token_created\": [\"$CURRENT_DATE\"],
\"token_expires\": [\"$EXPIRY_DATE\"],
\"created_by\": [\"setup_script\"],
\"department\": [\"IT\"],
\"access_level\": [\"developer\"]
},
\"credentials\": [{ \"credentials\": [{
\"type\": \"password\", \"type\": \"password\",
\"value\": \"$PASSWORD\", \"value\": \"$PASSWORD\",
\"temporary\": false \"temporary\": false
}] }]
}" > /dev/null }"
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 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 les tokens
test_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 # Lancer le setup
wait_for_keycloak wait_for_keycloak
setup_keycloak setup_keycloak
test_personal_token

Binary file not shown.

View File

@ -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,16 +77,134 @@ 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()
if not user: if not user:
@ -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"})

Some files were not shown because too many files have changed in this diff Show More