53 Commits

Author SHA1 Message Date
706b3dc275 WIP - Adding multi tenant 2025-03-27 00:11:53 +01:00
ff78f9da54 Migrating to fastapi-pagination 2025-03-17 17:46:04 +01:00
3a14528402 upgrading libraries 2025-03-17 16:58:15 +01:00
5c276faf78 Folding lists and opened variables 2023-03-27 01:29:56 +02:00
95b17947b2 Removing accordion on contract signature page 2023-03-27 01:00:11 +02:00
f20635e10e Spacing printed lis 2023-03-27 00:55:24 +02:00
8d4d64076a Correcting list navigation 2023-03-26 21:00:38 +02:00
f9867b3e2b Changing the contract file name to the contract label. Updating label content with a dash 2023-03-24 13:42:07 +01:00
b89d043880 Updating favicon 2023-03-21 21:33:22 +01:00
0ecbb99423 Square border radii 2023-03-20 16:02:27 +01:00
61bd5589ff Adding static meta to index.html 2023-03-20 16:00:46 +01:00
72065c0d0d Adding static meta data on signatures 2023-03-20 14:30:43 +01:00
cd8bb58dfb Revert "Adding a label to signature read entities"
This reverts commit 191a3d0018.
2023-03-20 14:05:25 +01:00
9ab1571067 Revert "Adding metadata for social networks on signature page"
This reverts commit 2948e9b961.
2023-03-20 14:04:27 +01:00
2948e9b961 Adding metadata for social networks on signature page 2023-03-19 13:55:57 +01:00
191a3d0018 Adding a label to signature read entities 2023-03-19 13:55:13 +01:00
8885969a07 Removing nickname from individual's labels 2023-03-19 13:54:38 +01:00
a997e54891 Adding an autogenerated label to contracts 2023-03-19 13:53:07 +01:00
0b12f8718a Decorating the sidenav 2023-03-18 18:29:37 +01:00
d5d2f31178 Correcting signature translation bug 2023-03-18 17:35:06 +01:00
1bd2774cdd Correcting redirection error after contract creation 2023-03-18 17:27:35 +01:00
e5375edda8 Changing website title 2023-03-18 17:20:08 +01:00
ae306bb89e Correcting overselecting menue 2023-03-18 17:13:36 +01:00
e877c36a3d Moving contracts/drafts to contract-drafts 2023-03-18 17:07:29 +01:00
7f639fc5a0 Correcting overselecting menue 2023-03-18 17:05:58 +01:00
7fbba953cd separating logoutbutton from rest of menu 2023-03-18 16:28:18 +01:00
4e52999b5d Progression translations 2023-03-18 16:21:55 +01:00
392d66b03e Correcting bug caused by not deep enough clones 2023-03-18 16:12:54 +01:00
dc6616bba6 Use address bar to remember list params 2023-03-17 17:45:50 +01:00
f3f0ddc004 Implementing enus is jsonschema utilities 2023-03-17 17:39:20 +01:00
0b9d5d42cb Increasing flashmessage display duration to human-readable length 2023-03-17 15:23:58 +01:00
bb0ecb5c13 Sending an error message on contract failure 2023-03-17 15:23:21 +01:00
5a5f1b3519 Correcting foreignKey not updated on update 2023-03-17 15:15:53 +01:00
2adecb99d2 Removing htmltags in list values 2023-03-17 15:06:31 +01:00
3db7c62b09 Filtering out htmlentities in rich text field 2023-03-17 14:36:40 +01:00
78c4ee119a Improving table list display 2023-03-17 14:35:55 +01:00
46ac3295e3 Merge branch 'master' of git.dorfsvald.net:ewandor/cht-lawfirm 2023-03-16 15:36:53 +01:00
2349f4c804 Adding a makefile 2023-03-16 15:36:36 +01:00
334150bc0f Preparing dockers for prod 2023-03-16 15:03:06 +01:00
ewandor
da19ef652e Merge branch 'master' of git.dorfsvald.net:ewandor/cht-lawfirm 2023-03-16 03:47:47 +01:00
ewandor
015fc00f6f using root when locating assets in contract print 2023-03-16 03:47:29 +01:00
e22c197d3a Involving gittea image registry 2023-03-16 02:55:55 +01:00
becab58c73 Adding templates to migration scripts 2023-03-16 02:24:35 +01:00
70a863f6e1 Correcting nginx configuration for prod 2023-03-16 02:24:21 +01:00
2b55d206e2 Naming containers and freezing mongo version to 4.4.19 2023-03-16 02:23:54 +01:00
f35182233b Adding a paste filter on richtext editor 2023-03-15 15:45:11 +01:00
43474c960f Merge remote-tracking branch 'origin/fix/default-date-contract-creation' 2023-03-15 15:19:21 +01:00
b3566b39b8 Removing a typo 2023-03-15 15:16:09 +01:00
7bebc05e08 fulltext search use each word separately 2023-03-15 15:15:47 +01:00
6b49b688ac Removing unused imports 2023-03-14 19:09:49 +01:00
7dbe4a1716 Changing db password 2023-03-14 19:06:13 +01:00
701ac8e1dc Merge branch 'master' of git.dorfsvald.net:ewandor/cht-lawfirm 2023-03-14 19:00:18 +01:00
2ba34a675d Production env for the front 2023-03-14 18:59:56 +01:00
54 changed files with 785 additions and 228 deletions

12
Makefile Normal file
View File

@@ -0,0 +1,12 @@
publish:
git checkout $(TAG)
docker build -f back/Dockerfile.prod -t git.dorfsvald.net/ewandor/cht-lawfirm-back-prod back
docker tag git.dorfsvald.net/ewandor/cht-lawfirm-back-prod:latest git.dorfsvald.net/ewandor/cht-lawfirm-back-prod:$(TAG)
docker push git.dorfsvald.net/ewandor/cht-lawfirm-back-prod:latest
docker push git.dorfsvald.net/ewandor/cht-lawfirm-back-prod:$(TAG)
docker build -f front/Dockerfile.prod -t git.dorfsvald.net/ewandor/cht-lawfirm-nginx-prod front
docker tag git.dorfsvald.net/ewandor/cht-lawfirm-nginx-prod:latest git.dorfsvald.net/ewandor/cht-lawfirm-nginx-prod:$(TAG)
docker push git.dorfsvald.net/ewandor/cht-lawfirm-nginx-prod:latest
docker push git.dorfsvald.net/ewandor/cht-lawfirm-nginx-prod:$(TAG)
git switch -

View File

@@ -1,17 +1,14 @@
FROM python:3.10
FROM python:3.13
RUN apt update && apt install -y xfonts-base xfonts-75dpi python3-pip python3-cffi python3-brotli libpango-1.0-0 libpangoft2-1.0-0 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /code
# copy both 'package.json' and 'package-lock.json' (if available)
COPY ./requirements.txt /code/requirements.txt
# install project dependencies
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
# copy project files and folders to the current working directory (i.e. 'app' folder)
COPY ./app /code/app
EXPOSE 8000

15
back/Dockerfile.prod Normal file
View File

