Skip to content

Commit f53bb80

Browse files
authored
Merge pull request #14 from 223MapAction/dev
Dev
2 parents 30e87b2 + 77ae5f1 commit f53bb80

30 files changed

+2720
-2
lines changed

.github/workflows/ci_cd.yml

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,177 @@ jobs:
239239
# docker image prune -af
240240
# docker volume prune -f
241241

242+
# deploy:
243+
# needs: setup-and-test
244+
# if: success() && github.event_name == 'push' && github.ref == 'refs/heads/main'
245+
# runs-on: self-hosted
246+
# permissions: write-all
247+
# steps:
248+
# - name: Pre-checkout cleanup
249+
# run: |
250+
# # Stop any Python processes
251+
# sudo pkill -f python || true
252+
253+
# # Remove problematic __pycache__ directories
254+
# sudo find /home/azureuser/actions-runner/_work/Mapapi/Mapapi -type d -name "__pycache__" -exec rm -rf {} + || true
255+
256+
# # Force remove the entire directory if needed
257+
# sudo rm -rf /home/azureuser/actions-runner/_work/Mapapi/Mapapi || true
258+
259+
# # Recreate directory with proper permissions
260+
# sudo mkdir -p /home/azureuser/actions-runner/_work/Mapapi/Mapapi
261+
# sudo chown -R $USER:$USER /home/azureuser/actions-runner/_work/Mapapi/Mapapi
262+
# sudo chmod -R 777 /home/azureuser/actions-runner/_work/Mapapi/Mapapi
263+
264+
# - name: Checkout Repository
265+
# uses: actions/checkout@v3
266+
267+
# - name: Login to Docker Hub
268+
# uses: docker/login-action@v2
269+
# with:
270+
# username: ${{ secrets.DOCKER_USERNAME }}
271+
# password: ${{ secrets.DOCKER_PASSWORD }}
272+
273+
# - name: Create entrypoint.sh
274+
# run: |
275+
# cat > entrypoint.sh << 'EOL'
276+
# #!/bin/sh
277+
# set -e
278+
279+
# # Wait for postgres to be ready
280+
# python manage.py wait_for_db
281+
282+
# # Apply database migrations
283+
# python manage.py migrate
284+
285+
# # Create superuser if it doesn't exist
286+
# python manage.py createsuperuser --noinput || true
287+
288+
# # Start server
289+
# exec python manage.py runserver 0.0.0.0:8000
290+
# EOL
291+
# shell: bash
292+
# continue-on-error: false
293+
294+
# - name: Create .env file
295+
# run: |
296+
# {
297+
# echo "ALLOWED_HOSTS=${{ secrets.ALLOWED_HOSTS }}"
298+
# echo "ANDROID_CLIENT_ID=${{ secrets.ANDROID_CLIENT_ID }}"
299+
# echo "DB_HOST=${{ secrets.DB_HOST }}"
300+
# echo "DJANGO_SUPERUSER_EMAIL=${{ secrets.DJANGO_SUPERUSER_EMAIL }}"
301+
# echo "DJANGO_SUPERUSER_FIRST_NAME=${{ secrets.DJANGO_SUPERUSER_FIRST_NAME }}"
302+
# echo "DJANGO_SUPERUSER_LAST_NAME=${{ secrets.DJANGO_SUPERUSER_LAST_NAME }}"
303+
# echo "DJANGO_SUPERUSER_PASSWORD=${{ secrets.DJANGO_SUPERUSER_PASSWORD }}"
304+
# echo "DJANGO_SUPERUSER_USERNAME=${{ secrets.DJANGO_SUPERUSER_USERNAME }}"
305+
# echo "IOS_CLIENT_ID=${{ secrets.IOS_CLIENT_ID }}"
306+
# echo "PORT=${{ secrets.PORT }}"
307+
# echo "POSTGRES_USER=${{ secrets.POSTGRES_USER }}"
308+
# echo "POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}"
309+
# echo "POSTGRES_DB=${{ secrets.POSTGRES_DB }}"
310+
# echo "SECRET_KEY=${{ secrets.SECRET_KEY }}"
311+
# echo "WEB_CLIENT_ID=${{ secrets.WEB_CLIENT_ID }}"
312+
# echo "WEB_CLIENT_SECRET=${{ secrets.WEB_CLIENT_SECRET }}"
313+
# echo "TWILIO_ACCOUNT_SID=${{ secrets.TWILIO_ACCOUNT_SID }}"
314+
# echo "TWILIO_AUTH_TOKEN=${{ secrets.TWILIO_AUTH_TOKEN }}"
315+
# echo "TWILIO_PHONE_NUMBER=${{ secrets.TWILIO_PHONE_NUMBER }}"
316+
# } > .env
317+
318+
# - name: Build and Run Docker Compose
319+
# env:
320+
# ALLOWED_HOSTS: ${{ secrets.ALLOWED_HOSTS }}
321+
# ANDROID_CLIENT_ID: ${{ secrets.ANDROID_CLIENT_ID }}
322+
# DB_HOST: ${{ secrets.DB_HOST }}
323+
# DJANGO_SUPERUSER_EMAIL: ${{ secrets.DJANGO_SUPERUSER_EMAIL }}
324+
# DJANGO_SUPERUSER_FIRST_NAME: ${{ secrets.DJANGO_SUPERUSER_FIRST_NAME }}
325+
# DJANGO_SUPERUSER_LAST_NAME: ${{ secrets.DJANGO_SUPERUSER_LAST_NAME }}
326+
# DJANGO_SUPERUSER_PASSWORD: ${{ secrets.DJANGO_SUPERUSER_PASSWORD }}
327+
# DJANGO_SUPERUSER_USERNAME: ${{ secrets.DJANGO_SUPERUSER_USERNAME }}
328+
# IOS_CLIENT_ID: ${{ secrets.IOS_CLIENT_ID }}
329+
# PORT: ${{ secrets.PORT }}
330+
# POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
331+
# POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
332+
# POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
333+
# SECRET_KEY: ${{ secrets.SECRET_KEY }}
334+
# TEST_POSTGRES_DB: ${{ secrets.TEST_POSTGRES_DB }}
335+
# WEB_CLIENT_ID: ${{ secrets.WEB_CLIENT_ID }}
336+
# WEB_CLIENT_SECRET: ${{ secrets.WEB_CLIENT_SECRET }}
337+
# TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }}
338+
# TWILIO_AUTH_TOKEN: ${{ secrets.TWILIO_AUTH_TOKEN }}
339+
# TWILIO_PHONE_NUMBER: ${{ secrets.TWILIO_PHONE_NUMBER }}
340+
# run: |
341+
# # Conditionally remove existing network if it exists
342+
# docker network ls | grep -q mapapi_micro-services-network && docker network rm mapapi_micro-services-network || true
343+
344+
# # Build and run Docker Compose
345+
# docker-compose -f _cd_pipeline.yml up --build -d
346+
347+
# - name: Post-deployment cleanup
348+
# if: always()
349+
# run: |
350+
# # Clean up dangling volumes and images
351+
# docker system prune -af --volumes
352+
# docker image prune -af
353+
# docker volume prune -f
354+
242355
deploy:
356+
<<<<<<< HEAD
357+
needs: setup-and-test
358+
if: success() && github.event_name == 'push' && github.ref == 'refs/heads/main'
359+
runs-on: ubuntu-latest
360+
361+
steps:
362+
- name: Set up SSH connection
363+
run: |
364+
mkdir -p ~/.ssh
365+
echo "${{ secrets.EC2_SSH_KEY }}" > ~/.ssh/id_rsa
366+
chmod 600 ~/.ssh/id_rsa
367+
ssh-keyscan -H 13.36.39.58 >> ~/.ssh/known_hosts
368+
369+
- name: Deploy to EC2 instance
370+
run: |
371+
ssh ec2-user@13.36.39.58 << 'EOF'
372+
cd ~/app
373+
374+
if [ ! -d ".git" ]; then
375+
git clone https://github.com/223MapAction/Mapapi.git .
376+
fi
377+
378+
git pull origin main
379+
380+
echo "Création du fichier .env..."
381+
cat > .env <<EOL
382+
ALLOWED_HOSTS: ${{ secrets.ALLOWED_HOSTS }}
383+
ANDROID_CLIENT_ID: ${{ secrets.ANDROID_CLIENT_ID }}
384+
DB_HOST: ${{ secrets.DB_HOST }}
385+
DJANGO_SUPERUSER_EMAIL: ${{ secrets.DJANGO_SUPERUSER_EMAIL }}
386+
DJANGO_SUPERUSER_FIRST_NAME: ${{ secrets.DJANGO_SUPERUSER_FIRST_NAME }}
387+
DJANGO_SUPERUSER_LAST_NAME: ${{ secrets.DJANGO_SUPERUSER_LAST_NAME }}
388+
DJANGO_SUPERUSER_PASSWORD: ${{ secrets.DJANGO_SUPERUSER_PASSWORD }}
389+
DJANGO_SUPERUSER_USERNAME: ${{ secrets.DJANGO_SUPERUSER_USERNAME }}
390+
IOS_CLIENT_ID: ${{ secrets.IOS_CLIENT_ID }}
391+
PORT: ${{ secrets.PORT }}
392+
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
393+
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
394+
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
395+
SECRET_KEY: ${{ secrets.SECRET_KEY }}
396+
TEST_POSTGRES_DB: ${{ secrets.TEST_POSTGRES_DB }}
397+
WEB_CLIENT_ID: ${{ secrets.WEB_CLIENT_ID }}
398+
WEB_CLIENT_SECRET: ${{ secrets.WEB_CLIENT_SECRET }}
399+
TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }}
400+
TWILIO_AUTH_TOKEN: ${{ secrets.TWILIO_AUTH_TOKEN }}
401+
TWILIO_PHONE_NUMBER: ${{ secrets.TWILIO_PHONE_NUMBER }}
402+
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
403+
SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
404+
USE_SUPABASE_STORAGE: ${{ secrets.USE_SUPABASE_STORAGE }}
405+
EMAIL_HOST: ${{ secrets.EMAIL_HOST }}
406+
EMAIL_HOST_USER: ${{ secrets.EMAIL_HOST_USER }}
407+
EMAIL_HOST_PASSWORD: ${{ secrets.EMAIL_HOST_PASSWORD }}
408+
EOL
409+
410+
./deploy.sh
411+
EOF
412+
=======
243413
needs: cleanup
244414
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
245415
runs-on: self-hosted
@@ -290,3 +460,4 @@ jobs:
290460
docker system prune -af --volumes
291461
docker image prune -af
292462
docker volume prune -f
463+
>>>>>>> f2c8cfb682a3a605275df0fab503112a426fc0c7

