222
.gitea/workflows/deploy-docs-dev.yaml
Normal file
222
.gitea/workflows/deploy-docs-dev.yaml
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
name: Deploy Documentation Local
|
||||||
|
run-name: Deploying ${{ gitea.repository }} docs locally
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
paths:
|
||||||
|
- "docs/**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-docs:
|
||||||
|
runs-on: windows:host # Ejecutar directamente en el host Windows
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Python for MkDocs
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
|
||||||
|
- name: Install MkDocs and dependencies
|
||||||
|
run: |
|
||||||
|
pip install mkdocs mkdocs-material
|
||||||
|
|
||||||
|
- name: Build documentation
|
||||||
|
run: |
|
||||||
|
cd docs
|
||||||
|
mkdocs build --site-dir ../build
|
||||||
|
|
||||||
|
- name: Deploy to IIS directory
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
$projectName = "${{ gitea.repository_name }}"
|
||||||
|
$basePath = "${{ secrets.DEPLOY_BASE_PATH }}"
|
||||||
|
$targetPath = Join-Path $basePath "dev" $projectName
|
||||||
|
|
||||||
|
# Crear directorio del proyecto si no existe
|
||||||
|
if (Test-Path $targetPath) {
|
||||||
|
Remove-Item -Path $targetPath -Recurse -Force
|
||||||
|
}
|
||||||
|
New-Item -ItemType Directory -Path $targetPath -Force
|
||||||
|
|
||||||
|
# Copiar archivos construidos
|
||||||
|
Copy-Item -Path "build\*" -Destination $targetPath -Recurse -Force
|
||||||
|
|
||||||
|
Write-Host "Documentation deployed to: $targetPath"
|
||||||
|
|
||||||
|
- name: Generate main index.html
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
$docsPath = "${{ secrets.DEPLOY_BASE_PATH }}"
|
||||||
|
$docsPath = Join-Path $docsPath "dev"
|
||||||
|
$indexPath = Join-Path $docsPath "index.html"
|
||||||
|
|
||||||
|
# Obtener todos los directorios de documentación
|
||||||
|
$docFolders = Get-ChildItem -Path $docsPath -Directory | Sort-Object Name
|
||||||
|
|
||||||
|
# Generar HTML del índice
|
||||||
|
$htmlContent = @"
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Documentación - MCV Ingenieros</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 300;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects {
|
||||||
|
display: grid;
|
||||||
|
gap: 25px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card:hover {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
box-shadow: 0 20px 40px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #333;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-meta {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 60px;
|
||||||
|
color: white;
|
||||||
|
opacity: 0.8;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header h1 { font-size: 2rem; }
|
||||||
|
.projects { grid-template-columns: 1fr; }
|
||||||
|
.project-card { padding: 20px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>📚 Centro de Documentación</h1>
|
||||||
|
<p>MCV Ingenieros - Documentación de Proyectos</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="projects">
|
||||||
|
"@
|
||||||
|
|
||||||
|
foreach ($folder in $docFolders) {
|
||||||
|
$folderName = $folder.Name
|
||||||
|
$lastWrite = $folder.LastWriteTime.ToString("dd/MM/yyyy HH:mm")
|
||||||
|
|
||||||
|
$htmlContent += @"
|
||||||
|
<div class="project-card">
|
||||||
|
<a href="./$folderName/" class="project-link">
|
||||||
|
<span class="project-icon">📖</span>
|
||||||
|
<h3 class="project-title">$folderName</h3>
|
||||||
|
<div class="project-meta">
|
||||||
|
<span>🕒</span>
|
||||||
|
<span>Actualizado: $lastWrite</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentDate = (Get-Date).ToString("dd/MM/yyyy HH:mm")
|
||||||
|
$htmlContent += @"
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>Índice generado automáticamente el $currentDate</p>
|
||||||
|
<p>Powered by Gitea Actions & IIS</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"@
|
||||||
|
|
||||||
|
# Escribir el archivo index.html
|
||||||
|
Set-Content -Path $indexPath -Value $htmlContent -Encoding UTF8
|
||||||
|
Write-Host "Index.html actualizado en: $indexPath"
|
220
.gitea/workflows/deploy-docs.yaml
Normal file
220
.gitea/workflows/deploy-docs.yaml
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
name: Deploy Documentation Local
|
||||||
|
run-name: Deploying ${{ gitea.repository }} docs locally
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "docs/**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-docs:
|
||||||
|
runs-on: windows:host # Ejecutar directamente en el host Windows
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Python for MkDocs
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
|
||||||
|
- name: Install MkDocs and dependencies
|
||||||
|
run: |
|
||||||
|
pip install mkdocs mkdocs-material
|
||||||
|
|
||||||
|
- name: Build documentation
|
||||||
|
run: |
|
||||||
|
cd docs
|
||||||
|
mkdocs build --site-dir ../build
|
||||||
|
|
||||||
|
- name: Deploy to IIS directory
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
$projectName = "${{ gitea.repository_name }}"
|
||||||
|
$basePath = "${{ secrets.DEPLOY_BASE_PATH }}"
|
||||||
|
$targetPath = Join-Path $basePath $projectName
|
||||||
|
|
||||||
|
# Crear directorio del proyecto si no existe
|
||||||
|
if (Test-Path $targetPath) {
|
||||||
|
Remove-Item -Path $targetPath -Recurse -Force
|
||||||
|
}
|
||||||
|
New-Item -ItemType Directory -Path $targetPath -Force
|
||||||
|
|
||||||
|
# Copiar archivos construidos
|
||||||
|
Copy-Item -Path "build\*" -Destination $targetPath -Recurse -Force
|
||||||
|
|
||||||
|
Write-Host "Documentation deployed to: $targetPath"
|
||||||
|
|
||||||
|
- name: Generate main index.html
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
$docsPath = "${{ secrets.DEPLOY_BASE_PATH }}"
|
||||||
|
$indexPath = Join-Path $docsPath "index.html"
|
||||||
|
|
||||||
|
# Obtener todos los directorios de documentación
|
||||||
|
$docFolders = Get-ChildItem -Path $docsPath -Directory | Where-Object { $_.Name -notin @("dev", "test") } | Sort-Object Name
|
||||||
|
|
||||||
|
# Generar HTML del índice
|
||||||
|
$htmlContent = @"
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Documentación - MCV Ingenieros</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 300;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects {
|
||||||
|
display: grid;
|
||||||
|
gap: 25px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card:hover {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
box-shadow: 0 20px 40px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #333;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-meta {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 60px;
|
||||||
|
color: white;
|
||||||
|
opacity: 0.8;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header h1 { font-size: 2rem; }
|
||||||
|
.projects { grid-template-columns: 1fr; }
|
||||||
|
.project-card { padding: 20px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>📚 Centro de Documentación</h1>
|
||||||
|
<p>MCV Ingenieros - Documentación de Proyectos</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="projects">
|
||||||
|
"@
|
||||||
|
|
||||||
|
foreach ($folder in $docFolders) {
|
||||||
|
$folderName = $folder.Name
|
||||||
|
$lastWrite = $folder.LastWriteTime.ToString("dd/MM/yyyy HH:mm")
|
||||||
|
|
||||||
|
$htmlContent += @"
|
||||||
|
<div class="project-card">
|
||||||
|
<a href="./$folderName/" class="project-link">
|
||||||
|
<span class="project-icon">📖</span>
|
||||||
|
<h3 class="project-title">$folderName</h3>
|
||||||
|
<div class="project-meta">
|
||||||
|
<span>🕒</span>
|
||||||
|
<span>Actualizado: $lastWrite</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentDate = (Get-Date).ToString("dd/MM/yyyy HH:mm")
|
||||||
|
$htmlContent += @"
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>Índice generado automáticamente el $currentDate</p>
|
||||||
|
<p>Powered by Gitea Actions & IIS</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"@
|
||||||
|
|
||||||
|
# Escribir el archivo index.html
|
||||||
|
Set-Content -Path $indexPath -Value $htmlContent -Encoding UTF8
|
||||||
|
Write-Host "Index.html actualizado en: $indexPath"
|
46
.vscode/launch.json
vendored
46
.vscode/launch.json
vendored
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
// Use IntelliSense to learn about possible attributes.
|
|
||||||
// Hover to view descriptions of existing attributes.
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "FRONT: DEBUG(Edge)",
|
|
||||||
"request": "launch",
|
|
||||||
"type": "msedge",
|
|
||||||
"url": "http://localhost:4200",
|
|
||||||
"webRoot": "${workspaceFolder}/front/v2",
|
|
||||||
"preLaunchTask": "Start Node server with nvs latest"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "(legacy) FRONT: DEBUG(Edge)",
|
|
||||||
"request": "launch",
|
|
||||||
"type": "msedge",
|
|
||||||
"url": "http://localhost:4200",
|
|
||||||
"webRoot": "${workspaceFolder}/front/v1",
|
|
||||||
"preLaunchTask": "(legacy) Start Node server with nvs latest"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Attach Edge",
|
|
||||||
"type": "msedge",
|
|
||||||
"request": "attach",
|
|
||||||
"url": "http://localhost:4200/#",
|
|
||||||
"webRoot": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Launch Edge (Test)",
|
|
||||||
"type": "msedge",
|
|
||||||
"request": "launch",
|
|
||||||
"url": "http://localhost:9876/debug.html",
|
|
||||||
"webRoot": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Launch Edge (E2E)",
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"program": "${workspaceFolder}/node_modules/protractor/bin/protractor",
|
|
||||||
"protocol": "inspector",
|
|
||||||
"args": ["${workspaceFolder}/protractor.conf.js"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
37
.vscode/tasks.json
vendored
37
.vscode/tasks.json
vendored
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"label": "(legacy) Start Node server with nvs latest",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "nvs use latest && npm run start",
|
|
||||||
"options": {
|
|
||||||
"cwd": "${workspaceFolder}/front/v1"
|
|
||||||
},
|
|
||||||
"isBackground": true,
|
|
||||||
"problemMatcher": [],
|
|
||||||
"presentation": {
|
|
||||||
"echo": true,
|
|
||||||
"reveal": "always",
|
|
||||||
"focus": false,
|
|
||||||
"panel": "shared"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Start Node server with nvs latest",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "nvs use latest && npm run start",
|
|
||||||
"options": {
|
|
||||||
"cwd": "${workspaceFolder}/front/v2"
|
|
||||||
},
|
|
||||||
"isBackground": true,
|
|
||||||
"problemMatcher": [],
|
|
||||||
"presentation": {
|
|
||||||
"echo": true,
|
|
||||||
"reveal": "always",
|
|
||||||
"focus": false,
|
|
||||||
"panel": "shared"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
309
README.md
309
README.md
@@ -1,213 +1,156 @@
|
|||||||
# mmorales.photo
|
# Galerías Fotográficas
|
||||||
|
|
||||||
## Index
|
|
||||||
|
|
||||||
1. [Problem](#problem)
|
|
||||||
2. [Proposal](#proposal)
|
|
||||||
1. [Objectives](#objectives)
|
|
||||||
2. [Monetization](#monetization)
|
|
||||||
3. [Scope](#scope)
|
|
||||||
4. [Risks and Mitigations](#risks-and-mitigations)
|
|
||||||
5. [Solution](#solution)
|
|
||||||
6. [System Architecture](#system-architecture)
|
|
||||||
3. [Glossary](#glossary)
|
|
||||||
4. [Additional Context](#additional-context)
|
|
||||||
|
|
||||||
## Additional Context
|
|
||||||
|
|
||||||
- [Frontend Documentation](docs/front/frontend-documentation.md)
|
|
||||||
- [Backend Documentation](docs/back)
|
|
||||||
- [Resources](docs/resources)
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
|
|
||||||
As digital photographers, there are few alternatives to showcase their work.
|
|
||||||
Many of them require web development skills.
|
|
||||||
Many lack support for selling or appointment booking.
|
|
||||||
Many do not offer enough storage.
|
|
||||||
Many do not offer custom layouts or domain naming.
|
|
||||||
Many rely on platforms that sell services as a service (SaaS).
|
|
||||||
Most rely on platforms that sell hosting as a service (IaaS).
|
|
||||||
Most do not have user interactivity, resulting in a simple gallery without feedback.
|
|
||||||
|
|
||||||
Many amateur photographers use Instagram as a gallery and portfolio. Then, they use Dropbox, WeTransfer, Google Drive, or similar platforms. They rely on manual methods for paying for services, such as wire transfer or cash, sometimes leading to loss of money or loss of a client due to client dissatisfaction.
|
|
||||||
|
|
||||||
## Proposal
|
|
||||||
|
|
||||||
A platform with three kinds of view:
|
|
||||||
|
|
||||||
1. A client view: A simple gallery with easy contact and appointment booking methods. With a history of purchases and services to access private galleries where images can be downloaded without restrictions. These galleries must be able to be shared across users so one pays, all get access.
|
|
||||||
|
|
||||||
2. A content manager view: A simple administration page for uploading content, customizing the current content (such as website name, icon, and current images displayed), and creating buyers' galleries. It must also be able to moderate user-generated content, such as public gallery comments.
|
|
||||||
|
|
||||||
3. An administrator view: A more complex display of web-related content. The admin can edit service-related items, such as adding content managers, editing payment methods, changing website name, icon, fonts, and component CSS. It must also be able to look up non-sensitive user details such as name or email to assist users in need.
|
|
||||||
|
|
||||||
The solutions must be oriented towards user experience and satisfaction.
|
|
||||||
It must also provide user security and protect photographers' work.
|
|
||||||
For non-techies, it must provide an easy way to manage, use, and host; the out-of-the-box experience must be excellent.
|
|
||||||
For techies, it must provide a self-hosted environment, allowing customization of less critical parts such as mailing, frontend layout, and database provider.
|
|
||||||
|
|
||||||
### Objectives
|
|
||||||
|
|
||||||
We expect:
|
|
||||||
|
|
||||||
- To fulfill the needs of new professionals and provide a solid alternative for established ones.
|
|
||||||
- A secure and professional way to deliver digital photography work to clients.
|
|
||||||
- A collaborative platform between photographers and clients to enable better work.
|
|
||||||
- The easiest-to-use and best out-of-the-box experience for new clients.
|
|
||||||
|
|
||||||
### Monetization
|
|
||||||
|
|
||||||
__What do we sell?__ We sell a fire-and-forget service.
|
|
||||||
__What do customers pay for?__ Non-techies pay for setup, management, support, and storage. Techies pay for support.
|
|
||||||
__How do customers receive the product?__ Non-techies receive a pre-made setup on their desired hosting. Techies receive a click-and-run service.
|
|
||||||
__How much do we receive per product?__ As a product, we do not receive anything. As a service, we license the service monthly, yearly, or by one-hour support service.
|
|
||||||
|
|
||||||
### Scope
|
|
||||||
|
|
||||||
#### What IS included
|
|
||||||
|
|
||||||
- Provide a friendly, comfortable, and fast frontend; an agile and resilient backend.
|
|
||||||
- Offer broad customization options regarding infrastructure.
|
|
||||||
- Deliver the most simple and satisfying out-of-the-box experience possible.
|
|
||||||
- Enable integration with multiple cloud storage providers and local storage.
|
|
||||||
- Support for multi-language and accessibility features.
|
|
||||||
- Ensure data privacy and security for users and photographers.
|
|
||||||
- Provide basic analytics and usage statistics for administrators.
|
|
||||||
- Allow easy deployment on various hosting environments (cloud, on-premises, hybrid).
|
|
||||||
|
|
||||||
#### What is NOT included
|
|
||||||
|
|
||||||
- Payment gateways.
|
|
||||||
- Extensive customization of the visual design.
|
|
||||||
- Hosting management tasks.
|
|
||||||
- Advanced marketing automation tools.
|
|
||||||
- Third-party plugin marketplace.
|
|
||||||
- Deep integration with external CRM or ERP systems.
|
|
||||||
|
|
||||||
### Risks and Mitigations
|
|
||||||
|
|
||||||
| Risk | Description | Mitigation |
|
|
||||||
|------|-------------|------------|
|
|
||||||
| Vendor lock-in | Dependence on a single cloud provider may limit flexibility and increase costs. | Support multiple storage providers and allow easy migration between them. |
|
|
||||||
| Data loss or corruption | Images or user data could be lost due to hardware failure or software bugs. | Implement regular backups, redundancy, and data integrity checks. |
|
|
||||||
| Security breaches | Unauthorized access to private galleries or sensitive user data. | Use strong encryption (HTTPS, AES), MFA, and regular security audits. |
|
|
||||||
| Scalability issues | Performance degradation as the number of users or images grows. | Design for horizontal scalability and monitor system performance. |
|
|
||||||
| Legal and compliance | Failure to comply with data protection laws (GDPR, etc.). | Store data in compliant regions and provide clear privacy policies. |
|
|
||||||
| User adoption | Users may find the platform difficult to use or not see its value. | Focus on user experience, provide onboarding guides, and collect feedback. |
|
|
||||||
| Third-party service outages | External services (cloud, identity providers) may become unavailable. | Implement fallback mechanisms and monitor service health. |
|
|
||||||
| Cost overruns | Unexpected expenses in infrastructure or development. | Monitor costs, set budgets, and optimize resource usage. |
|
|
||||||
|
|
||||||
### Solution
|
|
||||||
|
|
||||||
Based on a three-layered architecture, each layer must present a plug-and-play architecture.
|
|
||||||
|
|
||||||
__As frontend__, we are starting with Angular as a base, then creating Vue and React alternatives.
|
|
||||||
It will follow a clean architecture based on the scream architecture approach.
|
|
||||||
To make rendering easy for low-end/home servers, it will mainly use client-side rendering.
|
|
||||||
For identity verification, the frontend will rely on OpenID data provided by the backend, using a cookie ID and JWE for identification.
|
|
||||||
For UI layout, mobile and tablet will be preferred as end devices. Computers and large-sized devices will be last in layout responsibility. The UI will be built by composing different small components, following a Vue-like philosophy.
|
|
||||||
|
|
||||||
__As backend__, we are deploying a C# .NET engine. As a self-host-aware project, we need to ensure platform compatibility. A cloud-native approach will be taken, providing a system-agnostic platform. Cross-compiling will be a must. Database-agnostic, it will use SQLite as the default database for fallback and non-customized database. PostgreSQL will be used for SQL-based database development, making it pluggable and interchangeable with Microsoft SQL Server and other SQL providers like CockroachDB.
|
|
||||||
For architecture, we will take a Domain-Driven approach. Using EntityFramework for database operations makes development faster and database-agnostic.
|
|
||||||
For security, all communications will be made via HTTPS and encrypted with military-grade AES.
|
|
||||||
Also, to make user registration easier, we will use as many identity providers as possible, such as Google, Facebook, Instagram... Once registered, users need to access as easily as possible, so passwordless access should be implemented. For security, an MFA system will be required.
|
|
||||||
To be able to respond to energy shortages, we will use DAPR and some edge computing technologies.
|
|
||||||
To be more resilient against some attacks, we will implement BFF (Backend for Frontend) technology.
|
|
||||||
For custom gallery creation, we will use a parallel processor based on messaging.
|
|
||||||
|
|
||||||
__As database__, we are going to choose a database-agnostic approach.
|
|
||||||
Since during development we will ignore database layout, we are going to expose two configurations for two kinds of data levels.
|
|
||||||
User-related data will be presented as the SensibleData database.
|
|
||||||
Images and other blobs will be presented as the BlobData database.
|
|
||||||
As a cloud-native approach, we will take blob storage as a service-independent module, so users can use their own disk space or services such as:
|
|
||||||
|
|
||||||
- __Amazon S3:__ Highly scalable, reliable, and widely used object storage service.
|
|
||||||
- __Microsoft Azure Blob Storage:__ Secure and scalable storage for unstructured data, integrated with the Azure ecosystem.
|
|
||||||
- __Google Cloud Storage:__ Global object storage with strong integration to Google Cloud services.
|
|
||||||
- __DigitalOcean Spaces:__ Simple and cost-effective object storage compatible with the S3 API.
|
|
||||||
- __Backblaze B2:__ Affordable cloud storage with S3 compatibility.
|
|
||||||
- __Wasabi:__ High-performance, low-cost cloud storage with S3 API support.
|
|
||||||
- __IBM Cloud Object Storage:__ Enterprise-grade, scalable object storage.
|
|
||||||
- __MinIO:__ Self-hosted, S3-compatible object storage solution for on-premises or private cloud.
|
|
||||||
|
|
||||||
### System Architecture
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
At system level, since identity and data providers are not controlled parts, there are three main components:
|
|
||||||
|
|
||||||
- Frontend
|
|
||||||
- BFF
|
|
||||||
- Backend
|
|
||||||
|
|
||||||
Frontend will speak ONLY with BFF via REST commands. Only sensible data will be encrypted via AES.
|
|
||||||
Frontend will GET lazily all the images for galleries.
|
|
||||||
|
|
||||||
BFF will redirect GET image requests to configured CDN or will serve them if no CDN is configured.
|
|
||||||
BFF will cache repeated and critical requests.
|
|
||||||
BFF will redirect to Backend in case of user creation, login or update; selling activity as booking or buying; image creation, edition or delete.
|
|
||||||
|
|
||||||
Backend will contact Identity Providers such as Google or Instagram at login time.
|
|
||||||
Backend will generate and validate login tokens.
|
|
||||||
Backend will create, retrieve, edit and delete user and image data.
|
|
||||||
|
|
||||||
Since frontend will be device dependent in the future, BFF will give flexibility for identifying client users.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Glossary
|
## Que problema identificamos
|
||||||
|
|
||||||
__Frontend:__ The part of the application that users interact with directly, typically the website or app interface.
|
Existen multiples alternativas para la distribución de imágenes digitales, como redes sociales o proveedores de almacenamiento en la nube.
|
||||||
|
Las redes sociales exponen publicamente y con poco control el contenido de las imágenes.
|
||||||
|
Los proveedores de almacenamiento ofrecen un control mayor sobre la exposición de las imágenes a cambio de una suscripción o pago por uso.
|
||||||
|
|
||||||
__Backend:__ The server-side part of the application that handles business logic, data storage, and communication with other services.
|
Como profesional que espera cobrar por el trabajo, publicar fotos privadas en redes sociales abre camino a múltiples problemas: perdida de privacidad para el cliente, mala exposición y pobre posicionamiento web, perdida de calidad en la imágen, se impide reutilizar la imagen con otros fines sin herramientas de terceros que pueden ser perjudiciales para clientes y profesionales.
|
||||||
|
Usar proveedores como WeTransfer, Google Drive o Dropbox, requiere de pagos y un conocimiento mínimo sobre el uso de estos servicios por parte del cliente; además, para ahorrar costes los profesionales deben eliminar los grupos de imágenes con cierta frequencia o fracturar por tiempo de almacenamiento al cliente.
|
||||||
|
Ambas opciones limitan la cooperatividad y la recuperación de las imágenes por parte del cliente.
|
||||||
|
Además, expone fácilmente al profesional a varios riesgos y problemáticas emergentes con la evolución constante del software: la perdida de credenciales, el robo de identidad, el plagio, la pérdida de control sobre la calidad de transmisión de la imagen.
|
||||||
|
|
||||||
__SaaS (Software as a Service):__ A software distribution model in which applications are hosted by a service provider and made available to customers over the internet.
|
Como cliente, recibir una sesión por WeTransfer limita las opciones de feedback y mejora o personalización. Recibirla por correo electrónico limita la cantidad de imágenes recibidas. Recibirlas por Google Drive o similares nos quita espacio de nuestro almacenamiento en la nube. Si el profesional publica las imágenes, referenciándos puede afectar a nuestra huella digital e imagen en redes.
|
||||||
|
Además, según la configuración del servicio que use el profesional, es común tener a disposición las imágenes solamente durante un tiempo determinado, haciendo imposible recuperarlas o acceder a ellas pasado ese tiempo.
|
||||||
|
|
||||||
__IaaS (Infrastructure as a Service):__ A form of cloud computing that provides virtualized computing resources over the internet.
|
Ambos participantes de la actividad recaen en un moderno problema: necesitar diversos servicios para controlar un único recurso.
|
||||||
|
Como profesional, necesitas una web-portfolio, un perfil en redes, un servicio de almacenamiento y una forma de contacto con el cliente.
|
||||||
|
Como cliente, necesitas poder ver el trabajo anterior del profesional, contactar con él y poder revisar el trabajo de forma conjunta para lograr el resultado que uno espera.
|
||||||
|
|
||||||
__Client-side rendering:__ Rendering of web pages in the user's browser using JavaScript frameworks.
|
## Que queremos resolver
|
||||||
|
|
||||||
__Server-side rendering (SSR):__ Rendering of web pages on the server before sending them to the user's browser.
|
Desde el punto de vista del profesional, podemos agrupar todos los servicios en un único servico.
|
||||||
|
Una especie de Amazon para imágenes.
|
||||||
|
|
||||||
__OpenID:__ An authentication protocol that allows users to log in to multiple services with a single identity.
|
Desde el punto de vista del cliente, agrupandolo todo, evitamos la necesidad de acceder a tantos servicios diferentes y familiarizamos al usuario con una interfaz sencilla y directa, minimizando el roce con el aplicativo.
|
||||||
|
|
||||||
__JWE (JSON Web Encryption):__ A standard for encrypting data in JSON format, often used for secure transmission of authentication tokens.
|
## Alternativas a nuestra solución
|
||||||
|
|
||||||
__MFA (Multi-Factor Authentication):__ A security system that requires more than one method of authentication from independent categories of credentials.
|
Existen multitud de alternativas a una web hecha a mano, como Wix o Joomla.
|
||||||
|
Existen alternativas cloud-native para alojar un portfolio.
|
||||||
|
Existen muchos proveedores de almacenamiento web.
|
||||||
|
Existen muchas redes sociales donde compartir las imágenes.
|
||||||
|
Existen metodos de comunicación y trabajo colaborativo.
|
||||||
|
En su gran mayoría, en servicios diferenciados.
|
||||||
|
Sin embargo no existe servicios unificados.
|
||||||
|
|
||||||
__BFF (Backend for Frontend):__ An architectural pattern where a dedicated backend is built for each frontend application to optimize communication and security.
|
---
|
||||||
|
|
||||||
__Domain-Driven Design (DDD):__ An approach to software development that focuses on modeling software to match a domain's business concepts and logic.
|
## Despiece del problema
|
||||||
|
|
||||||
__EntityFramework:__ An object-relational mapper (ORM) for .NET, used to interact with databases using .NET objects.
|
### Almacenamiento
|
||||||
|
|
||||||
__Blob Storage:__ A service for storing large amounts of unstructured data, such as images, videos, and documents.
|
El profesional no quiere tener que gastar en almacenamiento ni preocuparse del estado de un proveedor.
|
||||||
|
Por tanto, el sistema tiene que poder almacenar las imágenes durante mucho tiempo y mantenerlas en linea el mismo tiempo.
|
||||||
|
|
||||||
__Plug-and-play architecture:__ A design approach that allows components to be easily added, removed, or replaced without affecting the rest of the system.
|
### Distribución
|
||||||
|
|
||||||
__CDN (Content Delivery Network):__ A network of servers distributed geographically to deliver content more efficiently to users based on their location.
|
El profesional no quiere tener que usar más de un servicio para enviar las imágenes al cliente o subirlas a redes sociales.
|
||||||
|
El cliente no quiere recibir las imágenes en un zip o tener que descargarlas desde un servicio de terceros.
|
||||||
|
|
||||||
__AES (Advanced Encryption Standard):__ A symmetric encryption algorithm widely used for securing sensitive data.
|
### Trabajo colaborativo
|
||||||
|
|
||||||
__REST (Representational State Transfer):__ An architectural style for designing networked applications, often used for APIs.
|
El profesional quiere que el cliente escoja la imagen que más el guste y le de feedback sobre el retoque y la edición de la imágen.
|
||||||
|
El cliente queire revisar el trabajo del profesional para escoger las imágenes que más le gusten.
|
||||||
|
|
||||||
__DAPR (Distributed Application Runtime):__ A runtime that simplifies building distributed systems by providing APIs for common tasks like state management and pub/sub messaging.
|
### Feedback
|
||||||
|
|
||||||
__Edge Computing:__ A distributed computing paradigm that brings computation and data storage closer to the location where it is needed to improve response times and save bandwidth.
|
El cliente puede dar feedback mediante comentarios en cada imágen como votando la imagen.
|
||||||
|
La nota asignada a cada imágen se determinará como: (votos positivos - votos negativos) / votos totales
|
||||||
|
Los comentarios se puntuarán de la misma forma.
|
||||||
|
El profesional podrá responder a los comentarios y los leerá de forma que el comentario con mejor puntuación quede el primero, en su defecto se ordenarán por fecha.
|
||||||
|
|
||||||
__Parallel Processing:__ A method of processing data in which multiple processors execute tasks simultaneously to increase efficiency and performance.
|
El feedback también podrá ser dado a imágenes y colecciones del portfolio público, solamente lo podrán hacer los clientes autenticados. Aquellos que adquieran una sesión podrán dejar feedback al profesional de forma pública.
|
||||||
|
|
||||||
__API (Application Programming Interface):__ Un conjunto de definiciones y protocolos para construir e integrar software de aplicaciones.
|
### Portfolio
|
||||||
|
|
||||||
__OAuth:__ Un estándar abierto para la autorización que permite a los usuarios compartir recursos entre aplicaciones sin compartir credenciales.
|
El profesional no quiere gastar en otro servicio más para alojar el protfolio.
|
||||||
|
El profesional quiere subir las imágenes al servicio y escoger cuales van a conformar el portfolio.
|
||||||
|
El profesional quiere que la primera ojeada que tenga el cliente sobre el servicio sea el portfolio.
|
||||||
|
|
||||||
__CI/CD (Continuous Integration/Continuous Deployment):__ Un conjunto de prácticas que automatizan el desarrollo, las pruebas y la implementación de software.
|
### Colecciones
|
||||||
|
|
||||||
__Microservicios:__ Un estilo arquitectónico que estructura una aplicación como un conjunto de servicios pequeños y autónomos.
|
El profesional quiere organizar las imágenes por fecha, personas que salen en ellas, categorías, eventos...
|
||||||
|
El profesional quiere poder reunir imágenes que se relacionen entre sí por categoría, evento, fechas, motivos...
|
||||||
|
El profesional quiere juntar todas las imágenes que van destinadas a un cliente.
|
||||||
|
|
||||||
__Load Balancer:__ Un dispositivo o software que distribuye tráfico de red o aplicación entre varios servidores para mejorar la eficiencia y la disponibilidad.
|
### Sesiones
|
||||||
|
|
||||||
__Cache:__ Un almacenamiento temporal de datos para acelerar el acceso a información frecuentemente utilizada.
|
El profesional hará varias imágenes que revisará con el cliente.
|
||||||
|
El cliente escogerá únicamente las mejoras y que más le gusten.
|
||||||
|
El profesional revisará esas imágenes, con un límite numérico de imágenes totales.
|
||||||
|
El cliente recibirá una colección de esas imágenes.
|
||||||
|
Múltiples clientes y profesionales podrán trabajar en una única sesión.
|
||||||
|
Durante el proceso, debe existir una colaboración mutua mediante feedback.
|
||||||
|
|
||||||
__JWT (JSON Web Token):__ Un estándar para transmitir información de forma segura entre partes como un objeto JSON.
|
### Multiples clientes
|
||||||
|
|
||||||
|
El profesional quiere poder trabajar sobre un proyecto y poder enviar el resultado del proyecto a multiples clientes.
|
||||||
|
|
||||||
|
### Venta del servicio
|
||||||
|
|
||||||
|
El profesional quiere cobrar una imágen única, una o varias colecciones, o una sesión.
|
||||||
|
Un cliente pagará por una imágen, una o diversas colección premaquetadas, y por una sesión.
|
||||||
|
Muchos clientes podrán pagar conjuntamente el servicio.
|
||||||
|
|
||||||
|
### Usuarios
|
||||||
|
|
||||||
|
Existirán 4 tipos de usuarios:
|
||||||
|
|
||||||
|
- Anonimo
|
||||||
|
- Cliente
|
||||||
|
- Profesional
|
||||||
|
- Administrador
|
||||||
|
|
||||||
|
El usuario anónimo podrá consultar el portfolio y ver los diferentes botones y enlaces para ver el resto de imágenes públicas, redes sociales, blog o identificarse.
|
||||||
|
|
||||||
|
El cliente, o usuario identificado, podrá hacer lo mismo que el anónimo. También podrá comprar las diferentes imágenes y colecciones disponibles. Además, podrá contratar una sesión, interactuar con la sesión que tenga activa dando feedback o escogiendo las imágenes; y consultar pedidos anteriores para descargar las imágenes, publicarlas en redes sociales o compartirlas de forma directa con otros clientes identificados.
|
||||||
|
|
||||||
|
El profesional, podrá editar las imágenes y colecciones públicas que haya en el portfolio; organizar nuevas sesiones, editar las sesiones en marcha y actualizarlas; crear y editar nuevas colecciones; crear y editar eventos y categorías.
|
||||||
|
|
||||||
|
El administrador, podrá hacer lo mismo que el profesional y además podrá añadir profesionales y editar aspectos clave de la web.
|
||||||
|
|
||||||
|
Para facilitar el registro y el inicio de sesión, los usuarios podrán iniciar sesión con Google, Microsoft, Meta y Apple. Además, contarán con la opción de usar correo y contraseña. Para ello nos adaptaremos a OpenId y OAuth 2.
|
||||||
|
Todos los usuarios tendrán un método MFA como sms o passkeys.
|
||||||
|
Aquellos usuarios que inicien sesión con contraseña, recibirán un magic link para entrar.
|
||||||
|
|
||||||
|
### Conexiones lentas
|
||||||
|
|
||||||
|
Todas las imágenes subidas se procesarán en 3 categorías:
|
||||||
|
|
||||||
|
- Baja resolución: 360p, comprimidas en WebP
|
||||||
|
- Media resolución: 720p, comprimidas en WebP
|
||||||
|
- Resolución Nativa: nativa, sin compresión
|
||||||
|
|
||||||
|
### Pagos
|
||||||
|
|
||||||
|
Se establecerá como recomendación el uso de linkpay o servicios como google wallet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tecnologías a usar
|
||||||
|
|
||||||
|
Back:
|
||||||
|
JWT, OpenId + OAuth 2 -> Duende Identity Server
|
||||||
|
DDD con CQRS -> MediatR
|
||||||
|
Entity Framework Core + sqlite -> Base de datos
|
||||||
|
AutoMapper -> Mapeo de objetos
|
||||||
|
Serilog + OpenTelemetry -> Logging y trazabilidad
|
||||||
|
Scalar + OpenApi -> Documentación y pruebas de API
|
||||||
|
FluentValidation -> Validación de modelos
|
||||||
|
Redis -> Almacenamiento en caché
|
||||||
|
mailkit -> Envío de correos electrónicos
|
||||||
|
ImageSharp -> Procesamiento de imágenes
|
||||||
|
|
||||||
|
Front:
|
||||||
|
TailwindCSS -> Estilos
|
||||||
|
Angular -> Framework
|
||||||
|
RxJS -> Programación reactiva
|
||||||
|
NgRx -> Manejo del estado
|
||||||
|
scss -> Preprocesador CSS
|
||||||
|
axios -> Cliente HTTP
|
||||||
|
Vite -> Bundler
|
||||||
|
Typescript -> Lenguaje
|
||||||
|
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"isRoot": true,
|
|
||||||
"tools": {
|
|
||||||
"dotnet-ef": {
|
|
||||||
"version": "9.0.8",
|
|
||||||
"commands": [
|
|
||||||
"dotnet-ef"
|
|
||||||
],
|
|
||||||
"rollForward": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
Binary file not shown.
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"email": "sys@t.em",
|
|
||||||
"key": "aa0e0979-99db-42e7-8b60-91c2d055b9d0",
|
|
||||||
"password": "+z1L[oYUupZ>L{4a"
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
namespace back.DTO;
|
|
||||||
|
|
||||||
public class PhotoFormModel
|
|
||||||
{
|
|
||||||
public required string UserId { get; set; }
|
|
||||||
public required string Title { get; set; }
|
|
||||||
public string? Description { get; set; }
|
|
||||||
public string[]? Tags { get; set; }
|
|
||||||
public string[]? People { get; set; }
|
|
||||||
public IFormFile? Image { get; set; }
|
|
||||||
public string? Ubicacion { get; set; }
|
|
||||||
public string? Evento { get; set; }
|
|
||||||
public bool IsPublic { get; set; }
|
|
||||||
}
|
|
@@ -1,9 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
|
|
||||||
namespace back.DTO;
|
|
||||||
|
|
||||||
public class UserDto
|
|
||||||
{
|
|
||||||
public string Id { get; set; } = null!;
|
|
||||||
public ICollection<Role> Roles { get; set; } = [];
|
|
||||||
}
|
|
@@ -1,8 +0,0 @@
|
|||||||
namespace back.DataModels;
|
|
||||||
|
|
||||||
public partial class EfmigrationsLock
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
public string Timestamp { get; set; } = null!;
|
|
||||||
}
|
|
@@ -1,44 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using Transactional.Abstractions;
|
|
||||||
|
|
||||||
namespace back.DataModels;
|
|
||||||
|
|
||||||
[Table("Events")]
|
|
||||||
public partial class Event : ISoftDeletable, IEquatable<Event>
|
|
||||||
{
|
|
||||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
|
||||||
public string Id { get; set; } = null!;
|
|
||||||
[Required, MaxLength(50)]
|
|
||||||
public string Title { get; set; } = null!;
|
|
||||||
[MaxLength(500)]
|
|
||||||
public string? Description { get; set; }
|
|
||||||
public string? Date { get; set; }
|
|
||||||
public string? Location { get; set; }
|
|
||||||
public string CreatedAt { get; set; } = null!;
|
|
||||||
public string UpdatedAt { get; set; } = null!;
|
|
||||||
public string? CreatedBy { get; set; }
|
|
||||||
public string? UpdatedBy { get; set; }
|
|
||||||
public int IsDeleted { get; set; }
|
|
||||||
public string? DeletedAt { get; set; }
|
|
||||||
public virtual ICollection<Gallery> Galleries { get; set; } = [];
|
|
||||||
public virtual ICollection<Photo> Photos { get; set; } = [];
|
|
||||||
public virtual ICollection<Tag> Tags { get; set; } = [];
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
=> HashCode.Combine(Id, Title, Date, Location);
|
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
=> obj is Event otherEvent && Equals(otherEvent);
|
|
||||||
|
|
||||||
public bool Equals(Event? other)
|
|
||||||
{
|
|
||||||
if (other is null) return false;
|
|
||||||
if (ReferenceEquals(this, other)) return true;
|
|
||||||
return
|
|
||||||
Id == other.Id
|
|
||||||
|| (Title == other.Title && Date == other.Date && Location == other.Location)
|
|
||||||
|| GetHashCode() == other.GetHashCode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@@ -1,44 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using Transactional.Abstractions;
|
|
||||||
|
|
||||||
namespace back.DataModels;
|
|
||||||
|
|
||||||
[Table("Galleries")]
|
|
||||||
public partial class Gallery: IEquatable<Gallery>, ISoftDeletable
|
|
||||||
{
|
|
||||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
|
||||||
public string Id { get; set; } = null!;
|
|
||||||
[MaxLength(100)]
|
|
||||||
public string? Title { get; set; }
|
|
||||||
[MaxLength(500)]
|
|
||||||
public string? Description { get; set; }
|
|
||||||
public string? CreatedAt { get; set; }
|
|
||||||
public string? UpdatedAt { get; set; }
|
|
||||||
public string CreatedBy { get; set; } = null!;
|
|
||||||
public int? IsPublic { get; set; }
|
|
||||||
public int? IsArchived { get; set; }
|
|
||||||
public int? IsFavorite { get; set; }
|
|
||||||
public int IsDeleted { get; set; }
|
|
||||||
public string? DeletedAt { get; set; }
|
|
||||||
public string? EventId { get; set; }
|
|
||||||
public virtual User CreatedByNavigation { get; set; } = null!;
|
|
||||||
public virtual Event? Event { get; set; }
|
|
||||||
public virtual ICollection<Photo> Photos { get; set; } = [];
|
|
||||||
public virtual ICollection<Tag> Tags { get; set; } = [];
|
|
||||||
public virtual ICollection<User> Users { get; set; } = [];
|
|
||||||
|
|
||||||
public Gallery() { }
|
|
||||||
|
|
||||||
public override int GetHashCode() => HashCode.Combine(Id, Title);
|
|
||||||
|
|
||||||
public override bool Equals(object? obj) => obj is Gallery otherEvent && Equals(otherEvent);
|
|
||||||
|
|
||||||
public bool Equals(Gallery? other)
|
|
||||||
{
|
|
||||||
if (other is null) return false;
|
|
||||||
if (ReferenceEquals(this, other)) return true;
|
|
||||||
return
|
|
||||||
Id == other.Id || GetHashCode() == other.GetHashCode();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,40 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace back.DataModels;
|
|
||||||
|
|
||||||
[Table("Permissions")]
|
|
||||||
public partial class Permission: IEquatable<Permission>
|
|
||||||
{
|
|
||||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
|
||||||
public string Id { get; set; } = null!;
|
|
||||||
[Required, MaxLength(100)]
|
|
||||||
public string Name { get; set; } = null!;
|
|
||||||
[MaxLength(255)]
|
|
||||||
public string? Description { get; set; }
|
|
||||||
public virtual ICollection<Role> Roles { get; set; } = [];
|
|
||||||
|
|
||||||
public override int GetHashCode() => HashCode.Combine(Id, Name);
|
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
=> obj is Permission other && Equals(other);
|
|
||||||
public bool Equals(Permission? other)
|
|
||||||
{
|
|
||||||
if (other is null) return false;
|
|
||||||
if (ReferenceEquals(this, other)) return true;
|
|
||||||
return
|
|
||||||
Id == other.Id || GetHashCode() == other.GetHashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static permissions
|
|
||||||
public static readonly Permission ViewContentPermission = new() { Id = "1", Name = "VIEW_CONTENT", Description = "Permission to view content" };
|
|
||||||
public static readonly Permission LikeContentPermission = new() { Id = "2", Name = "LIKE_CONTENT", Description = "Permission to like content" };
|
|
||||||
public static readonly Permission EditContentPermission = new() { Id = "3", Name = "EDIT_CONTENT", Description = "Permission to edit content" };
|
|
||||||
public static readonly Permission DeleteContentPermission = new() { Id = "4", Name = "DELETE_CONTENT", Description = "Permission to delete content" };
|
|
||||||
public static readonly Permission CreateContentPermission = new() { Id = "5", Name = "CREATE_CONTENT", Description = "Permission to create new content" };
|
|
||||||
public static readonly Permission EditUserPermission = new() { Id = "6", Name = "EDIT_USER", Description = "Permission to edit user" };
|
|
||||||
public static readonly Permission DeleteUserPermission = new() { Id = "7", Name = "DELETE_USER", Description = "Permission to delete user" };
|
|
||||||
public static readonly Permission DisableUserPermission = new() { Id = "8", Name = "DISABLE_USER", Description = "Permission to disable user" };
|
|
||||||
public static readonly Permission CreateUserPermission = new() { Id = "9", Name = "CREATE_USER", Description = "Permission to create new user" };
|
|
||||||
public static readonly Permission EditWebConfigPermission = new() { Id = "10", Name = "EDIT_WEB_CONFIG", Description = "Permission to edit web configuration" };
|
|
||||||
}
|
|
@@ -1,70 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using Transactional.Abstractions;
|
|
||||||
using Transactional.Abstractions.Interfaces;
|
|
||||||
|
|
||||||
namespace back.DataModels;
|
|
||||||
|
|
||||||
[Table("Persons")]
|
|
||||||
public partial class Person: IEntity<Person>, ISoftDeletable
|
|
||||||
{
|
|
||||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
|
||||||
public string Id { get; set; } = null!;
|
|
||||||
[Required, MaxLength(100)]
|
|
||||||
public string Name { get; set; } = null!;
|
|
||||||
public string? ProfilePicture { get; set; }
|
|
||||||
public string? Avatar { get; set; }
|
|
||||||
public string? SocialMediaId { get; set; }
|
|
||||||
[MaxLength(250)]
|
|
||||||
public string? Bio { get; set; } // Optional field for a short biography or description
|
|
||||||
public string CreatedAt { get; set; } = null!;
|
|
||||||
public string? UpdatedAt { get; set; }
|
|
||||||
public int IsDeleted { get; set; }
|
|
||||||
public string? DeletedAt { get; set; }
|
|
||||||
|
|
||||||
public virtual ICollection<Photo> Photos { get; set; } = [];
|
|
||||||
public virtual SocialMedia? SocialMedia { get; set; }
|
|
||||||
public virtual User? User { get; set; }
|
|
||||||
public virtual ICollection<Photo> PhotosNavigation { get; set; } = [];
|
|
||||||
|
|
||||||
|
|
||||||
public override int GetHashCode() => HashCode.Combine(Id, Name);
|
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
=> obj is Person other && Equals(other);
|
|
||||||
|
|
||||||
public bool Equals(Person? other)
|
|
||||||
{
|
|
||||||
if (other is null) return false;
|
|
||||||
if (ReferenceEquals(this, other)) return true;
|
|
||||||
return
|
|
||||||
Id == other.Id || GetHashCode() == other.GetHashCode();
|
|
||||||
}
|
|
||||||
public bool IsNull => this is null;
|
|
||||||
|
|
||||||
public object Clone() => (Person)MemberwiseClone();
|
|
||||||
|
|
||||||
public int CompareTo(object? obj)
|
|
||||||
{
|
|
||||||
if(obj is null) return 1;
|
|
||||||
if (obj is not Person other) throw new ArgumentException("Object is not a Person");
|
|
||||||
return CompareTo(other);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int CompareTo(Person? other)
|
|
||||||
{
|
|
||||||
if (other is null) return 1;
|
|
||||||
if (ReferenceEquals(this, other)) return 0;
|
|
||||||
return string.Compare(Id, other.Id, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
public const string SystemPersonId = "00000000-0000-0000-0000-000000000001";
|
|
||||||
|
|
||||||
public static readonly Person SystemPerson = new()
|
|
||||||
{
|
|
||||||
Id = SystemPersonId,
|
|
||||||
Name = "System",
|
|
||||||
CreatedAt = DateTime.UtcNow.ToString("dd-MM-yyyy HH:mm:ss zz"),
|
|
||||||
User = User.SystemUser
|
|
||||||
};
|
|
||||||
}
|
|
@@ -1,66 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using Transactional.Abstractions.Interfaces;
|
|
||||||
|
|
||||||
namespace back.DataModels;
|
|
||||||
|
|
||||||
[Table("Photos")]
|
|
||||||
public partial class Photo : IEntity<Photo>
|
|
||||||
{
|
|
||||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
|
||||||
public string Id { get; set; } = null!;
|
|
||||||
[Required, MaxLength(100), MinLength(1)]
|
|
||||||
public string Title { get; set; } = null!;
|
|
||||||
[MaxLength(500)]
|
|
||||||
public string? Description { get; set; }
|
|
||||||
public string? Extension { get; set; }
|
|
||||||
public string? LowResUrl { get; set; }
|
|
||||||
public string? MidResUrl { get; set; }
|
|
||||||
public string? HighResUrl { get; set; }
|
|
||||||
public string? CreatedAt { get; set; }
|
|
||||||
public string? UpdatedAt { get; set; }
|
|
||||||
public string CreatedBy { get; set; } = null!;
|
|
||||||
public string? UpdatedBy { get; set; }
|
|
||||||
public string? EventId { get; set; }
|
|
||||||
public string? RankingId { get; set; }
|
|
||||||
public int? IsFavorite { get; set; }
|
|
||||||
public int? IsPublic { get; set; }
|
|
||||||
public int? IsArchived { get; set; }
|
|
||||||
public virtual Person CreatedByNavigation { get; set; } = null!;
|
|
||||||
public virtual Event? Event { get; set; }
|
|
||||||
public virtual ICollection<Gallery> Galleries { get; set; } = [];
|
|
||||||
public virtual ICollection<Person> People { get; set; } = [];
|
|
||||||
public virtual ICollection<Tag> Tags { get; set; } = [];
|
|
||||||
public virtual ICollection<User> Users { get; set; } = [];
|
|
||||||
|
|
||||||
public override int GetHashCode() => HashCode.Combine(Id, Title);
|
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
=> obj is Photo otherEvent && Equals(otherEvent);
|
|
||||||
|
|
||||||
public bool Equals(Photo? other)
|
|
||||||
{
|
|
||||||
if (other is null) return false;
|
|
||||||
if (ReferenceEquals(this, other)) return true;
|
|
||||||
return
|
|
||||||
Id == other.Id || GetHashCode() == other.GetHashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsNull => this is null;
|
|
||||||
|
|
||||||
public object Clone() => (Photo)MemberwiseClone();
|
|
||||||
|
|
||||||
public int CompareTo(object? obj)
|
|
||||||
{
|
|
||||||
if (obj is null) return 1;
|
|
||||||
if (obj is not Photo other) throw new ArgumentException("Object is not a Person");
|
|
||||||
return CompareTo(other);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int CompareTo(Photo? other)
|
|
||||||
{
|
|
||||||
if (other is null) return 1;
|
|
||||||
if (ReferenceEquals(this, other)) return 0;
|
|
||||||
return string.Compare(Id, other.Id, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,119 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace back.DataModels;
|
|
||||||
|
|
||||||
public class RankingGroup
|
|
||||||
{
|
|
||||||
public const float ExtremlyBad = 1/5f;
|
|
||||||
public const float Bad = 2 / 5f;
|
|
||||||
public const float Normal = 3 / 5f;
|
|
||||||
public const float Good = 4 / 5f;
|
|
||||||
public const float ExtremlyGood = 5 / 5f;
|
|
||||||
|
|
||||||
public static string GetGroup(float score) => (float)Math.Ceiling(score) switch
|
|
||||||
{
|
|
||||||
<= ExtremlyBad => nameof(ExtremlyBad),
|
|
||||||
<= Bad => nameof(Bad),
|
|
||||||
<= Normal => nameof(Normal),
|
|
||||||
<= Good => nameof(Good),
|
|
||||||
_ => nameof(ExtremlyGood)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[Table("Rankings")]
|
|
||||||
public partial class Ranking : IEquatable<Ranking>
|
|
||||||
{
|
|
||||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
|
||||||
public string Id { get; set; } = null!;
|
|
||||||
public int TotalVotes { get; set; }
|
|
||||||
public int UpVotes { get; set; }
|
|
||||||
public int DownVotes { get; set; }
|
|
||||||
|
|
||||||
public Ranking(int totalVotes, int upVotes = 0, int downVotes = 0)
|
|
||||||
{
|
|
||||||
TotalVotes = totalVotes;
|
|
||||||
UpVotes = upVotes;
|
|
||||||
DownVotes = downVotes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Ranking()
|
|
||||||
{
|
|
||||||
TotalVotes = 0;
|
|
||||||
UpVotes = 0;
|
|
||||||
DownVotes = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DownVote()
|
|
||||||
{
|
|
||||||
DownVotes++;
|
|
||||||
TotalVotes++;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpVote()
|
|
||||||
{
|
|
||||||
UpVotes++;
|
|
||||||
TotalVotes++;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float Score
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (TotalVotes == 0) return 0;
|
|
||||||
return (float)(UpVotes - DownVotes) / TotalVotes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Group => RankingGroup.GetGroup(Score);
|
|
||||||
|
|
||||||
public override int GetHashCode() => HashCode.Combine(Id, TotalVotes, UpVotes, DownVotes);
|
|
||||||
|
|
||||||
public override bool Equals(object? obj) => obj is Ranking otherEvent && Equals(otherEvent);
|
|
||||||
|
|
||||||
public bool Equals(Ranking? other)
|
|
||||||
{
|
|
||||||
if (other is null) return false;
|
|
||||||
if (ReferenceEquals(this, other)) return true;
|
|
||||||
return
|
|
||||||
Id == other.Id
|
|
||||||
|| GetHashCode() == other.GetHashCode()
|
|
||||||
|| (TotalVotes == other.TotalVotes && UpVotes == other.UpVotes && DownVotes == other.DownVotes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(Ranking ranking1, Ranking ranking2)
|
|
||||||
{
|
|
||||||
if (ranking1 is null && ranking2 is null) return true;
|
|
||||||
if (ranking1 is null || ranking2 is null) return false;
|
|
||||||
return ranking1.Equals(ranking2);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(Ranking ranking1, Ranking ranking2)
|
|
||||||
{
|
|
||||||
if (ranking1 is null && ranking2 is null) return false;
|
|
||||||
if (ranking1 is null || ranking2 is null) return true;
|
|
||||||
return !ranking1.Equals(ranking2);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator < (Ranking ranking1, Ranking ranking2)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(ranking1, nameof(ranking1));
|
|
||||||
ArgumentNullException.ThrowIfNull(ranking2, nameof(ranking2));
|
|
||||||
return ranking1.Score < ranking2.Score;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator > (Ranking ranking1, Ranking ranking2)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(ranking1, nameof(ranking1));
|
|
||||||
ArgumentNullException.ThrowIfNull(ranking2, nameof(ranking2));
|
|
||||||
if (ranking1 is null && ranking2 is null) return true;
|
|
||||||
if (ranking1 is null || ranking2 is null) return false;
|
|
||||||
return ranking1.Score > ranking2.Score;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator <= (Ranking ranking1, Ranking ranking2)
|
|
||||||
=> ranking1 == ranking2 || ranking1 < ranking2;
|
|
||||||
|
|
||||||
public static bool operator >= (Ranking ranking1, Ranking ranking2)
|
|
||||||
=> ranking1 == ranking2 || ranking1 > ranking2;
|
|
||||||
}
|
|
@@ -1,94 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace back.DataModels;
|
|
||||||
|
|
||||||
[Table("Roles")]
|
|
||||||
public partial class Role : IEquatable<Role>
|
|
||||||
{
|
|
||||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
|
||||||
public string Id { get; set; } = null!;
|
|
||||||
[Required, MaxLength(100)]
|
|
||||||
public string Name { get; set; } = null!;
|
|
||||||
[MaxLength(250)]
|
|
||||||
public string? Description { get; set; }
|
|
||||||
public string? BaseRoleModelId { get; set; }
|
|
||||||
public virtual Role? BaseRoleModel { get; set; }
|
|
||||||
public virtual ICollection<Role> InverseBaseRoleModel { get; set; } = [];
|
|
||||||
public virtual ICollection<Permission> Permissions { get; set; } = new HashSet<Permission>();
|
|
||||||
public virtual ICollection<User> Users { get; set; } = [];
|
|
||||||
|
|
||||||
|
|
||||||
public bool IsAdmin() => BaseRoleModel != null ? BaseRoleModel.IsAdmin() : Id == AdminRole.Id;
|
|
||||||
public bool IsContentManager() => BaseRoleModel != null ? BaseRoleModel.IsContentManager() : Id == ContentManagerRole.Id;
|
|
||||||
public bool IsUser() => BaseRoleModel != null ? BaseRoleModel.IsUser() : Id == UserRole.Id;
|
|
||||||
|
|
||||||
public bool HasPermission(Permission permission)
|
|
||||||
{
|
|
||||||
var baseRoleHasPermission = BaseRoleModel != null && BaseRoleModel.HasPermission(permission);
|
|
||||||
return baseRoleHasPermission || Permissions.Any(p => p.Id == permission.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Role() { }
|
|
||||||
|
|
||||||
public Role(string id, string name, string description, List<Permission>? permissions = null, Role? baseRoleModel = null)
|
|
||||||
{
|
|
||||||
Id = id;
|
|
||||||
Name = name;
|
|
||||||
Description = description;
|
|
||||||
Permissions = permissions ?? [];
|
|
||||||
if (baseRoleModel != null)
|
|
||||||
{
|
|
||||||
BaseRoleModel = baseRoleModel;
|
|
||||||
BaseRoleModelId = baseRoleModel.Id;
|
|
||||||
foreach (var permission in baseRoleModel.Permissions)
|
|
||||||
{
|
|
||||||
if (!Permissions.Any(p => p.Id == permission.Id))
|
|
||||||
{
|
|
||||||
Permissions.Add(permission);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode() => HashCode.Combine(Id, Name);
|
|
||||||
|
|
||||||
public override bool Equals(object? obj) => obj is Role otherEvent && Equals(otherEvent);
|
|
||||||
|
|
||||||
public bool Equals(Role? other)
|
|
||||||
{
|
|
||||||
if (other is null) return false;
|
|
||||||
if (ReferenceEquals(this, other)) return true;
|
|
||||||
return Id == other.Id || GetHashCode() == other.GetHashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static readonly Role UserRole = new(
|
|
||||||
"1", "User", "Role for regular users",
|
|
||||||
[
|
|
||||||
Permission.ViewContentPermission,
|
|
||||||
Permission.LikeContentPermission
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
public static readonly Role ContentManagerRole = new(
|
|
||||||
"2", "Content Manager", "Role for managing content",
|
|
||||||
[
|
|
||||||
Permission.CreateContentPermission,
|
|
||||||
Permission.EditContentPermission,
|
|
||||||
Permission.DeleteContentPermission
|
|
||||||
],
|
|
||||||
UserRole
|
|
||||||
);
|
|
||||||
|
|
||||||
public static readonly Role AdminRole = new(
|
|
||||||
"3", "Admin", "Administrator role with full permissions",
|
|
||||||
[
|
|
||||||
Permission.CreateUserPermission,
|
|
||||||
Permission.DisableUserPermission,
|
|
||||||
Permission.EditUserPermission,
|
|
||||||
Permission.DeleteUserPermission,
|
|
||||||
Permission.EditWebConfigPermission
|
|
||||||
],
|
|
||||||
ContentManagerRole
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,33 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace back.DataModels;
|
|
||||||
|
|
||||||
[Table("SocialMedia")]
|
|
||||||
public partial class SocialMedia: IEquatable<SocialMedia>
|
|
||||||
{
|
|
||||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
|
||||||
public string Id { get; set; } = null!;
|
|
||||||
public string? Facebook { get; set; }
|
|
||||||
public string? Instagram { get; set; }
|
|
||||||
public string? Twitter { get; set; }
|
|
||||||
public string? BlueSky { get; set; }
|
|
||||||
public string? Tiktok { get; set; }
|
|
||||||
public string? Linkedin { get; set; }
|
|
||||||
public string? Pinterest { get; set; }
|
|
||||||
public string? Discord { get; set; }
|
|
||||||
public string? Reddit { get; set; }
|
|
||||||
public string? Other { get; set; }
|
|
||||||
public virtual ICollection<Person> People { get; set; } = [];
|
|
||||||
|
|
||||||
public override int GetHashCode() => HashCode.Combine(Id);
|
|
||||||
|
|
||||||
public override bool Equals(object? obj) => obj is SocialMedia otherEvent && Equals(otherEvent);
|
|
||||||
|
|
||||||
public bool Equals(SocialMedia? other)
|
|
||||||
{
|
|
||||||
if (other is null) return false;
|
|
||||||
if (ReferenceEquals(this, other)) return true;
|
|
||||||
return GetHashCode() == other.GetHashCode();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,15 +0,0 @@
|
|||||||
namespace back.DataModels;
|
|
||||||
|
|
||||||
public class SystemKey
|
|
||||||
{
|
|
||||||
public string Email { get; set; } = User.SystemUser.Email;
|
|
||||||
public string Key { get; set; } = Guid.NewGuid().ToString();
|
|
||||||
public required string Password { get; set; }
|
|
||||||
|
|
||||||
public bool IsValid(string email, string password, string key)
|
|
||||||
{
|
|
||||||
return Email.Equals(email, StringComparison.InvariantCultureIgnoreCase) &&
|
|
||||||
Password.Equals(password, StringComparison.InvariantCulture) &&
|
|
||||||
Key.Equals(key, StringComparison.InvariantCulture);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,32 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace back.DataModels;
|
|
||||||
|
|
||||||
[Table("Tags")]
|
|
||||||
public partial class Tag: IEquatable<Tag>
|
|
||||||
{
|
|
||||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
|
||||||
public string Id { get; set; } = null!;
|
|
||||||
[Required, MaxLength(25)]
|
|
||||||
public string Name { get; set; } = null!;
|
|
||||||
[Required]
|
|
||||||
public string CreatedAt { get; set; } = null!;
|
|
||||||
|
|
||||||
public virtual ICollection<Event> Events { get; set; } = [];
|
|
||||||
|
|
||||||
public virtual ICollection<Gallery> Galleries { get; set; } = [];
|
|
||||||
|
|
||||||
public virtual ICollection<Photo> Photos { get; set; } = [];
|
|
||||||
|
|
||||||
public override int GetHashCode() => HashCode.Combine(Id, Name);
|
|
||||||
|
|
||||||
public override bool Equals(object? obj) => obj is Tag tag && Equals(tag);
|
|
||||||
|
|
||||||
public bool Equals(Tag? other)
|
|
||||||
{
|
|
||||||
if (other is null) return false;
|
|
||||||
if (ReferenceEquals(this, other)) return true;
|
|
||||||
return Id == other.Id || GetHashCode() == other.GetHashCode();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,86 +0,0 @@
|
|||||||
using back.DTO;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using Transactional.Abstractions.Interfaces;
|
|
||||||
|
|
||||||
namespace back.DataModels;
|
|
||||||
|
|
||||||
[Table("Users")]
|
|
||||||
public class User : IEntity<User>
|
|
||||||
{
|
|
||||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
|
||||||
public string Id { get; set; } = null!;
|
|
||||||
[Required, EmailAddress]
|
|
||||||
public string Email { get; set; } = null!;
|
|
||||||
[Required, MinLength(8)]
|
|
||||||
public string Password { get; set; } = null!;
|
|
||||||
[Required]
|
|
||||||
public string Salt { get; set; } = null!;
|
|
||||||
public string CreatedAt { get; set; } = null!;
|
|
||||||
|
|
||||||
public virtual Person IdNavigation { get; set; } = null!;
|
|
||||||
public virtual ICollection<Gallery> Galleries { get; set; } = [];
|
|
||||||
public virtual ICollection<Gallery> GalleriesNavigation { get; set; } = [];
|
|
||||||
public virtual ICollection<Photo> Photos { get; set; } = [];
|
|
||||||
public virtual ICollection<Role> Roles { get; set; } = [];
|
|
||||||
|
|
||||||
public User() { }
|
|
||||||
public User(string id, string email, string password, DateTimeOffset createdAt)
|
|
||||||
{
|
|
||||||
Id = id;
|
|
||||||
Email = email;
|
|
||||||
Password = password;
|
|
||||||
CreatedAt = createdAt.ToString("dd-MM-yyyy HH:mm:ss zz");
|
|
||||||
}
|
|
||||||
|
|
||||||
public UserDto ToDto() => new()
|
|
||||||
{
|
|
||||||
Id = Id,
|
|
||||||
Roles = Roles
|
|
||||||
};
|
|
||||||
|
|
||||||
public bool IsAdmin() => Roles.Any(r => r.IsAdmin());
|
|
||||||
public bool IsContentManager() => Roles.Any(r => r.IsContentManager());
|
|
||||||
public bool IsUser() => Roles.Any(r => r.IsUser());
|
|
||||||
|
|
||||||
public override int GetHashCode() => HashCode.Combine(Id, Email);
|
|
||||||
|
|
||||||
public override bool Equals(object? obj) => obj is User otherEvent && Equals(otherEvent);
|
|
||||||
|
|
||||||
public bool Equals(User? other)
|
|
||||||
{
|
|
||||||
if (other is null) return false;
|
|
||||||
if (ReferenceEquals(this, other)) return true;
|
|
||||||
return Id == other.Id && Email == other.Email;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsNull => this is null;
|
|
||||||
|
|
||||||
public object Clone() => (User)MemberwiseClone();
|
|
||||||
|
|
||||||
public int CompareTo(object? obj)
|
|
||||||
{
|
|
||||||
if (obj is null) return 1;
|
|
||||||
if (obj is not User other) throw new ArgumentException("Object is not a Person");
|
|
||||||
return CompareTo(other);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int CompareTo(User? other)
|
|
||||||
{
|
|
||||||
if (other is null) return 1;
|
|
||||||
if (ReferenceEquals(this, other)) return 0;
|
|
||||||
return string.Compare(Id, other.Id, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
public const string SystemUserId = "00000000-0000-0000-0000-000000000001";
|
|
||||||
|
|
||||||
public static readonly User SystemUser = new(
|
|
||||||
id: SystemUserId,
|
|
||||||
email: "sys@t.em",
|
|
||||||
password: "",
|
|
||||||
createdAt: DateTime.UtcNow
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Roles = [Role.AdminRole, Role.ContentManagerRole, Role.UserRole]
|
|
||||||
};
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
namespace back.Options;
|
|
||||||
|
|
||||||
public sealed class DatabaseConfig
|
|
||||||
{
|
|
||||||
public const string BlobStorage = "Databases:Blob";
|
|
||||||
public const string DataStorage = "Databases:Data";
|
|
||||||
public required string Provider { get; set; }
|
|
||||||
public string? DatabaseName { get; set; }
|
|
||||||
public string? AccountKey { get; set; }
|
|
||||||
public string? TokenCredential { get; set; }
|
|
||||||
public string? BaseUrl { get; set; }
|
|
||||||
public string? ConnectionString { get; set; }
|
|
||||||
public string? SystemContainer { get; set; }
|
|
||||||
}
|
|
@@ -1,6 +0,0 @@
|
|||||||
namespace back.Options;
|
|
||||||
|
|
||||||
public sealed class Databases
|
|
||||||
{
|
|
||||||
public string? BaseDirectory { get; set; }
|
|
||||||
}
|
|
@@ -1,10 +0,0 @@
|
|||||||
namespace back.Options;
|
|
||||||
|
|
||||||
public sealed class MailServerOptions
|
|
||||||
{
|
|
||||||
public required string SmtpServer { get; set; }
|
|
||||||
public required int Puerto { get; set; }
|
|
||||||
public required string Usuario { get; set; }
|
|
||||||
public required string Password { get; set; }
|
|
||||||
public bool EnableSsl { get; set; }
|
|
||||||
}
|
|
@@ -1,52 +0,0 @@
|
|||||||
using back.ServicesExtensions;
|
|
||||||
using healthchecks;
|
|
||||||
|
|
||||||
namespace back;
|
|
||||||
|
|
||||||
public class Program
|
|
||||||
{
|
|
||||||
public static void Main(string[] args)
|
|
||||||
{
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
|
||||||
|
|
||||||
builder.Services.UseExtensions();
|
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
|
||||||
|
|
||||||
builder.Services.AddHealthChecks(options => {
|
|
||||||
options.CacheDuration = TimeSpan.FromMinutes(30);
|
|
||||||
options.Timeout = TimeSpan.FromSeconds(5);
|
|
||||||
options.AssembliesToScan = [typeof(Program).Assembly];
|
|
||||||
}).DiscoverHealthChecks();
|
|
||||||
|
|
||||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
|
||||||
builder.Services.AddSwaggerGen();
|
|
||||||
|
|
||||||
builder.Services.AddCors(options =>
|
|
||||||
{
|
|
||||||
options.AddPolicy("AllowAll",
|
|
||||||
builder => builder.AllowAnyOrigin()
|
|
||||||
.AllowAnyMethod()
|
|
||||||
.AllowAnyHeader());
|
|
||||||
});
|
|
||||||
|
|
||||||
var app = builder.Build();
|
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
|
||||||
if (app.Environment.IsDevelopment())
|
|
||||||
{
|
|
||||||
app.UseSwagger();
|
|
||||||
app.UseSwaggerUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
|
||||||
|
|
||||||
app.UseAuthorization();
|
|
||||||
|
|
||||||
app.UseCors("AllowAll");
|
|
||||||
|
|
||||||
app.MapControllers();
|
|
||||||
|
|
||||||
app.Run();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
|
||||||
"profiles": {
|
|
||||||
"http": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": false,
|
|
||||||
"applicationUrl": "http://localhost:5250",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": true,
|
|
||||||
"launchUrl": "swagger",
|
|
||||||
"applicationUrl": "https://localhost:7273;http://localhost:5250",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,49 +0,0 @@
|
|||||||
using back.Options;
|
|
||||||
using back.persistance.data;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace back.ServicesExtensions;
|
|
||||||
|
|
||||||
public static partial class ServicesExtensions
|
|
||||||
{
|
|
||||||
private static IServiceCollection AddDatabaseContext(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddContext<DataContext>();
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
private static IServiceCollection AddContext<T>(this IServiceCollection services)
|
|
||||||
where T : DbContext
|
|
||||||
{
|
|
||||||
var config = services
|
|
||||||
.BuildServiceProvider()
|
|
||||||
.GetRequiredService<IOptionsSnapshot<DatabaseConfig>>()
|
|
||||||
.Get(DatabaseConfig.DataStorage);
|
|
||||||
|
|
||||||
services.AddDbContext<T>(options =>
|
|
||||||
{
|
|
||||||
options.UseDatabaseConfig(config);
|
|
||||||
});
|
|
||||||
|
|
||||||
using var scope = services.BuildServiceProvider().CreateScope();
|
|
||||||
var context = scope.ServiceProvider
|
|
||||||
.GetRequiredService<T>();
|
|
||||||
var isDevelopment = scope.ServiceProvider
|
|
||||||
.GetRequiredService<IHostEnvironment>()
|
|
||||||
.IsDevelopment();
|
|
||||||
|
|
||||||
if (isDevelopment && !context.Database.HasPendingModelChanges())
|
|
||||||
{
|
|
||||||
context.Database.EnsureCreated();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Database.EnsureCreated();
|
|
||||||
context.Database.Migrate();
|
|
||||||
}
|
|
||||||
context.SaveChanges();
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,92 +0,0 @@
|
|||||||
using back.Options;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace back.ServicesExtensions;
|
|
||||||
|
|
||||||
public enum DatabaseProvider
|
|
||||||
{
|
|
||||||
/* -- Relational databases supported by EF Core -- */
|
|
||||||
SUPPORTED, // Placeholder for supported databases.
|
|
||||||
InMemory,
|
|
||||||
Sqlite,
|
|
||||||
PostgreSQL,
|
|
||||||
CockroachDB, // CockroachDB is compatible with PostgreSQL.
|
|
||||||
SQLServer,
|
|
||||||
MariaDB,
|
|
||||||
MySQL,
|
|
||||||
Oracle, // Oracle is supported by EF Core but requires a separate package.
|
|
||||||
|
|
||||||
/* -- NoSQL are not supported by EF -- */
|
|
||||||
|
|
||||||
NOT_SUPPORTED, // Placeholder for unsupported databases.
|
|
||||||
Firebird, // Firebird is supported by EF Core but requires a separate package.
|
|
||||||
Db2, // Db2 is supported by EF Core but requires a separate package.
|
|
||||||
SAPHana, // SAP HANA is supported by EF Core but requires a separate package.
|
|
||||||
Sybase, // Sybase is supported by EF Core but requires a separate package.
|
|
||||||
Cosmos, // Cosmos DB is database supported by EF Core.
|
|
||||||
MongoDB,
|
|
||||||
InfluxDB,
|
|
||||||
Redis,
|
|
||||||
Cassandra,
|
|
||||||
ElasticSearch,
|
|
||||||
CouchDB,
|
|
||||||
RavenDB,
|
|
||||||
Neo4j,
|
|
||||||
OrientDB,
|
|
||||||
ArangoDB,
|
|
||||||
ClickHouse,
|
|
||||||
Druid,
|
|
||||||
TimescaleDB,
|
|
||||||
}
|
|
||||||
|
|
||||||
public static partial class DbContextOptionsBuilderExtensions
|
|
||||||
{
|
|
||||||
private static string SupportedDbs()
|
|
||||||
=> string.Join(", ", Enum.GetValues<DatabaseProvider>()
|
|
||||||
.Where(db => db > DatabaseProvider.SUPPORTED && db < DatabaseProvider.NOT_SUPPORTED)
|
|
||||||
.OrderBy(db => db)
|
|
||||||
.Select(db => db.ToString()));
|
|
||||||
|
|
||||||
public static void UseDatabaseConfig(this DbContextOptionsBuilder options, DatabaseConfig config)
|
|
||||||
{
|
|
||||||
var providerName = Enum.GetNames<DatabaseProvider>()
|
|
||||||
.FirstOrDefault(name => name.Equals(config.Provider, StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
if (!Enum.TryParse(providerName, out DatabaseProvider provider))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Unsupported database provider: {config.Provider} -- Supported providers are: {SupportedDbs()}");
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (provider)
|
|
||||||
{
|
|
||||||
case DatabaseProvider.Sqlite:
|
|
||||||
var match = SQLiteRegex().Match(config.ConnectionString ?? string.Empty);
|
|
||||||
if (match.Success)
|
|
||||||
{
|
|
||||||
string? folder = null;
|
|
||||||
string path = match.Groups[1].Value.Replace("\\", "/");
|
|
||||||
folder = path.Contains('/') ? path[..path.IndexOf('/')] : path;
|
|
||||||
Directory.CreateDirectory(folder);
|
|
||||||
}
|
|
||||||
options.UseSqlite(config.ConnectionString);
|
|
||||||
break;
|
|
||||||
case DatabaseProvider.InMemory:
|
|
||||||
options.UseInMemoryDatabase(config.ConnectionString);
|
|
||||||
break;
|
|
||||||
case DatabaseProvider.PostgreSQL or DatabaseProvider.CockroachDB:
|
|
||||||
options.UseNpgsql(config.ConnectionString);
|
|
||||||
break;
|
|
||||||
case DatabaseProvider.SQLServer:
|
|
||||||
options.UseSqlServer(config.ConnectionString);
|
|
||||||
break;
|
|
||||||
case DatabaseProvider.MySQL or DatabaseProvider.MariaDB:
|
|
||||||
options.UseMySql(config.ConnectionString, ServerVersion.AutoDetect(config.ConnectionString));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new InvalidOperationException($"Unsupported database provider: {config.Provider}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[GeneratedRegex(@"Data Source=([^;]+)")]
|
|
||||||
private static partial Regex SQLiteRegex();
|
|
||||||
}
|
|
@@ -1,76 +0,0 @@
|
|||||||
using back.healthchecks.Options;
|
|
||||||
using back.Options;
|
|
||||||
|
|
||||||
namespace back.ServicesExtensions;
|
|
||||||
|
|
||||||
public static partial class ServicesExtensions
|
|
||||||
{
|
|
||||||
private static IConfiguration ConfigureOptions(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
IConfiguration config = services.BuildServiceProvider().GetRequiredService<IConfiguration>();
|
|
||||||
string? baseDirectory = null;
|
|
||||||
|
|
||||||
services.Configure<Databases>(config.GetSection(nameof(Databases)));
|
|
||||||
services.Configure<DatabaseConfig>(DatabaseConfig.DataStorage, config.GetSection(DatabaseConfig.DataStorage));
|
|
||||||
services.Configure<DatabaseConfig>(DatabaseConfig.BlobStorage, config.GetSection(DatabaseConfig.BlobStorage));
|
|
||||||
services.Configure<MailServerOptions>(config.GetSection(nameof(MailServerOptions)));
|
|
||||||
|
|
||||||
services.Configure<HealthChecksConfigs>(HealthChecksConfigs.Sqlite, config.GetSection(HealthChecksConfigs.Sqlite));
|
|
||||||
|
|
||||||
services.PostConfigure<Databases>(databases =>
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(databases.BaseDirectory) && !Directory.Exists(databases.BaseDirectory))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(databases.BaseDirectory);
|
|
||||||
Console.WriteLine($"Base directory created at: {databases.BaseDirectory}");
|
|
||||||
baseDirectory = databases.BaseDirectory;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Failed to create base directory at {databases.BaseDirectory}. " +
|
|
||||||
"Please ensure the path is valid and accessible.", ex
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
services.PostConfigure<DatabaseConfig>(DatabaseConfig.DataStorage, config =>
|
|
||||||
{
|
|
||||||
PostConfigureDatabaseConfig(config, baseDirectory);
|
|
||||||
});
|
|
||||||
|
|
||||||
services.PostConfigure<DatabaseConfig>(DatabaseConfig.BlobStorage, config =>
|
|
||||||
{
|
|
||||||
PostConfigureDatabaseConfig(config, baseDirectory);
|
|
||||||
});
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void PostConfigureDatabaseConfig(DatabaseConfig config, string? baseDirectory)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(config.SystemContainer))
|
|
||||||
{
|
|
||||||
var path = config.SystemContainer;
|
|
||||||
if (!string.IsNullOrEmpty(baseDirectory))
|
|
||||||
{
|
|
||||||
path = Path.Combine(baseDirectory, path);
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(path);
|
|
||||||
Console.WriteLine($"System container for {config.Provider} created at: {path}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Failed to create system container at {path}. " +
|
|
||||||
"Please ensure the path is valid and accessible.", ex
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,49 +0,0 @@
|
|||||||
using back.persistance.data;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using back.services.engine.SystemUser;
|
|
||||||
using DependencyInjector;
|
|
||||||
using System.Text.Json;
|
|
||||||
using Transactional.Abstractions.Interfaces;
|
|
||||||
using Transactional.Implementations.EntityFramework;
|
|
||||||
|
|
||||||
namespace back.ServicesExtensions;
|
|
||||||
|
|
||||||
public static partial class ServicesExtensions
|
|
||||||
{
|
|
||||||
public static IServiceCollection UseExtensions(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
//var config =
|
|
||||||
services.ConfigureOptions();
|
|
||||||
|
|
||||||
services.AddMemoryCache();
|
|
||||||
|
|
||||||
services.AddDatabaseContext();
|
|
||||||
services.AddServices();
|
|
||||||
|
|
||||||
services.AddScoped<ITransactionalService<DataContext>, EntityFrameworkTransactionalService<DataContext>>();
|
|
||||||
|
|
||||||
services.AddSingleton(new JsonSerializerOptions {
|
|
||||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
|
||||||
AllowTrailingCommas = true,
|
|
||||||
PropertyNameCaseInsensitive = true,
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
WriteIndented = true,
|
|
||||||
Converters = {
|
|
||||||
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase),
|
|
||||||
},
|
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
||||||
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString,
|
|
||||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
|
||||||
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
|
|
||||||
UnknownTypeHandling = JsonUnknownTypeHandling.JsonElement,
|
|
||||||
});
|
|
||||||
|
|
||||||
using var scope = services.BuildServiceProvider().CreateScope();
|
|
||||||
scope.ServiceProvider
|
|
||||||
.GetRequiredService<ISystemUserGenerator>().GenerateAsync().Wait();
|
|
||||||
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"Databases": {
|
|
||||||
"BaseDirectory": ".program_data",
|
|
||||||
"Data": {
|
|
||||||
"Provider": "sqlite",
|
|
||||||
"ConnectionString": "Data Source=.program_data/app.db"
|
|
||||||
},
|
|
||||||
"Blob": {
|
|
||||||
"Provider": "system",
|
|
||||||
"baseUrl": "https://localhost:7273/api/photo/{id}/{res}",
|
|
||||||
"SystemContainer": "imgs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MailServerOptions": {
|
|
||||||
"SmtpServer": "smtp.gmail.com",
|
|
||||||
"Puerto": 587,
|
|
||||||
"Usuario": "",
|
|
||||||
"Password": "",
|
|
||||||
"EnableSsl": true
|
|
||||||
},
|
|
||||||
"HealthChecksConfigs": {
|
|
||||||
"Sqlite": {
|
|
||||||
"RetryAttempts" : 2,
|
|
||||||
"Timeout" : "00:05:00",
|
|
||||||
"RetryDelay" : "00:00:10",
|
|
||||||
"Severity": "Info"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Databases": {
|
|
||||||
"Data": {
|
|
||||||
"Provider": "sqlite",
|
|
||||||
"ConnectionString": "Data Source=data/app.db;Cache=Shared"
|
|
||||||
},
|
|
||||||
"Blob": {
|
|
||||||
"Provider": "system",
|
|
||||||
"baseUrl": "https://back.mmorales.photo/api/photo/{id}/{res}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*"
|
|
||||||
}
|
|
@@ -1,55 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Azure.Identity" Version="1.15.0" />
|
|
||||||
<PackageReference Include="MailKit" Version="4.13.0" />
|
|
||||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
|
||||||
<PackageReference Include="Mapster.DependencyInjection" Version="1.0.1" />
|
|
||||||
<PackageReference Include="Mapster.EFCore" Version="5.1.1" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.8" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.8" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="9.0.8" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="9.0.8" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.8" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.8" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.8" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.8" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.8">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.UnitOfWork" Version="3.1.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.8" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.8" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.8" />
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
|
||||||
<PackageReference Include="Oracle.EntityFrameworkCore" Version="9.23.90" />
|
|
||||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="9.0.3" />
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="9.0.3" />
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3" />
|
|
||||||
<PackageReference Include="System.Diagnostics.EventLog" Version="9.0.8" />
|
|
||||||
<PackageReference Include="System.Text.Json" Version="9.0.8" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\nuget\DependencyInjector\DependencyInjector.csproj" />
|
|
||||||
<ProjectReference Include="..\..\nuget\healthchecks\healthchecks.csproj" />
|
|
||||||
<ProjectReference Include="..\..\nuget\Transactional\Transactional.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@@ -1,43 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
# Visual Studio Version 17
|
|
||||||
VisualStudioVersion = 17.14.36401.2
|
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "back", "back.csproj", "{392278F3-4B36-47F4-AD31-5FBFCC181AD4}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Transactional", "..\..\nuget\Transactional\Transactional.csproj", "{ED76105A-5E6F-4997-86FE-6A7902A2AEBA}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependencyInjector", "..\..\nuget\DependencyInjector\DependencyInjector.csproj", "{DBDF84A4-235C-4F29-8626-5BD1DC255970}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "healthchecks", "..\..\nuget\healthchecks\healthchecks.csproj", "{B21E2BEF-17B7-4981-9843-C0CC36D67010}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{392278F3-4B36-47F4-AD31-5FBFCC181AD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{392278F3-4B36-47F4-AD31-5FBFCC181AD4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{392278F3-4B36-47F4-AD31-5FBFCC181AD4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{392278F3-4B36-47F4-AD31-5FBFCC181AD4}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{ED76105A-5E6F-4997-86FE-6A7902A2AEBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{ED76105A-5E6F-4997-86FE-6A7902A2AEBA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{ED76105A-5E6F-4997-86FE-6A7902A2AEBA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{ED76105A-5E6F-4997-86FE-6A7902A2AEBA}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{DBDF84A4-235C-4F29-8626-5BD1DC255970}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{DBDF84A4-235C-4F29-8626-5BD1DC255970}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{DBDF84A4-235C-4F29-8626-5BD1DC255970}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{DBDF84A4-235C-4F29-8626-5BD1DC255970}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{B21E2BEF-17B7-4981-9843-C0CC36D67010}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{B21E2BEF-17B7-4981-9843-C0CC36D67010}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{B21E2BEF-17B7-4981-9843-C0CC36D67010}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{B21E2BEF-17B7-4981-9843-C0CC36D67010}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
|
||||||
SolutionGuid = {F531A9C8-70D1-45AA-B4AA-AC49FCADAE3D}
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
@@ -1,21 +0,0 @@
|
|||||||
using back.services.engine.Crypto;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace back.controllers;
|
|
||||||
|
|
||||||
[ApiController, Route("api/[controller]")]
|
|
||||||
public class CryptoController(ICryptoService cryptoService) : ControllerBase
|
|
||||||
{
|
|
||||||
[HttpGet("[action]")] public async Task<IActionResult> RSA([FromHeader(Name = "X-client-thumbprint")] string clientId)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(clientId))
|
|
||||||
{
|
|
||||||
return BadRequest("Client ID is required.");
|
|
||||||
}
|
|
||||||
var key = cryptoService.GetPublicCertificate(clientId);
|
|
||||||
if (key == null)
|
|
||||||
return StatusCode((int)HttpStatusCode.InternalServerError, "Failed to generate RSA keys.");
|
|
||||||
return Ok(new { PublicKey = key });
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,67 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using back.DTO;
|
|
||||||
using back.services.bussines.PhotoService;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace back.controllers;
|
|
||||||
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
[ApiController]
|
|
||||||
public class PhotosController(IPhotoService photoService) : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly IPhotoService _photoService = photoService;
|
|
||||||
|
|
||||||
// GET: api/<PhotoController>
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<ActionResult<IEnumerable<Photo>>> Get([FromQuery] int page = 1, [FromQuery] int pageSize = 20)
|
|
||||||
{
|
|
||||||
(int totalItems, IEnumerable<Photo>? pageData) = await _photoService.GetPage(page, pageSize);
|
|
||||||
Response.Headers.Append("X-Total-Count", totalItems.ToString());
|
|
||||||
return Ok(pageData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET api/<PhotoController>/5
|
|
||||||
[HttpGet("{id}/{res}")]
|
|
||||||
public async Task<IActionResult> Get(string id, string res)
|
|
||||||
{
|
|
||||||
(string? mediaType, byte[]? fileBytes) = await _photoService.GetBytes(id, res.ToLower());
|
|
||||||
if(fileBytes == null)
|
|
||||||
return NotFound();
|
|
||||||
return File(fileBytes, mediaType ?? "image/jpeg");
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST api/<PhotoController>
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<IActionResult> Post([FromForm] PhotoFormModel form)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (form.Image == null || form.Image.Length == 0)
|
|
||||||
return BadRequest("No image uploaded.");
|
|
||||||
|
|
||||||
await _photoService.Create(form);
|
|
||||||
|
|
||||||
return Created();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return BadRequest();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//// PUT api/<PhotoController>
|
|
||||||
[HttpPut]
|
|
||||||
public async Task<IActionResult> Put([FromBody] Photo photo)
|
|
||||||
{
|
|
||||||
await _photoService.Update(photo);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE api/<PhotoController>/5
|
|
||||||
[HttpDelete("{id}")]
|
|
||||||
public async Task<IActionResult> Delete(string id)
|
|
||||||
{
|
|
||||||
await _photoService.Delete(id);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,92 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using back.DTO;
|
|
||||||
using back.services.bussines;
|
|
||||||
using back.services.bussines.UserService;
|
|
||||||
using Mapster;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace back.controllers;
|
|
||||||
|
|
||||||
public record UserLoginFromModel(string Email, string Password, string? SystemKey);
|
|
||||||
public record ForgotPasswordFromModel(string Email);
|
|
||||||
public record RegisterFromModel(string Name, string Email, string Password);
|
|
||||||
|
|
||||||
[ApiController, Route("api/[controller]")]
|
|
||||||
public class UsersController(IUserService user) : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly IUserService _user = user;
|
|
||||||
// GET: api/<UsersController>
|
|
||||||
//[HttpGet]
|
|
||||||
//public async Task<ActionResult<IEnumerable<UserModel>>> Get([FromQuery] int page = 1, [FromQuery] int pageSize = 20)
|
|
||||||
//{
|
|
||||||
// var users = await _userContext.GetPage(page, pageSize);
|
|
||||||
// var totalItems = await _userContext.GetTotalItems();
|
|
||||||
// Response.Headers.Append("X-Total-Count", totalItems.ToString());
|
|
||||||
// return Ok(users);
|
|
||||||
//}
|
|
||||||
//// GET api/<UsersController>/5
|
|
||||||
//[HttpGet("{id}")]
|
|
||||||
//public async Task<IActionResult> Get(Guid id)
|
|
||||||
//{
|
|
||||||
// var user = await _userContext.GetById(id);
|
|
||||||
// if (user == null)
|
|
||||||
// return NotFound();
|
|
||||||
// return Ok(user);
|
|
||||||
//}
|
|
||||||
|
|
||||||
[HttpPost("[action]")]
|
|
||||||
public async Task<IActionResult> Login(
|
|
||||||
[FromHeader(Name = "X-client-thumbprint")] string clientId,
|
|
||||||
[FromBody] UserLoginFromModel user
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(clientId))
|
|
||||||
return BadRequest("Client ID cannot be null or empty");
|
|
||||||
|
|
||||||
if (user == null || string.IsNullOrEmpty(user.Email) || string.IsNullOrEmpty(user.Password))
|
|
||||||
return BadRequest(Errors.BadRequest.Description);
|
|
||||||
|
|
||||||
if (user.Email.Equals(DataModels.User.SystemUser.Email, StringComparison.InvariantCultureIgnoreCase))
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(user.SystemKey))
|
|
||||||
return Unauthorized(Errors.Unauthorized.Description);
|
|
||||||
var systemUser = await _user.ValidateSystemUser(user.Email, user.Password, user.SystemKey, clientId);
|
|
||||||
if (systemUser == null)
|
|
||||||
return Unauthorized(Errors.Unauthorized.Description);
|
|
||||||
return Ok(systemUser.Adapt<UserDto>());
|
|
||||||
}
|
|
||||||
|
|
||||||
var existingUser = await _user.Login(user.Email, user.Password, clientId);
|
|
||||||
if (existingUser == null)
|
|
||||||
return Unauthorized(Errors.Unauthorized.Description);
|
|
||||||
return Ok(existingUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("forgot-password")]
|
|
||||||
public async Task<IActionResult> ForgotPassword([FromBody] ForgotPasswordFromModel user)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(user.Email))
|
|
||||||
return BadRequest("Email cannot be null or empty");
|
|
||||||
await _user.SendResetPassword(user.Email);
|
|
||||||
return Ok("If the email exists, a reset password link has been sent.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST api/<UsersController>
|
|
||||||
[HttpPost("[action]")]
|
|
||||||
public async Task<IActionResult> Register(
|
|
||||||
[FromHeader(Name = "X-client-thumbprint")] string clientId,
|
|
||||||
[FromBody] RegisterFromModel user)
|
|
||||||
{
|
|
||||||
if (user == null)
|
|
||||||
return BadRequest("User cannot be null");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var createdUser = await _user.Create(clientId, new User() { Email = user.Email, Password = user.Password });
|
|
||||||
return Created();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return BadRequest(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,8 +0,0 @@
|
|||||||
using HealthChecksConfigsBase = healthchecks.Options.HealthChecksConfigs;
|
|
||||||
|
|
||||||
namespace back.healthchecks.Options;
|
|
||||||
|
|
||||||
public partial class HealthChecksConfigs : HealthChecksConfigsBase
|
|
||||||
{
|
|
||||||
public const string Sqlite = "Sqlite";
|
|
||||||
}
|
|
@@ -1,52 +0,0 @@
|
|||||||
using back.Options;
|
|
||||||
using healthchecks;
|
|
||||||
using healthchecks.Abstracts;
|
|
||||||
using back.healthchecks.Options;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace back.healthchecks;
|
|
||||||
|
|
||||||
public class SqliteHealthCheck(IOptionsMonitor<DatabaseConfig> databaseConfig, IOptionsMonitor<HealthChecksConfigs> healthchecksConfig) : IHealthCheck
|
|
||||||
{
|
|
||||||
private readonly DatabaseConfig databaseConfig = databaseConfig.Get(DatabaseConfig.DataStorage);
|
|
||||||
private readonly HealthChecksConfigs hcConfig = healthchecksConfig.Get(HealthChecksConfigs.Sqlite);
|
|
||||||
|
|
||||||
public string Description => "Conecta con la base de datos SQLite y trata de hacer una consulta sobre la tabla Users.";
|
|
||||||
public int? RetryAttempts => hcConfig.RetryAttempts ?? 2;
|
|
||||||
public TimeSpan? Timeout => hcConfig.Timeout ?? TimeSpan.FromSeconds(5);
|
|
||||||
public TimeSpan? RetryDelay => hcConfig.RetryDelay ?? TimeSpan.FromSeconds(1);
|
|
||||||
public HealthCheckSeverity? Severity => hcConfig.Severity ?? HealthCheckSeverity.Critical;
|
|
||||||
|
|
||||||
public Task<HealthCheckResult> CheckAsync(CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var isHealthy = false;
|
|
||||||
var details = string.Empty;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var connection = new Microsoft.Data.Sqlite.SqliteConnection(databaseConfig.ConnectionString);
|
|
||||||
connection.Open();
|
|
||||||
using var command = connection.CreateCommand();
|
|
||||||
command.CommandText = $"SELECT COUNT(1) FROM Users WHERE Id = '{DataModels.User.SystemUserId}';";
|
|
||||||
var result = command.ExecuteScalar();
|
|
||||||
if (result != null && Convert.ToInt32(result) == 1)
|
|
||||||
{
|
|
||||||
isHealthy = true;
|
|
||||||
details = "Connection to SQLite database successful and SystemUser exists.";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
details = "Connection to SQLite database successful but SystemUser does not exist.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
details = $"Failed to connect to SQLite database: {ex.Message}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult(new HealthCheckResult(isHealthy, null)
|
|
||||||
{
|
|
||||||
Details = details,
|
|
||||||
Severity = isHealthy ? HealthCheckSeverity.Info : HealthCheckSeverity.Critical
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,111 +0,0 @@
|
|||||||
using back.Options;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace back.persistance.blob;
|
|
||||||
|
|
||||||
public class FileSystemImageStorageService(
|
|
||||||
IOptions<Databases> systemOptions,
|
|
||||||
IOptionsMonitor<DatabaseConfig> options,
|
|
||||||
IMemoryCache memoryCache
|
|
||||||
) : IBlobStorageService
|
|
||||||
{
|
|
||||||
private readonly string RootPath = systemOptions.Value.BaseDirectory ?? "data";
|
|
||||||
private readonly DatabaseConfig config = options.Get(DatabaseConfig.BlobStorage);
|
|
||||||
private readonly IMemoryCache cache = memoryCache;
|
|
||||||
|
|
||||||
private string GetFullPath(string fileName)
|
|
||||||
{
|
|
||||||
// Ensure the directory exists
|
|
||||||
var path = Path.Join(RootPath, config.SystemContainer, fileName);
|
|
||||||
var directory = Path.GetDirectoryName(path);
|
|
||||||
if (directory != null && !Directory.Exists(directory))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(directory);
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Delete(string fileName)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var path = GetFullPath(fileName);
|
|
||||||
if (cache.TryGetValue(path, out Stream? cachedStream))
|
|
||||||
{
|
|
||||||
cachedStream?.Dispose(); // Dispose the cached stream if it exists
|
|
||||||
cache.Remove(path); // Remove from cache
|
|
||||||
}
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
File.Delete(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Error deleting file {fileName}: {ex.Message}", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Stream?> GetStream(string fileName)
|
|
||||||
{
|
|
||||||
var path = GetFullPath(fileName);
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
if (cache.TryGetValue(path, out Stream? cachedStream))
|
|
||||||
{
|
|
||||||
return cachedStream;
|
|
||||||
}
|
|
||||||
// open the file stream for multiple reads and cache it for performance
|
|
||||||
var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
||||||
|
|
||||||
cache.CreateEntry(path)
|
|
||||||
.SetValue(fileStream)
|
|
||||||
.SetSlidingExpiration(TimeSpan.FromMinutes(30)); // Cache for 30 minutes
|
|
||||||
|
|
||||||
return fileStream;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<byte[]?> GetBytes(string fileName)
|
|
||||||
{
|
|
||||||
var stream = await GetStream(fileName);
|
|
||||||
if (stream != null)
|
|
||||||
{
|
|
||||||
using var memoryStream = new MemoryStream();
|
|
||||||
await stream.CopyToAsync(memoryStream);
|
|
||||||
return memoryStream.ToArray();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Save(Stream blobStream, string fileName)
|
|
||||||
{
|
|
||||||
var path = GetFullPath(fileName);
|
|
||||||
if (cache.TryGetValue(path, out Stream? _) || File.Exists(path))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"File {fileName} already exists. Use Update for updating file info.");
|
|
||||||
}
|
|
||||||
using var fileStream = new FileStream(path, options: new FileStreamOptions {
|
|
||||||
Access = FileAccess.Write,
|
|
||||||
BufferSize = 4096,
|
|
||||||
Mode = FileMode.OpenOrCreate,
|
|
||||||
Share = FileShare.Read,
|
|
||||||
});
|
|
||||||
blobStream.Seek(0, SeekOrigin.Begin);
|
|
||||||
await blobStream.CopyToAsync(fileStream);
|
|
||||||
blobStream.Seek(0, SeekOrigin.Begin);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Update(Stream blobStream, string fileName)
|
|
||||||
{
|
|
||||||
var path = GetFullPath(fileName);
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
await Delete(fileName);
|
|
||||||
}
|
|
||||||
await Save(blobStream, fileName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@@ -1,13 +0,0 @@
|
|||||||
using DependencyInjector.Lifetimes;
|
|
||||||
|
|
||||||
namespace back.persistance.blob;
|
|
||||||
|
|
||||||
public interface IBlobStorageService : ISingleton
|
|
||||||
{
|
|
||||||
Task Save(Stream blobStream, string fileName);
|
|
||||||
Task<Stream?> GetStream(string fileName);
|
|
||||||
Task<byte[]?> GetBytes(string fileName);
|
|
||||||
Task Delete(string fileName);
|
|
||||||
Task Update(Stream blobStream, string fileName);
|
|
||||||
}
|
|
||||||
|
|
@@ -1,65 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using back.persistance.data.relations;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace back.persistance.data;
|
|
||||||
|
|
||||||
public partial class DataContext : DbContext
|
|
||||||
{
|
|
||||||
public DataContext() { }
|
|
||||||
public DataContext(DbContextOptions<DataContext> options) : base(options) { }
|
|
||||||
|
|
||||||
public virtual DbSet<EfmigrationsLock> EfmigrationsLocks { get; set; }
|
|
||||||
|
|
||||||
public virtual DbSet<Event> Events { get; set; }
|
|
||||||
|
|
||||||
public virtual DbSet<Gallery> Galleries { get; set; }
|
|
||||||
|
|
||||||
public virtual DbSet<Permission> Permissions { get; set; }
|
|
||||||
|
|
||||||
public virtual DbSet<Person> Persons { get; set; }
|
|
||||||
|
|
||||||
public virtual DbSet<Photo> Photos { get; set; }
|
|
||||||
|
|
||||||
public virtual DbSet<Ranking> Rankings { get; set; }
|
|
||||||
|
|
||||||
public virtual DbSet<Role> Roles { get; set; }
|
|
||||||
|
|
||||||
public virtual DbSet<SocialMedia> SocialMedia { get; set; }
|
|
||||||
|
|
||||||
public virtual DbSet<Tag> Tags { get; set; }
|
|
||||||
|
|
||||||
public virtual DbSet<User> Users { get; set; }
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<EfmigrationsLock>(entity =>
|
|
||||||
{
|
|
||||||
entity.ToTable("__EFMigrationsLock");
|
|
||||||
|
|
||||||
entity.Property(e => e.Id).ValueGeneratedNever();
|
|
||||||
});
|
|
||||||
|
|
||||||
typeof(IRelationEstablisher).Assembly.GetExportedTypes()
|
|
||||||
.Where(t => typeof(IRelationEstablisher).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract)
|
|
||||||
.ToList()
|
|
||||||
.ForEach(seederType =>
|
|
||||||
{
|
|
||||||
var relationEstablisher = (IRelationEstablisher?)Activator.CreateInstance(seederType);
|
|
||||||
relationEstablisher?.EstablishRelation(modelBuilder);
|
|
||||||
});
|
|
||||||
|
|
||||||
//typeof(ISeeder).Assembly.GetExportedTypes()
|
|
||||||
// .Where(t => typeof(ISeeder).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract)
|
|
||||||
// .ToList()
|
|
||||||
// .ForEach(seederType =>
|
|
||||||
// {
|
|
||||||
// var seeder = (ISeeder?)Activator.CreateInstance(seederType);
|
|
||||||
// seeder?.Seed(modelBuilder);
|
|
||||||
// });
|
|
||||||
|
|
||||||
OnModelCreatingPartial(modelBuilder);
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
|
|
||||||
}
|
|
@@ -1,752 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using back.persistance.data;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace back.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(DataContext))]
|
|
||||||
[Migration("20250824120656_InitialSetup")]
|
|
||||||
partial class InitialSetup
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.8");
|
|
||||||
|
|
||||||
modelBuilder.Entity("EventTag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("EventId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("TagId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("EventId", "TagId");
|
|
||||||
|
|
||||||
b.HasIndex("TagId");
|
|
||||||
|
|
||||||
b.ToTable("EventTags", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GalleryPhoto", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("GalleryId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PhotoId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("GalleryId", "PhotoId");
|
|
||||||
|
|
||||||
b.HasIndex("PhotoId");
|
|
||||||
|
|
||||||
b.ToTable("GalleryPhotos", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GalleryTag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("GalleryId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("TagId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("GalleryId", "TagId");
|
|
||||||
|
|
||||||
b.HasIndex("TagId");
|
|
||||||
|
|
||||||
b.ToTable("GalleryTags", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GalleryUserViewer", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("GalleryId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("GalleryId", "UserId");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("GalleryUserViewers", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PhotoPerson", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("PhotoId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PersonId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("PhotoId", "PersonId");
|
|
||||||
|
|
||||||
b.HasIndex("PersonId");
|
|
||||||
|
|
||||||
b.ToTable("PhotoPersons", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PhotoTag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("PhotoId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("TagId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("PhotoId", "TagId");
|
|
||||||
|
|
||||||
b.HasIndex("TagId");
|
|
||||||
|
|
||||||
b.ToTable("PhotoTags", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PhotoUserBuyer", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("PhotoId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("PhotoId", "UserId");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("PhotoUserBuyers", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("RolePermission", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("RoleId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PermissionId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("RoleId", "PermissionId");
|
|
||||||
|
|
||||||
b.HasIndex("PermissionId");
|
|
||||||
|
|
||||||
b.ToTable("RolePermissions", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("UserRole", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("RoleId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "RoleId");
|
|
||||||
|
|
||||||
b.HasIndex("RoleId");
|
|
||||||
|
|
||||||
b.ToTable("UserRoles", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.EfmigrationsLock", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Timestamp")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("__EFMigrationsLock", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Event", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedAt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedBy")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Date")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("DeletedAt")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasMaxLength(500)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int>("IsDeleted")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Location")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(50)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UpdatedAt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UpdatedBy")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Events");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Gallery", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedAt")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedBy")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("DeletedAt")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasMaxLength(500)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("EventId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int?>("IsArchived")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(0);
|
|
||||||
|
|
||||||
b.Property<int>("IsDeleted")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<int?>("IsFavorite")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(0);
|
|
||||||
|
|
||||||
b.Property<int?>("IsPublic")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(1);
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UpdatedAt")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CreatedBy");
|
|
||||||
|
|
||||||
b.HasIndex("EventId");
|
|
||||||
|
|
||||||
b.ToTable("Galleries");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Permission", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasMaxLength(255)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Permissions");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Person", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Avatar")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Bio")
|
|
||||||
.HasMaxLength(250)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedAt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("DeletedAt")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int>("IsDeleted")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ProfilePicture")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("SocialMediaId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UpdatedAt")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SocialMediaId");
|
|
||||||
|
|
||||||
b.ToTable("Persons");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Photo", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedAt")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedBy")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasMaxLength(500)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("EventId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Extension")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("HighResUrl")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int?>("IsArchived")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(0);
|
|
||||||
|
|
||||||
b.Property<int?>("IsFavorite")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(0);
|
|
||||||
|
|
||||||
b.Property<int?>("IsPublic")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(1);
|
|
||||||
|
|
||||||
b.Property<string>("LowResUrl")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("MidResUrl")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("RankingId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UpdatedAt")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UpdatedBy")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CreatedBy");
|
|
||||||
|
|
||||||
b.HasIndex("EventId");
|
|
||||||
|
|
||||||
b.ToTable("Photos");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Ranking", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int>("DownVotes")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<int>("TotalVotes")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<int>("UpVotes")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Rankings");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Role", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("BaseRoleModelId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasMaxLength(250)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("BaseRoleModelId");
|
|
||||||
|
|
||||||
b.ToTable("Roles");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.SocialMedia", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("BlueSky")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Discord")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Facebook")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Instagram")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Linkedin")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Other")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Pinterest")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Reddit")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Tiktok")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Twitter")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("SocialMedia");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Tag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedAt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(25)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex(new[] { "Name" }, "IX_Tags_Name")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Tags");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.User", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedAt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Email")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Password")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Salt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Users");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("EventTag", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Event", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("EventId")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.Tag", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("TagId")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GalleryPhoto", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Gallery", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("GalleryId")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.Photo", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PhotoId")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GalleryTag", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Gallery", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("GalleryId")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.Tag", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("TagId")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GalleryUserViewer", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Gallery", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("GalleryId")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.User", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PhotoPerson", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Person", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PersonId")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.Photo", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PhotoId")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PhotoTag", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Photo", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PhotoId")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.Tag", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("TagId")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PhotoUserBuyer", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Photo", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PhotoId")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.User", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("RolePermission", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Permission", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PermissionId")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.Role", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("UserRole", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Role", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.User", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Gallery", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.User", "CreatedByNavigation")
|
|
||||||
.WithMany("Galleries")
|
|
||||||
.HasForeignKey("CreatedBy")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.Event", "Event")
|
|
||||||
.WithMany("Galleries")
|
|
||||||
.HasForeignKey("EventId");
|
|
||||||
|
|
||||||
b.Navigation("CreatedByNavigation");
|
|
||||||
|
|
||||||
b.Navigation("Event");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Person", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.SocialMedia", "SocialMedia")
|
|
||||||
.WithMany("People")
|
|
||||||
.HasForeignKey("SocialMediaId");
|
|
||||||
|
|
||||||
b.Navigation("SocialMedia");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Photo", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Person", "CreatedByNavigation")
|
|
||||||
.WithMany("Photos")
|
|
||||||
.HasForeignKey("CreatedBy")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.Event", "Event")
|
|
||||||
.WithMany("Photos")
|
|
||||||
.HasForeignKey("EventId");
|
|
||||||
|
|
||||||
b.Navigation("CreatedByNavigation");
|
|
||||||
|
|
||||||
b.Navigation("Event");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Role", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Role", "BaseRoleModel")
|
|
||||||
.WithMany("InverseBaseRoleModel")
|
|
||||||
.HasForeignKey("BaseRoleModelId");
|
|
||||||
|
|
||||||
b.Navigation("BaseRoleModel");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.User", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Person", "IdNavigation")
|
|
||||||
.WithOne("User")
|
|
||||||
.HasForeignKey("back.DataModels.User", "Id")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("IdNavigation");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Event", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Galleries");
|
|
||||||
|
|
||||||
b.Navigation("Photos");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Person", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Photos");
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Role", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("InverseBaseRoleModel");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.SocialMedia", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("People");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.User", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Galleries");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,583 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace back.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class InitialSetup : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "__EFMigrationsLock",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "INTEGER", nullable: false),
|
|
||||||
Timestamp = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK___EFMigrationsLock", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Events",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Title = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
|
|
||||||
Description = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
|
||||||
Date = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
Location = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
CreatedAt = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
UpdatedAt = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
IsDeleted = table.Column<int>(type: "INTEGER", nullable: false),
|
|
||||||
DeletedAt = table.Column<string>(type: "TEXT", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Events", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Permissions",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
|
||||||
Description = table.Column<string>(type: "TEXT", maxLength: 255, nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Permissions", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Rankings",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
TotalVotes = table.Column<int>(type: "INTEGER", nullable: false),
|
|
||||||
UpVotes = table.Column<int>(type: "INTEGER", nullable: false),
|
|
||||||
DownVotes = table.Column<int>(type: "INTEGER", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Rankings", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Roles",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
|
||||||
Description = table.Column<string>(type: "TEXT", maxLength: 250, nullable: true),
|
|
||||||
BaseRoleModelId = table.Column<string>(type: "TEXT", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Roles", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Roles_Roles_BaseRoleModelId",
|
|
||||||
column: x => x.BaseRoleModelId,
|
|
||||||
principalTable: "Roles",
|
|
||||||
principalColumn: "Id");
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "SocialMedia",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Facebook = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
Instagram = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
Twitter = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
BlueSky = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
Tiktok = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
Linkedin = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
Pinterest = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
Discord = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
Reddit = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
Other = table.Column<string>(type: "TEXT", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_SocialMedia", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Tags",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Name = table.Column<string>(type: "TEXT", maxLength: 25, nullable: false),
|
|
||||||
CreatedAt = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Tags", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "RolePermissions",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
RoleId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
PermissionId = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_RolePermissions", x => new { x.RoleId, x.PermissionId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_RolePermissions_Permissions_PermissionId",
|
|
||||||
column: x => x.PermissionId,
|
|
||||||
principalTable: "Permissions",
|
|
||||||
principalColumn: "Id");
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_RolePermissions_Roles_RoleId",
|
|
||||||
column: x => x.RoleId,
|
|
||||||
principalTable: "Roles",
|
|
||||||
principalColumn: "Id");
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Persons",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
|
||||||
ProfilePicture = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
Avatar = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
SocialMediaId = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
Bio = table.Column<string>(type: "TEXT", maxLength: 250, nullable: true),
|
|
||||||
CreatedAt = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
UpdatedAt = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
IsDeleted = table.Column<int>(type: "INTEGER", nullable: false),
|
|
||||||
DeletedAt = table.Column<string>(type: "TEXT", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Persons", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Persons_SocialMedia_SocialMediaId",
|
|
||||||
column: x => x.SocialMediaId,
|
|
||||||
principalTable: "SocialMedia",
|
|
||||||
principalColumn: "Id");
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "EventTags",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
EventId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
TagId = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_EventTags", x => new { x.EventId, x.TagId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_EventTags_Events_EventId",
|
|
||||||
column: x => x.EventId,
|
|
||||||
principalTable: "Events",
|
|
||||||
principalColumn: "Id");
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_EventTags_Tags_TagId",
|
|
||||||
column: x => x.TagId,
|
|
||||||
principalTable: "Tags",
|
|
||||||
principalColumn: "Id");
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Photos",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Title = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
|
||||||
Description = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
|
||||||
Extension = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
LowResUrl = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
MidResUrl = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
HighResUrl = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
CreatedAt = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
UpdatedAt = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
CreatedBy = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
EventId = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
RankingId = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
IsFavorite = table.Column<int>(type: "INTEGER", nullable: true, defaultValue: 0),
|
|
||||||
IsPublic = table.Column<int>(type: "INTEGER", nullable: true, defaultValue: 1),
|
|
||||||
IsArchived = table.Column<int>(type: "INTEGER", nullable: true, defaultValue: 0)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Photos", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Photos_Events_EventId",
|
|
||||||
column: x => x.EventId,
|
|
||||||
principalTable: "Events",
|
|
||||||
principalColumn: "Id");
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Photos_Persons_CreatedBy",
|
|
||||||
column: x => x.CreatedBy,
|
|
||||||
principalTable: "Persons",
|
|
||||||
principalColumn: "Id");
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Users",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Email = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Password = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Salt = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
CreatedAt = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Users", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Users_Persons_Id",
|
|
||||||
column: x => x.Id,
|
|
||||||
principalTable: "Persons",
|
|
||||||
principalColumn: "Id");
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "PhotoPersons",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
PhotoId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
PersonId = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_PhotoPersons", x => new { x.PhotoId, x.PersonId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_PhotoPersons_Persons_PersonId",
|
|
||||||
column: x => x.PersonId,
|
|
||||||
principalTable: "Persons",
|
|
||||||
principalColumn: "Id");
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_PhotoPersons_Photos_PhotoId",
|
|
||||||
column: x => x.PhotoId,
|
|
||||||
principalTable: "Photos",
|
|
||||||
principalColumn: "Id");
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "PhotoTags",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
PhotoId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
TagId = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_PhotoTags", x => new { x.PhotoId, x.TagId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_PhotoTags_Photos_PhotoId",
|
|
||||||
column: x => x.PhotoId,
|
|
||||||
principalTable: "Photos",
|
|
||||||
principalColumn: "Id");
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_PhotoTags_Tags_TagId",
|
|
||||||
column: x => x.TagId,
|
|
||||||
principalTable: "Tags",
|
|
||||||
principalColumn: "Id");
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Galleries",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Title = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
|
||||||
Description = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
|
||||||
CreatedAt = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
UpdatedAt = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
CreatedBy = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
IsPublic = table.Column<int>(type: "INTEGER", nullable: true, defaultValue: 1),
|
|
||||||
IsArchived = table.Column<int>(type: "INTEGER", nullable: true, defaultValue: 0),
|
|
||||||
IsFavorite = table.Column<int>(type: "INTEGER", nullable: true, defaultValue: 0),
|
|
||||||
IsDeleted = table.Column<int>(type: "INTEGER", nullable: false),
|
|
||||||
DeletedAt = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
EventId = table.Column<string>(type: "TEXT", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Galleries", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Galleries_Events_EventId",
|
|
||||||
column: x => x.EventId,
|
|
||||||
principalTable: "Events",
|
|
||||||
principalColumn: "Id");
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Galleries_Users_CreatedBy",
|
|
||||||
column: x => x.CreatedBy,
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id");
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "PhotoUserBuyers",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
PhotoId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
UserId = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_PhotoUserBuyers", x => new { x.PhotoId, x.UserId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_PhotoUserBuyers_Photos_PhotoId",
|
|
||||||
column: x => x.PhotoId,
|
|
||||||
principalTable: "Photos",
|
|
||||||
principalColumn: "Id");
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_PhotoUserBuyers_Users_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id");
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "UserRoles",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
UserId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
RoleId = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_UserRoles", x => new { x.UserId, x.RoleId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_UserRoles_Roles_RoleId",
|
|
||||||
column: x => x.RoleId,
|
|
||||||
principalTable: "Roles",
|
|
||||||
principalColumn: "Id");
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_UserRoles_Users_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id");
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "GalleryPhotos",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
GalleryId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
PhotoId = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_GalleryPhotos", x => new { x.GalleryId, x.PhotoId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_GalleryPhotos_Galleries_GalleryId",
|
|
||||||
column: x => x.GalleryId,
|
|
||||||
principalTable: "Galleries",
|
|
||||||
principalColumn: "Id");
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_GalleryPhotos_Photos_PhotoId",
|
|
||||||
column: x => x.PhotoId,
|
|
||||||
principalTable: "Photos",
|
|
||||||
principalColumn: "Id");
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "GalleryTags",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
GalleryId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
TagId = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_GalleryTags", x => new { x.GalleryId, x.TagId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_GalleryTags_Galleries_GalleryId",
|
|
||||||
column: x => x.GalleryId,
|
|
||||||
principalTable: "Galleries",
|
|
||||||
principalColumn: "Id");
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_GalleryTags_Tags_TagId",
|
|
||||||
column: x => x.TagId,
|
|
||||||
principalTable: "Tags",
|
|
||||||
principalColumn: "Id");
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "GalleryUserViewers",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
GalleryId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
UserId = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_GalleryUserViewers", x => new { x.GalleryId, x.UserId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_GalleryUserViewers_Galleries_GalleryId",
|
|
||||||
column: x => x.GalleryId,
|
|
||||||
principalTable: "Galleries",
|
|
||||||
principalColumn: "Id");
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_GalleryUserViewers_Users_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id");
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_EventTags_TagId",
|
|
||||||
table: "EventTags",
|
|
||||||
column: "TagId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Galleries_CreatedBy",
|
|
||||||
table: "Galleries",
|
|
||||||
column: "CreatedBy");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Galleries_EventId",
|
|
||||||
table: "Galleries",
|
|
||||||
column: "EventId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_GalleryPhotos_PhotoId",
|
|
||||||
table: "GalleryPhotos",
|
|
||||||
column: "PhotoId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_GalleryTags_TagId",
|
|
||||||
table: "GalleryTags",
|
|
||||||
column: "TagId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_GalleryUserViewers_UserId",
|
|
||||||
table: "GalleryUserViewers",
|
|
||||||
column: "UserId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Persons_SocialMediaId",
|
|
||||||
table: "Persons",
|
|
||||||
column: "SocialMediaId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_PhotoPersons_PersonId",
|
|
||||||
table: "PhotoPersons",
|
|
||||||
column: "PersonId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Photos_CreatedBy",
|
|
||||||
table: "Photos",
|
|
||||||
column: "CreatedBy");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Photos_EventId",
|
|
||||||
table: "Photos",
|
|
||||||
column: "EventId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_PhotoTags_TagId",
|
|
||||||
table: "PhotoTags",
|
|
||||||
column: "TagId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_PhotoUserBuyers_UserId",
|
|
||||||
table: "PhotoUserBuyers",
|
|
||||||
column: "UserId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_RolePermissions_PermissionId",
|
|
||||||
table: "RolePermissions",
|
|
||||||
column: "PermissionId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Roles_BaseRoleModelId",
|
|
||||||
table: "Roles",
|
|
||||||
column: "BaseRoleModelId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Tags_Name",
|
|
||||||
table: "Tags",
|
|
||||||
column: "Name",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_UserRoles_RoleId",
|
|
||||||
table: "UserRoles",
|
|
||||||
column: "RoleId");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "__EFMigrationsLock");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "EventTags");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "GalleryPhotos");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "GalleryTags");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "GalleryUserViewers");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "PhotoPersons");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "PhotoTags");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "PhotoUserBuyers");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Rankings");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "RolePermissions");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "UserRoles");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Galleries");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Tags");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Photos");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Permissions");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Roles");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Users");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Events");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Persons");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "SocialMedia");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,749 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using back.persistance.data;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace back.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(DataContext))]
|
|
||||||
partial class DataContextModelSnapshot : ModelSnapshot
|
|
||||||
{
|
|
||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.8");
|
|
||||||
|
|
||||||
modelBuilder.Entity("EventTag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("EventId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("TagId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("EventId", "TagId");
|
|
||||||
|
|
||||||
b.HasIndex("TagId");
|
|
||||||
|
|
||||||
b.ToTable("EventTags", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GalleryPhoto", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("GalleryId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PhotoId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("GalleryId", "PhotoId");
|
|
||||||
|
|
||||||
b.HasIndex("PhotoId");
|
|
||||||
|
|
||||||
b.ToTable("GalleryPhotos", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GalleryTag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("GalleryId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("TagId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("GalleryId", "TagId");
|
|
||||||
|
|
||||||
b.HasIndex("TagId");
|
|
||||||
|
|
||||||
b.ToTable("GalleryTags", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GalleryUserViewer", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("GalleryId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("GalleryId", "UserId");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("GalleryUserViewers", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PhotoPerson", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("PhotoId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PersonId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("PhotoId", "PersonId");
|
|
||||||
|
|
||||||
b.HasIndex("PersonId");
|
|
||||||
|
|
||||||
b.ToTable("PhotoPersons", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PhotoTag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("PhotoId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("TagId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("PhotoId", "TagId");
|
|
||||||
|
|
||||||
b.HasIndex("TagId");
|
|
||||||
|
|
||||||
b.ToTable("PhotoTags", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PhotoUserBuyer", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("PhotoId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("PhotoId", "UserId");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("PhotoUserBuyers", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("RolePermission", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("RoleId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PermissionId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("RoleId", "PermissionId");
|
|
||||||
|
|
||||||
b.HasIndex("PermissionId");
|
|
||||||
|
|
||||||
b.ToTable("RolePermissions", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("UserRole", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("RoleId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "RoleId");
|
|
||||||
|
|
||||||
b.HasIndex("RoleId");
|
|
||||||
|
|
||||||
b.ToTable("UserRoles", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.EfmigrationsLock", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Timestamp")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("__EFMigrationsLock", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Event", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedAt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedBy")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Date")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("DeletedAt")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasMaxLength(500)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int>("IsDeleted")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Location")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(50)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UpdatedAt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UpdatedBy")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Events");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Gallery", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedAt")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedBy")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("DeletedAt")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasMaxLength(500)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("EventId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int?>("IsArchived")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(0);
|
|
||||||
|
|
||||||
b.Property<int>("IsDeleted")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<int?>("IsFavorite")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(0);
|
|
||||||
|
|
||||||
b.Property<int?>("IsPublic")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(1);
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UpdatedAt")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CreatedBy");
|
|
||||||
|
|
||||||
b.HasIndex("EventId");
|
|
||||||
|
|
||||||
b.ToTable("Galleries");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Permission", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasMaxLength(255)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Permissions");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Person", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Avatar")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Bio")
|
|
||||||
.HasMaxLength(250)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedAt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("DeletedAt")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int>("IsDeleted")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ProfilePicture")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("SocialMediaId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UpdatedAt")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SocialMediaId");
|
|
||||||
|
|
||||||
b.ToTable("Persons");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Photo", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedAt")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedBy")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasMaxLength(500)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("EventId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Extension")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("HighResUrl")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int?>("IsArchived")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(0);
|
|
||||||
|
|
||||||
b.Property<int?>("IsFavorite")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(0);
|
|
||||||
|
|
||||||
b.Property<int?>("IsPublic")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(1);
|
|
||||||
|
|
||||||
b.Property<string>("LowResUrl")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("MidResUrl")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("RankingId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UpdatedAt")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UpdatedBy")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CreatedBy");
|
|
||||||
|
|
||||||
b.HasIndex("EventId");
|
|
||||||
|
|
||||||
b.ToTable("Photos");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Ranking", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int>("DownVotes")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<int>("TotalVotes")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<int>("UpVotes")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Rankings");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Role", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("BaseRoleModelId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasMaxLength(250)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("BaseRoleModelId");
|
|
||||||
|
|
||||||
b.ToTable("Roles");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.SocialMedia", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("BlueSky")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Discord")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Facebook")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Instagram")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Linkedin")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Other")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Pinterest")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Reddit")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Tiktok")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Twitter")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("SocialMedia");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Tag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedAt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(25)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex(new[] { "Name" }, "IX_Tags_Name")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Tags");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.User", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedAt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Email")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Password")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Salt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Users");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("EventTag", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Event", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("EventId")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.Tag", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("TagId")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GalleryPhoto", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Gallery", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("GalleryId")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.Photo", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PhotoId")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GalleryTag", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Gallery", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("GalleryId")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.Tag", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("TagId")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GalleryUserViewer", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Gallery", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("GalleryId")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.User", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PhotoPerson", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Person", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PersonId")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.Photo", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PhotoId")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PhotoTag", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Photo", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PhotoId")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.Tag", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("TagId")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PhotoUserBuyer", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Photo", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PhotoId")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.User", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("RolePermission", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Permission", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PermissionId")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.Role", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("UserRole", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Role", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.User", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Gallery", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.User", "CreatedByNavigation")
|
|
||||||
.WithMany("Galleries")
|
|
||||||
.HasForeignKey("CreatedBy")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.Event", "Event")
|
|
||||||
.WithMany("Galleries")
|
|
||||||
.HasForeignKey("EventId");
|
|
||||||
|
|
||||||
b.Navigation("CreatedByNavigation");
|
|
||||||
|
|
||||||
b.Navigation("Event");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Person", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.SocialMedia", "SocialMedia")
|
|
||||||
.WithMany("People")
|
|
||||||
.HasForeignKey("SocialMediaId");
|
|
||||||
|
|
||||||
b.Navigation("SocialMedia");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Photo", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Person", "CreatedByNavigation")
|
|
||||||
.WithMany("Photos")
|
|
||||||
.HasForeignKey("CreatedBy")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("back.DataModels.Event", "Event")
|
|
||||||
.WithMany("Photos")
|
|
||||||
.HasForeignKey("EventId");
|
|
||||||
|
|
||||||
b.Navigation("CreatedByNavigation");
|
|
||||||
|
|
||||||
b.Navigation("Event");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Role", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Role", "BaseRoleModel")
|
|
||||||
.WithMany("InverseBaseRoleModel")
|
|
||||||
.HasForeignKey("BaseRoleModelId");
|
|
||||||
|
|
||||||
b.Navigation("BaseRoleModel");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.User", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("back.DataModels.Person", "IdNavigation")
|
|
||||||
.WithOne("User")
|
|
||||||
.HasForeignKey("back.DataModels.User", "Id")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("IdNavigation");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Event", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Galleries");
|
|
||||||
|
|
||||||
b.Navigation("Photos");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Person", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Photos");
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.Role", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("InverseBaseRoleModel");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.SocialMedia", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("People");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("back.DataModels.User", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Galleries");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,200 +0,0 @@
|
|||||||
-- Tabla de redes sociales (SocialMedia) y relación uno a uno con Person
|
|
||||||
CREATE TABLE IF NOT EXISTS SocialMedia (
|
|
||||||
Id TEXT PRIMARY KEY,
|
|
||||||
Facebook TEXT,
|
|
||||||
Instagram TEXT,
|
|
||||||
Twitter TEXT,
|
|
||||||
BlueSky TEXT,
|
|
||||||
Tiktok TEXT,
|
|
||||||
Linkedin TEXT,
|
|
||||||
Pinterest TEXT,
|
|
||||||
Discord TEXT,
|
|
||||||
Reddit TEXT,
|
|
||||||
Other TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Person: cada persona tiene un grupo de redes sociales (uno a uno, fk opcional)
|
|
||||||
CREATE TABLE IF NOT EXISTS Persons (
|
|
||||||
Id TEXT PRIMARY KEY,
|
|
||||||
Name TEXT NOT NULL,
|
|
||||||
ProfilePicture TEXT,
|
|
||||||
Avatar TEXT,
|
|
||||||
SocialMediaId TEXT,
|
|
||||||
Bio TEXT,
|
|
||||||
CreatedAt TEXT NOT NULL,
|
|
||||||
UpdatedAt TEXT,
|
|
||||||
FOREIGN KEY (SocialMediaId) REFERENCES SocialMedia(Id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- User: es una persona (herencia por clave primaria compartida)
|
|
||||||
CREATE TABLE IF NOT EXISTS Users (
|
|
||||||
Id TEXT PRIMARY KEY, -- MISMA clave y valor que Persons.Id
|
|
||||||
Email TEXT NOT NULL,
|
|
||||||
Password TEXT NOT NULL,
|
|
||||||
Salt TEXT NOT NULL,
|
|
||||||
FOREIGN KEY (Id) REFERENCES Persons(Id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Un usuario puede ver muchas galerías (muchos-a-muchos: Galleries <-> Users)
|
|
||||||
CREATE TABLE IF NOT EXISTS GalleryUserViewers (
|
|
||||||
GalleryId TEXT NOT NULL,
|
|
||||||
UserId TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (GalleryId, UserId),
|
|
||||||
FOREIGN KEY (GalleryId) REFERENCES Galleries(Id),
|
|
||||||
FOREIGN KEY (UserId) REFERENCES Users(Id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Un usuario ha creado muchas galerías (uno a muchos)
|
|
||||||
-- Una galería solo puede ser creada por un usuario
|
|
||||||
CREATE TABLE IF NOT EXISTS Galleries (
|
|
||||||
Id TEXT PRIMARY KEY,
|
|
||||||
Title TEXT,
|
|
||||||
Description TEXT,
|
|
||||||
CreatedAt TEXT,
|
|
||||||
UpdatedAt TEXT,
|
|
||||||
CreatedBy TEXT NOT NULL, -- FK a Users
|
|
||||||
IsPublic INTEGER DEFAULT 1,
|
|
||||||
IsArchived INTEGER DEFAULT 0,
|
|
||||||
IsFavorite INTEGER DEFAULT 0,
|
|
||||||
EventId TEXT, -- FK opcional a Events (una galería puede asociarse a un evento)
|
|
||||||
FOREIGN KEY (CreatedBy) REFERENCES Users(Id),
|
|
||||||
FOREIGN KEY (EventId) REFERENCES Events(Id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Galería-Photo: una galería contiene muchas imagenes, una imagen puede estar en muchas galerías (muchos-a-muchos)
|
|
||||||
CREATE TABLE IF NOT EXISTS GalleryPhotos (
|
|
||||||
GalleryId TEXT NOT NULL,
|
|
||||||
PhotoId TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (GalleryId, PhotoId),
|
|
||||||
FOREIGN KEY (GalleryId) REFERENCES Galleries(Id),
|
|
||||||
FOREIGN KEY (PhotoId) REFERENCES Photos(Id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabla de eventos
|
|
||||||
CREATE TABLE IF NOT EXISTS Events (
|
|
||||||
Id TEXT PRIMARY KEY,
|
|
||||||
Title TEXT NOT NULL,
|
|
||||||
Description TEXT,
|
|
||||||
Date TEXT,
|
|
||||||
Location TEXT,
|
|
||||||
CreatedAt TEXT NOT NULL,
|
|
||||||
UpdatedAt TEXT NOT NULL,
|
|
||||||
CreatedBy TEXT,
|
|
||||||
UpdatedBy TEXT,
|
|
||||||
IsDeleted INTEGER NOT NULL DEFAULT 0,
|
|
||||||
DeletedAt TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabla de fotos
|
|
||||||
CREATE TABLE IF NOT EXISTS Photos (
|
|
||||||
Id TEXT PRIMARY KEY,
|
|
||||||
Title TEXT NOT NULL,
|
|
||||||
Description TEXT,
|
|
||||||
Extension TEXT,
|
|
||||||
LowResUrl TEXT,
|
|
||||||
MidResUrl TEXT,
|
|
||||||
HighResUrl TEXT,
|
|
||||||
CreatedAt TEXT,
|
|
||||||
UpdatedAt TEXT,
|
|
||||||
CreatedBy TEXT NOT NULL, -- Persona que subió la foto: FK a Persons
|
|
||||||
UpdatedBy TEXT,
|
|
||||||
EventId TEXT, -- Una photo solo puede tener un evento asociado (FK)
|
|
||||||
RankingId TEXT,
|
|
||||||
IsFavorite INTEGER DEFAULT 0,
|
|
||||||
IsPublic INTEGER DEFAULT 1,
|
|
||||||
IsArchived INTEGER DEFAULT 0,
|
|
||||||
FOREIGN KEY (CreatedBy) REFERENCES Persons(Id),
|
|
||||||
FOREIGN KEY (EventId) REFERENCES Events(Id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Una persona puede salir en muchas fotos, y una foto puede tener muchas personas (muchos-a-muchos)
|
|
||||||
CREATE TABLE IF NOT EXISTS PhotoPersons (
|
|
||||||
PhotoId TEXT NOT NULL,
|
|
||||||
PersonId TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (PhotoId, PersonId),
|
|
||||||
FOREIGN KEY (PhotoId) REFERENCES Photos(Id),
|
|
||||||
FOREIGN KEY (PersonId) REFERENCES Persons(Id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Un usuario puede comprar muchas fotos para verlas, y una foto puede haber sido comprada por muchos usuarios
|
|
||||||
-- (solo necesario si IsPublic = 0)
|
|
||||||
CREATE TABLE IF NOT EXISTS PhotoUserBuyers (
|
|
||||||
PhotoId TEXT NOT NULL,
|
|
||||||
UserId TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (PhotoId, UserId),
|
|
||||||
FOREIGN KEY (PhotoId) REFERENCES Photos(Id),
|
|
||||||
FOREIGN KEY (UserId) REFERENCES Users(Id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabla de tags (únicos)
|
|
||||||
CREATE TABLE IF NOT EXISTS Tags (
|
|
||||||
Id TEXT PRIMARY KEY,
|
|
||||||
Name TEXT NOT NULL UNIQUE,
|
|
||||||
CreatedAt TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Una foto puede tener muchos tags (muchos-a-muchos)
|
|
||||||
CREATE TABLE IF NOT EXISTS PhotoTags (
|
|
||||||
PhotoId TEXT NOT NULL,
|
|
||||||
TagId TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (PhotoId, TagId),
|
|
||||||
FOREIGN KEY (PhotoId) REFERENCES Photos(Id),
|
|
||||||
FOREIGN KEY (TagId) REFERENCES Tags(Id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Un evento puede tener muchos tags (muchos-a-muchos)
|
|
||||||
CREATE TABLE IF NOT EXISTS EventTags (
|
|
||||||
EventId TEXT NOT NULL,
|
|
||||||
TagId TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (EventId, TagId),
|
|
||||||
FOREIGN KEY (EventId) REFERENCES Events(Id),
|
|
||||||
FOREIGN KEY (TagId) REFERENCES Tags(Id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Una galería puede tener muchos tags (muchos-a-muchos)
|
|
||||||
CREATE TABLE IF NOT EXISTS GalleryTags (
|
|
||||||
GalleryId TEXT NOT NULL,
|
|
||||||
TagId TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (GalleryId, TagId),
|
|
||||||
FOREIGN KEY (GalleryId) REFERENCES Galleries(Id),
|
|
||||||
FOREIGN KEY (TagId) REFERENCES Tags(Id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Rankings (por si corresponde)
|
|
||||||
CREATE TABLE IF NOT EXISTS Rankings (
|
|
||||||
Id TEXT PRIMARY KEY,
|
|
||||||
TotalVotes INTEGER NOT NULL,
|
|
||||||
UpVotes INTEGER NOT NULL,
|
|
||||||
DownVotes INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Permissions y Roles, tal y como en el mensaje anterior...
|
|
||||||
CREATE TABLE IF NOT EXISTS Permissions (
|
|
||||||
Id TEXT PRIMARY KEY,
|
|
||||||
Name TEXT NOT NULL,
|
|
||||||
Description TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS Roles (
|
|
||||||
Id TEXT PRIMARY KEY,
|
|
||||||
Name TEXT NOT NULL,
|
|
||||||
Description TEXT,
|
|
||||||
BaseRoleModelId TEXT,
|
|
||||||
FOREIGN KEY (BaseRoleModelId) REFERENCES Roles(Id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS UserRoles (
|
|
||||||
UserId TEXT NOT NULL,
|
|
||||||
RoleId TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (UserId, RoleId),
|
|
||||||
FOREIGN KEY (UserId) REFERENCES Users(Id),
|
|
||||||
FOREIGN KEY (RoleId) REFERENCES Roles(Id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS RolePermissions (
|
|
||||||
RoleId TEXT NOT NULL,
|
|
||||||
PermissionId TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (RoleId, PermissionId),
|
|
||||||
FOREIGN KEY (RoleId) REFERENCES Roles(Id),
|
|
||||||
FOREIGN KEY (PermissionId) REFERENCES Permissions(Id)
|
|
||||||
);
|
|
@@ -1,28 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace back.persistance.data.relations;
|
|
||||||
|
|
||||||
public class EventRelationEstablisher: IRelationEstablisher
|
|
||||||
{
|
|
||||||
public void EstablishRelation(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<Event>(entity =>
|
|
||||||
{
|
|
||||||
entity.HasMany(d => d.Tags).WithMany(p => p.Events)
|
|
||||||
.UsingEntity<Dictionary<string, object>>(
|
|
||||||
"EventTag",
|
|
||||||
r => r.HasOne<Tag>().WithMany()
|
|
||||||
.HasForeignKey("TagId")
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull),
|
|
||||||
l => l.HasOne<Event>().WithMany()
|
|
||||||
.HasForeignKey("EventId")
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull),
|
|
||||||
j =>
|
|
||||||
{
|
|
||||||
j.HasKey("EventId", "TagId");
|
|
||||||
j.ToTable("EventTags");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,68 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace back.persistance.data.relations;
|
|
||||||
|
|
||||||
public class GalleryRelationEstablisher : IRelationEstablisher
|
|
||||||
{
|
|
||||||
public void EstablishRelation(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<Gallery>(entity =>
|
|
||||||
{
|
|
||||||
entity.Property(e => e.IsArchived).HasDefaultValue(0);
|
|
||||||
entity.Property(e => e.IsFavorite).HasDefaultValue(0);
|
|
||||||
entity.Property(e => e.IsPublic).HasDefaultValue(1);
|
|
||||||
|
|
||||||
entity.HasOne(d => d.CreatedByNavigation).WithMany(p => p.Galleries)
|
|
||||||
.HasForeignKey(d => d.CreatedBy)
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull);
|
|
||||||
|
|
||||||
entity.HasOne(d => d.Event).WithMany(p => p.Galleries).HasForeignKey(d => d.EventId);
|
|
||||||
|
|
||||||
entity.HasMany(d => d.Photos).WithMany(p => p.Galleries)
|
|
||||||
.UsingEntity<Dictionary<string, object>>(
|
|
||||||
"GalleryPhoto",
|
|
||||||
r => r.HasOne<Photo>().WithMany()
|
|
||||||
.HasForeignKey("PhotoId")
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull),
|
|
||||||
l => l.HasOne<Gallery>().WithMany()
|
|
||||||
.HasForeignKey("GalleryId")
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull),
|
|
||||||
j =>
|
|
||||||
{
|
|
||||||
j.HasKey("GalleryId", "PhotoId");
|
|
||||||
j.ToTable("GalleryPhotos");
|
|
||||||
});
|
|
||||||
|
|
||||||
entity.HasMany(d => d.Tags).WithMany(p => p.Galleries)
|
|
||||||
.UsingEntity<Dictionary<string, object>>(
|
|
||||||
"GalleryTag",
|
|
||||||
r => r.HasOne<Tag>().WithMany()
|
|
||||||
.HasForeignKey("TagId")
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull),
|
|
||||||
l => l.HasOne<Gallery>().WithMany()
|
|
||||||
.HasForeignKey("GalleryId")
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull),
|
|
||||||
j =>
|
|
||||||
{
|
|
||||||
j.HasKey("GalleryId", "TagId");
|
|
||||||
j.ToTable("GalleryTags");
|
|
||||||
});
|
|
||||||
|
|
||||||
entity.HasMany(d => d.Users).WithMany(p => p.GalleriesNavigation)
|
|
||||||
.UsingEntity<Dictionary<string, object>>(
|
|
||||||
"GalleryUserViewer",
|
|
||||||
r => r.HasOne<User>().WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull),
|
|
||||||
l => l.HasOne<Gallery>().WithMany()
|
|
||||||
.HasForeignKey("GalleryId")
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull),
|
|
||||||
j =>
|
|
||||||
{
|
|
||||||
j.HasKey("GalleryId", "UserId");
|
|
||||||
j.ToTable("GalleryUserViewers");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,8 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace back.persistance.data.relations;
|
|
||||||
|
|
||||||
public interface IRelationEstablisher
|
|
||||||
{
|
|
||||||
void EstablishRelation(ModelBuilder modelBuilder);
|
|
||||||
}
|
|
@@ -1,68 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace back.persistance.data.relations;
|
|
||||||
|
|
||||||
public class PersonRelationEstablisher : IRelationEstablisher
|
|
||||||
{
|
|
||||||
public void EstablishRelation(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<Photo>(entity =>
|
|
||||||
{
|
|
||||||
entity.Property(e => e.IsArchived).HasDefaultValue(0);
|
|
||||||
entity.Property(e => e.IsFavorite).HasDefaultValue(0);
|
|
||||||
entity.Property(e => e.IsPublic).HasDefaultValue(1);
|
|
||||||
|
|
||||||
entity.HasOne(d => d.CreatedByNavigation).WithMany(p => p.Photos)
|
|
||||||
.HasForeignKey(d => d.CreatedBy)
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull);
|
|
||||||
|
|
||||||
entity.HasOne(d => d.Event).WithMany(p => p.Photos).HasForeignKey(d => d.EventId);
|
|
||||||
|
|
||||||
entity.HasMany(d => d.People).WithMany(p => p.PhotosNavigation)
|
|
||||||
.UsingEntity<Dictionary<string, object>>(
|
|
||||||
"PhotoPerson",
|
|
||||||
r => r.HasOne<Person>().WithMany()
|
|
||||||
.HasForeignKey("PersonId")
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull),
|
|
||||||
l => l.HasOne<Photo>().WithMany()
|
|
||||||
.HasForeignKey("PhotoId")
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull),
|
|
||||||
j =>
|
|
||||||
{
|
|
||||||
j.HasKey("PhotoId", "PersonId");
|
|
||||||
j.ToTable("PhotoPersons");
|
|
||||||
});
|
|
||||||
|
|
||||||
entity.HasMany(d => d.Tags).WithMany(p => p.Photos)
|
|
||||||
.UsingEntity<Dictionary<string, object>>(
|
|
||||||
"PhotoTag",
|
|
||||||
r => r.HasOne<Tag>().WithMany()
|
|
||||||
.HasForeignKey("TagId")
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull),
|
|
||||||
l => l.HasOne<Photo>().WithMany()
|
|
||||||
.HasForeignKey("PhotoId")
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull),
|
|
||||||
j =>
|
|
||||||
{
|
|
||||||
j.HasKey("PhotoId", "TagId");
|
|
||||||
j.ToTable("PhotoTags");
|
|
||||||
});
|
|
||||||
|
|
||||||
entity.HasMany(d => d.Users).WithMany(p => p.Photos)
|
|
||||||
.UsingEntity<Dictionary<string, object>>(
|
|
||||||
"PhotoUserBuyer",
|
|
||||||
r => r.HasOne<User>().WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull),
|
|
||||||
l => l.HasOne<Photo>().WithMany()
|
|
||||||
.HasForeignKey("PhotoId")
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull),
|
|
||||||
j =>
|
|
||||||
{
|
|
||||||
j.HasKey("PhotoId", "UserId");
|
|
||||||
j.ToTable("PhotoUserBuyers");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,307 +0,0 @@
|
|||||||
//using back.DataModels;
|
|
||||||
//using back.DTO;
|
|
||||||
//using back.persistance.blob;
|
|
||||||
//using back.services.ImageResizer;
|
|
||||||
//using Microsoft.EntityFrameworkCore;
|
|
||||||
//using Microsoft.Extensions.Hosting.Internal;
|
|
||||||
|
|
||||||
//namespace back.persistance.data.relations;
|
|
||||||
|
|
||||||
//public class PhotoContext : DbContext
|
|
||||||
//{
|
|
||||||
|
|
||||||
// private readonly IImageResizer _Resizer;
|
|
||||||
// private readonly IBlobStorageService _BlobStorage;
|
|
||||||
|
|
||||||
// private readonly TagContext _tagContext;
|
|
||||||
// private readonly EventContext _eventContext;
|
|
||||||
// private readonly PersonRelationEstablisher _personContext;
|
|
||||||
|
|
||||||
// public PhotoContext(DbContextOptions<PhotoContext> options, IHostEnvironment hostingEnvironment,
|
|
||||||
// IImageResizer resizer,
|
|
||||||
// IBlobStorageService blobStorage,
|
|
||||||
// TagContext tags,
|
|
||||||
// EventContext events,
|
|
||||||
// PersonRelationEstablisher persons
|
|
||||||
// ) : base(options)
|
|
||||||
// {
|
|
||||||
// _Resizer = resizer;
|
|
||||||
// _BlobStorage = blobStorage;
|
|
||||||
// _tagContext = tags;
|
|
||||||
// _eventContext = events;
|
|
||||||
// _personContext = persons;
|
|
||||||
|
|
||||||
// if (hostingEnvironment.IsDevelopment())
|
|
||||||
// {
|
|
||||||
// Database.EnsureCreated();
|
|
||||||
// }
|
|
||||||
// else
|
|
||||||
// {
|
|
||||||
// Database.Migrate();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
// {
|
|
||||||
// // Photo -> Tags (muchos-a-muchos)
|
|
||||||
// modelBuilder.Entity<Photo>()
|
|
||||||
// .HasMany(p => p.Tags)
|
|
||||||
// .WithMany(t => t.Photos)
|
|
||||||
// .UsingEntity(j => j.ToTable("PhotoTags"));
|
|
||||||
|
|
||||||
// // Photo -> Persons (muchos-a-muchos)
|
|
||||||
// modelBuilder.Entity<Photo>()
|
|
||||||
// .HasMany(p => p.PersonsIn)
|
|
||||||
// .WithMany(per => per.Photos)
|
|
||||||
// .UsingEntity(j => j.ToTable("PhotoPersons"));
|
|
||||||
|
|
||||||
// // Photo -> Event (muchos-a-uno)
|
|
||||||
// modelBuilder.Entity<Photo>()
|
|
||||||
// .HasOne(p => p.Event)
|
|
||||||
// .WithMany() // Un evento puede tener múltiples fotos
|
|
||||||
// .HasForeignKey(p => p.EventId);
|
|
||||||
|
|
||||||
// // Photo -> Ranking (uno-a-uno)
|
|
||||||
// modelBuilder.Entity<Photo>()
|
|
||||||
// .HasOne(p => p.Ranking)
|
|
||||||
// .WithOne(r => r.Photo) // Un ranking está asociado a una sola foto
|
|
||||||
// .HasForeignKey<Photo>(p => p.RankingId);
|
|
||||||
|
|
||||||
// base.OnModelCreating(modelBuilder);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public async Task CreateNew(PhotoFormModel? form)
|
|
||||||
// {
|
|
||||||
// if (form == null) { return; }
|
|
||||||
|
|
||||||
// var photo = new Photo(
|
|
||||||
// Guid.NewGuid().ToString(),
|
|
||||||
// form.Title,
|
|
||||||
// form.Description ?? string.Empty,
|
|
||||||
// string.Empty, // LowResUrl will be set later
|
|
||||||
// string.Empty, // MidResUrl will be set later
|
|
||||||
// string.Empty, // HighResUrl will be set later
|
|
||||||
// DateTime.UtcNow,
|
|
||||||
// DateTime.UtcNow,
|
|
||||||
// form.UserId,
|
|
||||||
// form.UserId
|
|
||||||
// )
|
|
||||||
// {
|
|
||||||
// IsPublic = form.IsPublic
|
|
||||||
// };
|
|
||||||
|
|
||||||
// List<Task> tasks = [
|
|
||||||
// SaveBlob(photo, form),
|
|
||||||
// LinkTags(photo, form.Tags ?? [], form.UserId),
|
|
||||||
// LinkEvent(photo, form.Evento ?? "", form.UserId),
|
|
||||||
// LinkPersons(photo, form.People ?? [], form.UserId),
|
|
||||||
// ];
|
|
||||||
|
|
||||||
// await Task.WhenAll(tasks);
|
|
||||||
// await Photos.AddAsync(photo);
|
|
||||||
// await SaveChangesAsync();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// private async Task LinkPersons(Photo photo, string[] personas, string updatedBy = "SYSTEM")
|
|
||||||
// {
|
|
||||||
// if (photo == null || personas == null || personas.Length == 0) return;
|
|
||||||
// foreach (var personId in personas)
|
|
||||||
// {
|
|
||||||
// var person = await _personContext.GetById(personId);
|
|
||||||
// if (person != null)
|
|
||||||
// {
|
|
||||||
// await LinkPersons(photo, person, updatedBy);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// private async Task LinkPersons(Photo photo, Person tag, string updatedBy = "SYSTEM")
|
|
||||||
// {
|
|
||||||
// if (tag == null) return;
|
|
||||||
// // Ensure the tag exists
|
|
||||||
// if (await _personContext.Exists(tag.Id))
|
|
||||||
// {
|
|
||||||
// photo.PersonsIn ??= [];
|
|
||||||
// photo.PersonsIn.Add(tag);
|
|
||||||
// photo.UpdatedAt = DateTime.UtcNow;
|
|
||||||
// photo.UpdatedBy = updatedBy; // or use a more appropriate value
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// private async Task LinkTags(Photo photo, string[] tags, string updatedBy = "SYSTEM")
|
|
||||||
// {
|
|
||||||
// if (photo == null || tags == null || tags.Length == 0) return;
|
|
||||||
// foreach (var tagId in tags)
|
|
||||||
// {
|
|
||||||
// var tag = await _tagContext.GetById(tagId);
|
|
||||||
// if (tag != null)
|
|
||||||
// {
|
|
||||||
// await LinkTag(photo, tag, updatedBy);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// private async Task LinkTag(Photo photo, Tag tag, string updatedBy = "SYSTEM")
|
|
||||||
// {
|
|
||||||
// if (tag == null) return;
|
|
||||||
// // Ensure the tag exists
|
|
||||||
// if (await _tagContext.Exists(tag.Id))
|
|
||||||
// {
|
|
||||||
// photo.Tags.Add(tag);
|
|
||||||
// photo.UpdatedAt = DateTime.UtcNow;
|
|
||||||
// photo.UpdatedBy = updatedBy; // or use a more appropriate value
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// private async Task LinkEvent(Photo photo, string eventId, string updatedBy = "SYSTEM")
|
|
||||||
// {
|
|
||||||
// if (string.IsNullOrEmpty(eventId)) return;
|
|
||||||
// var evento = await _eventContext.GetById(eventId);
|
|
||||||
// if (evento != null)
|
|
||||||
// {
|
|
||||||
// await LinkEvent(photo, evento, updatedBy);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// private async Task LinkEvent(Photo photo, Event? evento, string updatedBy = "SYSTEM")
|
|
||||||
// {
|
|
||||||
// if (evento == null) return;
|
|
||||||
// // Ensure the event exists
|
|
||||||
// if (await _eventContext.Exists(evento.Id))
|
|
||||||
// {
|
|
||||||
// photo.Event = evento;
|
|
||||||
// photo.UpdatedAt = DateTime.UtcNow;
|
|
||||||
// photo.UpdatedBy = updatedBy;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// private async Task SaveBlob(Photo photo, PhotoFormModel form)
|
|
||||||
// {
|
|
||||||
// if (form.Image != null && form.Image.Length > 0)
|
|
||||||
// {
|
|
||||||
// var lowRes = await _Resizer.ResizeImage(form.Image, 480);
|
|
||||||
// var midRes = await _Resizer.ResizeImage(form.Image, 720);
|
|
||||||
// // Upload images to blob storage
|
|
||||||
// photo.Extension = form.Image.FileName.Split('.').Last();
|
|
||||||
// photo.LowResUrl = $"low/{photo.Id}.webp";
|
|
||||||
// photo.MidResUrl = $"mid/{photo.Id}.webp";
|
|
||||||
// photo.HighResUrl = $"high/{photo.Id}.{photo.Extension}";
|
|
||||||
// await _BlobStorage.SaveAsync(lowRes, photo.LowResUrl);
|
|
||||||
// await _BlobStorage.SaveAsync(midRes, photo.MidResUrl);
|
|
||||||
// await _BlobStorage.SaveAsync(form.Image.OpenReadStream(), photo.HighResUrl);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public async Task<Photo?> GetById(string id)
|
|
||||||
// {
|
|
||||||
// return await GetById(Guid.Parse(id));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public async Task<Photo?> GetById(Guid id)
|
|
||||||
// {
|
|
||||||
// try
|
|
||||||
// {
|
|
||||||
// return await Photos.FindAsync(id);
|
|
||||||
// }
|
|
||||||
// catch
|
|
||||||
// {
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public async Task<int> GetTotalItems()
|
|
||||||
// {
|
|
||||||
// try
|
|
||||||
// {
|
|
||||||
// return await Photos.CountAsync();
|
|
||||||
// }
|
|
||||||
// catch
|
|
||||||
// {
|
|
||||||
// return 0;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public async Task<IEnumerable<Photo>?> GetPage(int page = 1, int pageSize = 20)
|
|
||||||
// {
|
|
||||||
// if (page < 1) page = 1;
|
|
||||||
// if (pageSize < 1) pageSize = 20;
|
|
||||||
// try
|
|
||||||
// {
|
|
||||||
// return await Photos
|
|
||||||
// .OrderByDescending(p => p.CreatedAt)
|
|
||||||
// .Skip((page - 1) * pageSize)
|
|
||||||
// .Take(pageSize)
|
|
||||||
// .ToListAsync();
|
|
||||||
// }
|
|
||||||
// catch
|
|
||||||
// {
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public async Task<bool> Exists(Photo? photo)
|
|
||||||
// {
|
|
||||||
// try
|
|
||||||
// {
|
|
||||||
// if (photo == null) return false;
|
|
||||||
// if (string.IsNullOrEmpty(photo.Id)) return false;
|
|
||||||
// return await Photos.AnyAsync(p => p.Id == photo.Id);
|
|
||||||
// }
|
|
||||||
// catch
|
|
||||||
// {
|
|
||||||
// return false; // Handle exceptions gracefully
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public async Task<bool> Exists(string id)
|
|
||||||
// {
|
|
||||||
// try
|
|
||||||
// {
|
|
||||||
// if (string.IsNullOrEmpty(id)) return false;
|
|
||||||
// return await Photos.AnyAsync(p => p.Id == id);
|
|
||||||
// }
|
|
||||||
// catch
|
|
||||||
// {
|
|
||||||
// return false; // Handle exceptions gracefully
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public async Task Delete(Photo photo)
|
|
||||||
// {
|
|
||||||
// if (photo == null) return;
|
|
||||||
// if (await Exists(photo))
|
|
||||||
// {
|
|
||||||
// // Delete the photo from blob storage
|
|
||||||
// if (!string.IsNullOrEmpty(photo.LowResUrl))
|
|
||||||
// await _BlobStorage.DeleteAsync(photo.LowResUrl);
|
|
||||||
// if (!string.IsNullOrEmpty(photo.MidResUrl))
|
|
||||||
// await _BlobStorage.DeleteAsync(photo.MidResUrl);
|
|
||||||
// if (!string.IsNullOrEmpty(photo.HighResUrl))
|
|
||||||
// await _BlobStorage.DeleteAsync(photo.HighResUrl);
|
|
||||||
// Photos.Remove(photo);
|
|
||||||
// await SaveChangesAsync();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public async Task Update(Photo photo)
|
|
||||||
// {
|
|
||||||
// if (photo == null) return;
|
|
||||||
// if (await Exists(photo))
|
|
||||||
// {
|
|
||||||
// var evento = photo.Event;
|
|
||||||
// photo.Event = null;
|
|
||||||
// await LinkEvent(photo, evento, photo.UpdatedBy);
|
|
||||||
|
|
||||||
// var tags = photo.Tags.Select(t => t.Id);
|
|
||||||
// photo.Tags.Clear();
|
|
||||||
// await LinkTags(photo, [.. tags], photo.UpdatedBy);
|
|
||||||
|
|
||||||
// var persons = photo.PersonsIn?.Select(t => t.Id) ?? [];
|
|
||||||
// photo.PersonsIn = null;
|
|
||||||
// await LinkPersons(photo, [.. persons], photo.UpdatedBy);
|
|
||||||
|
|
||||||
// Photos.Update(photo);
|
|
||||||
// await SaveChangesAsync();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
@@ -1,15 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace back.persistance.data.relations;
|
|
||||||
|
|
||||||
public class PhotoRelationEstablisher : IRelationEstablisher
|
|
||||||
{
|
|
||||||
public void EstablishRelation(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<Person>(entity =>
|
|
||||||
{
|
|
||||||
entity.HasOne(d => d.SocialMedia).WithMany(p => p.People).HasForeignKey(d => d.SocialMediaId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,25 +0,0 @@
|
|||||||
//using back.DataModels;
|
|
||||||
//using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
//namespace back.persistance.data.relations;
|
|
||||||
|
|
||||||
|
|
||||||
//public class RoleContext : DbContext
|
|
||||||
//{
|
|
||||||
// protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
// {
|
|
||||||
// // Role -> Permissions (muchos-a-muchos)
|
|
||||||
// modelBuilder.Entity<Role>()
|
|
||||||
// .HasMany(r => r.Permissions)
|
|
||||||
// .WithMany(p => p.Roles)
|
|
||||||
// .UsingEntity(j => j.ToTable("RolePermissions"));
|
|
||||||
|
|
||||||
// // Role -> BaseRole (auto-referencial)
|
|
||||||
// modelBuilder.Entity<Role>()
|
|
||||||
// .HasOne(r => r.BaseRoleModel)
|
|
||||||
// .WithMany() // Un rol base puede ser heredado por múltiples roles
|
|
||||||
// .HasForeignKey(r => r.BaseRoleModelId);
|
|
||||||
|
|
||||||
// base.OnModelCreating(modelBuilder);
|
|
||||||
// }
|
|
||||||
//}
|
|
@@ -1,30 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace back.persistance.data.relations;
|
|
||||||
|
|
||||||
public class RoleRelationEstablisher : IRelationEstablisher
|
|
||||||
{
|
|
||||||
public void EstablishRelation(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<Role>(entity =>
|
|
||||||
{
|
|
||||||
entity.HasOne(d => d.BaseRoleModel).WithMany(p => p.InverseBaseRoleModel).HasForeignKey(d => d.BaseRoleModelId);
|
|
||||||
|
|
||||||
entity.HasMany(d => d.Permissions).WithMany(p => p.Roles)
|
|
||||||
.UsingEntity<Dictionary<string, object>>(
|
|
||||||
"RolePermission",
|
|
||||||
r => r.HasOne<Permission>().WithMany()
|
|
||||||
.HasForeignKey("PermissionId")
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull),
|
|
||||||
l => l.HasOne<Role>().WithMany()
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull),
|
|
||||||
j =>
|
|
||||||
{
|
|
||||||
j.HasKey("RoleId", "PermissionId");
|
|
||||||
j.ToTable("RolePermissions");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,40 +0,0 @@
|
|||||||
//using back.DataModels;
|
|
||||||
//using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
//namespace back.persistance.data.relations;
|
|
||||||
|
|
||||||
//public class SeedingDbContext : DbContext
|
|
||||||
//{
|
|
||||||
// protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
// {
|
|
||||||
// // 3. CONFIGURAR RELACIONES
|
|
||||||
// modelBuilder.Entity<Role>()
|
|
||||||
// .HasMany(r => r.Permissions)
|
|
||||||
// .WithMany(p => p.Roles)
|
|
||||||
// .UsingEntity<Dictionary<string, object>>(
|
|
||||||
// "RolePermissions",
|
|
||||||
// j => j.HasOne<Permission>().WithMany().HasForeignKey("PermissionsId"),
|
|
||||||
// j => j.HasOne<Role>().WithMany().HasForeignKey("RolesId"),
|
|
||||||
// j => j.HasData(
|
|
||||||
// // Usuario: VIEW_CONTENT y LIKE_CONTENT
|
|
||||||
// new { RolesId = "1", PermissionsId = "1" },
|
|
||||||
// new { RolesId = "1", PermissionsId = "2" },
|
|
||||||
|
|
||||||
// // Content Manager: permisos adicionales
|
|
||||||
// new { RolesId = "2", PermissionsId = "5" },
|
|
||||||
// new { RolesId = "2", PermissionsId = "3" },
|
|
||||||
// new { RolesId = "2", PermissionsId = "4" },
|
|
||||||
// new { RolesId = "2", PermissionsId = "9" },
|
|
||||||
// new { RolesId = "2", PermissionsId = "8" },
|
|
||||||
|
|
||||||
// // Admin: permisos adicionales
|
|
||||||
// new { RolesId = "3", PermissionsId = "6" },
|
|
||||||
// new { RolesId = "3", PermissionsId = "7" },
|
|
||||||
// new { RolesId = "3", PermissionsId = "10" }
|
|
||||||
// )
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // Resto de configuraciones...
|
|
||||||
// base.OnModelCreating(modelBuilder);
|
|
||||||
// }
|
|
||||||
//}
|
|
@@ -1,15 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace back.persistance.data.relations;
|
|
||||||
|
|
||||||
public class TagRelationEstablisher : IRelationEstablisher
|
|
||||||
{
|
|
||||||
public void EstablishRelation(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<Tag>(entity =>
|
|
||||||
{
|
|
||||||
entity.HasIndex(e => e.Name, "IX_Tags_Name").IsUnique();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,32 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace back.persistance.data.relations;
|
|
||||||
|
|
||||||
public class UserRelationEstablisher : IRelationEstablisher
|
|
||||||
{
|
|
||||||
public void EstablishRelation(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<User>(entity =>
|
|
||||||
{
|
|
||||||
entity.HasOne(d => d.IdNavigation).WithOne(p => p.User)
|
|
||||||
.HasForeignKey<User>(d => d.Id)
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull);
|
|
||||||
|
|
||||||
entity.HasMany(d => d.Roles).WithMany(p => p.Users)
|
|
||||||
.UsingEntity<Dictionary<string, object>>(
|
|
||||||
"UserRole",
|
|
||||||
r => r.HasOne<Role>().WithMany()
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull),
|
|
||||||
l => l.HasOne<User>().WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull),
|
|
||||||
j =>
|
|
||||||
{
|
|
||||||
j.HasKey("UserId", "RoleId");
|
|
||||||
j.ToTable("UserRoles");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,10 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using DependencyInjector.Lifetimes;
|
|
||||||
using Transactional.Abstractions.Interfaces;
|
|
||||||
|
|
||||||
namespace back.persistance.data.repositories.Abstracts;
|
|
||||||
|
|
||||||
public interface IPersonRepository : IRepository<Person>, IScoped
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
@@ -1,8 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using DependencyInjector.Lifetimes;
|
|
||||||
using Transactional.Abstractions.Interfaces;
|
|
||||||
|
|
||||||
namespace back.persistance.data.repositories.Abstracts;
|
|
||||||
|
|
||||||
public interface IPhotoRepository : IRepository<Photo>, IScoped
|
|
||||||
{ }
|
|
@@ -1,14 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using DependencyInjector.Lifetimes;
|
|
||||||
using Transactional.Abstractions.Interfaces;
|
|
||||||
|
|
||||||
namespace back.persistance.data.repositories.Abstracts;
|
|
||||||
|
|
||||||
public interface IUserRepository : IRepository<User>, IScoped
|
|
||||||
{
|
|
||||||
Task<User?> GetByEmail(string email);
|
|
||||||
Task<string?> GetUserSaltByEmail(string email);
|
|
||||||
Task<User?> Login(string email, string password);
|
|
||||||
Task<bool> ExistsByEmail(string email);
|
|
||||||
//Task<bool> IsContentManager(string userId);
|
|
||||||
}
|
|
@@ -1,10 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using back.persistance.data.repositories.Abstracts;
|
|
||||||
using Transactional.Implementations.EntityFramework;
|
|
||||||
|
|
||||||
namespace back.persistance.data.repositories;
|
|
||||||
|
|
||||||
public class PersonRepository(DataContext context) : ReadWriteRepository<Person>(context), IPersonRepository
|
|
||||||
{
|
|
||||||
// Implement methods specific to Photo repository if needed
|
|
||||||
}
|
|
@@ -1,10 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using back.persistance.data.repositories.Abstracts;
|
|
||||||
using Transactional.Implementations.EntityFramework;
|
|
||||||
|
|
||||||
namespace back.persistance.data.repositories;
|
|
||||||
|
|
||||||
public class PhotoRepository(DataContext context) : ReadWriteRepository<Photo>(context), IPhotoRepository
|
|
||||||
{
|
|
||||||
// Implement methods specific to Photo repository if needed
|
|
||||||
}
|
|
@@ -1,70 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using back.persistance.data.repositories.Abstracts;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Transactional.Implementations.EntityFramework;
|
|
||||||
|
|
||||||
namespace back.persistance.data.repositories;
|
|
||||||
|
|
||||||
public class UserRepository(DataContext context) : ReadWriteRepository<User>(context), IUserRepository
|
|
||||||
{
|
|
||||||
public async Task<User?> GetByEmail(string email)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(email)) return null;
|
|
||||||
return await Entities.FirstOrDefaultAsync(u => u.Email == email);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string?> GetUserSaltByEmail(string email)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(email)) return string.Empty;
|
|
||||||
var user = await Entities.FirstOrDefaultAsync(u => u.Email == email);
|
|
||||||
return user?.Salt ?? string.Empty;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<User?> Login(string email, string password)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password)) return null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await Entities.FirstOrDefaultAsync(u => u.Email == email && u.Password == password);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ExistsByEmail(string email)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(email)) return false;
|
|
||||||
return await Entities.AnyAsync(u => u.Email == email);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//public async Task<bool> IsContentManager(string userId)
|
|
||||||
//{
|
|
||||||
// var user = await GetById(userId);
|
|
||||||
// if (user == null)
|
|
||||||
// return false;
|
|
||||||
// return user.Roles.Any(role => role.IsContentManager() || role.IsAdmin());
|
|
||||||
//}
|
|
||||||
}
|
|
@@ -1,8 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace back.persistance.data.seeders;
|
|
||||||
|
|
||||||
public interface ISeeder
|
|
||||||
{
|
|
||||||
void Seed(ModelBuilder modelBuilder);
|
|
||||||
}
|
|
@@ -1,23 +0,0 @@
|
|||||||
//using back.DataModels;
|
|
||||||
//using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
//namespace back.persistance.data.seeders;
|
|
||||||
|
|
||||||
//public class PermissionSeeder : ISeeder
|
|
||||||
//{
|
|
||||||
// public void Seed(ModelBuilder modelBuilder)
|
|
||||||
// {
|
|
||||||
// modelBuilder.Entity<Permission>().HasData(
|
|
||||||
// Permission.ViewContentPermission,
|
|
||||||
// Permission.LikeContentPermission,
|
|
||||||
// Permission.EditContentPermission,
|
|
||||||
// Permission.DeleteContentPermission,
|
|
||||||
// Permission.CreateContentPermission,
|
|
||||||
// Permission.EditUserPermission,
|
|
||||||
// Permission.DeleteUserPermission,
|
|
||||||
// Permission.DisableUserPermission,
|
|
||||||
// Permission.CreateUserPermission,
|
|
||||||
// Permission.EditWebConfigPermission
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
//}
|
|
@@ -1,16 +0,0 @@
|
|||||||
//using back.DataModels;
|
|
||||||
//using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
//namespace back.persistance.data.seeders;
|
|
||||||
|
|
||||||
//public class RoleSeeder : ISeeder
|
|
||||||
//{
|
|
||||||
// public void Seed(ModelBuilder modelBuilder)
|
|
||||||
// {
|
|
||||||
// modelBuilder.Entity<Permission>().HasData(
|
|
||||||
// new Role { Id = "1", Name = "User", Description = "Role for regular users", BaseRoleModelId = null },
|
|
||||||
// new Role { Id = "2", Name = "Content Manager", Description = "Role for managing content", BaseRoleModelId = "1" },
|
|
||||||
// new Role { Id = "3", Name = "Admin", Description = "Administrator role with full permissions", BaseRoleModelId = "2" }
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
//}
|
|
@@ -1,14 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace back.persistance.data.seeders;
|
|
||||||
|
|
||||||
public class SystemUserSeeder : ISeeder
|
|
||||||
{
|
|
||||||
public void Seed(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<Permission>().HasData(
|
|
||||||
User.SystemUser
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,11 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace back.services.bussines;
|
|
||||||
|
|
||||||
public static class Errors
|
|
||||||
{
|
|
||||||
public static readonly HttpErrorMap Unauthorized =
|
|
||||||
new(HttpStatusCode.Unauthorized, "Invalid user data. Email or password are wrong.");
|
|
||||||
public static readonly HttpErrorMap BadRequest =
|
|
||||||
new(HttpStatusCode.BadRequest, "Missing user data.");
|
|
||||||
}
|
|
@@ -1,5 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace back.services.bussines;
|
|
||||||
|
|
||||||
public record HttpErrorMap(HttpStatusCode Code, string Description);
|
|
@@ -1,15 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using back.DTO;
|
|
||||||
using DependencyInjector.Lifetimes;
|
|
||||||
|
|
||||||
namespace back.services.bussines.PhotoService;
|
|
||||||
|
|
||||||
public interface IPhotoService: IScoped
|
|
||||||
{
|
|
||||||
Task<Photo?> Create(PhotoFormModel form);
|
|
||||||
Task Delete(string id, string userId = "00000000-0000-0000-0000-000000000001");
|
|
||||||
Task<Photo?> Get(string id, string userId = "00000000-0000-0000-0000-000000000001");
|
|
||||||
Task<(string? mediaType, byte[]? fileBytes)> GetBytes(string id, string res = "");
|
|
||||||
Task<(int totalItems, IEnumerable<Photo>? pageData)> GetPage(int page, int pageSize);
|
|
||||||
Task<Photo?> Update(Photo photo, string userId = "00000000-0000-0000-0000-000000000001");
|
|
||||||
}
|
|
@@ -1,82 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using back.DTO;
|
|
||||||
using back.persistance.blob;
|
|
||||||
using back.persistance.data.repositories.Abstracts;
|
|
||||||
|
|
||||||
namespace back.services.bussines.PhotoService;
|
|
||||||
|
|
||||||
public class PhotoService(
|
|
||||||
IPhotoRepository photoRepository,
|
|
||||||
IUserRepository userRepository,
|
|
||||||
IBlobStorageService blobStorageService
|
|
||||||
) : IPhotoService
|
|
||||||
{
|
|
||||||
public async Task<Photo?> Create(PhotoFormModel form)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(form);
|
|
||||||
if (form.Image == null || form.Image.Length == 0)
|
|
||||||
throw new ArgumentException("No image uploaded.", nameof(form));
|
|
||||||
//if (string.IsNullOrEmpty(form.UserId) || await userRepository.IsContentManager(form.UserId))
|
|
||||||
// throw new ArgumentException("Invalid user ID or user is not a content manager.", nameof(form.UserId));
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Delete(string id, string userId = User.SystemUserId)
|
|
||||||
{
|
|
||||||
//if (string.IsNullOrEmpty(userId) || await userRepository.IsContentManager(userId))
|
|
||||||
// throw new ArgumentException("Invalid user ID or user is not a content manager.", nameof(userId));
|
|
||||||
photoRepository.Delete(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Photo?> Get(string id, string userId = User.SystemUserId)
|
|
||||||
{
|
|
||||||
Photo? photo = await photoRepository.GetById(id);
|
|
||||||
return photo;
|
|
||||||
//return photo?.CanBeSeenBy(userId) ?? false
|
|
||||||
// ? photo
|
|
||||||
// : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<(string? mediaType, byte[]? fileBytes)> GetBytes(string id, string res = "")
|
|
||||||
{
|
|
||||||
var photo = await photoRepository.GetById(id);
|
|
||||||
if (photo == null)
|
|
||||||
return (null, null);
|
|
||||||
|
|
||||||
string filePath = res.ToLower() switch
|
|
||||||
{
|
|
||||||
"high" => photo.HighResUrl,
|
|
||||||
"mid" => photo.MidResUrl,
|
|
||||||
"low" or _ => photo.LowResUrl
|
|
||||||
};
|
|
||||||
|
|
||||||
string? mediaType = res.ToLower() switch
|
|
||||||
{
|
|
||||||
"high" => $"image/{photo.Extension}",
|
|
||||||
"mid" or "low" or _ => "image/webp",
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
mediaType,
|
|
||||||
await blobStorageService.GetBytes(filePath) ?? throw new FileNotFoundException("File not found.", filePath)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<(int totalItems, IEnumerable<Photo>? pageData)> GetPage(int page, int pageSize)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
totalItems: await photoRepository.GetTotalItems(),
|
|
||||||
pageData: photoRepository.GetPage(page, pageSize)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Photo?> Update(Photo photo, string userId = "00000000-0000-0000-0000-000000000001")
|
|
||||||
{
|
|
||||||
//if (string.IsNullOrEmpty(userId) || await userRepository.IsContentManager(userId))
|
|
||||||
// throw new ArgumentException("Invalid user ID or user is not a content manager.", nameof(userId));
|
|
||||||
return await photoRepository.Update(photo);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,13 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using DependencyInjector.Lifetimes;
|
|
||||||
|
|
||||||
namespace back.services.bussines.UserService;
|
|
||||||
|
|
||||||
public interface IUserService: IScoped
|
|
||||||
{
|
|
||||||
Task<User?> Create(string clientId, User user);
|
|
||||||
Task<User?> Login(string email, string password, string clientId);
|
|
||||||
Task SendResetPassword(string email);
|
|
||||||
Task<User?> Update(User user);
|
|
||||||
Task<User?> ValidateSystemUser(string email, string password, string systemKey, string clientId);
|
|
||||||
}
|
|
@@ -1,141 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using back.persistance.blob;
|
|
||||||
using back.persistance.data.repositories.Abstracts;
|
|
||||||
using back.services.engine.Crypto;
|
|
||||||
using back.services.engine.mailing;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace back.services.bussines.UserService;
|
|
||||||
|
|
||||||
public class UserService(
|
|
||||||
IUserRepository userRepository, ICryptoService cryptoService,
|
|
||||||
IEmailService emailService,
|
|
||||||
IBlobStorageService blobStorageService,
|
|
||||||
JsonSerializerOptions jsonSerializerOptions
|
|
||||||
) : IUserService
|
|
||||||
{
|
|
||||||
private readonly IUserRepository _repository = userRepository ?? throw new ArgumentNullException(nameof(userRepository));
|
|
||||||
private readonly ICryptoService _cryptoService = cryptoService;
|
|
||||||
private readonly IEmailService _emailService = emailService;
|
|
||||||
private readonly IBlobStorageService _blobStorageService = blobStorageService;
|
|
||||||
|
|
||||||
public async Task<User?> Create(string clientId, User user)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(user);
|
|
||||||
|
|
||||||
if (user.Id != null && await _repository.Exists(user.Id))
|
|
||||||
{
|
|
||||||
return await _repository.GetById(user.Id);
|
|
||||||
}
|
|
||||||
if (string.IsNullOrEmpty(user.Email) || string.IsNullOrEmpty(user.Password))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (await _repository.Exists(user.Email))
|
|
||||||
{
|
|
||||||
return await _repository.GetByEmail(user.Email);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(user.Salt))
|
|
||||||
{
|
|
||||||
user.Salt = _cryptoService.Salt();
|
|
||||||
}
|
|
||||||
user.Password = _cryptoService.Decrypt(clientId, user.Password) ?? string.Empty;
|
|
||||||
user.Password = _cryptoService.HashPassword(user.Password, user.Salt) ?? string.Empty;
|
|
||||||
|
|
||||||
user.CreatedAt = DateTimeOffset.UtcNow.ToString("dd-MM-yyyy HH:mm:ss zz");
|
|
||||||
|
|
||||||
//user.Roles.Add(Role.UserRole);
|
|
||||||
|
|
||||||
await _repository.Insert(user);
|
|
||||||
await _repository.SaveChanges();
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<User?> Update(User user)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(user);
|
|
||||||
if (user.Id == null || !await _repository.Exists(user.Id))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var existingUser = await _repository.GetById(user.Id);
|
|
||||||
if (existingUser == null) return null;
|
|
||||||
existingUser.Email = user.Email;
|
|
||||||
await _repository.Update(existingUser);
|
|
||||||
await _repository.SaveChanges();
|
|
||||||
return existingUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<User?> Login(string email, string decryptedPass)
|
|
||||||
{
|
|
||||||
var salt = await _repository.GetUserSaltByEmail(email);
|
|
||||||
var hashedPassword = _cryptoService.HashPassword(decryptedPass, salt);
|
|
||||||
var user = await _repository.Login(email, hashedPassword ?? string.Empty);
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<User?> Login(string email, string password, string clientId)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password)) return null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var decryptedPass = _cryptoService.Decrypt(clientId, password);
|
|
||||||
var user = await Login(email, decryptedPass ?? string.Empty);
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendResetPassword(string email)
|
|
||||||
{
|
|
||||||
var exists = await _repository.ExistsByEmail(email);
|
|
||||||
if (!exists)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await _emailService.SendEmailAsync(
|
|
||||||
tos: email,
|
|
||||||
from: "admin@mmorales.photo",
|
|
||||||
subject: "Reset Password",
|
|
||||||
body: "If you received this email, it means that you have requested a password reset. Please follow the instructions in the email to reset your password."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<User?> ValidateSystemUser(string email, string password, string systemKey, string clientId)
|
|
||||||
{
|
|
||||||
var decryptedPassword = _cryptoService.Decrypt(clientId, password) ?? string.Empty;
|
|
||||||
var decryptedsystemKey = _cryptoService.Decrypt(clientId, systemKey) ?? string.Empty;
|
|
||||||
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(decryptedPassword) || string.IsNullOrEmpty(decryptedsystemKey))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!email.Equals(User.SystemUser.Email, StringComparison.InvariantCultureIgnoreCase))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var systemKeyBytes = await _blobStorageService.GetBytes("systemkey.lock");
|
|
||||||
var systemKeyString = Encoding.UTF8.GetString(systemKeyBytes ?? []);
|
|
||||||
var systemKeyObject = JsonSerializer.Deserialize<SystemKey>(systemKeyString, jsonSerializerOptions);
|
|
||||||
if (systemKeyObject == null || !systemKeyObject.IsValid(email, decryptedPassword, decryptedsystemKey))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!await _repository.ExistsByEmail(email))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var user = await _repository.GetByEmail(email);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return await Login(user.Email!, decryptedPassword);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,175 +0,0 @@
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
|
|
||||||
namespace back.services.engine.Crypto;
|
|
||||||
|
|
||||||
public class CryptoService(IMemoryCache cache) : ICryptoService
|
|
||||||
{
|
|
||||||
private readonly IMemoryCache _cache = cache;
|
|
||||||
private readonly MemoryCacheEntryOptions _CacheOptions = new()
|
|
||||||
{
|
|
||||||
AbsoluteExpiration = DateTimeOffset.Now.AddHours(1),
|
|
||||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1),
|
|
||||||
SlidingExpiration = TimeSpan.FromMinutes(30),
|
|
||||||
Priority = CacheItemPriority.High,
|
|
||||||
PostEvictionCallbacks =
|
|
||||||
{
|
|
||||||
new PostEvictionCallbackRegistration
|
|
||||||
{
|
|
||||||
EvictionCallback = (key, value, reason, state) =>
|
|
||||||
{
|
|
||||||
var clientId = key.ToString()?.Replace("_public","").Replace("_private","");
|
|
||||||
if(string.IsNullOrEmpty(clientId)) { return; }
|
|
||||||
// Handle the eviction of the certificate - removing public/private keys from the cache
|
|
||||||
try{ cache.Remove($"{clientId}_public"); } catch{ }
|
|
||||||
try{ cache.Remove($"{clientId}_private"); } catch{ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public string? Encrypt(string clientId,string plainText)
|
|
||||||
{
|
|
||||||
// get keys from cache
|
|
||||||
if (!_cache.TryGetValue($"{clientId}_private", out string? privateCert) || string.IsNullOrEmpty(privateCert))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Private certificate not found for the client.");
|
|
||||||
}
|
|
||||||
if (!_cache.TryGetValue($"{clientId}_public", out string? publicCert) || string.IsNullOrEmpty(publicCert))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Public certificate not found for the client.");
|
|
||||||
}
|
|
||||||
// import rsa keys and configure RSA for encryption
|
|
||||||
using var rsa = RSA.Create(2048);
|
|
||||||
rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(publicCert), out _);
|
|
||||||
rsa.ImportRSAPrivateKey(Convert.FromBase64String(privateCert), out _);
|
|
||||||
// Encrypt the plain text using RSA
|
|
||||||
string? encryptedText = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var plainBytes = System.Text.Encoding.UTF8.GetBytes(plainText);
|
|
||||||
var encryptedBytes = rsa.Encrypt(plainBytes, RSAEncryptionPadding.OaepSHA256);
|
|
||||||
encryptedText = Convert.ToBase64String(encryptedBytes);
|
|
||||||
}
|
|
||||||
catch (CryptographicException ex)
|
|
||||||
{
|
|
||||||
// Handle encryption errors
|
|
||||||
throw new InvalidOperationException("Encryption failed.", ex);
|
|
||||||
}
|
|
||||||
return encryptedText;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? Decrypt(string clientId, string encryptedText)
|
|
||||||
{
|
|
||||||
// get keys from cache
|
|
||||||
if (!_cache.TryGetValue($"{clientId}_private", out string? privateCert) || string.IsNullOrEmpty(privateCert))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Private certificate not found for the client.");
|
|
||||||
}
|
|
||||||
if (!_cache.TryGetValue($"{clientId}_public", out string? publicCert) || string.IsNullOrEmpty(publicCert))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Private certificate not found for the client.");
|
|
||||||
}
|
|
||||||
// import rsa keys and configure RSA for decryption
|
|
||||||
using var rsa = RSA.Create(2048);
|
|
||||||
rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(publicCert), out _);
|
|
||||||
rsa.ImportRSAPrivateKey(Convert.FromBase64String(privateCert), out _);
|
|
||||||
// Decrypt the encrypted text using RSA
|
|
||||||
string? plainText = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var encryptedBytes = Convert.FromBase64String(encryptedText);
|
|
||||||
var decryptedBytes = rsa.Decrypt(encryptedBytes, RSAEncryptionPadding.OaepSHA256);
|
|
||||||
plainText = System.Text.Encoding.UTF8.GetString(decryptedBytes);
|
|
||||||
}
|
|
||||||
catch (CryptographicException ex)
|
|
||||||
{
|
|
||||||
// Handle decryption errors
|
|
||||||
throw new InvalidOperationException("Decryption failed.", ex);
|
|
||||||
}
|
|
||||||
return plainText;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetPublicCertificate(string clientId)
|
|
||||||
{
|
|
||||||
if (_cache.TryGetValue($"{clientId}_public", out string? publicCert) && !string.IsNullOrEmpty(publicCert))
|
|
||||||
{
|
|
||||||
return publicCert;
|
|
||||||
}
|
|
||||||
(publicCert, string privateCert) = GenerateCertificate();
|
|
||||||
_cache.Set($"{clientId}_public", publicCert, _CacheOptions);
|
|
||||||
_cache.Set($"{clientId}_private", privateCert, _CacheOptions);
|
|
||||||
return publicCert;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetPrivateCertificate(string clientId)
|
|
||||||
{
|
|
||||||
if (_cache.TryGetValue($"{clientId}_private", out string? privateCert) && !string.IsNullOrEmpty(privateCert))
|
|
||||||
{
|
|
||||||
return privateCert;
|
|
||||||
}
|
|
||||||
(string publicCert, privateCert) = GenerateCertificate();
|
|
||||||
_cache.Set($"{clientId}_public", publicCert, _CacheOptions);
|
|
||||||
_cache.Set($"{clientId}_private", privateCert, _CacheOptions);
|
|
||||||
return privateCert;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (string publicCert, string privateCert) GenerateCertificate()
|
|
||||||
{
|
|
||||||
// Generate a new RSA key pair for the client
|
|
||||||
using var rsa = RSA.Create(2048);
|
|
||||||
var publicKey = rsa.ExportSubjectPublicKeyInfo();
|
|
||||||
var privateKey = rsa.ExportRSAPrivateKey();
|
|
||||||
// Convert to Base64 strings for storage
|
|
||||||
var publicCert = Convert.ToBase64String(publicKey);
|
|
||||||
var privateCert = Convert.ToBase64String(privateKey);
|
|
||||||
return (publicCert, privateCert);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? Hash(string plainText)
|
|
||||||
{
|
|
||||||
string? hash = null;
|
|
||||||
if (string.IsNullOrEmpty(plainText))
|
|
||||||
{
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
var plainBytes = System.Text.Encoding.UTF8.GetBytes(plainText);
|
|
||||||
var hashBytes = SHA256.HashData(plainBytes);
|
|
||||||
hash = Convert.ToBase64String(hashBytes);
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool VerifyHash(string plainText, string hash)
|
|
||||||
{
|
|
||||||
var plainTextHash = Hash(plainText);
|
|
||||||
if (string.IsNullOrEmpty(plainTextHash) || string.IsNullOrEmpty(hash))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return plainTextHash.Equals(hash, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Pepper()
|
|
||||||
{
|
|
||||||
// get pepper from environtment variable
|
|
||||||
var pepper = Environment.GetEnvironmentVariable("PEPPER");
|
|
||||||
if (string.IsNullOrEmpty(pepper))
|
|
||||||
{
|
|
||||||
return "BactilForteFlash20mg";
|
|
||||||
}
|
|
||||||
return pepper;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Salt()
|
|
||||||
{
|
|
||||||
var saltBytes = new byte[32]; // 256 bits
|
|
||||||
using var rng = RandomNumberGenerator.Create();
|
|
||||||
rng.GetBytes(saltBytes);
|
|
||||||
return Convert.ToBase64String(saltBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? HashPassword(string plainPassword, string plainSalt)
|
|
||||||
{
|
|
||||||
return Hash($"{plainPassword}{plainSalt}{Pepper()}");
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,16 +0,0 @@
|
|||||||
using DependencyInjector.Lifetimes;
|
|
||||||
|
|
||||||
namespace back.services.engine.Crypto;
|
|
||||||
|
|
||||||
public interface ICryptoService : ISingleton
|
|
||||||
{
|
|
||||||
string? Encrypt(string clientId, string plainText);
|
|
||||||
string? Decrypt(string clientId, string encryptedText);
|
|
||||||
string? Hash(string plainText);
|
|
||||||
string? HashPassword(string? plainPassword, string? plainSalt);
|
|
||||||
bool VerifyHash(string plainText, string hash);
|
|
||||||
string Salt();
|
|
||||||
string Pepper();
|
|
||||||
string GetPublicCertificate(string clientId);
|
|
||||||
string GetPrivateCertificate(string clientId);
|
|
||||||
}
|
|
@@ -1,8 +0,0 @@
|
|||||||
using DependencyInjector.Lifetimes;
|
|
||||||
|
|
||||||
namespace back.services.engine.ImageResizer;
|
|
||||||
|
|
||||||
public interface IImageResizer : ISingleton
|
|
||||||
{
|
|
||||||
Task<Stream> ResizeImage(IFormFile image, int v);
|
|
||||||
}
|
|
@@ -1,23 +0,0 @@
|
|||||||
using SixLabors.ImageSharp;
|
|
||||||
using SixLabors.ImageSharp.Processing;
|
|
||||||
|
|
||||||
namespace back.services.engine.ImageResizer;
|
|
||||||
|
|
||||||
public sealed class ImageResizer : IImageResizer
|
|
||||||
{
|
|
||||||
public async Task<Stream> ResizeImage(IFormFile image, int maxRes)
|
|
||||||
{
|
|
||||||
if (image == null || image.Length == 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Invalid image file.");
|
|
||||||
}
|
|
||||||
using var inputStream = image.OpenReadStream();
|
|
||||||
using var outputStream = new MemoryStream();
|
|
||||||
using var img = Image.Load(inputStream);
|
|
||||||
|
|
||||||
img.Mutate(x => x.Resize(new ResizeOptions { Size = new Size(maxRes, 0), Mode = ResizeMode.Max }));
|
|
||||||
await img.SaveAsWebpAsync(outputStream);
|
|
||||||
outputStream.Position = 0;
|
|
||||||
return outputStream;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,8 +0,0 @@
|
|||||||
using DependencyInjector.Lifetimes;
|
|
||||||
|
|
||||||
namespace back.services.engine.PasswordGenerator;
|
|
||||||
|
|
||||||
public interface IPasswordGenerator : ISingleton
|
|
||||||
{
|
|
||||||
string Generate(int length, bool includeNumbers = true, bool includeMayus = true, bool includeMinus = true, bool includeSpecials = true);
|
|
||||||
}
|
|
@@ -1,40 +0,0 @@
|
|||||||
namespace back.services.engine.PasswordGenerator;
|
|
||||||
|
|
||||||
public class PasswordGenerator : IPasswordGenerator
|
|
||||||
{
|
|
||||||
public string Generate(int length, bool includeNumbers = true, bool includeMayus = true, bool includeMinus = true, bool includeSpecials = true)
|
|
||||||
{
|
|
||||||
const string numbers = "0123456789";
|
|
||||||
const string mayus = "ABCÇDEFGHIJKLMNÑOPQRSTUVWXYZ";
|
|
||||||
const string minus = "abcçdefghijklmnñopqrstuvwxyz";
|
|
||||||
const string specials = "!@#$%^&*()_+[]{}|;:,.<>?";
|
|
||||||
var characters = minus;
|
|
||||||
if (includeNumbers) characters += numbers;
|
|
||||||
if (includeMayus) characters += mayus;
|
|
||||||
if (includeSpecials) characters += specials;
|
|
||||||
var random = new Random((int)DateTimeOffset.UtcNow.Ticks);
|
|
||||||
var password = new char[length];
|
|
||||||
|
|
||||||
for (int i = 0; i < length; i++)
|
|
||||||
{
|
|
||||||
password[i] = characters[random.Next(characters.Length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
var positionPool = new List<int>();
|
|
||||||
for (int i = 0; i < length; i++) positionPool.Add(i);
|
|
||||||
var forcedRandomNumber = random.Next(0, positionPool.Count);
|
|
||||||
positionPool.RemoveAt(forcedRandomNumber);
|
|
||||||
var forcedRandomMayus = random.Next(0, positionPool.Count);
|
|
||||||
positionPool.RemoveAt(forcedRandomMayus);
|
|
||||||
var forcedRandomMinus = random.Next(0, positionPool.Count);
|
|
||||||
positionPool.RemoveAt(forcedRandomMinus);
|
|
||||||
var forcedRandomSpecial = random.Next(0, positionPool.Count);
|
|
||||||
positionPool.RemoveAt(forcedRandomSpecial);
|
|
||||||
|
|
||||||
password[forcedRandomNumber] = numbers[random.Next(numbers.Length)];
|
|
||||||
password[forcedRandomMayus] = mayus[random.Next(mayus.Length)];
|
|
||||||
password[forcedRandomMinus] = minus[random.Next(minus.Length)];
|
|
||||||
password[forcedRandomSpecial] = specials[random.Next(specials.Length)];
|
|
||||||
return new string(password);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,8 +0,0 @@
|
|||||||
using DependencyInjector.Lifetimes;
|
|
||||||
|
|
||||||
namespace back.services.engine.SystemUser;
|
|
||||||
|
|
||||||
public interface ISystemUserGenerator: IScoped
|
|
||||||
{
|
|
||||||
Task GenerateAsync();
|
|
||||||
}
|
|
@@ -1,55 +0,0 @@
|
|||||||
using back.DataModels;
|
|
||||||
using back.persistance.blob;
|
|
||||||
using back.persistance.data;
|
|
||||||
using back.persistance.data.repositories.Abstracts;
|
|
||||||
using back.services.engine.Crypto;
|
|
||||||
using back.services.engine.PasswordGenerator;
|
|
||||||
using System.Text.Json;
|
|
||||||
using Transactional.Abstractions.Interfaces;
|
|
||||||
|
|
||||||
namespace back.services.engine.SystemUser;
|
|
||||||
|
|
||||||
public class SystemUserGenerator(
|
|
||||||
ITransactionalService<DataContext> transactional,
|
|
||||||
JsonSerializerOptions jsonSerializerOptions,
|
|
||||||
IUserRepository userRepository,
|
|
||||||
IPersonRepository personRepository,
|
|
||||||
ICryptoService cryptoService,
|
|
||||||
IBlobStorageService blobStorageService,
|
|
||||||
IPasswordGenerator passwordGenerator) : ISystemUserGenerator
|
|
||||||
{
|
|
||||||
public async Task GenerateAsync()
|
|
||||||
{
|
|
||||||
var systemKey = new SystemKey() {
|
|
||||||
Password = passwordGenerator.Generate(16),
|
|
||||||
};
|
|
||||||
var systemKeyJson = JsonSerializer.Serialize(systemKey, options: jsonSerializerOptions);
|
|
||||||
|
|
||||||
using Stream stream = new MemoryStream(new System.Text.UTF8Encoding(true).GetBytes(systemKeyJson));
|
|
||||||
|
|
||||||
await blobStorageService.Delete("systemkey.lock");
|
|
||||||
|
|
||||||
await blobStorageService.Save(
|
|
||||||
stream,
|
|
||||||
"systemkey.lock"
|
|
||||||
);
|
|
||||||
|
|
||||||
User.SystemUser.Password = systemKey.Password;
|
|
||||||
User.SystemUser.Salt = cryptoService.Salt();
|
|
||||||
User.SystemUser.Password = cryptoService.HashPassword(User.SystemUser.Password, User.SystemUser.Salt) ?? string.Empty;
|
|
||||||
|
|
||||||
if (!await userRepository.Exists(User.SystemUser.Id!))
|
|
||||||
{
|
|
||||||
await transactional.DoTransaction(async () =>
|
|
||||||
{
|
|
||||||
await personRepository.Insert(Person.SystemPerson);
|
|
||||||
await userRepository.Insert(User.SystemUser);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await userRepository.Update(User.SystemUser);
|
|
||||||
await userRepository.SaveChanges();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,66 +0,0 @@
|
|||||||
using back.Options;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Mail;
|
|
||||||
|
|
||||||
namespace back.services.engine.mailing;
|
|
||||||
|
|
||||||
public class EmailService(IOptions<MailServerOptions> options) : IEmailService
|
|
||||||
{
|
|
||||||
public async Task SendEmailAsync(List<string> tos, string from, string subject, string body, Dictionary<string, object>? attachments = null, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Parallel.ForEachAsync(tos, async (to, cancellationToken) => {
|
|
||||||
await SendEmailAsync(to, from, subject, body, attachments, cancellationToken);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Log the exception or handle it as needed
|
|
||||||
Console.WriteLine($"Error sending email to multiple recipients: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public async Task SendEmailAsync(string to, string from, string subject, string body, Dictionary<string, object>? attachments = null, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var message = new MailMessage();
|
|
||||||
message.From = new MailAddress(from);
|
|
||||||
message.To.Add(to);
|
|
||||||
message.Subject = subject;
|
|
||||||
message.Body = body;
|
|
||||||
message.IsBodyHtml = true;
|
|
||||||
message.Priority = MailPriority.Normal;
|
|
||||||
message.DeliveryNotificationOptions = DeliveryNotificationOptions.Never;
|
|
||||||
|
|
||||||
if (attachments != null)
|
|
||||||
{
|
|
||||||
foreach (var attachment in attachments)
|
|
||||||
{
|
|
||||||
if (attachment.Value is FileStream fileStream)
|
|
||||||
{
|
|
||||||
message.Attachments.Add(new Attachment(fileStream, attachment.Key));
|
|
||||||
}
|
|
||||||
if (attachment.Value is string filePath && File.Exists(filePath))
|
|
||||||
{
|
|
||||||
message.Attachments.Add(new Attachment(filePath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
using var cliente = new SmtpClient(options.Value.SmtpServer, options.Value.Puerto);
|
|
||||||
cliente.UseDefaultCredentials = false;
|
|
||||||
cliente.Credentials = new NetworkCredential(options.Value.Usuario, options.Value.Password);
|
|
||||||
cliente.EnableSsl = options.Value.EnableSsl;
|
|
||||||
cliente.DeliveryMethod = SmtpDeliveryMethod.Network;
|
|
||||||
|
|
||||||
await cliente.SendMailAsync(message, cancellationToken);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Log the exception or handle it as needed
|
|
||||||
Console.WriteLine($"Error sending email: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,9 +0,0 @@
|
|||||||
using DependencyInjector.Lifetimes;
|
|
||||||
|
|
||||||
namespace back.services.engine.mailing;
|
|
||||||
|
|
||||||
public interface IEmailService : IScoped
|
|
||||||
{
|
|
||||||
Task SendEmailAsync(List<string> tos, string from, string subject, string body, Dictionary<string, object>? attachments = null, CancellationToken cancellationToken = default);
|
|
||||||
Task SendEmailAsync(string tos, string from, string subject, string body, Dictionary<string, object>? attachments = null, CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
61
docs/acta-constitucion.md
Normal file
61
docs/acta-constitucion.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Galerías Fotográficas - Acta de Constitución
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Propósito
|
||||||
|
|
||||||
|
Crear una plataforma web que permita a fotógrafos profesionales almacenar, distribuir y colaborar con sus clientes en la gestión de imágenes digitales de manera eficiente y segura.
|
||||||
|
|
||||||
|
## Objetivos
|
||||||
|
|
||||||
|
1. Facilitar el almacenamiento y la organización de imágenes digitales.
|
||||||
|
2. Mejorar la comunicación y colaboración entre fotógrafos y clientes.
|
||||||
|
3. Proporcionar herramientas para la retroalimentación y revisión de imágenes.
|
||||||
|
4. Ofrecer una experiencia de usuario intuitiva y accesible.
|
||||||
|
5. Ofrecer una solución integral que unifique todos los aspectos de la gestión de imágenes digitales.
|
||||||
|
6. Ofrecer un portfolio público para atraer nuevos clientes.
|
||||||
|
|
||||||
|
## Alcance
|
||||||
|
|
||||||
|
Funcionalidades incluidas:
|
||||||
|
|
||||||
|
- Subida y almacenamiento de imágenes.
|
||||||
|
- Visualización y descarga de imágenes en diferentes tamaños y resoluciones.
|
||||||
|
- Etiquetado y clasificación manual de imágenes.
|
||||||
|
- Creación y gestión de colecciones de imágenes.
|
||||||
|
- Creación y gestión de eventos temporales asociados a colecciones.
|
||||||
|
- Compra de sesiones por parte de clientes.
|
||||||
|
- Compra de imágenes individuales o colecciones.
|
||||||
|
- Generación de portfolio para el profesional.
|
||||||
|
- Gestión de usuarios con diferentes roles (anónimo, cliente, profesional, administrador).
|
||||||
|
- Sistema de feedback y comentarios en imágenes, colecciones, sesiones y portfolio general.
|
||||||
|
- Compresión de imágenes para optimizar su almacenamiento y distribución.
|
||||||
|
|
||||||
|
Funcionalidades excluidas o relegadas a futuro:
|
||||||
|
|
||||||
|
- Generación de títulos y descripciones automáticas para imágenes.
|
||||||
|
- Etiquetado y clasificación automática de imágenes.
|
||||||
|
- Integración con redes sociales para compartir imágenes.
|
||||||
|
- Edición avanzada de imágenes dentro de la plataforma.
|
||||||
|
- Soporte para videos y otros formatos multimedia.
|
||||||
|
- Integración con servicios de impresión y productos físicos.
|
||||||
|
- Funcionalidades de inteligencia artificial para la mejora automática de imágenes.
|
||||||
|
- Soporte para múltiples idiomas y localización.
|
||||||
|
- Integración con herramientas de marketing y análisis de datos.
|
||||||
|
- Funcionalidades de automatización de flujos de trabajo.
|
||||||
|
- Soporte para la gestión de derechos de autor y licencias de imágenes.
|
||||||
|
- Integración con servicios de terceros para la importación de las colecciones compradas por el usuario.
|
||||||
|
- Uso de CDNs para la distribución de imágenes.
|
||||||
|
- Uso de la nube para almacenar las imágenes.
|
||||||
|
|
||||||
|
## Interesados
|
||||||
|
|
||||||
|
- Fotógrafos profesionales y empresas de imagen
|
||||||
|
|
||||||
|
## Restricciones y limitaciones
|
||||||
|
|
||||||
|
- Todas las imágenes vistas en la web tendrán una resolución máxima de 1080p.
|
||||||
|
- No se permitirá la descarga de imágenes en alta resolución sin comprarla.
|
||||||
|
- No se generarán rutas para las imágenes.
|
||||||
|
- Solo se permitirá la subida de imágenes en formatos JPEG, JPG y PNG.
|
||||||
|
- No se permitirá comprar a usuarios no autenticados.
|
142
docs/analisis-tecnico.md
Normal file
142
docs/analisis-tecnico.md
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
# Galerías Fotográficas - Análisis técnico
|
||||||
|
|
||||||
|
## Arquitectura del sistema
|
||||||
|
|
||||||
|
Queremos hacer un sistema modular, con componentes independientes que se puedan desarrollar, probar e implementar de forma aislada.
|
||||||
|
Esto permitirá una mayor flexibilidad y escalabilidad en el desarrollo del sistema.
|
||||||
|
|
||||||
|
Queremos abstraer todo lo posible las dependencias entre capas. De tal forma que el frontal y backend puedan intercambiarse con diferentes implementaciones sin afectar al resto del sistema.
|
||||||
|
Para ello, vamos a hacer el backend database-agnostic, puediendo conectar con diferentes servidores de bases de datos. La responsabilidad de escoger motor de base de datos recaerá sobre el cliente.
|
||||||
|
Nosotros usaremos Sqlite para desarrollo local y PostgreSQL para producción.
|
||||||
|
|
||||||
|
Para poder desarrollarlo rápidamente y tener una base sobre la que iterar, empezaremos con un monolito modular.
|
||||||
|
Este monolito quedará estructurado en módulos que puedan migrarse a microservicios en el futuro.
|
||||||
|
Comandaremos el desarrollo mediante DDD (Domain-Driven Design) y CQRS (Command Query Responsibility Segregation).
|
||||||
|
Algunos procesos como el procesado de imágenes se harán de forma desacoplada y asíncrona.
|
||||||
|
|
||||||
|
Para el frontal, Angular nos ofrece componentes que usaremos para componer vistas siguiendo una arquitectura MVVM (Model-View-ViewModel).
|
||||||
|
|
||||||
|
Para que el frontend y el backend no dependan entre ellos, vamos a establecer una serie de estándares de comunicación.
|
||||||
|
Siempre usaremos el estandar HTTP mediante TLS. (HTTPS)
|
||||||
|
En toda comunicación se preferirá enviar y recibir los datos mediante el body de la petición.
|
||||||
|
|
||||||
|
Toda la información sensible, como contraseñas, se enviará cifrada mediante RSA 256.
|
||||||
|
Para ello, el cliente generará un thumprint único que incluirá el user-agent y un dato único del navegador mas un UUIDv4; hará una petición HEAD al endpoint `/security/rsa` enviando el thumbprint en el header `XXX-Thumbprint`.
|
||||||
|
El cliente recibirá una respuesta vacía (204 No Content) con un header `XXX-encryption-key` que se utilizará para cifrar los valores de los campos concretos.
|
||||||
|
En la siguiente petición, donde se enviarán los datos sensibles, el cliente incluirá `XXX-Thumbprint` en el header.
|
||||||
|
Al recibir la petición, tendrá que rotar el thumbprint.
|
||||||
|
Cada thumbprint será válido durante 5 minutos en caso de conexiones lentas. Sin embargo, no se permitirá su reutilización.
|
||||||
|
En caso de que hubiese un MIM (Man-In-The-Middle) o cualquier otro tipo de atacante tratando de interceptar la comunicación, debe ser capaz de detectar y leer el doble cifrado de HTTPS + thumbprint-rsa.
|
||||||
|
|
||||||
|
De esta forma, todas las peticiones deben cumplir con el siguiente contrato:
|
||||||
|
|
||||||
|
0. Como requisito indispensable, toda la comunicación debe realizarse mediante HTTPS.
|
||||||
|
1. Todas las peticiones deben incluir un header `XXX-Thumbprint` con el thumbprint actual.
|
||||||
|
2. Todas las peticiones incluirán un body que sigue el patron `Request<T>`.
|
||||||
|
3. Las peticiones con datos sensibles, tendrán sus datos cifrados mediante RSA 256.
|
||||||
|
|
||||||
|
El patrón `Request<T>` incluirá un campo con los datos de la petición, otro con el thumprint duplicado y un último campo que indica si tiene datos sensibles o no.
|
||||||
|
Con una implementación como la que sigue:
|
||||||
|
|
||||||
|
```c# Request<T>.cs
|
||||||
|
public class Request<T> where T : IDTOSerializable
|
||||||
|
{
|
||||||
|
public T Data { get; set; }
|
||||||
|
public string Thumbprint { get; set; }
|
||||||
|
public bool HasSensitiveData { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
En el caso de errores, seguiremos el estandar Problem Details extendido.
|
||||||
|
En el caso de respuestas exitosas, las apis responderan mediante el patron `Response<T>`.
|
||||||
|
Una `Response<T>` estará compuesta por diferentes campos que indicarán el estado de la transacción, el resultado de la transacción, y los posibles errores lógicos pertenecientes a los datos enviados.
|
||||||
|
De esta forma, garantizamos un contrato como el siguiente:
|
||||||
|
|
||||||
|
```c# Response<T>.cs
|
||||||
|
public class Response<T> where T : IDTOSerializable
|
||||||
|
{
|
||||||
|
public bool IsSuccess { get; set; } = false;
|
||||||
|
public T Result { get; set; }
|
||||||
|
public List<DataError>? Errors { get; set; } = null;
|
||||||
|
|
||||||
|
public static Response<T> Success(T result)
|
||||||
|
{
|
||||||
|
return new Response<T>
|
||||||
|
{
|
||||||
|
IsSuccess = true,
|
||||||
|
Result = result
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Response<T> Failure(T result, List<DataError> errors)
|
||||||
|
{
|
||||||
|
return new Response<T>
|
||||||
|
{
|
||||||
|
IsSuccess = false,
|
||||||
|
Result = result,
|
||||||
|
Errors = errors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
En caso de que los datos enviados al backend provoquen un error lógico, por ejemplo: una imagen que se ha subido no se puede guardar por que ha llegado corrupta; devolveremos una `Response<T>` que contenga un `DataError` como este:
|
||||||
|
|
||||||
|
``````c#
|
||||||
|
public class DataError
|
||||||
|
{
|
||||||
|
public string Message { get; set; }
|
||||||
|
public string? Details { get; set; }
|
||||||
|
}
|
||||||
|
``````
|
||||||
|
|
||||||
|
De esta forma, el cliente podrá interpretar los errores y mostrarlos al usuario de forma adecuada.
|
||||||
|
|
||||||
|
La autenticación y autorización se manejarán mediante JWT (JSON Web Tokens) y OAuth 2.0.
|
||||||
|
Para ello el front atacará al backend para obtener el token de acceso y luego lo incluirá en las cabeceras de las peticiones con este formato `Authorization: Bearer <token>`.
|
||||||
|
También incluirá el token de refresco en las peticiones que requieran autenticación, con este formato `XXX-Refresh-Token: <token>`.
|
||||||
|
|
||||||
|
## El stack
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
Vamos a usar ASP.NET Core como framework principal, junto con Entity Framework Core para la gestión de la base de datos.
|
||||||
|
Redis se utilizará como caché distribuido para mejorar el rendimiento.
|
||||||
|
PostgreSQL se utilizará como sistema de gestión de bases de datos relacional. En desarrollo se utilizará SQLite.
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
Vamos a usar Angular como framework principal, junto con TailwindCSS para el diseño y NgRx para la gestión del estado.
|
||||||
|
Vite como empaquetador y Node.js como entorno de ejecución.
|
||||||
|
Usaremos RxJS para la programación reactiva.
|
||||||
|
|
||||||
|
## Base de datos: Esquema y relaciones
|
||||||
|
|
||||||
|
## Infraestructura
|
||||||
|
|
||||||
|
Inicialmente se utilizará un enfoque monolítico, pero se diseñará con la posibilidad de escalar a microservicios en el futuro.
|
||||||
|
El producto final debe poder desplegarse en docker y kubernetes. Para ello usarmos Podman como herramienta para gestionar los contenedores.
|
||||||
|
Usaremos una caché para resultados intermedios, como el portfolio.
|
||||||
|
|
||||||
|
## Seguridad
|
||||||
|
|
||||||
|
Para mantener seguro el sistema obligaremos el uso de HTTPS en todas las comunicaciones.
|
||||||
|
Los usuarios deberán autenticarse mediante OAuth 2.0 y OpenID Connect usando JWT (JSON Web Tokens) para la gestión de sesiones. Firmaremos los tokens JWT con una clave secreta almacenada de forma segura. Validaremos la firma siempre. El certificado usado variará cada 24 horas y en cada reinicio.
|
||||||
|
Los JWT tendrán asociados un Refresh Token que permitirá obtener nuevos tokens de acceso sin necesidad de volver a autenticarse.
|
||||||
|
Para protegernos contra ataques CSRF (Cross-Site Request Forgery), implementaremos tokens CSRF en todas las solicitudes que modifiquen datos.
|
||||||
|
Habilitaremos CORS (Cross-Origin Resource Sharing) para permitir que solo dominios específicos puedan acceder a nuestra API.
|
||||||
|
Implementaremos políticas de contraseñas seguras, incluyendo longitud mínima, complejidad y expiración periódica.
|
||||||
|
Además, se instará a los usuarios a habilitar la autenticación de dos factores (2FA) para añadir una capa adicional de seguridad.
|
||||||
|
Todos los usuarios que inicien sesión mediante usuario y contraseña recibirán un correo electrónico de acceso único, con un token de acceso que caducará en 5 minutos.
|
||||||
|
Para proteger los datos sensibles, como contraseñas y tokens, se utilizará hashing y cifrado.
|
||||||
|
Implementaremos un sistema de roles y permisos para controlar el acceso a diferentes partes del sistema.
|
||||||
|
|
||||||
|
Para evitar ataques de fuerza bruta y ataques de tiempo, implementaremos 4 políticas:
|
||||||
|
|
||||||
|
- Limitar el número de intentos de inicio de sesión fallidos y bloquear el acceso temporalmente.
|
||||||
|
- Implementar un sistema de CAPTCHA que aparecerá aleatoriamente en cada intento de inicio de sesión y después de 3 intentos fallidos.
|
||||||
|
- Monitorear y registrar los intentos de inicio de sesión para detectar patrones sospechosos.
|
||||||
|
- Retrasar aleatoriamente las respuestas del servidor una cantidad de tiempo variable inferior a los 3 segundos. (Sacrificamos un poco de la experiencia por algo más de seguridad)
|
||||||
|
|
||||||
|
Guardaremos el mínimo número de cookies posibles y evitaremos usar cookies de sesión.
|
||||||
|
Todas las cookies que usemos serán seguras, HttpOnly y SameSite=Strict.
|
275
docs/casos-uso.md
Normal file
275
docs/casos-uso.md
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
# Galerías Fotográficas - Casos de Uso
|
||||||
|
|
||||||
|
## Caso de uso 1: Administrador da de alta un nuevo profesional
|
||||||
|
|
||||||
|
1. El administrador accede al sistema.
|
||||||
|
2. El administrador navega a la sección de gestión de profesionales.
|
||||||
|
3. El administrador selecciona la opción "Agregar nuevo profesional".
|
||||||
|
4. El administrador completa el formulario con los datos del nuevo profesional.
|
||||||
|
5. El administrador envía el formulario.
|
||||||
|
6. El sistema confirma la creación del nuevo profesional.
|
||||||
|
|
||||||
|
## Caso de uso 2: Administrador crea un portfolio
|
||||||
|
|
||||||
|
1. El administrador accede al sistema.
|
||||||
|
2. El administrador navega a la sección de gestión de portfolios.
|
||||||
|
3. El administrador selecciona la opción "Crear nuevo portfolio".
|
||||||
|
4. El administrador completa el formulario con los datos del nuevo portfolio.
|
||||||
|
5. El administrador envía el formulario.
|
||||||
|
6. El sistema confirma la creación del nuevo portfolio.
|
||||||
|
|
||||||
|
## Caso de uso 3: Profesional crea una imagen
|
||||||
|
|
||||||
|
1. El profesional accede al sistema.
|
||||||
|
2. El profesional navega a la sección de gestión de imágenes.
|
||||||
|
3. El profesional selecciona la opción "Crear nueva imagen".
|
||||||
|
4. El profesional completa el formulario con los datos de la nueva imagen.
|
||||||
|
5. El profesional envía el formulario.
|
||||||
|
6. El sistema confirma la creación de la nueva imagen.
|
||||||
|
|
||||||
|
## Caso de uso 4: Profesional crea un conjunto de imágenes
|
||||||
|
|
||||||
|
1. El profesional accede al sistema.
|
||||||
|
2. El profesional navega a la sección de gestión de conjuntos de imágenes.
|
||||||
|
3. El profesional selecciona la opción "Crear nuevo conjunto de imágenes".
|
||||||
|
4. El profesional completa el formulario con los datos del nuevo conjunto de imágenes.
|
||||||
|
5. El profesional envía el formulario.
|
||||||
|
6. El sistema confirma la creación del nuevo conjunto de imágenes.
|
||||||
|
|
||||||
|
## Caso de uso 5: Profesional crea una colección
|
||||||
|
|
||||||
|
1. El profesional accede al sistema.
|
||||||
|
2. El profesional navega a la sección de gestión de colecciones.
|
||||||
|
3. El profesional selecciona la opción "Crear nueva colección".
|
||||||
|
4. El profesional completa el formulario con los datos de la nueva colección.
|
||||||
|
5. El profesional envía el formulario.
|
||||||
|
6. El sistema confirma la creación de la nueva colección.
|
||||||
|
|
||||||
|
## Caso de uso 6: Profesional añade una imagen al portfolio
|
||||||
|
|
||||||
|
1. El profesional accede al sistema.
|
||||||
|
2. El profesional navega a la sección de gestión de portfolios.
|
||||||
|
3. El profesional selecciona el portfolio al que desea añadir la imagen.
|
||||||
|
4. El profesional selecciona la opción "Añadir imagen".
|
||||||
|
5. El profesional completa el formulario con los datos de la imagen.
|
||||||
|
6. El profesional envía el formulario.
|
||||||
|
7. El sistema confirma la adición de la imagen al portfolio.
|
||||||
|
|
||||||
|
## Caso de uso 7: Profesional añade varias imágenes al portfolio
|
||||||
|
|
||||||
|
1. El profesional accede al sistema.
|
||||||
|
2. El profesional navega a la sección de gestión de portfolios.
|
||||||
|
3. El profesional selecciona el portfolio al que desea añadir las imágenes.
|
||||||
|
4. El profesional selecciona la opción "Añadir varias imágenes".
|
||||||
|
5. El profesional completa el formulario con los datos de las imágenes.
|
||||||
|
6. El profesional envía el formulario.
|
||||||
|
7. El sistema confirma la adición de las imágenes al portfolio.
|
||||||
|
|
||||||
|
## Caso de uso 8: Profesional añade una colección al portfolio
|
||||||
|
|
||||||
|
1. El profesional accede al sistema.
|
||||||
|
2. El profesional navega a la sección de gestión de portfolios.
|
||||||
|
3. El profesional selecciona el portfolio al que desea añadir la colección.
|
||||||
|
4. El profesional selecciona la opción "Añadir colección".
|
||||||
|
5. El profesional completa el formulario con los datos de la colección.
|
||||||
|
6. El profesional envía el formulario.
|
||||||
|
7. El sistema confirma la adición de la colección al portfolio.
|
||||||
|
|
||||||
|
## Caso de uso 9: Profesional añade varias colecciones al portfolio
|
||||||
|
|
||||||
|
1. El profesional accede al sistema.
|
||||||
|
2. El profesional navega a la sección de gestión de portfolios.
|
||||||
|
3. El profesional selecciona el portfolio al que desea añadir las colecciones.
|
||||||
|
4. El profesional selecciona la opción "Añadir varias colecciones".
|
||||||
|
5. El profesional completa el formulario con los datos de las colecciones.
|
||||||
|
6. El profesional envía el formulario.
|
||||||
|
7. El sistema confirma la adición de las colecciones al portfolio.
|
||||||
|
|
||||||
|
## Caso de uso 10: Cliente compra una imagen individual
|
||||||
|
|
||||||
|
1. El cliente accede al sistema.
|
||||||
|
2. El cliente navega a la sección de imágenes.
|
||||||
|
3. El cliente selecciona la imagen que desea comprar.
|
||||||
|
4. El cliente añade la imagen al carrito de compras.
|
||||||
|
5. El cliente procede al pago.
|
||||||
|
6. El sistema confirma la compra de la imagen.
|
||||||
|
|
||||||
|
## Caso de uso 11: Cliente compra un conjunto de imágenes individuales
|
||||||
|
|
||||||
|
1. El cliente accede al sistema.
|
||||||
|
2. El cliente navega a la sección de conjuntos de imágenes.
|
||||||
|
3. El cliente selecciona el conjunto de imágenes que desea comprar.
|
||||||
|
4. El cliente añade el conjunto de imágenes al carrito de compras.
|
||||||
|
5. El cliente procede al pago.
|
||||||
|
6. El sistema confirma la compra del conjunto de imágenes.
|
||||||
|
|
||||||
|
## Caso de uso 12: Cliente compra una colección
|
||||||
|
|
||||||
|
1. El cliente accede al sistema.
|
||||||
|
2. El cliente navega a la sección de colecciones.
|
||||||
|
3. El cliente selecciona la colección que desea comprar.
|
||||||
|
4. El cliente añade la colección al carrito de compras.
|
||||||
|
5. El cliente procede al pago.
|
||||||
|
6. El sistema confirma la compra de la colección.
|
||||||
|
|
||||||
|
## Caso de uso 13: Cliente ve en el historial de compras una imagen individual
|
||||||
|
|
||||||
|
1. El cliente accede al sistema.
|
||||||
|
2. El cliente navega a la sección de historial de compras.
|
||||||
|
3. El cliente selecciona la imagen individual que desea ver.
|
||||||
|
4. El sistema muestra los detalles de la imagen individual.
|
||||||
|
|
||||||
|
## Caso de uso 14: Cliente ve en el historial de compras un conjunto de imágenes individuales
|
||||||
|
|
||||||
|
1. El cliente accede al sistema.
|
||||||
|
2. El cliente navega a la sección de historial de compras.
|
||||||
|
3. El cliente selecciona el conjunto de imágenes individuales que desea ver.
|
||||||
|
4. El sistema muestra los detalles del conjunto de imágenes individuales.
|
||||||
|
|
||||||
|
## Caso de uso 15: Cliente ve en el historial de compras una colección
|
||||||
|
|
||||||
|
1. El cliente accede al sistema.
|
||||||
|
2. El cliente navega a la sección de historial de compras.
|
||||||
|
3. El cliente selecciona la colección que desea ver.
|
||||||
|
4. El sistema muestra los detalles de la colección.
|
||||||
|
|
||||||
|
## Caso de uso 16: Cliente contrata una sesión
|
||||||
|
|
||||||
|
1. El cliente accede al sistema.
|
||||||
|
2. El cliente navega a la sección de sesiones.
|
||||||
|
3. El cliente selecciona la sesión que desea contratar.
|
||||||
|
4. El cliente completa el formulario de contratación.
|
||||||
|
5. El cliente envía el formulario.
|
||||||
|
6. El sistema confirma la contratación de la sesión.
|
||||||
|
|
||||||
|
## Caso de uso 17: Profesional acepta una sesión
|
||||||
|
|
||||||
|
1. El profesional accede al sistema.
|
||||||
|
2. El profesional navega a la sección de sesiones.
|
||||||
|
3. El profesional selecciona la sesión que desea aceptar.
|
||||||
|
4. El profesional confirma la aceptación de la sesión.
|
||||||
|
5. El sistema notifica al cliente sobre la aceptación de la sesión.
|
||||||
|
|
||||||
|
## Caso de uso 18: Profesional rechaza una sesión
|
||||||
|
|
||||||
|
1. El profesional accede al sistema.
|
||||||
|
2. El profesional navega a la sección de sesiones.
|
||||||
|
3. El profesional selecciona la sesión que desea rechazar.
|
||||||
|
4. El profesional confirma el rechazo de la sesión.
|
||||||
|
5. El sistema notifica al cliente sobre el rechazo de la sesión.
|
||||||
|
|
||||||
|
## Caso de uso 19: Profesional añade una imagen a una sesión
|
||||||
|
|
||||||
|
1. El profesional accede al sistema.
|
||||||
|
2. El profesional navega a la sección de sesiones.
|
||||||
|
3. El profesional selecciona la sesión a la que desea añadir la imagen.
|
||||||
|
4. El profesional sube la imagen.
|
||||||
|
5. El sistema confirma la adición de la imagen a la sesión.
|
||||||
|
|
||||||
|
## Caso de uso 20: Cliente da feedback de una imagen
|
||||||
|
|
||||||
|
1. El cliente accede al sistema.
|
||||||
|
2. El cliente navega a la sección de imágenes.
|
||||||
|
3. El cliente selecciona la imagen a la que desea dar feedback.
|
||||||
|
4. El cliente proporciona su feedback.
|
||||||
|
5. El sistema confirma la recepción del feedback.
|
||||||
|
|
||||||
|
## Caso de uso 21: Profesional crea una versión de una imagen
|
||||||
|
|
||||||
|
1. El profesional accede al sistema.
|
||||||
|
2. El profesional navega a la sección de imágenes.
|
||||||
|
3. El profesional selecciona la imagen de la que desea crear una versión.
|
||||||
|
4. El profesional realiza las modificaciones necesarias en la imagen.
|
||||||
|
5. El profesional guarda la nueva versión de la imagen.
|
||||||
|
6. El sistema confirma la creación de la nueva versión de la imagen.
|
||||||
|
|
||||||
|
## Caso de uso 22: Cliente da feedback de una version de una imagen
|
||||||
|
|
||||||
|
1. El cliente accede al sistema.
|
||||||
|
2. El cliente navega a la sección de imágenes.
|
||||||
|
3. El cliente selecciona la versión de la imagen a la que desea dar feedback.
|
||||||
|
4. El cliente proporciona su feedback.
|
||||||
|
5. El sistema confirma la recepción del feedback.
|
||||||
|
|
||||||
|
## Caso de uso 23: Cliente selecciona imagen para finalizar sesión
|
||||||
|
|
||||||
|
1. El cliente accede al sistema.
|
||||||
|
2. El cliente navega a la sección de imágenes.
|
||||||
|
3. El cliente selecciona la imagen que desea utilizar para finalizar la sesión.
|
||||||
|
4. El sistema muestra la imagen seleccionada.
|
||||||
|
|
||||||
|
## Caso de uso 24: Profesional finaliza una sesión
|
||||||
|
|
||||||
|
1. El profesional accede al sistema.
|
||||||
|
2. El profesional navega a la sección de sesiones.
|
||||||
|
3. El profesional selecciona la sesión que desea finalizar.
|
||||||
|
4. El profesional confirma la finalización de la sesión.
|
||||||
|
5. El sistema notifica al cliente sobre la finalización de la sesión.
|
||||||
|
|
||||||
|
## Caso de uso 25: Profesional crea un evento-colección
|
||||||
|
|
||||||
|
1. El profesional accede al sistema.
|
||||||
|
2. El profesional navega a la sección de eventos.
|
||||||
|
3. El profesional selecciona la opción de crear un nuevo evento-colección.
|
||||||
|
4. El profesional completa el formulario de creación del evento-colección.
|
||||||
|
5. El profesional envía el formulario.
|
||||||
|
6. El sistema confirma la creación del evento-colección.
|
||||||
|
|
||||||
|
## Caso de uso 26: Profesional añade una imagen a un evento
|
||||||
|
|
||||||
|
1. El profesional accede al sistema.
|
||||||
|
2. El profesional navega a la sección de eventos.
|
||||||
|
3. El profesional selecciona el evento al que desea añadir la imagen.
|
||||||
|
4. El profesional sube la imagen.
|
||||||
|
5. El sistema confirma la adición de la imagen al evento.
|
||||||
|
|
||||||
|
## Caso de uso 27: Profesional añade un tag a un evento
|
||||||
|
|
||||||
|
1. El profesional accede al sistema.
|
||||||
|
2. El profesional navega a la sección de eventos.
|
||||||
|
3. El profesional selecciona el evento al que desea añadir el tag.
|
||||||
|
4. El profesional introduce el tag.
|
||||||
|
5. El sistema confirma la adición del tag al evento.
|
||||||
|
|
||||||
|
## Caso de uso 28: Profesional añade un tag a una imagen
|
||||||
|
|
||||||
|
1. El profesional accede al sistema.
|
||||||
|
2. El profesional navega a la sección de imágenes.
|
||||||
|
3. El profesional selecciona la imagen a la que desea añadir el tag.
|
||||||
|
4. El profesional introduce el tag.
|
||||||
|
5. El sistema confirma la adición del tag a la imagen.
|
||||||
|
|
||||||
|
## Caso de uso 29: Anónimo busca imagen por tags
|
||||||
|
|
||||||
|
1. El anónimo accede al sistema.
|
||||||
|
2. El anónimo navega a la sección de imágenes.
|
||||||
|
3. El anónimo introduce el tag por el que desea buscar.
|
||||||
|
4. El sistema muestra las imágenes que coinciden con el tag.
|
||||||
|
|
||||||
|
## Caso de uso 30: Anónimo busca evento por tags
|
||||||
|
|
||||||
|
1. El anónimo accede al sistema.
|
||||||
|
2. El anónimo navega a la sección de eventos.
|
||||||
|
3. El anónimo introduce el tag por el que desea buscar.
|
||||||
|
4. El sistema muestra los eventos que coinciden con el tag.
|
||||||
|
|
||||||
|
## Caso de uso 31: Anónimo busca colección por tags
|
||||||
|
|
||||||
|
1. El anónimo accede al sistema.
|
||||||
|
2. El anónimo navega a la sección de colecciones.
|
||||||
|
3. El anónimo introduce el tag por el que desea buscar.
|
||||||
|
4. El sistema muestra las colecciones que coinciden con el tag.
|
||||||
|
|
||||||
|
## Caso de uso 32: Anónimo filtra imagen por fecha
|
||||||
|
|
||||||
|
1. El anónimo accede al sistema.
|
||||||
|
2. El anónimo navega a la sección de imágenes.
|
||||||
|
3. El anónimo selecciona el rango de fechas para filtrar las imágenes.
|
||||||
|
4. El sistema muestra las imágenes que coinciden con el rango de fechas.
|
||||||
|
|
||||||
|
## Caso de uso 33: Anónimo filtra eventos por fecha
|
||||||
|
|
||||||
|
1. El anónimo accede al sistema.
|
||||||
|
2. El anónimo navega a la sección de eventos.
|
||||||
|
3. El anónimo selecciona el rango de fechas para filtrar los eventos.
|
||||||
|
4. El sistema muestra los eventos que coinciden con el rango de fechas.
|
40
docs/decisiones-arquitectura.md
Normal file
40
docs/decisiones-arquitectura.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Galerías Fotográficas - Decisiones de Arquitectura
|
||||||
|
|
||||||
|
## ¿Por qué Angular en lugar de React?
|
||||||
|
|
||||||
|
Como producto final, se pretende vender varias plantillas frontales.
|
||||||
|
Vamos a empezar por el diseño de la primera plantilla con Angular por su fácil escalabilidad y modularidad para el caso en que se incorporen nuevo programadores.
|
||||||
|
|
||||||
|
## ¿Por qué .NET en lugar de Node.js?
|
||||||
|
|
||||||
|
.NET ofrece un rendimiento superior y una mejor integración con herramientas empresariales, lo que lo hace más adecuado para aplicaciones de gran escala.
|
||||||
|
Además, permite un desarrollo más rápido y eficiente gracias a su robusto ecosistema de bibliotecas y herramientas.
|
||||||
|
|
||||||
|
## Decisiones sobre manejo de imágenes
|
||||||
|
|
||||||
|
Como objetivo final, se pretende tener una independencia total del proveedor de almacenamiento de imágenes. Para ello, se optará por una arquitectura que permita cambiar de proveedor sin afectar al resto del sistema.
|
||||||
|
Por facilidad y sencillez durante el desarrollo, se comenzará con un sqlite + el sistema de archivos local.
|
||||||
|
|
||||||
|
Para tratar los datos usaremos EntityFramework con una visión Data First, es decir, primero se definirá el modelo de datos y las relaciones en base de datos y posteriorimente se generarán las migraciones y se modificará el modelo del back.
|
||||||
|
Usaremos Serilog + OpenTelemetry para el monitoreo y trazabilidad de las aplicaciones de forma sencilla.
|
||||||
|
Usaremos Scalar + OpenApi para documentar la API de forma sencilla y visual.
|
||||||
|
Usaremos MailKit para enviar correos electrónicos de forma sencilla y eficiente.
|
||||||
|
Usaremos MediatR para la implementación de patrones CQRS (Command Query Responsibility Segregation) en el backend.
|
||||||
|
Usaremos Polly para la gestión de resiliencia y manejo de fallos en las aplicaciones.
|
||||||
|
Usaremos FluentValidation para la validación de datos de forma sencilla y eficiente.
|
||||||
|
Usaremos Hangfire para la gestión de trabajos en segundo plano.
|
||||||
|
Usaremos Redis como sistema de caché distribuido.
|
||||||
|
Usaremos PostgreSQL como sistema de gestión de bases de datos. Durante el desarrollo, y en entornos locales, usaremos sqlite.
|
||||||
|
Usaremos AutoMapper para la mapeo de objetos de forma sencilla y eficiente.
|
||||||
|
|
||||||
|
Para el front usaremos TailwindCSS para el diseño de interfaces de usuario de forma rápida.
|
||||||
|
Usaremos NgRx para la gestión del estado de la aplicación de forma predecible y eficiente.
|
||||||
|
|
||||||
|
## Elección de patrones arquitectónicos
|
||||||
|
|
||||||
|
En el backend se usará DDD ( Domain-Driven Design ) y CQRS ( Command Query Responsibility Segregation ).
|
||||||
|
Esto facilitará la escalabilidad y el mantenimiento del sistema a largo plazo.
|
||||||
|
Además, permite una rápida implementación de nuevas funcionalidades y una adaptación más simple para nuevo programadores.
|
||||||
|
|
||||||
|
En el frontend se utilizará una arquitectura MVVM (Model-View-ViewModel) repartida en componentes reutilizables.
|
||||||
|
Esto ayudará en la separación de responsabilidades de cada componente y facilitará la adición de nuevas pantallas.
|
107
docs/documentacion-api.md
Normal file
107
docs/documentacion-api.md
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# Galerías Fotográficas - Documentación API
|
||||||
|
|
||||||
|
## Autenticación y autorización
|
||||||
|
|
||||||
|
POST `/auth/login` -> Inicia sesión y devuelve un token de autenticación.
|
||||||
|
|
||||||
|
POST `/auth/logout` -> Cierra sesión y revoca el token de autenticación.
|
||||||
|
|
||||||
|
## Endpoints de usuarios
|
||||||
|
|
||||||
|
GET `/users/` -> Obtiene la lista de usuarios.
|
||||||
|
|
||||||
|
GET `/users/{id}` -> Obtiene los detalles de un usuario específico.
|
||||||
|
|
||||||
|
POST `/users/` -> Crea un nuevo usuario. Se espera que el cuerpo de la solicitud contenga los datos del usuario.
|
||||||
|
|
||||||
|
PUT `/users/{id}` -> Actualiza los detalles de un usuario específico. Se espera que el cuerpo de la solicitud contenga los datos actualizados del usuario.
|
||||||
|
|
||||||
|
DELETE `/users/{id}` -> Elimina un usuario específico.
|
||||||
|
|
||||||
|
## Endpoints de imágenes
|
||||||
|
|
||||||
|
GET `/images/` -> Obtiene la lista de imágenes. En el body se incluirá una variable indicando si tiene que devolver las imágenes de un usuario específico o de un profesional.
|
||||||
|
|
||||||
|
POST `/images/` -> Crea una nueva imagen. Se espera que el cuerpo de la solicitud contenga los datos de la imagen, incluidos los metadatos.
|
||||||
|
|
||||||
|
GET `/images/{id}` -> Obtiene los detalles de una imagen específica. Incluye los metadatos asociados a la imagen. Según la querystring, devolverá la imagen en 360p, 720p o 1080p.
|
||||||
|
|
||||||
|
PUT `/images/{id}` -> Actualiza los detalles de una imagen específica. Se espera que el cuerpo de la solicitud contenga los datos actualizados de la imagen.
|
||||||
|
|
||||||
|
DELETE `/images/{id}` -> Elimina una imagen específica.
|
||||||
|
|
||||||
|
## Endpoints de eventos
|
||||||
|
|
||||||
|
GET `/events/` -> Obtiene la lista de eventos. En el body se incluirá una variable indicando si tiene que devolver los eventos de un usuario específico o de un profesional.
|
||||||
|
|
||||||
|
POST `/events/` -> Crea un nuevo evento. Se espera que el cuerpo de la solicitud contenga los datos del evento, incluidos los metadatos.
|
||||||
|
|
||||||
|
GET `/events/{id}` -> Obtiene los detalles de un evento específico. Incluye los metadatos asociados al evento.
|
||||||
|
|
||||||
|
PUT `/events/{id}` -> Actualiza los detalles de un evento específico. Se espera que el cuerpo de la solicitud contenga los datos actualizados del evento.
|
||||||
|
|
||||||
|
DELETE `/events/{id}` -> Elimina un evento específico.
|
||||||
|
|
||||||
|
## Endpoints de colecciones
|
||||||
|
|
||||||
|
GET `/collection/` -> Obtiene la lista de colecciones. En el body se incluirá una variable indicando si tiene que devolver las colecciones de un usuario específico o de un profesional.
|
||||||
|
|
||||||
|
POST `/collection/` -> Crea una nueva colección. Devuelve los detalles de la colección creada.
|
||||||
|
|
||||||
|
GET `/collection/{id}` -> Obtiene los detalles de una colección específica. En el body se indica el ID del usuario que la consulta, si no está relacionado con esa colección, se devolverá un error 404 (Not Found).
|
||||||
|
|
||||||
|
PUT `/collection/{id}` -> Actualiza una colección existente.
|
||||||
|
|
||||||
|
DELETE `/collection/{id}` -> Elimina una colección.
|
||||||
|
|
||||||
|
GET `/collection/{id}/images` -> Obtiene la lista de imágenes de una colección específica. Cada imagen incluirá información como el ID de la imagen, la URL de la imagen y los metadatos asociados.
|
||||||
|
|
||||||
|
POST `/collection/{id}/images` -> Crea una nueva imagen en una colección específica. Se espera que el cuerpo de la solicitud contenga los datos de la imagen, incluidos los metadatos.
|
||||||
|
|
||||||
|
GET `/collection/{id}/images/{id}` -> Obtiene los detalles de una imagen específica de una colección. Incluye los metadatos asociados a la imagen.
|
||||||
|
|
||||||
|
PUT `/collection/{id}/images/{id}` -> Actualiza los detalles de una imagen específica de una colección. Se espera que el cuerpo de la solicitud contenga los datos actualizados de la imagen.
|
||||||
|
|
||||||
|
DELETE `/collection/{id}/images/{id}` -> Elimina una imagen específica de una colección.
|
||||||
|
|
||||||
|
## Endpoints de sesiones
|
||||||
|
|
||||||
|
GET `/session/` -> Obtiene la lista de sesiones. En el body se incluirá una variable indicando si tiene que devolver las sesiones activas de un usuario específico o de un profesional.
|
||||||
|
|
||||||
|
POST `/session/` -> Crea una nueva sesión. Devuelve los detalles de la sesión creada.
|
||||||
|
|
||||||
|
GET `/session/{id}` -> Obtiene los detalles de una sesión específica. En el body se indica el ID del usuario que la consulta, si no está relacionado con esa sesión, se devolverá un error 404 (Not Found).
|
||||||
|
|
||||||
|
PUT `/session/{id}` -> Actualiza una sesión existente.
|
||||||
|
|
||||||
|
DELETE `/session/{id}` -> Elimina una sesión.
|
||||||
|
|
||||||
|
GET `/session/{id}/image/` -> Obtiene la lista de imágenes de una sesión específica. Cada imagen incluirá información como el ID de la imagen, la URL de la imagen y los metadatos asociados y el feedback asociado.
|
||||||
|
|
||||||
|
POST `/session/{sessionId}/image` -> Crea una nueva imagen en una sesión específica. Se espera que el cuerpo de la solicitud contenga los datos de la imagen, incluidos los metadatos.
|
||||||
|
|
||||||
|
GET `/session/{sessionId}/image/{imageId}` -> Obtiene los detalles de una imagen específica de una sesión. Incluye el feedback asociado a la imagen.
|
||||||
|
|
||||||
|
PUT `/session/{sessionId}/image/{imageId}` -> Actualiza los detalles de una imagen específica de una sesión. Incluye el feedback asociado a la imagen.
|
||||||
|
|
||||||
|
PATCH `/session/{sessionId}/image/{imageId}` -> Actualiza el feedback asociado a una imagen específica de una sesión.
|
||||||
|
|
||||||
|
DELETE `/session/{sessionId}/image/{imageId}` -> Elimina una imagen específica de una sesión.
|
||||||
|
|
||||||
|
GET `/session/{sessionId}/image/{imageId}/revision/{revisionId}` -> Obtiene los detalles de una revisión específica de una imagen en una sesión.
|
||||||
|
|
||||||
|
POST `/session/{sessionId}/image/{imageId}/revision/{revisionId}` -> Crea una nueva revisión para una imagen específica en una sesión. Se espera que el cuerpo de la solicitud contenga los datos de la revisión.
|
||||||
|
|
||||||
|
PUT `/session/{sessionId}/image/{imageId}/revision/{revisionId}` -> Actualiza los detalles de una revisión específica de una imagen en una sesión.
|
||||||
|
|
||||||
|
DELETE `/session/{sessionId}/image/{imageId}/revision/{revisionId}` -> Elimina una revisión específica de una imagen en una sesión.
|
||||||
|
|
||||||
|
## Endpoints de pagos
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
## Códigos de error y manejo de excepciones
|
||||||
|
|
||||||
|
Todos los errores se manejarán mediante el patrón `Response<T>` descrito anteriormente.
|
||||||
|
En caso de que sean errores de validación, se devolverá un código de estado 400 (Bad Request) junto con una lista de errores detallados.
|
||||||
|
En cualquier otro caso, se tratarán mediante el estandar Problem Details.
|
@@ -1,69 +0,0 @@
|
|||||||
# Documentación del Frontend
|
|
||||||
|
|
||||||
## **Tecnología Base**
|
|
||||||
|
|
||||||
- **Framework:** Angular 20
|
|
||||||
- **Lenguaje:** TypeScript
|
|
||||||
- **Estilos:** SCSS
|
|
||||||
- **Gestión de Estado:** RxJS (si aplica)
|
|
||||||
- **Rutas:** Angular Router
|
|
||||||
|
|
||||||
## **Estructura del Proyecto**
|
|
||||||
|
|
||||||
El proyecto estará organizado en módulos y componentes para maximizar la reutilización y la escalabilidad. La estructura inicial será:
|
|
||||||
|
|
||||||
```FileSystem
|
|
||||||
src/
|
|
||||||
app/
|
|
||||||
core/ # Servicios y lógica compartida
|
|
||||||
shared/ # Componentes y directivas reutilizables
|
|
||||||
features/ # Módulos específicos de características
|
|
||||||
home/ # Página principal
|
|
||||||
gallery/ # Página de galería
|
|
||||||
profile/ # Página de perfil
|
|
||||||
app-routing.module.ts
|
|
||||||
app.module.ts
|
|
||||||
assets/ # Recursos estáticos como imágenes
|
|
||||||
environments/ # Configuraciones de entorno
|
|
||||||
```
|
|
||||||
|
|
||||||
## **Componentes Principales**
|
|
||||||
|
|
||||||
### **Menú de Navegación (`MenuComponent`)**
|
|
||||||
|
|
||||||
- **Descripción:** Componente para la navegación entre las diferentes secciones de la aplicación.
|
|
||||||
- **Propiedades:**
|
|
||||||
- `links: Array<{ label: string, route: string }>`: Lista de enlaces.
|
|
||||||
- **Eventos:**
|
|
||||||
- `onLinkClick`: Evento emitido al hacer clic en un enlace.
|
|
||||||
|
|
||||||
### **Galería (`GalleryComponent`)**
|
|
||||||
|
|
||||||
- **Descripción:** Componente para mostrar una colección de imágenes.
|
|
||||||
- **Propiedades:**
|
|
||||||
- `images: Array<{ src: string, alt: string }>`: Lista de imágenes.
|
|
||||||
- **Eventos:**
|
|
||||||
- `onImageClick`: Evento emitido al seleccionar una imagen.
|
|
||||||
|
|
||||||
### **Imagen (`ImageComponent`)**
|
|
||||||
|
|
||||||
- **Descripción:** Componente para mostrar una imagen individual.
|
|
||||||
- **Propiedades:**
|
|
||||||
- `src: string`: URL de la imagen.
|
|
||||||
- `alt: string`: Texto alternativo.
|
|
||||||
- **Eventos:**
|
|
||||||
- `onClick`: Evento emitido al hacer clic en la imagen.
|
|
||||||
|
|
||||||
### **Pie de Página (`FooterComponent`)**
|
|
||||||
|
|
||||||
- **Descripción:** Componente para mostrar información adicional en la parte inferior de la página.
|
|
||||||
- **Propiedades:**
|
|
||||||
- `links: Array<{ label: string, url: string }>`: Enlaces a recursos externos.
|
|
||||||
|
|
||||||
### **Página Principal (`HomePageComponent`)**
|
|
||||||
|
|
||||||
- **Descripción:** Página que combina el menú, la galería y el pie de página.
|
|
||||||
- **Componentes Hijos:**
|
|
||||||
- `MenuComponent`
|
|
||||||
- `GalleryComponent`
|
|
||||||
- `FooterComponent`
|
|
77
docs/guia-estilo.md
Normal file
77
docs/guia-estilo.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Galerías Fotográficas - Guía de Estilo
|
||||||
|
|
||||||
|
## Materiales
|
||||||
|
|
||||||
|
Usaremos [fluent ui](https://github.com/microsoft/fluentui-system-icons) como base de iconos.
|
||||||
|
Usaremos Tailwind CSS como framework de diseño.
|
||||||
|
Crearemos 2 temas: claro y oscuro.
|
||||||
|
|
||||||
|
## Paleta de colores
|
||||||
|
|
||||||
|
Se definirán 2 paletas de colores base: una para el modo claro y otra para el modo oscuro. Cada paleta incluirá colores primarios, secundarios, de fondo y colores de acento para los estados de la interfaz y los resaltos de la misma.
|
||||||
|
|
||||||
|
## Tipografías
|
||||||
|
|
||||||
|
Se seleccionarán tipografías que sean legibles y que se alineen con la estética de la marca. Se establecerán jerarquías tipográficas claras para títulos, subtítulos y texto de cuerpo.
|
||||||
|
|
||||||
|
## Componentes reutilizables de Angular
|
||||||
|
|
||||||
|
Se crearán diferentes componentes reutilizables en Angular para garantizar la consistencia y facilitar el desarrollo.
|
||||||
|
Los principales componentes serán:
|
||||||
|
|
||||||
|
- Inputs de texto: Los labels aparecerán dentro del campo. El campo tendrá un borde inferior resaltado en un color neutro al tema (gris suave para el modo claro y gris oscuro para el modo oscuro). Al clicar en el campo, el label se desplazará hacia arriba y se reducirá de tamaño y aparecerán los bordes faltantes. Si el campo tiene validación, al terminar de validarse, el color del borde cambiará al color de acento en caso de que sea correcto; rojo en caso contrario. Los errores de validación de mostrarán a continuación del input.
|
||||||
|
|
||||||
|
- Los botones: Borde, texto e iconos del color de boton; fondo trasparente al 90%. Al hacer hover, incremento de opacidad al 100% e invertir el color del texto y los iconos por el fondo primario.
|
||||||
|
|
||||||
|
- Login: mismo funcionamiento que los botones pero con color de acento login.
|
||||||
|
- Registro: mismo funcionamiento que los botones pero con color de acento registro.
|
||||||
|
- Cerrar sesión: mismo funcionamiento que los botones pero con color de acento cerrar sesión.
|
||||||
|
- Recuperar contraseña: mismo funcionamiento que los botones pero con color de acento recuperar contraseña.
|
||||||
|
- Comprar: mismo funcionamiento que los botones pero con color de acento comprar.
|
||||||
|
- Añadir al carrito: mismo funcionamiento que los botones pero con color de acento comprar oscurecido un 20%.
|
||||||
|
- Eliminar del carrito: mismo funcionamiento que los botones pero con color de acento eliminar.
|
||||||
|
- Confirmar compra: mismo funcionamiento que los botones pero con color de acento comprar.
|
||||||
|
|
||||||
|
- Tarjetas de imagen: tendrán los bordes ligeramente redondeados, al estilo windows 11. Como tamaño máximo tendrán un alto de 360p. Como máximo ocuparán un 1/4 del ancho de la pantalla y un máximo de 1/4 del alto de la pantalla. En pantallas pequeñas, se adaptarán para ocupar un 100% del ancho disponible para crear un feed infinito de imágenes. Se mantendrá siempre la relación de aspecto de la imágen original.
|
||||||
|
|
||||||
|
- Galería de tarjetas: se organizarán en cuadrícula. Las imágenes se ajustarán automáticamente al espacio disponible, manteniendo siempre su proporción original. El espacio entre las imágenes será consistente y se adaptará al tamaño de la pantalla. Las imágenes muy largas o muy anchas desplazarán a todas las demás imágenes hacía arriba o abajo. Las imágenes mas altas, ocuparán más espacio vertical, empujando las imágenes de su columna hacia arriba o abajo. En tablets, al estar en vertical, se mostrarán en el mismo formato que en móviles, ocupando todo el ancho de la pantalla; en horizontal, se mostrará una cuadrícula. lo más pequeña posible respetando el tamaño mínimo de cada imagen.
|
||||||
|
|
||||||
|
## Patrones de diseño UX/UI
|
||||||
|
|
||||||
|
Nos basaremos en principios de diseño centrados en el usuario.
|
||||||
|
Usaremos ejemplos de [UI Patterns](https://ui-patterns.com/explore) para inspirar nuestras decisiones de diseño.
|
||||||
|
|
||||||
|
Todas las galerías menos la del portfolio, seguirán el mismo diseño, basado en un scroll infinito, hasta alcanzar el total de imágenes disponibles.
|
||||||
|
|
||||||
|
Cada imagen se podrá ampliar y ver en detalle, accediendo a una página detalle donde se podrá descargar, comprar, compartir, votar y comentar la imagen.
|
||||||
|
|
||||||
|
Cuando se sube una nueva imagen o se inicie un proceso que llame al back, se cargará una animación de carga.
|
||||||
|
|
||||||
|
Siempre habrá un header horizontal presente, que muestre el logo de la aplicación, un botón de eventos, un botón de colecciones, un botón de búsqueda y un botón de login. Para usuarios autenticados, se mostrará un botón de menú que expondrá opciones adicionales como el historial de compra, edición de perfil, etc. Al autenticarse, el botón de login se convertirá en el botón de logout. Para usuarios profesionales, en el menú se incluirán opciones adicionales como la gestión del portfolio o la configuración del aplicativo. El menú se mostrará como un menu de hamburguesa.
|
||||||
|
|
||||||
|
Habrán imágenes agrupadas por colecciones que corresponden a agrupaciones de categorias y fechas.
|
||||||
|
Habrán eventos, que serán colecciones definidas con una fecha de inicio/fin y una agrupación de categorias repetidas en la colección.
|
||||||
|
Al ver las colecciones, se podrá filtrar por fecha, categoría y otros metadatos relevantes.
|
||||||
|
Al ver los eventos, se podrá filtrar por fecha, categoría y localización.
|
||||||
|
En la vista de colecciones, aparecerán todas las colecciones en una cuadrícula que se adaptará al tamaño de la pantalla. Sobre el grid, aparecerán los diferentes filtros (fecha y categorías) y un cuadro de búsqueda para buscar palabras relevantes.
|
||||||
|
En la vista de eventos, aparecerán listados los eventos en una cuadrícula similar a la de colecciones, con filtros para la localización, la fecha de inicio y fin del evento, y categoría del evento y un cuadro de búsqueda para buscar palabras clave de cada evento.
|
||||||
|
Además, habrá una vista con todas las imágenes subidas y que sean públicas, ordenadas por fecha. Al lateral aparecerán los filtros de evento, colección y categorías. Esta vista será un scroll infinito hasta el fin del contenido.
|
||||||
|
|
||||||
|
## Responsive design guidelines
|
||||||
|
|
||||||
|
Como unidad base usaremos `svw` y `svh`. Para tamaños relativos usaremos `rem` y porcentajes.
|
||||||
|
Usaremos `flexbox` para la disposición de los elementos.
|
||||||
|
Las cuadrículas usarán `grid` para organizar las imágenes de manera eficiente.
|
||||||
|
|
||||||
|
Nos aseguraremos que la aplicación sea usable en dispositivos de diferentes tamaños.
|
||||||
|
Usaremos `media queries` para adaptar el diseño a diferentes resoluciones de pantalla.
|
||||||
|
Como referencia de dispositivos usaremos:
|
||||||
|
Móvil: Oppo A94
|
||||||
|
Tablet: Samsung Galaxy Tab S6 Lite
|
||||||
|
Escritorio: Monitor 1920x1080
|
||||||
|
|
||||||
|
Nos basaremos en componentes Tailwind CSS para la creación de la interfaz de usuario y adaptar las interfaces.
|
||||||
|
|
||||||
|
## Accesibilidad
|
||||||
|
|
||||||
|
Se implementarán prácticas de accesibilidad para garantizar que la aplicación sea usable por personas con discapacidades. Esto incluirá el uso de etiquetas ARIA y la navegación por teclado.
|
156
docs/introduccion.md
Normal file
156
docs/introduccion.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# Galerías Fotográficas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Que problema identificamos
|
||||||
|
|
||||||
|
Existen multiples alternativas para la distribución de imágenes digitales, como redes sociales o proveedores de almacenamiento en la nube.
|
||||||
|
Las redes sociales exponen publicamente y con poco control el contenido de las imágenes.
|
||||||
|
Los proveedores de almacenamiento ofrecen un control mayor sobre la exposición de las imágenes a cambio de una suscripción o pago por uso.
|
||||||
|
|
||||||
|
Como profesional que espera cobrar por el trabajo, publicar fotos privadas en redes sociales abre camino a múltiples problemas: perdida de privacidad para el cliente, mala exposición y pobre posicionamiento web, perdida de calidad en la imágen, se impide reutilizar la imagen con otros fines sin herramientas de terceros que pueden ser perjudiciales para clientes y profesionales.
|
||||||
|
Usar proveedores como WeTransfer, Google Drive o Dropbox, requiere de pagos y un conocimiento mínimo sobre el uso de estos servicios por parte del cliente; además, para ahorrar costes los profesionales deben eliminar los grupos de imágenes con cierta frequencia o fracturar por tiempo de almacenamiento al cliente.
|
||||||
|
Ambas opciones limitan la cooperatividad y la recuperación de las imágenes por parte del cliente.
|
||||||
|
Además, expone fácilmente al profesional a varios riesgos y problemáticas emergentes con la evolución constante del software: la perdida de credenciales, el robo de identidad, el plagio, la pérdida de control sobre la calidad de transmisión de la imagen.
|
||||||
|
|
||||||
|
Como cliente, recibir una sesión por WeTransfer limita las opciones de feedback y mejora o personalización. Recibirla por correo electrónico limita la cantidad de imágenes recibidas. Recibirlas por Google Drive o similares nos quita espacio de nuestro almacenamiento en la nube. Si el profesional publica las imágenes, referenciándos puede afectar a nuestra huella digital e imagen en redes.
|
||||||
|
Además, según la configuración del servicio que use el profesional, es común tener a disposición las imágenes solamente durante un tiempo determinado, haciendo imposible recuperarlas o acceder a ellas pasado ese tiempo.
|
||||||
|
|
||||||
|
Ambos participantes de la actividad recaen en un moderno problema: necesitar diversos servicios para controlar un único recurso.
|
||||||
|
Como profesional, necesitas una web-portfolio, un perfil en redes, un servicio de almacenamiento y una forma de contacto con el cliente.
|
||||||
|
Como cliente, necesitas poder ver el trabajo anterior del profesional, contactar con él y poder revisar el trabajo de forma conjunta para lograr el resultado que uno espera.
|
||||||
|
|
||||||
|
## Que queremos resolver
|
||||||
|
|
||||||
|
Desde el punto de vista del profesional, podemos agrupar todos los servicios en un único servico.
|
||||||
|
Una especie de Amazon para imágenes.
|
||||||
|
|
||||||
|
Desde el punto de vista del cliente, agrupandolo todo, evitamos la necesidad de acceder a tantos servicios diferentes y familiarizamos al usuario con una interfaz sencilla y directa, minimizando el roce con el aplicativo.
|
||||||
|
|
||||||
|
## Alternativas a nuestra solución
|
||||||
|
|
||||||
|
Existen multitud de alternativas a una web hecha a mano, como Wix o Joomla.
|
||||||
|
Existen alternativas cloud-native para alojar un portfolio.
|
||||||
|
Existen muchos proveedores de almacenamiento web.
|
||||||
|
Existen muchas redes sociales donde compartir las imágenes.
|
||||||
|
Existen metodos de comunicación y trabajo colaborativo.
|
||||||
|
En su gran mayoría, en servicios diferenciados.
|
||||||
|
Sin embargo no existe servicios unificados.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Despiece del problema
|
||||||
|
|
||||||
|
### Almacenamiento
|
||||||
|
|
||||||
|
El profesional no quiere tener que gastar en almacenamiento ni preocuparse del estado de un proveedor.
|
||||||
|
Por tanto, el sistema tiene que poder almacenar las imágenes durante mucho tiempo y mantenerlas en linea el mismo tiempo.
|
||||||
|
|
||||||
|
### Distribución
|
||||||
|
|
||||||
|
El profesional no quiere tener que usar más de un servicio para enviar las imágenes al cliente o subirlas a redes sociales.
|
||||||
|
El cliente no quiere recibir las imágenes en un zip o tener que descargarlas desde un servicio de terceros.
|
||||||
|
|
||||||
|
### Trabajo colaborativo
|
||||||
|
|
||||||
|
El profesional quiere que el cliente escoja la imagen que más el guste y le de feedback sobre el retoque y la edición de la imágen.
|
||||||
|
El cliente queire revisar el trabajo del profesional para escoger las imágenes que más le gusten.
|
||||||
|
|
||||||
|
### Feedback
|
||||||
|
|
||||||
|
El cliente puede dar feedback mediante comentarios en cada imágen como votando la imagen.
|
||||||
|
La nota asignada a cada imágen se determinará como: (votos positivos - votos negativos) / votos totales
|
||||||
|
Los comentarios se puntuarán de la misma forma.
|
||||||
|
El profesional podrá responder a los comentarios y los leerá de forma que el comentario con mejor puntuación quede el primero, en su defecto se ordenarán por fecha.
|
||||||
|
|
||||||
|
El feedback también podrá ser dado a imágenes y colecciones del portfolio público, solamente lo podrán hacer los clientes autenticados. Aquellos que adquieran una sesión podrán dejar feedback al profesional de forma pública.
|
||||||
|
|
||||||
|
### Portfolio
|
||||||
|
|
||||||
|
El profesional no quiere gastar en otro servicio más para alojar el protfolio.
|
||||||
|
El profesional quiere subir las imágenes al servicio y escoger cuales van a conformar el portfolio.
|
||||||
|
El profesional quiere que la primera ojeada que tenga el cliente sobre el servicio sea el portfolio.
|
||||||
|
|
||||||
|
### Colecciones
|
||||||
|
|
||||||
|
El profesional quiere organizar las imágenes por fecha, personas que salen en ellas, categorías, eventos...
|
||||||
|
El profesional quiere poder reunir imágenes que se relacionen entre sí por categoría, evento, fechas, motivos...
|
||||||
|
El profesional quiere juntar todas las imágenes que van destinadas a un cliente.
|
||||||
|
|
||||||
|
### Sesiones
|
||||||
|
|
||||||
|
El profesional hará varias imágenes que revisará con el cliente.
|
||||||
|
El cliente escogerá únicamente las mejoras y que más le gusten.
|
||||||
|
El profesional revisará esas imágenes, con un límite numérico de imágenes totales.
|
||||||
|
El cliente recibirá una colección de esas imágenes.
|
||||||
|
Múltiples clientes y profesionales podrán trabajar en una única sesión.
|
||||||
|
Durante el proceso, debe existir una colaboración mutua mediante feedback.
|
||||||
|
|
||||||
|
### Multiples clientes
|
||||||
|
|
||||||
|
El profesional quiere poder trabajar sobre un proyecto y poder enviar el resultado del proyecto a multiples clientes.
|
||||||
|
|
||||||
|
### Venta del servicio
|
||||||
|
|
||||||
|
El profesional quiere cobrar una imágen única, una o varias colecciones, o una sesión.
|
||||||
|
Un cliente pagará por una imágen, una o diversas colección premaquetadas, y por una sesión.
|
||||||
|
Muchos clientes podrán pagar conjuntamente el servicio.
|
||||||
|
|
||||||
|
### Usuarios
|
||||||
|
|
||||||
|
Existirán 4 tipos de usuarios:
|
||||||
|
|
||||||
|
- Anonimo
|
||||||
|
- Cliente
|
||||||
|
- Profesional
|
||||||
|
- Administrador
|
||||||
|
|
||||||
|
El usuario anónimo podrá consultar el portfolio y ver los diferentes botones y enlaces para ver el resto de imágenes públicas, redes sociales, blog o identificarse.
|
||||||
|
|
||||||
|
El cliente, o usuario identificado, podrá hacer lo mismo que el anónimo. También podrá comprar las diferentes imágenes y colecciones disponibles. Además, podrá contratar una sesión, interactuar con la sesión que tenga activa dando feedback o escogiendo las imágenes; y consultar pedidos anteriores para descargar las imágenes, publicarlas en redes sociales o compartirlas de forma directa con otros clientes identificados.
|
||||||
|
|
||||||
|
El profesional, podrá editar las imágenes y colecciones públicas que haya en el portfolio; organizar nuevas sesiones, editar las sesiones en marcha y actualizarlas; crear y editar nuevas colecciones; crear y editar eventos y categorías.
|
||||||
|
|
||||||
|
El administrador, podrá hacer lo mismo que el profesional y además podrá añadir profesionales y editar aspectos clave de la web.
|
||||||
|
|
||||||
|
Para facilitar el registro y el inicio de sesión, los usuarios podrán iniciar sesión con Google, Microsoft, Meta y Apple. Además, contarán con la opción de usar correo y contraseña. Para ello nos adaptaremos a OpenId y OAuth 2.
|
||||||
|
Todos los usuarios tendrán un método MFA como sms o passkeys.
|
||||||
|
Aquellos usuarios que inicien sesión con contraseña, recibirán un magic link para entrar.
|
||||||
|
|
||||||
|
### Conexiones lentas
|
||||||
|
|
||||||
|
Todas las imágenes subidas se procesarán en 3 categorías:
|
||||||
|
|
||||||
|
- Baja resolución: 360p, comprimidas en WebP
|
||||||
|
- Media resolución: 720p, comprimidas en WebP
|
||||||
|
- Resolución Nativa: nativa, sin compresión
|
||||||
|
|
||||||
|
### Pagos
|
||||||
|
|
||||||
|
Se establecerá como recomendación el uso de linkpay o servicios como google wallet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tecnologías a usar
|
||||||
|
|
||||||
|
Back:
|
||||||
|
JWT, OpenId + OAuth 2 -> Duende Identity Server
|
||||||
|
DDD con CQRS -> MediatR
|
||||||
|
Entity Framework Core + sqlite -> Base de datos
|
||||||
|
AutoMapper -> Mapeo de objetos
|
||||||
|
Serilog + OpenTelemetry -> Logging y trazabilidad
|
||||||
|
Scalar + OpenApi -> Documentación y pruebas de API
|
||||||
|
FluentValidation -> Validación de modelos
|
||||||
|
Redis -> Almacenamiento en caché
|
||||||
|
mailkit -> Envío de correos electrónicos
|
||||||
|
ImageSharp -> Procesamiento de imágenes
|
||||||
|
|
||||||
|
Front:
|
||||||
|
TailwindCSS -> Estilos
|
||||||
|
Angular -> Framework
|
||||||
|
RxJS -> Programación reactiva
|
||||||
|
NgRx -> Manejo del estado
|
||||||
|
scss -> Preprocesador CSS
|
||||||
|
axios -> Cliente HTTP
|
||||||
|
Vite -> Bundler
|
||||||
|
Typescript -> Lenguaje
|
9
docs/manual-usuario.md
Normal file
9
docs/manual-usuario.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<!-- TODO: Se hará una revisión completa del contenido y se actualizarán las secciones según sea necesario.
|
||||||
|
|
||||||
|
Guía para profesionales
|
||||||
|
|
||||||
|
Guía para clientes
|
||||||
|
|
||||||
|
FAQ y resolución de problemas
|
||||||
|
|
||||||
|
Videos tutoriales (cuando aplique) -->
|
82
docs/plan-desarrollo.md
Normal file
82
docs/plan-desarrollo.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Galerías Fotográficas - Plan de Desarrollo
|
||||||
|
|
||||||
|
## Fase 1 Backend: Configuración básica y autenticación
|
||||||
|
|
||||||
|
- Configuración del entorno de desarrollo.
|
||||||
|
- Configuración de Sacalar, OpenTelemetry y Serilog.
|
||||||
|
- Creación del modelo de usuario. Primera migración de la tabla Usuarios.
|
||||||
|
- Implementación de la autenticación de usuarios (registro, inicio de sesión, cierre de sesión, cambio de contraseña).
|
||||||
|
|
||||||
|
## Fase 1 Frontend: Maquetación login
|
||||||
|
|
||||||
|
- Creación de la interfaz de usuario para el login.
|
||||||
|
- Implementación de formularios de registro, inicio de sesión y cambio de contraseña.
|
||||||
|
- Implementación de la lógica de validación de formularios.
|
||||||
|
- Integración con el backend para la autenticación de usuarios.
|
||||||
|
|
||||||
|
## Fase 2 Backend: Gestión básica de usuarios
|
||||||
|
|
||||||
|
- Implementación de roles y permisos.
|
||||||
|
- Integración con el sistema de autenticación.
|
||||||
|
- Protección de rutas y gestión de sesiones.
|
||||||
|
|
||||||
|
## Fase 3 Backend: Gestión de imágenes
|
||||||
|
|
||||||
|
- Implementación de la gestión de imágenes (subida, edición, eliminación).
|
||||||
|
- Integración con el sistema de almacenamiento (local o en la nube).
|
||||||
|
- Implementación del sistema de colecciones de imágenes y eventos.
|
||||||
|
- Implementación del sistema de etiquetado de imágenes.
|
||||||
|
- Implementación del sistema de búsqueda y filtrado de imágenes.
|
||||||
|
|
||||||
|
## Fase 3 Frontend: Gestión de imágenes
|
||||||
|
|
||||||
|
- Implementación de la interfaz de usuario para la gestión de imágenes.
|
||||||
|
- Integración con el backend para la subida, edición y eliminación de imágenes.
|
||||||
|
- Implementación de la lógica de búsqueda y filtrado de imágenes.
|
||||||
|
- Implementación de la interfaz de colecciones y eventos.
|
||||||
|
|
||||||
|
## Fase 4 Backend: Sistema de portfolios
|
||||||
|
|
||||||
|
- Implementación de la gestión de portfolios (CRUD).
|
||||||
|
- Integración con el sistema de roles.
|
||||||
|
|
||||||
|
## Fase 4 Frontend: Sistema de portfolios
|
||||||
|
|
||||||
|
- Implementación de la interfaz de usuario para la gestión de portfolios.
|
||||||
|
- Integración con el backend para la creación, edición y eliminación de portfolios.
|
||||||
|
- Implementación de la lógica de visualización de portfolios.
|
||||||
|
|
||||||
|
## Fase 4 Backend: Funcionalidades de colaboración y feedback
|
||||||
|
|
||||||
|
- Implementación de un sistema de comentarios en imágenes y portfolios.
|
||||||
|
- Implementación de un sistema de notificaciones para interacciones relevantes.
|
||||||
|
- Implementacion del sistema de versionado de imagenes.
|
||||||
|
|
||||||
|
## Fase 4 Frontend: Funcionalidades de colaboración y feedback
|
||||||
|
|
||||||
|
- Implementación de la interfaz de usuario para el sistema de comentarios.
|
||||||
|
- Implementación de la interfaz de usuario para las notificaciones.
|
||||||
|
- Implementación de la interfaz de usuario para el versionado de imágenes.
|
||||||
|
|
||||||
|
## Fase 5: Sistema de pagos y comercialización
|
||||||
|
|
||||||
|
- Implementación de la pasarela de pagos.
|
||||||
|
- Integración con plataformas de pago (linkpay, google wallet...).
|
||||||
|
- Implementación de la lógica de compras.
|
||||||
|
- Implementación de la interfaz de usuario para la gestión de pagos.
|
||||||
|
- Maquetación de galería del usuario e historial de compras.
|
||||||
|
|
||||||
|
## Fase 6: Optimizaciones y características avanzadas
|
||||||
|
|
||||||
|
Queda abierta a ser completada durante la realización del proyecto. Esta sección se usará a modo de backlog.
|
||||||
|
|
||||||
|
- Implemntación propia de IdentityServer.
|
||||||
|
- Añadir más integraciones para proveedores de base de datos.
|
||||||
|
- Añadir más integraciones con proveedores de almacenamiento.
|
||||||
|
- Implementación de un sistema de caché para mejorar el rendimiento.
|
||||||
|
- Implementación de un sistema de logging y monitoreo.
|
||||||
|
- Implementación de un sistema de pruebas automatizadas.
|
||||||
|
- Implementación de un sistema de CI/CD.
|
||||||
|
- Implementación de un sistema de análisis y reportes.
|
||||||
|
- Implementar frontends alternativos (React, Vue, etc.).
|
||||||
|
- Añadir más integraciones con proveedores de servicios de terceros.
|
11
docs/plan-testing.md
Normal file
11
docs/plan-testing.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!-- TODO: Se hará una revisión completa del contenido y se actualizarán las secciones según sea necesario.
|
||||||
|
|
||||||
|
Pruebas unitarias: Backend y frontend
|
||||||
|
|
||||||
|
Pruebas de integración: APIs y base de datos
|
||||||
|
|
||||||
|
Pruebas E2E: Flujos completos de usuario
|
||||||
|
|
||||||
|
Pruebas de rendimiento: Carga de imágenes
|
||||||
|
|
||||||
|
Criterios de aceptación -->
|
145
docs/requisitos-funcionales.md
Normal file
145
docs/requisitos-funcionales.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# Galerías Fotográficas - Requisitos funcionales
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gestión de usuarios (4 tipos: anónimo, cliente, profesional, administrador)
|
||||||
|
|
||||||
|
Existirán 4 tipos de usuarios:
|
||||||
|
|
||||||
|
- Anonimo
|
||||||
|
- Cliente
|
||||||
|
- Profesional
|
||||||
|
- Administrador
|
||||||
|
|
||||||
|
Todos los tipos parten de la base de anónimo, es decir, que un usuario anónimo podrá convertirse en cliente, profesional o administrador a través de un proceso de registro y autenticación y conservará los permisos básicos del anónimo.
|
||||||
|
|
||||||
|
### Anónimo
|
||||||
|
|
||||||
|
Podrá consultar el portfolio y ver los enlaces y botones de la página principal y navegar por ella para ver las imágenes públicas, los últimos eventos realizados a modo de blog, las redes sociales del profesional e identificarse como cliente registrado.
|
||||||
|
|
||||||
|
Para registrarse, necesitará correo electrónico, nombre y apellidos, y una contraseña. En su defecto podrá registrarse usando un proveedor de identidad (Google, Microsoft, Meta o Apple). Adaptaremos a OpenId y OAuth 2.
|
||||||
|
A todos los usuarios se les recomendará habilitar la autenticación de dos factores para mejorar la seguridad de sus cuentas. Aquellos que no la habilitén serán forzados a usar magic links (se les enviará un enlace de acceso por correo electrónico).
|
||||||
|
|
||||||
|
### Cliente
|
||||||
|
|
||||||
|
Podrá puntuar imágenes, colecciones, sesiones o el portfolio.
|
||||||
|
También podrá dejar comentarios que justifiquen su puntuación.
|
||||||
|
|
||||||
|
Podrá comprar imágenes individuales o colecciones. También podrá contratar una sesión fotográfica.
|
||||||
|
|
||||||
|
Si contrata una sesión tendrá que seleccionar las imágenes que desea incluir en la sesión y proporcionar feedback sobre el avance de la misma.
|
||||||
|
|
||||||
|
Podrá acceder a un historico de sus compras y sesiones para descargarse las imágenes y compartirlas con otros clientes.
|
||||||
|
|
||||||
|
### Profesional
|
||||||
|
|
||||||
|
Podrá editar el panel principal del portfolio, añadiendo, organizando y eliminando imágenes.
|
||||||
|
Podrá editar las subsecciones del portfolio añadiendo, organizando y eliminando categorias y colecciones.
|
||||||
|
Podrá editar la información asociada a una imágen, como el título, la descripción, las etiquetas, el evento en la que se tomó, si se debe ocultar al público, a que collección pertenece o si pertenece a una sesión.
|
||||||
|
Podrá crear y editar las colecciones y eventos, modificando fechas, títulos, descripciones, visibilidad y otros metadatos.
|
||||||
|
Podrá crear y editar las sesiones, añadiendo y eliminando clientes, imágenes, fechas, descripciones y otros metadatos.
|
||||||
|
|
||||||
|
### Administrador
|
||||||
|
|
||||||
|
Podrá añadir, editar y eliminar profesionales.
|
||||||
|
También moderará el contenido generado por los usuarios.
|
||||||
|
Será responsable de la gestión de la plataforma y de la atención al cliente.
|
||||||
|
|
||||||
|
## Sistema de almacenamiento
|
||||||
|
|
||||||
|
Solo se permitirán subir imágenes en formato JPEG, JPG y PNG.
|
||||||
|
|
||||||
|
Las imágenes se subirán sin compresión, manteniendo su calidad original.
|
||||||
|
|
||||||
|
Al momento de subirlas, se crearán 3 copias.
|
||||||
|
La copia 1, se almacenará en 360p en formato Webp. Se usará a modo de vista previa o thumbnail de la imágen original, actuará de proxy. Se llamará `low-res-proxy-<id>`.
|
||||||
|
La copia 2, se almacenará en 720p en formato Webp. Se usará a modo de detalle o ampliación en las visualizaciones desde pantallas superiores a 720p. Se llamará `mid-res-proxy-<id>`.
|
||||||
|
La copia 3, se almacenará en 1080p en formato Webp. Se usará a modo de detalle o ampliación en las visualizaciones desde pantallas superiores a 1080p. Se llamará `high-res-proxy-<id>`.
|
||||||
|
La imagen original se almacenará en su formato original, sin pérdidas. Solo será visible desde las galerías o colecciones a las que pertenezca y solo cuando se descargue por un cliente que haya pagado por ella. Se llamará `original-<id>`.
|
||||||
|
|
||||||
|
## Distribución a clientes
|
||||||
|
|
||||||
|
Las imágenes se servirán de forma diferida o lazy-loading.
|
||||||
|
El profesional creará una colección, añadirá las imágenes que la compongan, y añadirá a los usuarios que tengan acceso a ella.
|
||||||
|
Los usuarios podrán ver y comentar las imágenes de la colección que les haya sido compartida.
|
||||||
|
Los usuarios podrán ver colecciones que estén marcadas como públicas o a las que hayan sido invitados.
|
||||||
|
Los usuarios podrán compartir las colecciones que les hayan sido asignadas pero no podrán compartir las imágenes de esas colecciones fuera de la plataforma.
|
||||||
|
|
||||||
|
## Funcionalidades de trabajo colaborativo
|
||||||
|
|
||||||
|
El profesional creará una sesión fotográfica, añadirá las imágenes que la compongan, y añadirá a los usuarios que tengan acceso a ella.
|
||||||
|
Los clientes podrá votar y comentar las imágenes de la sesión.
|
||||||
|
El profesional podrá leer y responder a los comentarios de los clientes, que se leerán ordenados según su fecha de creación y votación.
|
||||||
|
El profesional podrá entonces escoger un número limite de imágenes para actualizar, todas las demás se deshabilitarán y ya no se podrán descargar por parte del cliente.
|
||||||
|
Las imágenes marcadas se podrán actualizar, subiendo la nueva versión de la imagen.
|
||||||
|
Se mantendrá un historial de versiones de cada imagen, permitiendo al cliente acceder a versiones anteriores y votar cual quiere conservar y cual quiere descartar.
|
||||||
|
|
||||||
|
## Sistema de feedback y valoraciones
|
||||||
|
|
||||||
|
Los clientes podrán dejar comentarios y valoraciones sobre las imágenes y colecciones que hayan adquirido.
|
||||||
|
El profesional podrá leer y responder a los comentarios de los clientes.
|
||||||
|
|
||||||
|
Hay 2 tipos de votos:
|
||||||
|
|
||||||
|
- Positivo
|
||||||
|
- Negativo
|
||||||
|
|
||||||
|
El cómputo total de valoración se hará como: (positivo - negativo) / ( totales )
|
||||||
|
|
||||||
|
El cliente identificado podrá dejar un comentario, anónimo o no, sobre la imagen, colección o profesional.
|
||||||
|
Los comentarios se ordenarán descendentemente según su fecha de creación y votación.
|
||||||
|
Las imágenes en una colección se ordenarán descendentemente según su puntuación.
|
||||||
|
Los comentarios de una colección se ordenarán descendentemente según su fecha de creación y votación.
|
||||||
|
Las colecciones se ordenarán manualmente y las votaciones no afectarán su orden.
|
||||||
|
|
||||||
|
## Gestión de portfolio y colecciones
|
||||||
|
|
||||||
|
El portfolio contendrá 2 secciones destacadas:
|
||||||
|
|
||||||
|
1. **Colección principal**: Contendrá la lista de imágenes ordenadas escogidas por el profesional.
|
||||||
|
2. **Timeline de colecciones**: Contendrá un registro de las colecciones destacadas por el profesional y un enlace al resto de ellas.
|
||||||
|
|
||||||
|
Estas secciones se verán como 2 paneles en la página del portfolio cuando se visauliza en pantallas grandes.
|
||||||
|
En pantallas pequeñas, se visualizará únicamente la colección principal.
|
||||||
|
|
||||||
|
Un profesional podrá crear y gestionar el portfolio de su página principal.
|
||||||
|
Podrá añadir, organizar y eliminar imágenes en el portfolio.
|
||||||
|
Podrá añadir, organizar y eliminar colecciones en el portfolio.
|
||||||
|
|
||||||
|
La sección de colección principal:
|
||||||
|
|
||||||
|
- El profesional podrá añadir hasta un número límite de imágenes.
|
||||||
|
- Podrá cambiar el orden y tamaño de las tarjetas de cada imagen, haciendo uso de un sistema de arrastrar y soltar (drag-and-drop) para reorganizarlas.
|
||||||
|
- Podrá eliminar imágenes de la colección principal.
|
||||||
|
|
||||||
|
* Cada imagen se visualizará en una tarjeta de dimensiones variables, sin datos asociados. Y al hacerle click, se expandirá para verla a mayor tamaño, solo en dispositivos de resolución mayor a 720p. Todas las colecciones compartirán el mismo diseño de tarjeta.
|
||||||
|
|
||||||
|
La sección de timeline de colecciones:
|
||||||
|
|
||||||
|
- El profesional podrá añadir hasta un número límite de colecciones.
|
||||||
|
- Podrá cambiar el orden las colecciones que aparecen.
|
||||||
|
- Podrá eliminar colecciones del timeline.
|
||||||
|
|
||||||
|
* Cada colección se visualizará con su imagen destacada, título, descripción y fecha.
|
||||||
|
|
||||||
|
Al final del portfolio se incluirá una sección de comentarios y votaciones al portfolio.
|
||||||
|
Los clientes podrán dejar comentarios y valoraciones sobre el portfolio.
|
||||||
|
El profesional podrá leer y responder a los comentarios de los clientes.
|
||||||
|
|
||||||
|
## Sistema de sesiones fotográficas
|
||||||
|
|
||||||
|
Una sesión es una colección de imágenes que no se pueden descargar por parte del cliente hasta finalizar la sesión.
|
||||||
|
Todas las imágenes de la sesión tendrán una valoración inicial de 0, y una lista de comentarios vacia.
|
||||||
|
El profesional puede versionar las imágenes de la sesión.
|
||||||
|
El cliente puede votar las imágenes de la sesión.
|
||||||
|
Para cerrar una sesión y finalizarla, el cliente tiene que haber escogido al menos una imagen hasta un número máximo definido por el profesional.
|
||||||
|
Una vez finalizada la sesión, el cliente podrá descargar todas las imágenes de la sesión que haya seleccionado, todas las demás se eliminarán.
|
||||||
|
|
||||||
|
## Funcionalidades de venta y pagos
|
||||||
|
|
||||||
|
Todas las imágenes se pueden comprar y descargar.
|
||||||
|
Todas las colecciones se pueden comprar y descargar.
|
||||||
|
Todos los clientes puede reservar una sesión.
|
||||||
|
|
||||||
|
Para los pagos se usará google pay o linkpay o stripe.
|
||||||
|
Se intentará ofrecer una experiencia de pago fluida y segura para los clientes.
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 45 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user