@@ -0,0 +1,15 @@
FROM python:3.10
RUN apt update && apt install -y xfonts-base xfonts-75dpi python3-pip python3-cffi python3-brotli libpango-1.0-0 libpangoft2-1.0-0 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /code
COPY ./requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
COPY ./app /code/app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -1,8 +1,8 @@
import datetime
from typing import List, Literal
from typing import List, Literal, Optional
from enum import Enum
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, validator
from beanie.operators import ElemMatch
from ..core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry
@@ -52,10 +52,10 @@ class DraftParty(BaseModel):
class Party(BaseModel):
entity: Entity
part: str
representative: Entity = None
representative: Optional[Entity] = None
signature_uuid: str
signature_affixed: bool = False
signature_png: str = None
signature_png: Optional[str] = None
class ProvisionGenuine(BaseModel):
@@ -90,7 +90,7 @@ class DraftProvision(BaseModel):
provision: ContractProvisionTemplateReference | ProvisionGenuine = Field(..., discriminator='type')
class Config:
title = 'Provision'
title = 'Clause'
class Provision(BaseModel):
@@ -166,6 +166,10 @@ class ContractDraft(CrudDocument):
class Contract(CrudDocument):
"""
Contrat publié. Les contrats ne peuvent pas être modifiés.
Ils peuvent seulement être signés par les parties et imprimés par l'avocat
"""
name: str = Field(title="Nom")
title: str = Field(title="Titre")
parties: List[Party] = Field(title="Parties")
@@ -177,6 +181,19 @@ class Contract(CrudDocument):
lawyer: Entity = Field(title="Avocat en charge")
location: str = Field(title="Lieu")
date: datetime.date = Field(title="Date")
label: Optional[str] = None
@validator("label", always=True)
def generate_label(cls, v, values, **kwargs):
if not v:
contract_label = values['title']
for p in values['parties']:
contract_label = contract_label + f" - {p.entity.label}"
contract_label = contract_label + f" - {values['date'].strftime('%m/%d/%Y')}"
return contract_label
return v
class Settings(CrudDocument.Settings):
fulltext_search = ['name', 'title']

View File

@@ -81,7 +81,7 @@ async def render_css(root_url, contract):
async def preview_draft(draft_id: str, request: Request) -> str:
draft = await build_model(await ContractDraft.get(draft_id))
return await render_print(f'{request.url.scheme}://{request.url.hostname}', draft)
return await render_print('', draft)
@print_router.get("/preview/signature/{signature_id}", response_class=HTMLResponse)
@@ -91,7 +91,7 @@ async def preview_contract_by_signature(signature_id: str, request: Request) ->
if p.signature_affixed:
p.signature_png = retrieve_signature_png(f'media/signatures/{p.signature_uuid}.png')
return await render_print(f'{request.url.scheme}://{request.url.hostname}', contract)
return await render_print('', contract)
@print_router.get("/preview/{contract_id}", response_class=HTMLResponse)
@@ -101,7 +101,7 @@ async def preview_contract(contract_id: str, request: Request) -> str:
if p.signature_affixed:
p.signature_png = retrieve_signature_png(f'media/signatures/{p.signature_uuid}.png')
return await render_print(f'{request.url.scheme}://{request.url.hostname}', contract)
return await render_print('', contract)
@print_router.get("/pdf/{contract_id}", response_class=FileResponse)
@@ -130,7 +130,7 @@ async def create_pdf(contract_id: str) -> str:
return FileResponse(
contract_path,
media_type="application/pdf",
filename=contract.name)
filename=contract.label)
def retrieve_signature_png(filepath):
@@ -139,3 +139,18 @@ def retrieve_signature_png(filepath):
base64_utf8_str = base64.b64encode(b_content).decode('utf-8')
ext = filepath.split('.')[-1]
return f'data:image/{ext};base64,{base64_utf8_str}'
@print_router.get("/opengraph/{signature_id}", response_class=HTMLResponse)
async def get_signature_opengraph(signature_id: str, request: Request) -> str:
contract = await Contract.find_by_signature_id(signature_id)
signature = contract.get_signature(signature_id)
template = templates.get_template("opengraph.html")
signatory = signature.representative.label if signature.representative else signature.entity.label
return template.render({
"signatory": signatory,
"title": contract.label,
"origin_url": f"{request.url.scheme}://{request.url.hostname}"
})

View File

@@ -0,0 +1,10 @@
<html>
<head>
<meta property="og:title" content="{{ title }}" />
<meta property="og:description" content="Cette page est à la destination exclusive de {{ signatory }}
Si vous n'êtes pas {{ signatory }}, veuillez fermer cette page immédiatement et surpprimer tous les liens en votre possession menant vers celle-ci.
En vous maintenant et/ou en interagissant avec cette page, vous enfreignez l'article L.229 du code pénal de l'Etat de San Andreas pour usurpation d'identité et vous vous exposez ainsi à une amende de 20 000$ ainsi qu'à des poursuites civiles.
Le cabinet Cooper, Hillman & Toshi LLC" />
<meta property="og:image" content="{{ origin_url }}/assets/logo.png" />
</head>
</html>

View File

@@ -109,6 +109,11 @@ h2 {
text-indent: 2em;
}
.content td p {
text-indent: 0;
text-align: left;
}
.provision {
page-break-inside: avoid;
}
@@ -117,13 +122,17 @@ p {
text-align: justify;
}
li {
margin: 16px 0;
}
.footer {
margin-top: 30px;
page-break-inside: avoid;
}
.mention {
margin: 0px;
margin: 0;
font-size: 0.9em;
}

View File