Mapapi/middleware.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from django.utils.deprecation import MiddlewareMixin
2+
from .models import Organisation
3+
4+
class OrganisationFromSubdomainMiddleware(MiddlewareMixin):
5+
"""
6+
Middleware pour extraire le sous-domaine de la requête et attacher l'organisation à request.organisation
7+
"""
8+
def process_request(self, request):
9+
host = request.get_host().split(':')[0] # retire le port éventuel
10+
# On suppose que le domaine principal est map-action.com
11+
# et que le sous-domaine correspond à l'organisation
12+
parts = host.split('.')
13+
if len(parts) < 2:
14+
request.organisation = None
15+
return
16+
subdomain = parts[0]
17+
try:
18+
organisation = Organisation.objects.get(subdomain=subdomain)
19+
request.organisation = organisation
20+
except Organisation.DoesNotExist:
21+
request.organisation = None
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Generated by Django 4.2.7 on 2025-07-17 10:21
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('Mapapi', '0010_organisationtag'),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name='Organisation',
16+
fields=[
17+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18+
('name', models.CharField(max_length=255, unique=True)),
19+
('is_premium', models.BooleanField(default=False)),
20+
('subdomain', models.CharField(max_length=255, unique=True)),
21+
('logo_url', models.URLField(blank=True, null=True)),
22+
('primary_color', models.CharField(default='#4CAF50', max_length=7)),
23+
('secondary_color', models.CharField(default='#8BC34A', max_length=7)),
24+
('background_color', models.CharField(default='#F0F0F0', max_length=7)),
25+
('created_at', models.DateTimeField(auto_now_add=True)),
26+
],
27+
),
28+
migrations.AlterField(
29+
model_name='evenement',
30+
name='audio',
31+
field=models.FileField(blank=True, null=True, upload_to='events/'),
32+
),
33+
migrations.AlterField(
34+
model_name='evenement',
35+
name='photo',
36+
field=models.ImageField(blank=True, null=True, upload_to='events/'),
37+
),
38+
migrations.AlterField(
39+
model_name='evenement',
40+
name='video',
41+
field=models.FileField(blank=True, null=True, upload_to='events/'),
42+
),
43+
migrations.AlterField(
44+
model_name='incident',
45+
name='audio',
46+
field=models.FileField(blank=True, null=True, upload_to='incidents/'),
47+
),
48+
migrations.AlterField(
49+
model_name='incident',
50+
name='photo',
51+
field=models.ImageField(blank=True, null=True, upload_to='incidents/'),
52+
),
53+
migrations.AlterField(
54+
model_name='incident',
55+
name='video',
56+
field=models.FileField(blank=True, null=True, upload_to='incidents/'),
57+
),
58+
migrations.AlterField(
59+
model_name='rapport',
60+
name='file',
61+
field=models.FileField(blank=True, null=True, upload_to='reports/'),
62+
),
63+
migrations.AlterField(
64+
model_name='zone',
65+
name='photo',
66+
field=models.ImageField(blank=True, null=True, upload_to='zones/'),
67+
),
68+
migrations.AlterField(
69+
model_name='user',
70+
name='organisation',
71+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='users', to='Mapapi.organisation'),
72+
),
73+
]

