mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-22 06:03:20 +00:00
commit
e37ce4172e
@ -21,19 +21,12 @@ from club.models import Club
|
|||||||
from core.models import Group, SithFile, User
|
from core.models import Group, SithFile, User
|
||||||
from core.views.site import search_user
|
from core.views.site import search_user
|
||||||
from counter.models import Counter, Customer, Product
|
from counter.models import Counter, Customer, Product
|
||||||
|
from counter.utils import is_logged_in_counter
|
||||||
|
|
||||||
def check_token(request):
|
|
||||||
return (
|
|
||||||
"counter_token" in request.session.keys()
|
|
||||||
and request.session["counter_token"]
|
|
||||||
and Counter.objects.filter(token=request.session["counter_token"]).exists()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RightManagedLookupChannel(LookupChannel):
|
class RightManagedLookupChannel(LookupChannel):
|
||||||
def check_auth(self, request):
|
def check_auth(self, request):
|
||||||
if not request.user.was_subscribed and not check_token(request):
|
if not request.user.was_subscribed and not is_logged_in_counter(request):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
|
||||||
|
|
||||||
|
@ -1137,11 +1137,9 @@ class SithFile(models.Model):
|
|||||||
else:
|
else:
|
||||||
self._check_path_consistence()
|
self._check_path_consistence()
|
||||||
|
|
||||||
def __getattribute__(self, attr):
|
@property
|
||||||
if attr == "is_file":
|
def is_file(self):
|
||||||
return not self.is_folder
|
return not self.is_folder
|
||||||
else:
|
|
||||||
return super().__getattribute__(attr)
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def as_picture(self):
|
def as_picture(self):
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
from urllib.parse import quote, urljoin
|
||||||
|
|
||||||
# This file contains all the views that concern the page model
|
# This file contains all the views that concern the page model
|
||||||
from wsgiref.util import FileWrapper
|
from wsgiref.util import FileWrapper
|
||||||
@ -21,7 +22,7 @@ from django import forms
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.forms.models import modelform_factory
|
from django.forms.models import modelform_factory
|
||||||
from django.http import Http404, HttpResponse
|
from django.http import Http404, HttpRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.http import http_date
|
from django.utils.http import http_date
|
||||||
@ -37,33 +38,42 @@ from core.views import (
|
|||||||
CanViewMixin,
|
CanViewMixin,
|
||||||
can_view,
|
can_view,
|
||||||
)
|
)
|
||||||
from counter.models import Counter
|
from counter.utils import is_logged_in_counter
|
||||||
|
|
||||||
|
|
||||||
def send_file(request, file_id, file_class=SithFile, file_attr="file"):
|
def send_file(
|
||||||
"""Send a file through Django without loading the whole file into
|
request: HttpRequest,
|
||||||
memory at once. The FileWrapper will turn the file object into an
|
file_id: int,
|
||||||
iterator for chunks of 8KB.
|
file_class: type[SithFile] = SithFile,
|
||||||
|
file_attr: str = "file",
|
||||||
|
) -> HttpResponse:
|
||||||
|
"""Send a protected file, if the user can see it.
|
||||||
|
|
||||||
|
In prod, the server won't handle the download itself,
|
||||||
|
but set the appropriate headers in the response to make the reverse-proxy
|
||||||
|
deal with it.
|
||||||
|
In debug mode, the server will directly send the file.
|
||||||
"""
|
"""
|
||||||
f = get_object_or_404(file_class, id=file_id)
|
f = get_object_or_404(file_class, id=file_id)
|
||||||
if not (
|
if not can_view(f, request.user) and not is_logged_in_counter(request):
|
||||||
can_view(f, request.user)
|
|
||||||
or (
|
|
||||||
"counter_token" in request.session.keys()
|
|
||||||
and request.session["counter_token"]
|
|
||||||
and Counter.objects.filter( # check if not null for counters that have no token set
|
|
||||||
token=request.session["counter_token"]
|
|
||||||
).exists()
|
|
||||||
)
|
|
||||||
):
|
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
name = f.__getattribute__(file_attr).name
|
name = getattr(f, file_attr).name
|
||||||
filepath = settings.MEDIA_ROOT / name
|
filepath = settings.MEDIA_ROOT / name
|
||||||
|
|
||||||
# check if file exists on disk
|
# check if file exists on disk
|
||||||
if not filepath.exists():
|
if not filepath.exists():
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
|
if not settings.DEBUG:
|
||||||
|
# When receiving a response with the Accel-Redirect header,
|
||||||
|
# the reverse proxy will automatically handle the file sending.
|
||||||
|
# This is really hard to test (thus isn't tested)
|
||||||
|
# so please do not mess with this.
|
||||||
|
response = HttpResponse(status=200)
|
||||||
|
response["Content-Type"] = ""
|
||||||
|
response["X-Accel-Redirect"] = quote(urljoin(settings.MEDIA_URL, name))
|
||||||
|
return response
|
||||||
|
|
||||||
with open(filepath, "rb") as filename:
|
with open(filepath, "rb") as filename:
|
||||||
wrapper = FileWrapper(filename)
|
wrapper = FileWrapper(filename)
|
||||||
response = HttpResponse(wrapper, content_type=f.mime_type)
|
response = HttpResponse(wrapper, content_type=f.mime_type)
|
||||||
|
36
counter/utils.py
Normal file
36
counter/utils.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from django.http import HttpRequest
|
||||||
|
from django.urls import resolve
|
||||||
|
|
||||||
|
from counter.models import Counter
|
||||||
|
|
||||||
|
|
||||||
|
def is_logged_in_counter(request: HttpRequest) -> bool:
|
||||||
|
"""Check if the request is sent from a device logged to a counter.
|
||||||
|
|
||||||
|
The request must also be sent within the frame of a counter's activity.
|
||||||
|
Trying to use this function to manage access to non-sas
|
||||||
|
related resources probably won't work.
|
||||||
|
|
||||||
|
A request is considered as coming from a logged counter if :
|
||||||
|
|
||||||
|
- Its referer comes from the counter app
|
||||||
|
(eg. fetching user pictures from the click UI)
|
||||||
|
or the request path belongs to the counter app
|
||||||
|
(eg. the barman went back to the main by missclick and go back
|
||||||
|
to the counter)
|
||||||
|
- The current session has a counter token associated with it.
|
||||||
|
- A counter with this token exists.
|
||||||
|
"""
|
||||||
|
referer = urlparse(request.META["HTTP_REFERER"]).path
|
||||||
|
path_ok = (
|
||||||
|
request.resolver_match.app_name == "counter"
|
||||||
|
or resolve(referer).app_name == "counter"
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
path_ok
|
||||||
|
and "counter_token" in request.session
|
||||||
|
and request.session["counter_token"]
|
||||||
|
and Counter.objects.filter(token=request.session["counter_token"]).exists()
|
||||||
|
)
|
@ -80,6 +80,7 @@ from counter.models import (
|
|||||||
Selling,
|
Selling,
|
||||||
StudentCard,
|
StudentCard,
|
||||||
)
|
)
|
||||||
|
from counter.utils import is_logged_in_counter
|
||||||
|
|
||||||
|
|
||||||
class CounterAdminMixin(View):
|
class CounterAdminMixin(View):
|
||||||
@ -901,15 +902,9 @@ class RefillingDeleteView(DeleteView):
|
|||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
"""We have here a very particular right handling, we can't inherit from CanEditPropMixin."""
|
"""We have here a very particular right handling, we can't inherit from CanEditPropMixin."""
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
if (
|
if timezone.now() - self.object.date <= timedelta(
|
||||||
timezone.now() - self.object.date
|
minutes=settings.SITH_LAST_OPERATIONS_LIMIT
|
||||||
<= timedelta(minutes=settings.SITH_LAST_OPERATIONS_LIMIT)
|
) and is_logged_in_counter(request):
|
||||||
and "counter_token" in request.session.keys()
|
|
||||||
and request.session["counter_token"]
|
|
||||||
and Counter.objects.filter( # check if not null for counters that have no token set
|
|
||||||
token=request.session["counter_token"]
|
|
||||||
).exists()
|
|
||||||
):
|
|
||||||
self.success_url = reverse(
|
self.success_url = reverse(
|
||||||
"counter:details", kwargs={"counter_id": self.object.counter.id}
|
"counter:details", kwargs={"counter_id": self.object.counter.id}
|
||||||
)
|
)
|
||||||
@ -932,15 +927,9 @@ class SellingDeleteView(DeleteView):
|
|||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
"""We have here a very particular right handling, we can't inherit from CanEditPropMixin."""
|
"""We have here a very particular right handling, we can't inherit from CanEditPropMixin."""
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
if (
|
if timezone.now() - self.object.date <= timedelta(
|
||||||
timezone.now() - self.object.date
|
minutes=settings.SITH_LAST_OPERATIONS_LIMIT
|
||||||
<= timedelta(minutes=settings.SITH_LAST_OPERATIONS_LIMIT)
|
) and is_logged_in_counter(request):
|
||||||
and "counter_token" in request.session.keys()
|
|
||||||
and request.session["counter_token"]
|
|
||||||
and Counter.objects.filter( # check if not null for counters that have no token set
|
|
||||||
token=request.session["counter_token"]
|
|
||||||
).exists()
|
|
||||||
):
|
|
||||||
self.success_url = reverse(
|
self.success_url = reverse(
|
||||||
"counter:details", kwargs={"counter_id": self.object.counter.id}
|
"counter:details", kwargs={"counter_id": self.object.counter.id}
|
||||||
)
|
)
|
||||||
@ -1175,14 +1164,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
"""We have here again a very particular right handling."""
|
"""We have here again a very particular right handling."""
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
if (
|
if is_logged_in_counter(request) and self.object.barmen_list:
|
||||||
self.object.barmen_list
|
|
||||||
and "counter_token" in request.session.keys()
|
|
||||||
and request.session["counter_token"]
|
|
||||||
and Counter.objects.filter( # check if not null for counters that have no token set
|
|
||||||
token=request.session["counter_token"]
|
|
||||||
).exists()
|
|
||||||
):
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
return HttpResponseRedirect(
|
return HttpResponseRedirect(
|
||||||
reverse("counter:details", kwargs={"counter_id": self.object.id})
|
reverse("counter:details", kwargs={"counter_id": self.object.id})
|
||||||
@ -1215,14 +1197,7 @@ class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
"""We have here again a very particular right handling."""
|
"""We have here again a very particular right handling."""
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
if (
|
if is_logged_in_counter(request) and self.object.barmen_list:
|
||||||
self.object.barmen_list
|
|
||||||
and "counter_token" in request.session.keys()
|
|
||||||
and request.session["counter_token"]
|
|
||||||
and Counter.objects.filter( # check if not null for counters that have no token set
|
|
||||||
token=request.session["counter_token"]
|
|
||||||
).exists()
|
|
||||||
):
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
return HttpResponseRedirect(
|
return HttpResponseRedirect(
|
||||||
reverse("counter:details", kwargs={"counter_id": self.object.id})
|
reverse("counter:details", kwargs={"counter_id": self.object.id})
|
||||||
|
@ -9,6 +9,33 @@ que votre environnement de développement soit encore plus
|
|||||||
proche de celui en production.
|
proche de celui en production.
|
||||||
Voici les étapes à suivre pour ça.
|
Voici les étapes à suivre pour ça.
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
|
||||||
|
Configurer les dépendances du projet
|
||||||
|
peut demander beaucoup d'allers et retours entre
|
||||||
|
votre répertoire projet et divers autres emplacements.
|
||||||
|
|
||||||
|
Vous pouvez gagner du temps en déclarant un alias :
|
||||||
|
|
||||||
|
=== "bash/zsh"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
alias cdp="cd /repertoire/du/projet"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "nu"
|
||||||
|
|
||||||
|
```nu
|
||||||
|
alias cdp = cd /repertoire/du/projet
|
||||||
|
```
|
||||||
|
|
||||||
|
Chaque fois qu'on vous demandera de retourner au répertoire
|
||||||
|
projet, vous aurez juste à faire :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cdp
|
||||||
|
```
|
||||||
|
|
||||||
## Installer les dépendances manquantes
|
## Installer les dépendances manquantes
|
||||||
|
|
||||||
Pour installer complètement le projet, il va falloir
|
Pour installer complètement le projet, il va falloir
|
||||||
@ -20,19 +47,19 @@ Commencez par installer les dépendances système :
|
|||||||
=== "Debian/Ubuntu"
|
=== "Debian/Ubuntu"
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt install postgresql redis libq-dev
|
sudo apt install postgresql redis libq-dev nginx
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "Arch Linux"
|
=== "Arch Linux"
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo pacman -S postgresql redis
|
sudo pacman -S postgresql redis nginx
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "macOS"
|
=== "macOS"
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install postgresql redis nginx lipbq
|
brew install postgresql redis lipbq nginx
|
||||||
export PATH="/usr/local/opt/libpq/bin:$PATH"
|
export PATH="/usr/local/opt/libpq/bin:$PATH"
|
||||||
source ~/.zshrc
|
source ~/.zshrc
|
||||||
```
|
```
|
||||||
@ -139,6 +166,102 @@ poetry run ./manage.py populate
|
|||||||
N'oubliez de quitter la session de l'utilisateur
|
N'oubliez de quitter la session de l'utilisateur
|
||||||
postgres après avoir configuré la db.
|
postgres après avoir configuré la db.
|
||||||
|
|
||||||
|
## Configurer nginx
|
||||||
|
|
||||||
|
Nginx est utilisé comme reverse-proxy.
|
||||||
|
|
||||||
|
!!!warning
|
||||||
|
|
||||||
|
Nginx ne sert pas les fichiers de la même manière que Django.
|
||||||
|
Les fichiers statiques servis seront ceux du dossier `/static`,
|
||||||
|
tels que générés par les commandes `collectstatic` et
|
||||||
|
`compilestatic`.
|
||||||
|
Si vous changez du css ou du js sans faire tourner
|
||||||
|
ces commandes, ces changements ne seront pas reflétés.
|
||||||
|
|
||||||
|
De manière générale, utiliser nginx en dev n'est pas très utile,
|
||||||
|
voire est gênant si vous travaillez sur le front.
|
||||||
|
Ne vous embêtez pas avec ça, sauf par curiosité intellectuelle,
|
||||||
|
ou bien si vous voulez tester spécifiquement
|
||||||
|
des interactions avec le reverse proxy.
|
||||||
|
|
||||||
|
|
||||||
|
Placez-vous dans le répertoire `/etc/nginx`,
|
||||||
|
et créez les dossiers et fichiers nécessaires :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /etc/nginx/
|
||||||
|
sudo mkdir sites-enabled sites-available
|
||||||
|
sudo touch sites-available/sith.conf
|
||||||
|
sudo ln -s /etc/nginx/sites-available/sith.conf sites-enabled/sith.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis ouvrez le fichier `sites-available/sith.conf` et mettez-y le contenu suivant :
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 8000;
|
||||||
|
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
location /static/;
|
||||||
|
root /repertoire/du/projet;
|
||||||
|
}
|
||||||
|
location ~ ^/data/(products|com|club_logos)/ {
|
||||||
|
root /repertoire/du/projet;
|
||||||
|
}
|
||||||
|
location ~ ^/data/(SAS|profiles|users|.compressed|.thumbnails)/ {
|
||||||
|
# https://nginx.org/en/docs/http/ngx_http_core_module.html#internal
|
||||||
|
internal;
|
||||||
|
root /repertoire/du/projet;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8001;
|
||||||
|
include uwsgi_params;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ouvrez le fichier `nginx.conf`, et ajoutez la configuration suivante :
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
http {
|
||||||
|
# Toute la configuration
|
||||||
|
# éventuellement déjà là
|
||||||
|
|
||||||
|
include /etc/nginx/sites-enabled/sith.conf;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Vérifiez que votre configuration est bonne :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nginx -t
|
||||||
|
```
|
||||||
|
|
||||||
|
Si votre configuration n'est pas bonne, corrigez-la.
|
||||||
|
Puis lancez ou relancez nginx :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
Dans votre `settings_custom.py`, remplacez `DEBUG=True` par `DEBUG=False`.
|
||||||
|
|
||||||
|
Enfin, démarrez le serveur Django :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /repertoire/du/projet
|
||||||
|
poetry run ./manage.py runserver 8001
|
||||||
|
```
|
||||||
|
|
||||||
|
Et c'est bon, votre reverse-proxy est prêt à tourner devant votre serveur.
|
||||||
|
Nginx écoutera sur le port 8000.
|
||||||
|
Toutes les requêtes vers des fichiers statiques et les medias publiques
|
||||||
|
seront seront servies directement par nginx.
|
||||||
|
Toutes les autres requêtes seront transmises au serveur django.
|
||||||
|
|
||||||
|
|
||||||
## Mettre à jour la base de données antispam
|
## Mettre à jour la base de données antispam
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user