@@ -3,10 +3,10 @@ from beanie.odm.operators.find.comparison import In
from beanie.operators import And, RegEx, Eq
from fastapi import APIRouter, HTTPException, Depends
from fastapi_paginate import Page, Params, add_pagination
from fastapi_paginate.ext.motor import paginate
from fastapi_pagination import Page, Params, add_pagination
from fastapi_pagination.ext.beanie import paginate
from ..user.manager import get_current_user, get_current_superuser
from ..user.manager import get_current_user, get_current_superuser, get_current_user_and_firm
def parse_sort(sort_by):
@@ -39,7 +39,10 @@ def parse_query(query: str, model):
or_array = []
for field in model.Settings.fulltext_search:
or_array.append(RegEx(field, value, 'i'))
words_and_array = []
for word in value.split(' '):
words_and_array.append(RegEx(field, word, 'i'))
or_array.append(And(*words_and_array) if len(words_and_array) > 1 else words_and_array[0])
operand = Or(or_array) if len(or_array) > 1 else or_array[0]
elif operator == 'eq':
@@ -60,28 +63,27 @@ def get_crud_router(model, model_create, model_read, model_update):
router = APIRouter()
@router.post("/", response_description="{} added to the database".format(model.__name__))
async def create(item: model_create, user=Depends(get_current_user)) -> dict:
async def create(instance: str, firm: str, item: model_create, user=Depends(get_current_user)) -> dict:
await item.validate_foreign_key()
o = await model(**item.dict()).create()
return {"message": "{} added successfully".format(model.__name__), "id": o.id}
@router.get("/{id}", response_description="{} record retrieved".format(model.__name__))
async def read_id(id: PydanticObjectId, user=Depends(get_current_user)) -> model_read:
async def read_id(instance: str, firm: str, id: PydanticObjectId, user=Depends(get_current_user)) -> model_read:
item = await model.get(id)
return model_read(**item.dict())
@router.get("/", response_model=Page[model_read], response_description="{} records retrieved".format(model.__name__))
async def read_list(size: int = 50, page: int = 1, sort_by: str = None, query: str = None,
user=Depends(get_current_user)) -> Page[model_read]:
async def read_list(instance: str, firm: str, size: int = 50, page: int = 1, sort_by: str = None, query: str = None,
user=Depends(get_current_user_and_firm)) -> Page[model_read]:
sort = parse_sort(sort_by)
query = parse_query(query, model_read)
collection = model.get_motor_collection()
items = paginate(collection, query, Params(**{'size': size, 'page': page}), sort=sort)
items = paginate(model.find(query), Params(**{'size': size, 'page': page}))
return await items
@router.put("/{id}", response_description="{} record updated".format(model.__name__))
async def update(id: PydanticObjectId, req: model_update, user=Depends(get_current_user)) -> model_read:
async def update(instance: str, firm: str, id: PydanticObjectId, req: model_update, user=Depends(get_current_user)) -> model_read:
req = {k: v for k, v in req.dict().items() if v is not None}
update_query = {"$set": {
field: value for field, value in req.items()
@@ -98,7 +100,7 @@ def get_crud_router(model, model_create, model_read, model_update):
return model_read(**item.dict())
@router.delete("/{id}", response_description="{} record deleted from the database".format(model.__name__))
async def delete(id: PydanticObjectId, user=Depends(get_current_superuser)) -> dict:
async def delete(instance: str, firm: str, id: PydanticObjectId, user=Depends(get_current_superuser)) -> dict:
item = await model.get(id)
if not item:

View File

@@ -5,10 +5,10 @@ from beanie import init_beanie
from .user import User, AccessToken
from .entity.models import Entity
from .template.models import ContractTemplate, ProvisionTemplate
from .order.models import Order
from .contract.models import ContractDraft, Contract
DATABASE_URL = "mongodb://root:example@mongo:27017/"
DB_PASSWORD = "IBO3eber0mdw2R9pnInLdtFykQFY2f06"
DATABASE_URL = f"mongodb://root:{DB_PASSWORD}@mongo:27017/"
async def init_db():

View File

@@ -23,13 +23,13 @@ class Individual(EntityType):
props={"items-per-row": "4", "numbered": True},
title="Surnoms"
)
day_of_birth: date = Field(default=None, title='Date de naissance')
place_of_birth: str = Field(default="", title='Lieu de naissance')
day_of_birth: Optional[date] = Field(default=None, title='Date de naissance')
place_of_birth: Optional[str] = Field(default="", title='Lieu de naissance')
@property
def label(self) -> str:
if len(self.surnames) > 0:
return '{} "{}" {}'.format(self.firstname, self.surnames[0], self.lastname)
# if len(self.surnames) > 0:
# return '{} "{}" {}'.format(self.firstname, self.surnames[0], self.lastname)
return '{} {}'.format(self.firstname, self.lastname)

View File

@@ -17,10 +17,12 @@ async def on_startup():
app.include_router(get_auth_router(), prefix="/auth", tags=["auth"], )
app.include_router(user_router, prefix="/users", tags=["users"], )
app.include_router(entity_router, prefix="/entity", tags=["entity"], )
app.include_router(template_router, prefix="/template", tags=["template"], )
app.include_router(contract_router, prefix="/contract", tags=["contract"], )
# app.include_router(order_router, prefix="/order", tags=["order"], )
multitenant_prefix = "/{instance}/{firm}"
app.include_router(entity_router, prefix=f"{multitenant_prefix}/entity", tags=["entity"], )
app.include_router(template_router, prefix=f"{multitenant_prefix}/template", tags=["template"], )
app.include_router(contract_router, prefix=f"{multitenant_prefix}/contract", tags=["contract"], )
if __name__ == '__main__':
import uvicorn

View File

@@ -1,5 +1,5 @@
import uuid
from typing import Any, Dict, Generic, Optional
from typing import Any
from bson import ObjectId
from fastapi import Depends
@@ -109,6 +109,9 @@ fastapi_users = FastAPIUsers[User, uuid.UUID](
get_current_user = fastapi_users.current_user(active=True)
get_current_superuser = fastapi_users.current_user(active=True, superuser=True)
def get_current_user_and_firm(user=Depends(get_current_user)):
return user
def get_auth_router():
return fastapi_users.get_auth_router(auth_backend)

View File

@@ -1,7 +1,7 @@
from typing import Optional, TypeVar
from datetime import datetime
from pydantic import Field
from beanie import PydanticObjectId
from beanie import Document
from fastapi_users.db import BeanieBaseUser, BeanieUserDatabase
from fastapi_users_db_beanie.access_token import BeanieAccessTokenDatabase, BeanieBaseAccessToken
@@ -9,11 +9,11 @@ from fastapi_users_db_beanie.access_token import BeanieAccessTokenDatabase, Bean
from pymongo import IndexModel
class AccessToken(BeanieBaseAccessToken[PydanticObjectId]):
class AccessToken(BeanieBaseAccessToken, Document):
pass
class User(BeanieBaseUser[PydanticObjectId]):
class User(BeanieBaseUser, Document):
login: str
entity_id: str
created_at: datetime = Field(default=datetime.utcnow(), nullable=False)

View File

@@ -1,4 +1,6 @@
from pydantic import BaseModel
from typing import Annotated
from pydantic import BaseModel, Field
from fastapi_users import schemas
from .models import User
@@ -9,12 +11,8 @@ class UserBase(schemas.CreateUpdateDictModel):
class UserRead(User):
class Config:
fields = {
'_id': {'alias': 'id'},
'hashed_password': {'exclude': True}
}
_id: Annotated[str, Field(alias='id')]
hashed_password: Annotated[str, Field(exclude=True)]
class UserCreate(UserBase):
login: str

View File

@@ -3,10 +3,10 @@ import asyncio
import json
from os import path
from app.db import init_db, Entity, Order, Contract, User, AccessToken
from app.db import init_db, Entity, Contract, ContractTemplate, ProvisionTemplate, User
models = [Entity, Order, Contract, User]
models = [Entity, Contract, User, ContractTemplate, ProvisionTemplate]
async def handle_migration(args):

View File

@@ -1,8 +1,7 @@
fastapi==0.88.0
fastapi_users==10.2.1
fastapi_users_db_beanie==1.1.2
motor==3.1.1
fastapi-paginate==0.1.0
fastapi
fastapi_users
fastapi_users_db_beanie
fastapi-pagination
uvicorn
jinja2
weasyprint

27
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,27 @@
version: "3.9"
services:
back:
image: git.dorfsvald.net/ewandor/cht-lawfirm-back-prod:latest
restart: always
volumes:
- ${ROOT_PATH}/back/media:/code/media
nginx:
image: git.dorfsvald.net/ewandor/cht-lawfirm-nginx-prod:latest
restart: always
ports:
- "3820:80"
mongo:
image: "mongo:4.4.19"
restart: always
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: IBO3eber0mdw2R9pnInLdtFykQFY2f06
volumes:
- database:/data/db
volumes:
database:

View File

@@ -0,0 +1,42 @@
version: "3.9"
services:
back:
build:
context: ${ROOT_PATH}/back
restart: always
ports:
- "8000:8000"
volumes:
- ${ROOT_PATH}/back/app:/code/app
- ${ROOT_PATH}/back/media:/code/media
front:
build:
context: ${ROOT_PATH}/front
restart: always
ports:
- "4200:4200"
volumes:
- ${ROOT_PATH}/front/app/src:/app/src
- ${ROOT_PATH}/front/app/public:/app/public
nginx:
build:
context: ${ROOT_PATH}/nginx
restart: always
ports:
- "3820:80"
mongo:
image: "mongo:4.4.18"
restart: always
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: IBO3eber0mdw2R9pnInLdtFykQFY2f06
volumes:
- database:/data/db
volumes:
database:

View File

@@ -1,8 +1,8 @@
version: "3.9"
services:
back:
build:
context: ./back
image: cht-lawfirm-back-dev
restart: always
ports:
- "8000:8000"
@@ -13,6 +13,7 @@ services:
front:
build:
context: ./front
image: cht-lawfirm-front-dev
restart: always
ports:
- "4200:4200"
@@ -23,18 +24,19 @@ services:
nginx:
build:
context: ./nginx
image: cht-lawfirm-nginx-dev
restart: always
ports:
- "80:80"
mongo:
image: "mongo:4.4.18"
image: "mongo:4.4.19"
restart: always
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
MONGO_INITDB_ROOT_PASSWORD: IBO3eber0mdw2R9pnInLdtFykQFY2f06
volumes:
- database:/data/db

View File

@@ -1,22 +1,11 @@
FROM node:lts-alpine
# install simple http server for serving static content
RUN npm install -g http-server
# make the 'app' folder the current working directory
WORKDIR /app
RUN npm install -g @angular/cli
# copy both 'package.json' and 'package-lock.json' (if available)
RUN npm install -g @angular/cli http-server
COPY app/package*.json ./
# install project dependencies
RUN npm install
# copy project files and folders to the current working directory (i.e. 'app' folder)
COPY app/ .
# build app for production with minification
RUN npm run build
EXPOSE 4200

17
front/Dockerfile.prod Normal file
View File

@@ -0,0 +1,17 @@
FROM node:lts-alpine AS builder
WORKDIR /app
RUN npm install -g @angular/cli
COPY app/package*.json ./
RUN npm install
COPY app/ .
RUN npm run build --prod
FROM nginx:alpine
COPY nginx.prod.conf /etc/nginx/nginx.conf
COPY --from=builder /app/dist/app/fr/ /usr/share/nginx/html/

View File

@@ -13,7 +13,8 @@
"sourceLocale": "en-US",
"locales": {
"fr": {
"translation": "src/locale/messages.fr.xlf"
"translation": "src/locale/messages.fr.xlf",
"baseHref": ""
}
}
},
@@ -63,9 +64,6 @@
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
},
"fr": {
"localize": ["fr"]
}
},
"defaultConfiguration": "production"
@@ -80,7 +78,7 @@
"browserTarget": "app:build:development"
},
"fr": {
"browserTarget": "App:build:fr"
"browserTarget": "app:build:fr"
}
},
"defaultConfiguration": "development"