Mapapi/models.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@
4848
)
4949

5050

51+
# Modèle d'organisation pour gérer les organisations liées aux utilisateurs
52+
class Organisation(models.Model):
53+
name = models.CharField(max_length=255, unique=True)
54+
is_premium = models.BooleanField(default=False)
55+
subdomain = models.CharField(max_length=255, unique=True) # ex: wetlands
56+
logo_url = models.URLField(null=True, blank=True)
57+
primary_color = models.CharField(max_length=7, default="#4CAF50") # hex
58+
secondary_color = models.CharField(max_length=7, default="#8BC34A")
59+
background_color = models.CharField(max_length=7, default="#F0F0F0")
60+
created_at = models.DateTimeField(auto_now_add=True)
61+
62+
def __str__(self):
63+
return self.name
64+
5165
# Creation du model User pour les utilisateurs de l'application pour securiser l'entree des commandes
5266

5367
class UserManager(BaseUserManager):
@@ -146,8 +160,13 @@ class User(AbstractBaseUser, PermissionsMixin):
146160
community = models.ForeignKey('Communaute', db_column='user_communaute_id', related_name='user_communaute',
147161
on_delete=models.CASCADE, null=True, blank=True)
148162
provider = models.CharField(_('provider'), max_length=255, blank=True, null=True)
149-
organisation = models.CharField(max_length=255, blank=True,
150-
null=True)
163+
organisation = models.ForeignKey(
164+
Organisation,
165+
on_delete=models.SET_NULL,
166+
null=True,
167+
blank=True,
168+
related_name="users"
169+
)
151170
points = models.IntegerField(null=True, blank=True, default=0)
152171
zones = models.ManyToManyField('Zone', blank=True)
153172
verification_token = models.UUIDField(default=uuid.uuid4, editable=False, null=True, blank=True)

Mapapi/serializer.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
from rest_framework import serializers, generics, permissions, status
22
from .models import *
3+
4+
class OrganisationSerializer(serializers.ModelSerializer):
5+
class Meta:
6+
model = Organisation
7+
fields = '__all__'
8+
39
from rest_framework import serializers
410
from django.contrib.auth import authenticate
511
from rest_framework.serializers import ModelSerializer

Mapapi/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
from .views import PasswordResetView
1515

1616
urlpatterns = [
17+
path('tenant-config/', TenantConfigView.as_view(), name='tenant_config'),
18+
path('organisations/', OrganisationViewSet.as_view(), name='organisation-list-create'),
19+
path('organisations/<int:pk>', OrganisationViewSet.as_view(), name='organisation-detail'),
1720
# URL PATTERNS for the documentation
1821
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
1922
# Optional UI:

Mapapi/views.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,39 @@ def get_random(length=6):
5959

6060
logger = logging.getLogger(__name__)
6161

62+
from rest_framework.views import APIView
63+
from rest_framework.response import Response
64+
from rest_framework import status
65+
from .models import Organisation
66+
from rest_framework import permissions, generics
67+
68+
class OrganisationViewSet(generics.ListCreateAPIView, generics.RetrieveUpdateDestroyAPIView):
69+
queryset = Organisation.objects.all()
70+
serializer_class = OrganisationSerializer
71+
permission_classes = []
72+
73+
def get_queryset(self):
74+
# Optionnel : filtrer selon les droits de l'utilisateur
75+
return Organisation.objects.all()
76+
77+
class TenantConfigView(APIView):
78+
permission_classes = [] # Accessible sans authentification, car utilisé pour personnaliser le front dès le login
79+
80+
def get(self, request, format=None):
81+
org = getattr(request, 'organisation', None)
82+
if org is None:
83+
return Response({'detail': 'Organisation not found for this subdomain.'}, status=status.HTTP_404_NOT_FOUND)
84+
data = {
85+
'name': org.name,
86+
'subdomain': org.subdomain,
87+
'logo_url': org.logo_url,
88+
'primary_color': org.primary_color,
89+
'secondary_color': org.secondary_color,
90+
'background_color': org.background_color,
91+
'is_premium': org.is_premium,
92+
}
93+
return Response(data)
94+
6295
N = 7
6396

6497
def get_random():

backend/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
MIDDLEWARE = [
109109
'django.middleware.security.SecurityMiddleware',
110110
'django.contrib.sessions.middleware.SessionMiddleware',
111+
'Mapapi.middleware.OrganisationFromSubdomainMiddleware',
111112
'django.middleware.common.CommonMiddleware',
112113
'django.middleware.csrf.CsrfViewMiddleware',
113114
'django.contrib.auth.middleware.AuthenticationMiddleware',
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pip

0 commit comments

Comments
 (0)