View File

@@ -1,6 +1,5 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {ContractsModule} from "./views/contracts/contracts.module";
const routes: Routes = [
{
@@ -30,6 +29,11 @@ const routes: Routes = [
loadChildren: () =>
import('./views/templates/templates.module').then((m) => m.TemplatesModule)
},
{
path: 'contract-drafts',
loadChildren: () =>
import('./views/contract-drafts/contract-drafts.module').then((m) => m.ContractDraftsModule)
},
{
path: 'contracts',
loadChildren: () =>

View File

@@ -0,0 +1,5 @@
.sidenav {
background-image: url("/assets/leather_texture.png");
background-size: 220px;
max-width: 220px;
}

View File

@@ -3,7 +3,7 @@
<main style="margin-top: 0px;">
<div class="container-fluid">
<div class="row flex-nowrap">
<div class="col-auto col-md-3 col-xl-2 px-sm-2 px-0 bg-dark" style="max-width: 200px;">
<div class="sidenav col-auto col-md-3 col-xl-2 px-sm-2 px-0 bg-dark">
<sidenav class="sticky-top"></sidenav>
</div>
<div class="col py-3">

View File

@@ -1,4 +1,5 @@
import { Component } from '@angular/core';
import {Title} from "@angular/platform-browser";
@Component({
selector: 'app-root',
@@ -8,5 +9,7 @@ import { Component } from '@angular/core';
export class AppComponent {
title = 'Cooper, Hillman & Toshi';
constructor() {}
constructor(private titleService: Title) {
titleService.setTitle(this.title)
}
}

View File

@@ -1,7 +1,7 @@
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<ngb-toast
*ngFor="let flashmessage of flashmessagesService.toasts"
[header]="flashmessage.type" [autohide]="true" [delay]="flashmessage.delay || 1500"
[header]="flashmessage.type" [autohide]="true" [delay]="flashmessage.delay || 5000"
(hiddden)="flashmessagesService.remove(flashmessage)"
>
<ng-container [ngSwitch]="flashmessage.type">

View File

@@ -0,0 +1,3 @@
.logout {
margin-top: 25px;
}

View File

@@ -11,6 +11,6 @@
</a>
</li>
</ng-container>
<li><logout></logout></li>
<li class="logout"><logout></logout></li>
</ul>
</div>

View File

@@ -3,13 +3,21 @@ import { Router } from '@angular/router';
import { IconNamesEnum } from "ngx-bootstrap-icons";
import { AuthService } from "../auth/auth.service";
interface MenuItem {
title: string,
link: string,
icon: IconNamesEnum
}
@Component({
selector: "sidenav",
templateUrl: "./sidenav.component.html",
styleUrls: ["./sidenav.component.css"]
})
export class SidenavComponent {
Menu = [
Menu: MenuItem[] = [
{
title: $localize`Dashboard`,
link: "/dashboard",
@@ -32,7 +40,7 @@ export class SidenavComponent {
},
{
title: $localize`Contracts&nbsp;Drafts`,
link: "/contracts/drafts",
link: "/contract-drafts",
icon: IconNamesEnum.PencilSquare
},
{
@@ -54,7 +62,7 @@ export class SidenavComponent {
})
}
is_current_page(menu_item: any) {
return this.router.url.indexOf(menu_item.link) > -1;
is_current_page(menu_item: MenuItem) {
return this.router.url.startsWith(menu_item.link);
}
}

View File

@@ -0,0 +1,44 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { DraftsCardComponent, DraftsListComponent, DraftsNewComponent } from "./drafts.component";
const routes: Routes = [
{
path: '',
data: {
title: 'Contract Drafts',
},
children: [
{ path: '', redirectTo: 'list', pathMatch: 'full' },
{
path: 'list',
component: DraftsListComponent,
data: {
title: 'List',
},
},
{
path: 'new',
component: DraftsNewComponent,
data: {
title: 'New',
},
},
{
path: ':id',
component: DraftsCardComponent,
data: {
title: 'Card',
},
},
],
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ContractDraftsRoutingModule {}

View File

@@ -0,0 +1,42 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { BaseViewModule } from "../base-view/base-view.module";
import { ContractDraftsRoutingModule } from './contract-drafts-routing.module';
import { DraftsCardComponent, DraftsListComponent, DraftsNewComponent, DraftsNewFormComponent } from "./drafts.component";
import { FormlyModule } from "@ngx-formly/core";
import { FormlyBootstrapModule } from "@ngx-formly/bootstrap";
import { ForeignkeyTypeComponent } from "@common/crud/types/foreignkey.type";
import { CrudService, ImageUploaderCrudService } from "@common/crud/crud.service";
import { NgbAccordionModule, NgbCollapseModule } from "@ng-bootstrap/ng-bootstrap";
import { allIcons, NgxBootstrapIconsModule } from "ngx-bootstrap-icons";
import { ClipboardModule } from "@angular/cdk/clipboard";
@NgModule({
imports: [
CommonModule,
BaseViewModule,
ContractDraftsRoutingModule,
NgbAccordionModule,
NgbCollapseModule,
NgxBootstrapIconsModule.pick(allIcons),
FormlyModule.forRoot({
types: [
{ name: 'foreign-key', component: ForeignkeyTypeComponent }
]
}),
FormlyBootstrapModule,
ClipboardModule,
],
declarations: [
DraftsListComponent,
DraftsNewComponent,
DraftsCardComponent,
DraftsNewFormComponent
],
providers: [CrudService, ImageUploaderCrudService]
})
export class ContractDraftsModule {
}

View File

@@ -6,6 +6,7 @@ import { CrudService } from "@common/crud/crud.service";
import { ActivatedRoute, ParamMap, Router } from "@angular/router";
import { formatDate } from "@angular/common";
import {FlashmessagesService} from "../../layout/flashmessages/flashmessages.service";
export class BaseDraftsComponent {
@@ -97,10 +98,10 @@ export class DraftsNewComponent extends BaseDraftsComponent implements OnInit {
(resourceReceived)="this.onResourceReceived($event)"
>
</base-card>
<a class="btn btn-link" href="/api/v1/contract/print/preview/draft/{{this.resource_id}}" target="_blank">Preview</a>
<a class="btn btn-link" href="/api/v1/contract/print/preview/draft/{{this.resource_id}}" target="_blank" i18n>Preview</a>
<ng-container *ngIf="this.isReadyForPublication;">
<formly-form [fields]="newContractFormfields" [form]="newContractForm" [model]="newContractModel"></formly-form>
<button class="btn btn-success" (click)="publish()">Publish</button>
<button class="btn btn-success" (click)="publish()" i18n>Publish</button>
</ng-container>
`
})
@@ -142,6 +143,7 @@ export class DraftsCardComponent extends BaseDraftsComponent implements OnInit {
private formlyJsonschema: CrudFormlyJsonschemaService,
private crudService: CrudService,
private router: Router,
private flashService: FlashmessagesService,
) {
super();
}
@@ -159,8 +161,9 @@ export class DraftsCardComponent extends BaseDraftsComponent implements OnInit {
}
publish() {
this.crudService.create('contract', this.newContractModel).subscribe((response: any) => {
this.router.navigate([`../../${response.id}`], {relativeTo: this.route});
this.crudService.create('contract', this.newContractModel).subscribe({
next: (response: any) => this.router.navigate([`../../contracts/${response.id}`], {relativeTo: this.route}),
error: (err) => this.flashService.error(err)
});
}

View File

@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { DraftsCardComponent, DraftsListComponent, DraftsNewComponent } from "./drafts.component";
import { ContractsCardComponent, ContractsListComponent, ContractsNewComponent, ContractsSignatureComponent} from "./contracts.component";
@@ -13,6 +12,7 @@ const routes: Routes = [
},
children: [
{ path: '', redirectTo: 'list', pathMatch: 'full' },
{ path: 'drafts', redirectTo: '/contract-drafts/list' },
{
path: 'list',
component: ContractsListComponent,
@@ -34,36 +34,6 @@ const routes: Routes = [
title: 'New',
},
},
{
path: 'drafts',
data: {
title: 'Drafts',
},
children: [
{ path: '', redirectTo: 'list', pathMatch: 'full' },
{
path: 'list',
component: DraftsListComponent,
data: {
title: 'List',
},
},
{
path: 'new',
component: DraftsNewComponent,
data: {
title: 'New',
},
},
{
path: ':id',
component: DraftsCardComponent,
data: {
title: 'Card',
},
},
],
},
{
path: ':id',
component: ContractsCardComponent,

View File

@@ -78,37 +78,22 @@ export class ContractsCardComponent extends BaseContractsComponent{
@Component({
template: `
<ngb-accordion #acc="ngbAccordion" activeIds="ngb-panel-1">
<ngb-panel>
<ng-template ngbPanelTitle>
<span>Preview</span>
</ng-template>
<ng-template ngbPanelContent>
<div>
<iframe width="100%"
[src]="getPreview()"
onload='javascript:(function(o){o.style.height=o.contentWindow.document.body.scrollHeight+"px";o.style.width=o.contentWindow.document.body.scrollWidth+"px";}(this));' style="height:200px;width:100%;border:none;overflow:hidden;"></iframe>
</ng-template>
</ngb-panel>
<ngb-panel>
<ng-template ngbPanelTitle>
<span>Signature</span>
</ng-template>
<ng-template ngbPanelContent>
<ng-container *ngIf="this.affixed">This Contract has already been signed by {{ this.signatory }}</ng-container>
onload='javascript:(function(o){o.style.height=o.contentWindow.document.body.scrollHeight+"px";o.style.width=o.contentWindow.document.body.scrollWidth+"px";}(this));' style="height:200px;width:100%;border:none;overflow:hidden;">
</iframe>
</div>
<div class="row" *ngIf="!this.affixed">
<signature-drawer class="col-7"
(signatureDrawn$)="postSignature($event)"></signature-drawer>
<div class="col-5">
<div class="col-5" i18n>
<p>Cette page est à la destination exclusive de <strong>{{ this.signatory }}</strong></p>
<p>Si vous n'êtes <strong>pas</strong> {{ this.signatory }}, veuillez <strong>fermer cette page immédiatement</strong> et surpprimer tous les liens en votre possession menant vers celle-ci.</p>
<p>En vous maintenant et/ou en interagissant avec cette page, vous enfreignez l'article L.229 du code pénal de l'Etat de San Andreas pour <strong>usurpation d'identité</strong> et vous vous exposez ainsi à une amende de 20 000$ ainsi qu'à des poursuites civiles.</p>
<p>Le cabinet Cooper, Hillman & Toshi LLC</p>
</div>
</div>
</ng-template>
</ngb-panel>
</ngb-accordion>
`
</div>`
})
export class ContractsSignatureComponent implements OnInit {
signature_id: string | null = null;

View File

@@ -3,7 +3,6 @@ import { NgModule } from '@angular/core';
import { BaseViewModule } from "../base-view/base-view.module";
import { ContractsRoutingModule } from './contracts-routing.module';
import { DraftsCardComponent, DraftsListComponent, DraftsNewComponent, DraftsNewFormComponent } from "./drafts.component";
import { FormlyModule } from "@ngx-formly/core";
import { FormlyBootstrapModule } from "@ngx-formly/bootstrap";
import { ForeignkeyTypeComponent } from "@common/crud/types/foreignkey.type";
@@ -33,10 +32,6 @@ import { ClipboardModule } from "@angular/cdk/clipboard";
ClipboardModule,
],
declarations: [
DraftsListComponent,
DraftsNewComponent,
DraftsCardComponent,
DraftsNewFormComponent,
ContractsListComponent,
ContractsNewComponent,
ContractsCardComponent,

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -112,10 +112,10 @@ export class CardComponent implements OnInit {
model._id = this.resource_id;
this.crudService.update(this.resource!, model).subscribe( {
next: (model: any) => {
this.model = model;
this._modelLoading$.next(false);
this.resourceUpdated.emit(model._id);
this.resourceReceived.emit(model);
this.model = model;
this._modelLoading$.next(false);
},
error: (err) => this.error.emit("Error updating the entity:" + err)
});

View File

@@ -6,7 +6,7 @@ import { ListComponent } from "./list/list.component";
const routes: Routes = [
{ path: '', component: ListComponent },
{ path: ':id', component: CardComponent },
];;
];
@NgModule({
imports: [RouterModule.forChild(routes)],

View File

@@ -30,7 +30,7 @@ export class JsonschemasService {
buildResource(resourceName: string) {
let resource;
resource = { ... this.rawSchemas.components.schemas[resourceName]};
resource = structuredClone(this.rawSchemas.components.schemas[resourceName]);
resource.components = { schemas: {} };
for (let prop_name in resource.properties) {
let prop = resource.properties[prop_name];
@@ -162,6 +162,14 @@ export class JsonschemasService {
}
return false;
}
} else if (this.is_enum(resource)) {
for (const ref of resource.allOf!) {
// @ts-ignore
if (this.has_descendant(ref, property_name)) {
return true;
}
return false;
}
}
throw new Error("Jsonschema format not implemented in property finder");
return false;
@@ -186,6 +194,14 @@ export class JsonschemasService {
return this.get_descendant(ref, property_name);
}
}
} else if (this.is_enum(resource)) {
for (const ref of resource.allOf!) {
// @ts-ignore
if (this.has_descendant(ref, property_name)) {
// @ts-ignore
return this.get_descendant(ref, property_name);
}
}
}
throw new Error("property not found or Jsonschema format not implemented");
}

View File

@@ -14,13 +14,14 @@ import {CrudFormlyJsonschemaService} from "@common/crud/crud-formly-jsonschema.s
export class FilterListComponent implements OnInit {
@Input() filters: string[] = [];
@Input() schema = "";
@Input() values = {};
@Output() filterChange: EventEmitter<{[key: string]: any}> = new EventEmitter();
form = new FormGroup({});
fields: FormlyFieldConfig[] = [];
searchTerms = {}
searchTerms: {[key: string]: string | {}} = {}
public fieldJson = {
components: {},
@@ -48,6 +49,16 @@ export class FilterListComponent implements OnInit {
prop = schema.components.schemas[prop.allOf![0]['$ref'].replace('#/components/schemas/', '')];
prop.type = "array";
prop.items = {"type": "string", "enum": prop.enum};
if (filter in this.values) {
this.searchTerms[filter] = {};
// @ts-ignore
for (let val of this.values[filter]) {
// @ts-ignore
this.searchTerms[filter][val] = true;
}
}
} else if(true) {
}
if (prop.hasOwnProperty('readOnly') && prop.readOnly) {

View File

@@ -0,0 +1,3 @@
.table-row-link {
cursor: pointer;
}

View File

@@ -14,20 +14,27 @@
/>
</div>
<div class="col-xs-3 col-sm-auto">
<crud-list-filter-list [filters]="this.filters" [schema]="this.schema!" (filterChange)="onFilterChange($event)"></crud-list-filter-list>
<crud-list-filter-list
[filters]="this.filters"
[schema]="this.schema!"
[values]="this.searchFilters"
(filterChange)="onFilterChange($event)"
></crud-list-filter-list>
</div>
<span class="col col-form-label" i18n *ngIf="loading$ | async">Loading...</span>
</div>
<div class="table-responsive-md">
<table class="table table-striped">
<table class="table table-striped table-hover">
<thead>
<tr>
<th *ngFor="let col of this.displayedColumns" scope="col" sortable="name" (sort)="onSort($event)">{{ col.title }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of listData$ | async" (click)="onSelect(row._id)">
<td *ngFor="let col of this.displayedColumns">
<tr *ngIf="loading$ | async">
<td class="text-center" [attr.colspan]="this.displayedColumns.length" i18n>Loading...</td>
</tr>
<tr *ngFor="let row of listData$ | async" (click)="onRowClick(row._id)" (auxclick)="onRowMiddleClick(row._id);" class="table-row-link">
<td class="text-truncate" *ngFor="let col of this.displayedColumns" style="max-width: 150px;">
<ngb-highlight [result]="getColumnValue(row, col.path)" [term]="searchTerm"></ngb-highlight>
</td>
</tr>
@@ -35,7 +42,7 @@
</table>
</div>
<div class="d-flex justify-content-between p-2">
<div class="d-flex justify-content-between p-2" *ngIf="! (loading$ | async)" >
<ngb-pagination [collectionSize]="(total$ | async)!" [(page)]="page" [pageSize]="pageSize">
</ngb-pagination>

View File

@@ -40,7 +40,6 @@ export class ListComponent implements OnInit {
public displayedColumns: Column[] = [];
private _loading$ = new BehaviorSubject<boolean>(true);
//private _search$ = new Subject<void>();
private _listData$ = new BehaviorSubject<any[]>([]);
private _total$ = new BehaviorSubject<number>(0);
@@ -64,7 +63,13 @@ export class ListComponent implements OnInit {
next: (schema: any) => this.getColumnDefinition(schema),
error: (err) => this.error.emit("Error loading the schema:" + err)
});
this._search();
this.route.queryParams.subscribe(params => {
let parsedParams = {...params};
if (parsedParams.hasOwnProperty('searchFilters')) {
parsedParams['searchFilters'] = JSON.parse(parsedParams['searchFilters']);
}
this._set(parsedParams)
});
}
getColumnDefinition(schema: JSONSchema7) {
@@ -100,7 +105,7 @@ export class ListComponent implements OnInit {
parent = parent[key];
}
}
return parent;
return parent.replace(/<[^>]*>/g, '');
}
private _search() {
@@ -144,10 +149,15 @@ export class ListComponent implements OnInit {
this.sortDirection = direction;
}
onSelect(id: string) {
onRowClick(id: string) {
this.router.navigate([`../${id}`], {relativeTo: this.route});
}
onRowMiddleClick(id: string) {
let newUrl = window.location.href.replace('list', id).split('?')[0]
window.open(newUrl, '_blank');
}
onCreate() {
this.router.navigate([`../new`], {relativeTo: this.route});
}
@@ -181,22 +191,35 @@ export class ListComponent implements OnInit {
}
set page(page: number) {
this._set({ page });
this.updateState({ page });
}
set pageSize(pageSize: number) {
this._set({ pageSize });
this.updateState({ pageSize });
}
set searchTerm(searchTerm: string) {
this._set({ searchTerm });
this.updateState({ searchTerm });
}
set searchFilters(searchFilters: {[key: string]: any}) {
this._set({ searchFilters });
this.updateState({ searchFilters: JSON.stringify(searchFilters) });
}
set sortColumn(sortColumn: SortColumn) {
this._set({ sortColumn });
this.updateState({ sortColumn });
}
set sortDirection(sortDirection: SortDirection) {
this._set({ sortDirection });
this.updateState({ sortDirection });
}
private updateState(patch: any) {
this.router.navigate([], {
relativeTo: this.route,
queryParams: patch ,
queryParamsHandling: 'merge'
});
}
private _set(patch: Partial<State>) {

View File

@@ -4,12 +4,17 @@ import { FieldArrayType } from '@ngx-formly/core';
@Component({
selector: 'formly-array-type',
template: `
<div>
<div class="mb-3">
<ngb-accordion #acc="ngbAccordion" activeIds="ngb-panel-0">
<ngb-panel id="ngb-panel-0">
<ng-template ngbPanelTitle>
<label *ngIf="props.label" class="form-label">{{ props.label }}</label>
<p *ngIf="props.description">{{ props.description }}</p>
<div class="alert alert-danger" role="alert" *ngIf="showError && formControl.errors">
<formly-validation-message [field]="field"></formly-validation-message>
</div>
</ng-template>
<ng-template ngbPanelContent>
<div class="row row-cols-1 row-cols-md-{{this.itemsPerRow}} g-1">
<div *ngFor="let entry of field.fieldGroup; let i = index" class="col">
<div class="card">
@@ -29,7 +34,10 @@ import { FieldArrayType } from '@ngx-formly/core';
</div>
</div>
</div>
<button *ngIf="! this.field.props.readonly" class="btn btn-success col-sm-12" type="button" (click)="add()"><i-bs name="plus-square-fill"></i-bs></button>
<button *ngIf="! this.field.props.readonly" class="btn btn-success col-sm-12 gap-3" type="button" (click)="add()"><i-bs name="plus-square-fill"></i-bs></button>
</ng-template>
</ngb-panel>
</ngb-accordion>
</div>
`,
})

View File

@@ -10,7 +10,7 @@ import {FormlyJsonschema} from "@ngx-formly/core/json-schema";
template: `
<div class="mb-3">
<ngb-accordion #acc="ngbAccordion" activeIds="ngb-panel-0">
<ngb-panel>
<ngb-panel id="ngb-panel-0">
<ng-template ngbPanelTitle>
<label *ngIf="props.label && props['hideLabel'] !== true" [attr.for]="id"
class="form-label">{{ props.label }}

View File

@@ -1,6 +1,7 @@
import { Component, OnInit } from "@angular/core";
import { FormlyFieldInput } from "@ngx-formly/bootstrap/input";
@Component({
selector: 'formly-richtext-type',
template: `
@@ -42,11 +43,20 @@ export class RichtextTypeComponent extends FormlyFieldInput implements OnInit {
statusbar: false,
autoresize_bottom_margin: 0,
body_class: "contract-body",
content_style: ".contract-body { font-family: 'Century Schoolbook', 'sans-serif' }"
content_style: ".contract-body { font-family: 'Century Schoolbook', 'sans-serif' }",
entity_encoding: 'raw',
paste_preprocess: function (plugin: any, args: any) {
console.log(args.content)
let container = document.createElement('div');
container.innerHTML = args.content.trim();
cleanPastedElement(container)
console.log(container.innerHTML);
args.content = container.innerHTML;
}
}
init_multiline = {
plugins: 'lists image imagetools table code searchreplace autoresize',
plugins: 'lists image imagetools table code searchreplace paste autoresize',
menubar: 'edit insert format tools table',
menu: {
edit: { title: 'Edit', items: 'undo redo | cut copy paste | selectall | searchreplace' },
@@ -59,7 +69,7 @@ export class RichtextTypeComponent extends FormlyFieldInput implements OnInit {
}
init_singleline = {
plugins: 'autoresize',
plugins: 'paste autoresize',
menubar: '',
toolbar: 'undo redo | bold italic underline',
}
@@ -71,7 +81,7 @@ export class RichtextTypeComponent extends FormlyFieldInput implements OnInit {
}
}
getInitConfig() {
getInitConfig(): any {
return {...this.init_common, ...( this.multiline ? this.init_multiline : this.init_singleline)};
}
@@ -92,7 +102,49 @@ export class RichtextTypeComponent extends FormlyFieldInput implements OnInit {
}
}
}
}
function cleanPastedElement(htmlElement: HTMLElement): string {
if (! htmlElement.innerHTML) {
return "";
}
let innerHtml = ""
for(let i = 0; i < htmlElement.childNodes.length; i++){
const childNode = htmlElement.childNodes[i] as HTMLElement
if (childNode.nodeName == "#text") {
innerHtml += childNode.nodeValue;
} else {
innerHtml += cleanPastedElement(childNode);
}
}
htmlElement.innerHTML = innerHtml
if (htmlElement.tagName == "SPAN") {
let text = htmlElement.innerHTML
const style = htmlElement.style
if (style.fontWeight == "700") {
let strong = document.createElement('b');
strong.innerHTML = text
text = strong.outerHTML;
}
if (style.textDecoration == "underline") {
let underline = document.createElement('u');
underline.innerHTML = text;
text = underline.outerHTML;
}
if (style.fontStyle == "italic") {
let italic = document.createElement('em');
italic.innerHTML = text;
text = italic.outerHTML;
}
return text;
}
htmlElement.style.removeProperty("line-height")
htmlElement.style.removeProperty("margin")
return htmlElement.outerHTML
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 948 B

After

Width:  |  Height:  |  Size: 318 B

View File

@@ -2,10 +2,13 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>App</title>
<title>Cooper, Hillman & Toshi</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<meta property="og:title" content="Cooper, Hillman & Toshi">
<meta property="og:description" content="Interface d'administration des contrats">
<meta property="og:image" content="/assets/logo.png">
</head>
<body>
<app-root></app-root>

View File

@@ -354,5 +354,69 @@
<target>Mettre à jour</target>
</segment>
</unit>
<unit id="6061331044524123789">
<segment state="initial">
<source>Authentication required</source>
<target>Authentification nécessaire</target>
</segment>
</unit>
<unit id="3624268617519726175">
<segment state="initial">
<source>Permissions too low</source>
<target>Permissions trop basses</target>
</segment>
</unit>
<unit id="6833883791906187270">
<segment state="initial">
<source>Download Link:</source>
<target>Lien de téléchargement:</target>
</segment>
</unit>
<unit id="8168599357858858911">
<segment state="initial">
<source>Preview Link:</source>
<target>Lien de prévisualisation:</target>
</segment>
</unit>
<unit id="1295614462098694869">
<segment state="initial">
<source>Preview</source>
<target>Prévisualisation</target>
</segment>
</unit>
<unit id="2445188258613609179">
<segment state="initial">
<source>Signature</source>
<target>Signature</target>
</segment>
</unit>
<unit id="8474971383445371291">
<segment state="initial">
<source>
<pc id="0" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Cette page est à la destination exclusive de <pc id="1" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;"><ph id="2" equiv="INTERPOLATION" disp="{{ this.signatory }}"/></pc></pc>
<pc id="3" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Si vous n&apos;êtes <pc id="4" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">pas</pc> <ph id="5" equiv="INTERPOLATION" disp="{{ this.signatory }}"/>, veuillez <pc id="6" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">fermer cette page immédiatement</pc> et surpprimer tous les liens en votre possession menant vers celle-ci.</pc>
<pc id="7" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">En vous maintenant et/ou en interagissant avec cette page, vous enfreignez l&apos;article L.229 du code pénal de l&apos;Etat de San Andreas pour <pc id="8" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">usurpation d&apos;identité</pc> et vous vous exposez ainsi à une amende de 20 000$ ainsi qu&apos;à des poursuites civiles.</pc>
<pc id="9" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Le cabinet Cooper, Hillman &amp; Toshi LLC</pc>
</source>
<target>
<pc id="0" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Cette page est à la destination exclusive de <pc id="1" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;"><ph id="2" equiv="INTERPOLATION" disp="{{ this.signatory }}"/></pc></pc>
<pc id="3" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Si vous n&apos;êtes <pc id="4" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">pas</pc> <ph id="5" equiv="INTERPOLATION" disp="{{ this.signatory }}"/>, veuillez <pc id="6" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">fermer cette page immédiatement</pc> et surpprimer tous les liens en votre possession menant vers celle-ci.</pc>
<pc id="7" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">En vous maintenant et/ou en interagissant avec cette page, vous enfreignez l&apos;article L.229 du code pénal de l&apos;Etat de San Andreas pour <pc id="8" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">usurpation d&apos;identité</pc> et vous vous exposez ainsi à une amende de 20 000$ ainsi qu&apos;à des poursuites civiles.</pc>
<pc id="9" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Le cabinet Cooper, Hillman &amp; Toshi LLC</pc>
</target>
</segment>
</unit>
<unit id="7430416142942514215">
<segment state="initial">
<source>Publish</source>
<target>Publier</target>
</segment>
</unit>
<unit id="2990108023996257960">
<segment state="initial">
<source>This Contract has already been signed by</source>
<target>Ce contrat a déjà été signé par</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -292,5 +292,55 @@
<source>100 items per page</source>
</segment>
</unit>
<unit id="3624268617519726175">
<segment>
<source>Permissions too low</source>
</segment>
</unit>
<unit id="6061331044524123789">
<segment>
<source>Authentication required</source>
</segment>
</unit>
<unit id="6833883791906187270">
<segment>
<source>Download Link:</source>
</segment>
</unit>
<unit id="8168599357858858911">
<segment>
<source>Preview Link:</source>
</segment>
</unit>
<unit id="1295614462098694869">
<segment>
<source>Preview</source>
</segment>
</unit>
<unit id="2445188258613609179">
<segment>
<source>Signature</source>
</segment>
</unit>
<unit id="7430416142942514215">
<segment>
<source>Publish</source>
</segment>
</unit>
<unit id="8474971383445371291">
<segment>
<source>
<pc id="0" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Cette page est à la destination exclusive de <pc id="1" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;"><ph id="2" equiv="INTERPOLATION" disp="{{ this.signatory }}"/></pc></pc>
<pc id="3" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Si vous n&apos;êtes <pc id="4" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">pas</pc> <ph id="5" equiv="INTERPOLATION" disp="{{ this.signatory }}"/>, veuillez <pc id="6" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">fermer cette page immédiatement</pc> et surpprimer tous les liens en votre possession menant vers celle-ci.</pc>
<pc id="7" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">En vous maintenant et/ou en interagissant avec cette page, vous enfreignez l&apos;article L.229 du code pénal de l&apos;Etat de San Andreas pour <pc id="8" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">usurpation d&apos;identité</pc> et vous vous exposez ainsi à une amende de 20 000$ ainsi qu&apos;à des poursuites civiles.</pc>
<pc id="9" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Le cabinet Cooper, Hillman &amp; Toshi LLC</pc>
</source>
</segment>
</unit>
<unit id="2990108023996257960">
<segment>
<source>This Contract has already been signed by</source>
</segment>
</unit>
</file>
</xliff>

View File

@@ -26,3 +26,15 @@
.contract-body {
font-family: 'Century Schoolbook';
}
.nav-link {
color: #D2BA6F;
--bs-nav-link-hover-color: #9c8a51;
--bs-nav-pills-link-active-color: #D2BA6F;
--bs-nav-pills-link-active-bg: #114856;
}
.nav-link.active {
border: #D2BA6F solid 2px;
border-radius: 0;
}

72
front/nginx.prod.conf Normal file
View File

@@ -0,0 +1,72 @@
worker_processes 1;
events { worker_connections 1024; }
http {
sendfile on;
upstream docker-back {
server back:8000;
}
types {
module js;
}
include /etc/nginx/mime.types;
server {
listen 80;
gzip on;
gzip_http_version 1.1;
gzip_disable "MSIE [1-6]\.";
gzip_min_length 256;
gzip_vary on;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
gzip_comp_level 9;
root /usr/share/nginx/html;
location / {
try_files $uri $uri/ /index.html?$args;
}
location ~* ^.+\.css$ {
default_type text/css;
}
location ~* ^.+\.js$ {
default_type text/javascript;
}
location /contracts/signature/ {
set $is_robot 0;
if ($http_user_agent ~* "Discordbot|googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest\/0\.|pinterestbot|slackbot|vkShare|W3C_Validator|whatsapp") {
rewrite /contracts/signature/(.*) /api/v1/contract/print/opengraph/$1 last;
proxy_pass http://docker-back;
set $is_robot 1;
}
if ($is_robot = 0) {
rewrite ^ /index.html?$args last;
}
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
location /api/v1/ {
proxy_pass http://docker-back/;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
}
}

View File

@@ -26,6 +26,26 @@ http {
proxy_set_header X-Forwarded-Host $server_name;
}
location /contracts/signature/ {
set $is_robot 0;
if ($http_user_agent ~* "Discordbot|googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest\/0\.|pinterestbot|slackbot|vkShare|W3C_Validator|whatsapp") {
rewrite /contracts/signature/(.*) /api/v1/contract/print/opengraph/$1 last;
proxy_pass http://docker-back;
break;
set $is_robot 1;
}
if ($is_robot = 0) {
proxy_pass http://docker-front;
}
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
location /api/v1/ {
proxy_pass http://docker-back/;
proxy_redirect off;