55 Commits

Author SHA1 Message Date
750bcc81ef Merge pull request 'workflow' (#38) from feature/docs into dev
All checks were successful
Deploy Documentation Local / deploy-docs (push) Successful in 45s
Deploy Documentation Local / deploy-docs (pull_request) Successful in 27s
Reviewed-on: #38
2025-09-02 00:55:29 +02:00
258b4ebfec --allow-unrelated-histories 2025-09-02 00:54:09 +02:00
2360630544 --allow-unrelated-histories
Some checks failed
Auto-Merge Dev / auto-merge-dev (push) Failing after 1m8s
2025-09-02 00:52:01 +02:00
1ded384fd7 workflows
Some checks failed
Auto-Merge Dev / auto-merge-dev (push) Failing after 1m18s
2025-09-02 00:46:34 +02:00
67e7fe35f9 workflow 2025-09-02 00:22:34 +02:00
a223dfc7c0 workflow
Some checks failed
Auto-Merge Dev / auto-merge-dev (push) Failing after 1m28s
2025-09-02 00:20:09 +02:00
c173cc3b3b workflow
Some checks failed
Auto-Merge Dev / auto-merge-dev (push) Failing after 1m11s
2025-09-02 00:16:26 +02:00
f121899b3b workflow
Some checks failed
Auto-Merge Dev / auto-merge-dev (push) Failing after 34s
2025-09-02 00:13:33 +02:00
523c147957 workflow
All checks were successful
Auto-Merge Dev / auto-merge-dev (push) Successful in 1m28s
2025-09-02 00:10:41 +02:00
ffe955788f Merge pull request 'feature/29' (#37) from feature/29 into master
All checks were successful
Deploy Documentation Local / deploy-docs (push) Successful in 33s
Reviewed-on: #37
2025-09-01 23:15:58 +02:00
0a2353d738 añade automapper 2025-09-01 23:14:57 +02:00
09c211a0fe proyecto preparado para DDD + CQRS 2025-09-01 23:06:17 +02:00
895e40edc0 inicio bases 2025-09-01 22:25:21 +02:00
0ba01a91fa Merge pull request 'merge master' (#11) from dev into master
Reviewed-on: #11
2025-09-01 16:56:38 +02:00
152904671a merge master 2025-09-01 16:56:24 +02:00
2ea9b4363b Merge pull request 'dev' (#10) from dev into master
Reviewed-on: #10
2025-09-01 16:55:10 +02:00
f96b5ee0d7 merge master 2025-09-01 16:55:01 +02:00
a5fdd18315 docs 2025-09-01 16:53:21 +02:00
42c30478e7 docs 2025-09-01 16:52:56 +02:00
e6ce22ca93 Merge pull request 'Actualizar .gitea/workflows/deploy-docs.yaml' (#9) from manuel-patch-1 into master
Reviewed-on: #9
2025-09-01 16:51:22 +02:00
ab5d43f50b Actualizar .gitea/workflows/deploy-docs.yaml 2025-09-01 16:51:07 +02:00
b8186644d0 Merge pull request 'docs' (#8) from dev into master
Reviewed-on: #8
2025-09-01 16:50:07 +02:00
c5072e28a6 docs 2025-09-01 16:49:56 +02:00
2012fcdee0 Merge pull request 'docs' (#7) from dev into master
Reviewed-on: #7
2025-09-01 16:48:33 +02:00
7208296a35 docs 2025-09-01 16:48:23 +02:00
6e048cc906 Merge pull request 'dev' (#5) from dev into master
Reviewed-on: #5
2025-09-01 16:41:58 +02:00
d957bfc07d docs
All checks were successful
Deploy Documentation Local / deploy-docs (push) Successful in 32s
2025-09-01 16:41:36 +02:00
be987dfbed docs
All checks were successful
Deploy Documentation Local / deploy-docs (push) Successful in 26s
2025-09-01 16:33:58 +02:00
57fe16f9ce docs 2025-09-01 16:26:26 +02:00
480ff06731 docs 2025-09-01 16:22:35 +02:00
8e3511472d docs 2025-09-01 16:20:34 +02:00
c6bdd2fe2e docs 2025-09-01 16:15:27 +02:00
4404ec6504 docs 2025-09-01 16:12:59 +02:00
52d19ed9f5 docs 2025-09-01 16:09:26 +02:00
f014199042 docs 2025-09-01 16:06:10 +02:00
393a7c031d docs 2025-09-01 16:00:10 +02:00
2c22e88e63 docs
Some checks failed
Deploy Documentation Local / deploy-docs (push) Failing after 10s
2025-09-01 15:46:45 +02:00
2d07f0feba docs 2025-09-01 15:21:22 +02:00
a993f2bcbf Merge pull request 'workflow' (#4) from dev into master
Reviewed-on: #4
2025-09-01 02:23:20 +02:00
ed67bcd1e3 workflow 2025-09-01 02:22:16 +02:00
5a2b7d3acd Merge pull request 'workflow' (#3) from dev into master
Reviewed-on: #3
2025-09-01 02:16:42 +02:00
1c3a21eece workflow 2025-09-01 02:16:30 +02:00
3a1aaf0967 Merge pull request 'corrige errores workflow' (#2) from dev into master
Reviewed-on: #2
2025-09-01 02:15:35 +02:00
7114d2801b corrige errores workflow 2025-09-01 02:15:15 +02:00
704619d243 Merge pull request 'dev' (#1) from dev into master
Reviewed-on: #1
2025-09-01 02:10:41 +02:00
c7c8fa36cd adds manual dispatch 2025-09-01 01:49:18 +02:00
2e3e05940e gitea dev runner
Some checks failed
Deploy Documentation Local / deploy-docs (push) Has been cancelled
2025-09-01 01:24:20 +02:00
083bcd0fc9 adds actions 2025-09-01 01:20:55 +02:00
89a553a95b docu 2025-08-30 15:28:44 +02:00
f7fb9c4b12 docu 2025-08-29 00:34:45 +02:00
1360ccb46c docu 2025-08-29 00:31:56 +02:00
cbcf1f245b docu 2025-08-28 23:41:06 +02:00
585eca0420 docu 2025-08-28 19:36:16 +02:00
55e7e000a9 añade primeras documentaciones 2025-08-28 18:26:28 +02:00
8681056139 docu 2025-08-28 16:54:27 +02:00
538 changed files with 2345 additions and 37950 deletions

View File

@@ -0,0 +1,35 @@
name: Cleanup old test branches
on:
schedule:
- cron: "0 12 * * *"
workflow_dispatch:
jobs:
cleanup_branch:
runs-on: windows
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Delete test branches older than 7 days
shell: powershell
run: |
# Obtener la fecha límite (7 días antes)
$limitDate = (Get-Date).AddDays(-7)
# Obtener todas las ramas remotas test/*
$branches = git branch -r | Where-Object { $_ -match 'origin/test/' }
foreach ($branch in $branches) {
$branchName = $branch.Trim() -replace '^origin/', ''
# Obtener fecha de creación de la rama (aproximación por el primer commit)
$firstCommitDate = git log $branchName --reverse --format="%ci" | Select-Object -First 1
$branchDate = Get-Date $firstCommitDate
if ($branchDate -lt $limitDate) {
Write-Host "Eliminando rama $branchName creada el $branchDate"
git push origin --delete $branchName
}
}

View File

@@ -0,0 +1,74 @@
name: Create Test Construct
run-name: Creating test construct
on:
workflow_dispatch:
inputs:
branch:
description: "Branch to build"
required: true
default: "dev"
schedule:
- cron: "0 12 * * *"
jobs:
create_test_branch_and_build:
runs-on: windows
steps:
- name: Checkout dev branch
uses: actions/checkout@v4
with:
ref: dev
- name: Create test branch with date
id: create_branch
shell: powershell
run: |
$date = Get-Date -Format "yyyyMMdd"
$branchName = "test/$date"
git checkout -b $branchName
git push origin $branchName
Write-Output "::set-output name=branch::$branchName"
deploy_docs:
needs: create_test_branch_and_build
runs-on: windows
steps:
- name: Checkout test branch
uses: actions/checkout@v4
with:
ref: ${{ steps.create_branch.outputs.branch }}
- name: Deploy documentation
uses: ./.gitea/workflows/deploy-docs.yaml
with:
branch: ${{ steps.create_branch.outputs.branch }}
deploy_back:
needs: create_test_branch_and_build
runs-on: windows
steps:
- name: Checkout test branch
uses: actions/checkout@v4
with:
ref: ${{ steps.create_branch.outputs.branch }}
- name: Deploy .net project
uses: ./.gitea/workflows/deploy-back.yaml
with:
branch: ${{ steps.create_branch.outputs.branch }}
deploy_front:
needs: create_test_branch_and_build
runs-on: windows
steps:
- name: Checkout test branch
uses: actions/checkout@v4
with:
ref: ${{ steps.create_branch.outputs.branch }}
- name: Deploy front project
uses: ./.gitea/workflows/deploy-front.yaml
with:
branch: ${{ steps.create_branch.outputs.branch }}

View File

@@ -0,0 +1,49 @@
name: Create Test Construct
run-name: Creating test construct
on:
pull_request:
types: [closed]
branches: [dev, "test/**"]
paths: ["back/**"]
workflow_dispatch:
inputs:
branch:
description: "Branch to deploy"
required: true
default: "dev"
workflow_call:
jobs:
build_and_deploy:
runs-on: windows
steps:
- name: Checkout branch
uses: actions/checkout@v4
# build .net project
- name: Build .NET Project
run: |
dotnet restore
dotnet build
# deploy .net to iis site with path = "D:\iis\es\mcvingenieros\mmorales.photo\back"
- name: Deploy .net project to iis
shell: powershell
run: |
# deploy to iis site back.mmorales.photo that has path = D:\iis\es\mcvingenieros\mmorales.photo\back\
$sourcePath = "D:\iis\es\mcvingenieros\mmorales.photo\back\bin\Release\net9.0\publish"
$destinationPath = "D:\iis\es\mcvingenieros\mmorales.photo\back\"
# Stop IIS site
Stop-WebAppPool -Name "mmorales.photo.back"
# Remove old files
Remove-Item -Path $destinationPath\* -Recurse -Force
# Copy new files
Copy-Item -Path $sourcePath\* -Destination $destinationPath -Recurse
# Start IIS site
Start-WebAppPool -Name "mmorales.photo.back"

View File

@@ -0,0 +1,223 @@
name: Deploy Documentation Local
run-name: Deploying ${{ vars.GIT_REPOSITORY }} docs locally
on:
push:
branches: [dev]
paths: ["docs/**", "mkdocs.yml", ".gitea/workflows/deploy-docs-dev.yaml"]
workflow_dispatch:
inputs:
branch:
description: "Branch to deploy"
required: true
default: "dev"
jobs:
deploy-docs:
runs-on: windows # 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.10"
- name: Install MkDocs and dependencies
run: |
pip install mkdocs mkdocs-material
- name: Build documentation
run: |
ls
mkdocs build --site-dir ../build
- name: Deploy to IIS directory
shell: powershell
run: |
$projectName = "${{ vars.GIT_REPOSITORY }}"
$basePath = Join-Path "${{ secrets.DEPLOY_BASE_PATH }}" "dev"
$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 }}"
$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>Documentacion - 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 Documentacion</h1>
<p>MCV Ingenieros - Documentacion 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">
<h3 class="project-title">$folderName</h3>
<div class="project-meta">
<span>Actualizado: $lastWrite</span>
</div>
</a>
</div>
"@
}
$currentDate = (Get-Date).ToString("dd/MM/yyyy HH:mm")
$htmlContent += @"
</div>
<div class="footer">
<p>Indice generado automaticamente 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"

View File

@@ -0,0 +1,227 @@
name: Deploy Documentation Local
run-name: Deploying ${{ gitea.repository }} docs locally
on:
pull_request:
types: [closed]
branches: [master]
paths: ["docs/**", "mkdocs.yml", ".gitea/workflows/deploy-docs.yaml"]
push:
branches: [master]
paths: ["docs/**", "mkdocs.yml", ".gitea/workflows/deploy-docs.yaml"]
workflow_dispatch:
inputs:
branch:
description: "Branch to deploy"
required: true
default: "master"
workflow_call:
jobs:
deploy-docs:
runs-on: windows # 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.10"
- name: Install MkDocs and dependencies
run: |
pip install mkdocs mkdocs-material
- name: Build documentation
run: |
ls
mkdocs build --site-dir ../build
- name: Deploy to IIS directory
shell: powershell
run: |
$projectName = "${{ vars.GIT_REPOSITORY }}"
$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 | 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>Documentacion - 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 Documentacion</h1>
<p>MCV Ingenieros - Documentacion 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">
<h3 class="project-title">$folderName</h3>
<div class="project-meta">
<span>Actualizado: $lastWrite</span>
</div>
</a>
</div>
"@
}
$currentDate = (Get-Date).ToString("dd/MM/yyyy HH:mm")
$htmlContent += @"
</div>
<div class="footer">
<p>Indice generado automaticamente 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"

View File

@@ -0,0 +1,48 @@
name: deploy front
run-name: Deploy Frontend
on:
pull_request:
types: [closed]
branches: [dev, "test/**"]
paths: ["front/**"]
workflow_dispatch:
inputs:
branch:
description: "Branch to deploy"
required: true
default: "dev"
workflow_call:
jobs:
build_and_deploy:
runs-on: windows
steps:
- name: Checkout branch
uses: actions/checkout@v4
# build angular
- name: Build Angular
run: |
npm install
npm run build
- name: Deploy to IIS
shell: powershell
run: |
# deploy to iis site front.mmorales.photo that has path = D:\iis\es\mcvingenieros\mmorales.photo\front\
$sourcePath = "D:\iis\es\mcvingenieros\mmorales.photo\front\dist"
$destinationPath = "D:\iis\es\mcvingenieros\mmorales.photo\front\"
# Stop IIS site
Stop-WebAppPool -Name "mmorales.photo.front"
# Remove old files
Remove-Item -Path $destinationPath\* -Recurse -Force
# Copy new files
Copy-Item -Path $sourcePath\* -Destination $destinationPath -Recurse
# Start IIS site
Start-WebAppPool -Name "mmorales.photo.front"

46
.vscode/launch.json vendored
View File

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

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

@@ -1,213 +1,156 @@
# mmorales.photo
## 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
![Diagram of the connections between the described components. Frontend connected to BFF, BFF connected to BlobProvider, BFF connected to Backend, Backend connected to DataProvider and Backend connected to Identity provider.](docs/resources/root/architecturalComponentLayout.svg)
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.
# Galerías Fotográficas
---
## 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

View File

@@ -1,13 +0,0 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "9.0.8",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}

View File

@@ -1,5 +0,0 @@
{
"email": "sys@t.em",
"key": "b60e166e-d4a5-416e-a7c9-142d05fb7f31",
"password": "8C3,uTÑ<hñ61qQs3"
}

View File

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

View File

@@ -1,9 +0,0 @@
using back.DataModels;
namespace back.DTO;
public class UserDto
{
public string Id { get; set; } = null!;
public ICollection<RoleDto> Roles { get; set; } = [];
}

View File

@@ -1,8 +0,0 @@
namespace back.DataModels;
public partial class EfmigrationsLock
{
public int Id { get; set; }
public string Timestamp { get; set; } = null!;
}

View File

@@ -1,44 +0,0 @@
using MCVIngenieros.Transactional.Abstractions;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
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();
}
}

View File

@@ -1,44 +0,0 @@
using MCVIngenieros.Transactional.Abstractions;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
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();
}
}

View File

@@ -1,72 +0,0 @@
using MCVIngenieros.Transactional.Abstractions.Interfaces;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace back.DataModels;
public record PermissionDto
{
public string Id { get; set; } = null!;
}
[Table("Permissions")]
public partial class Permission : IEntity<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();
}
public bool IsNull => this is null;
public object Clone() => (Permission)MemberwiseClone();
public int CompareTo(object? obj)
{
if (obj is null) return 1;
if (obj is not Permission other) throw new ArgumentException("Object is not a Person");
return CompareTo(other);
}
public int CompareTo(Permission? other)
{
if (other is null) return 1;
if (ReferenceEquals(this, other)) return 0;
return string.Compare(Id, other.Id, StringComparison.OrdinalIgnoreCase);
}
public PermissionDto ToDto()
{
return new PermissionDto
{
Id = Id
};
}
// 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" };
}

View File

@@ -1,70 +0,0 @@
using MCVIngenieros.Transactional.Abstractions;
using MCVIngenieros.Transactional.Abstractions.Interfaces;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
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
};
}

View File

@@ -1,66 +0,0 @@
using MCVIngenieros.Transactional.Abstractions.Interfaces;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
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);
}
}

View File

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

View File

@@ -1,127 +0,0 @@
using MCVIngenieros.Transactional.Abstractions.Interfaces;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace back.DataModels;
public class RoleDto
{
public string Id { get; set; } = null!;
public List<PermissionDto> Permissions { get; set; } = [];
}
[Table("Roles")]
public partial class Role : IEntity<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 bool IsNull => this is null;
public object Clone() => (Role)MemberwiseClone();
public int CompareTo(object? obj)
{
if (obj is null) return 1;
if (obj is not Role other) throw new ArgumentException("Object is not a Person");
return CompareTo(other);
}
public int CompareTo(Role? other)
{
if (other is null) return 1;
if (ReferenceEquals(this, other)) return 0;
return string.Compare(Id, other.Id, StringComparison.OrdinalIgnoreCase);
}
public RoleDto ToDto()
{
return new RoleDto
{
Id = Id,
Permissions = [.. Permissions.Select(p => p.ToDto())]
};
}
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
);
}

View File

@@ -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();
}
}

View File

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

View File

@@ -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();
}
}

View File

@@ -1,86 +0,0 @@
using back.DTO;
using MCVIngenieros.Transactional.Abstractions.Interfaces;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
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.Select(r => r.ToDto())]
};
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]
};
}

View File

@@ -0,0 +1,12 @@
using Microsoft.EntityFrameworkCore;
namespace back.Domain;
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
Database.EnsureCreated();
Database.Migrate();
}
}

14
back/Domain/IEntity.cs Normal file
View File

@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace back.Domain;
public interface IEntity
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
string Id
{
get;
set;
}
}

View File

@@ -0,0 +1,20 @@
using AutoMapper;
namespace back.Infrastructure;
public class AutoMapperProfile : Profile
{
public AutoMapperProfile()
{
//CreateMap<Users, User>()
// .ForMember(dest => dest.UserId, opt => opt.MapFrom(src => src.UserId))
// .ForMember(dest => dest.FirstName, opt => opt.MapFrom(src => src.FirstName))
// .ForMember(dest => dest.LastName, opt => opt.MapFrom(src => src.LastName))
// .ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email))
// .ForMember(dest => dest.BirthYear, opt => opt.MapFrom(src => src.Birthday.Year))
// .ForMember(dest => dest.BirthMonth, opt => opt.MapFrom(src => src.Birthday.Month))
// .ForMember(dest => dest.BirthDay, opt => opt.MapFrom(src => src.Birthday.Day))
// .ForMember(dest => dest.OccupationName, opt => opt.Ignore())
}
}

View File

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

View File

@@ -1,6 +0,0 @@
namespace back.Options;
public sealed class Databases
{
public string? BaseDirectory { get; set; }
}

View File

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

View File

@@ -1,48 +1,150 @@
using back.ServicesExtensions;
using Autofac.Extensions.DependencyInjection;
using AutoMapper;
using back.Domain;
using back.Infrastructure;
using MCVIngenieros.Healthchecks;
using MediatR.Extensions.FluentValidation.AspNetCore;
using Microsoft.EntityFrameworkCore;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using Scalar.AspNetCore;
using Serilog;
namespace back;
public class Program
{
public static void Main(string[] args)
{
var configFiles = Path.Combine(AppContext.BaseDirectory, "configs");
if (!Directory.Exists(configFiles))
{
Directory.CreateDirectory(configFiles);
}
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";
var configurationBuilder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{environment}.json", optional: false, reloadOnChange: true);
var configs = Directory.GetFiles(configFiles, "*.json", SearchOption.AllDirectories);
foreach (var config in configs)
{
configurationBuilder.AddJsonFile(config, optional: true, reloadOnChange: true);
}
var configuration = configurationBuilder.Build();
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.MinimumLevel.Verbose()
.Enrich.FromLogContext()
.CreateLogger();
try
{
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddConfiguration(configuration);
builder.Host.UseSerilog();
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
builder.Services.UseExtensions();
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
});
builder.Services.AddControllers();
builder.Services.AddFluentValidation([typeof(Program).Assembly]);
builder.Services.AddAutoMapper(opts =>
{
opts.AddProfile<AutoMapperProfile>();
opts.AllowNullCollections = true;
opts.AllowNullDestinationValues = true;
opts.DestinationMemberNamingConvention = new PascalCaseNamingConvention();
});
builder.Services.AddHealthChecksSupport().DiscoverHealthChecks();
builder.Services.AddLogging();
builder.Logging.AddOpenTelemetry(options =>
{
options
.SetResourceBuilder(
ResourceBuilder.CreateDefault()
.AddService(AppDomain.CurrentDomain.FriendlyName))
.AddConsoleExporter();
});
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService(AppDomain.CurrentDomain.FriendlyName))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddConsoleExporter())
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddConsoleExporter());
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddSwaggerGen();
builder.Services.AddDataProtection();
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
options.AddDefaultPolicy(policy =>
{
policy
.WithOrigins(builder.Configuration.GetSection("AllowedHosts").Get<string[]>() ?? [])
.SetIsOriginAllowedToAllowWildcardSubdomains()
.SetPreflightMaxAge(TimeSpan.FromMinutes(10))
.AllowCredentials()
.AllowAnyHeader()
.AllowAnyMethod();
});
});
builder.Services.AddHttpContextAccessor();
builder.Services.AddAntiforgery(options =>
{
options.HeaderName = "X-XSRF-TOKEN";
});
builder.Services.AddDbContext<ApplicationDbContext>(opts =>
{
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
opts.UseSqlite(connectionString);
});
builder.Services.AddControllers(options =>
{
options.Filters.Add(new Microsoft.AspNetCore.Mvc.RequireHttpsAttribute());
});
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
app.MapOpenApi();
app.MapScalarApiReference("/api-docs", opt =>
{
opt.WithTitle("My API Documentation");
});
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.UseCors("AllowAll");
app.MapControllers();
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application start-up failed");
}
finally
{
Log.CloseAndFlush();
}
}
}

View File

@@ -1,21 +1,12 @@
{
"$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",
"launchUrl": "api-docs",
"applicationUrl": "https://localhost:7157",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}

View File

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

View File

@@ -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();
}

View File

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

View File

@@ -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 MCVIngenieros.Transactional.Abstractions.Interfaces;
using MCVIngenieros.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;
}
}

View File

@@ -1,34 +1,9 @@
{
"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"
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"MailServerOptions": {
"SmtpServer": "smtp.gmail.com",
"Puerto": 587,
"Usuario": "",
"Password": "",
"EnableSsl": true
},
"HealthChecksConfigs": {
"CacheDuration": "00:30:00",
"Timeout": "00:00:05",
"AssembliesToScan": [
"back"
],
"Sqlite": {
"RetryAttempts": 2,
"Timeout": "00:05:00",
"RetryDelay": "00:00:10",
"Severity": "Info"
}
}
"AllowedHosts": "*"
}

View File

@@ -5,14 +5,5 @@
"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}"
}
}
"AllowedHosts": "mmorales.photo"
}

View File

@@ -1,9 +1,3 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -7,48 +7,64 @@
</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="Autofac" Version="8.4.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="10.0.0" />
<PackageReference Include="Autofac.WebApi2" Version="6.1.1" />
<PackageReference Include="FluentValidation" Version="12.0.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0" />
<PackageReference Include="MCVIngenieros.Healthchecks" Version="0.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.8" />
<PackageReference Include="MediatR" Version="13.0.0" />
<PackageReference Include="MediatR.Contracts" Version="2.0.1" />
<PackageReference Include="MediatR.Extensions.Autofac.DependencyInjection" Version="13.1.0" />
<PackageReference Include="MediatR.Extensions.FluentValidation.AspNetCore" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Antiforgery" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="9.0.8" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Abstractions" Version="9.0.8" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Extensions" Version="9.0.8" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" 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.Proxies" 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.Sqlite.Core" 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="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.8" />
<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" />
<PackageReference Include="OpenTelemetry" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Api" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Api.ProviderBuilderExtensions" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
<PackageReference Include="Scalar.AspNetCore" Version="2.7.2" />
<PackageReference Include="Scalar.AspNetCore.Microsoft" Version="2.7.2" />
<PackageReference Include="Scalar.AspNetCore.Swashbuckle" Version="2.7.2" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
<PackageReference Include="Serilog.Enrichers.Process" Version="3.0.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.OpenTelemetry" Version="4.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="9.0.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\nuget\DependencyInjector\DependencyInjector.csproj" />
<ProjectReference Include="..\..\nuget\Transactional\MCVIngenieros.Transactional.csproj" />
<Folder Include="Application\" />
<Folder Include="Infrastructure\" />
<Folder Include="Presentation\" />
</ItemGroup>
</Project>

View File

@@ -1,15 +1,9 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36401.2
VisualStudioVersion = 17.14.36401.2 d17.14
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}") = "MCVIngenieros.Transactional", "..\..\nuget\Transactional\MCVIngenieros.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}") = "Presentation", "..\backend\Presentation\Presentation.csproj", "{F1DD9D2A-0467-41EE-B3BB-303F1A0C18D6}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "back", "back.csproj", "{C78E8225-44D3-434B-AC2A-C8F4459BB18C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -17,27 +11,15 @@ Global
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
{F1DD9D2A-0467-41EE-B3BB-303F1A0C18D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F1DD9D2A-0467-41EE-B3BB-303F1A0C18D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F1DD9D2A-0467-41EE-B3BB-303F1A0C18D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F1DD9D2A-0467-41EE-B3BB-303F1A0C18D6}.Release|Any CPU.Build.0 = Release|Any CPU
{C78E8225-44D3-434B-AC2A-C8F4459BB18C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C78E8225-44D3-434B-AC2A-C8F4459BB18C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C78E8225-44D3-434B-AC2A-C8F4459BB18C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C78E8225-44D3-434B-AC2A-C8F4459BB18C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F531A9C8-70D1-45AA-B4AA-AC49FCADAE3D}
SolutionGuid = {D5ABA005-3E91-4220-9B2C-874C0BED7E34}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,15 @@
{
"HealthChecksConfigs": {
"CacheDuration": "00:30:00",
"Timeout": "00:00:05",
"AssembliesToScan": [
"back"
]
//"MyCheck": {
// "RetryAttempts": 2,
// "Timeout": "00:05:00",
// "RetryDelay": "00:00:10",
// "Severity": "Info"
//}
}
}

41
back/configs/serilog.json Normal file
View File

@@ -0,0 +1,41 @@
{
"Serilog": {
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.OpenTelemetry" ],
"MinimumLevel": {
"Default": "Information"
},
"WriteTo": [
{
"Name": "Console"
},
{
"Name": "File",
"Args": {
"path": "Logs/log-.txt",
"rollingInterval": "Day",
"fileSizeLimitBytes": 5242880,
"rollOnFileSizeLimit": true,
"retainedFileCountLimit": 31,
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "OpenTelemetry",
"Args": {
"endpoint": "http://localhost:4317",
"protocol": "Grpc",
"resourceAttributes": {
"service.name": "back.mmorales.photo",
"deployment.environment": "development"
}
}
}
],
"Enrich": [
"FromLogContext",
"WithThreadId",
"WithProcessId",
"WithEnvironmentName"
]
}
}

View File

@@ -1,44 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace back.controllers;
public class ValidationErrors
{
public string? Field { get; set; }
public string? Message { get; set; }
}
public class ExecutionErrors
{
public Exception? Exception { get; set; }
public string? Message { get; set; }
}
public abstract class ResponseBase
{
public object? Data { get; set; }
public string? Message { get; set; }
public bool Success { get; set; }
public int StatusCode { get; set; }
public ValidationErrors[] ValidationErrors { get; set; }
public ExecutionErrors[] ExecutionErrors { get; set; }
}
public record LoginRequest(string Username, string Password);
[ApiController, Route("api/[controller]")]
public class AuthController(IAuthService authService) : ControllerBase
{
private readonly IAuthService _authService = authService;
[HttpPost, Route("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest loginRequest)
{
// validar que el usuario y la contraseña sean correctos
// obtener el token JWT encriptado
// obtener el refresh token
// devolver el token JWT y el refresh token en los headers de las respuestas
// devolver datos del usuario en el body de la respuesta
}
}

View File

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

View File

@@ -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();
}
}

View File

@@ -1,90 +0,0 @@
using back.DataModels;
using back.services.bussines;
using back.services.bussines.UserService;
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.ToDto());
}
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);
}
}
}

View File

@@ -1,6 +0,0 @@
namespace back.healthchecks.Options;
public partial class HealthChecksConfigs : MCVIngenieros.Healthchecks.Options.HealthChecksConfigs
{
public const string Sqlite = "Sqlite";
}

View File

@@ -1,52 +0,0 @@
using back.healthchecks.Options;
using back.Options;
using MCVIngenieros.Healthchecks;
using MCVIngenieros.Healthchecks.Abstracts;
using Microsoft.Extensions.Options;
namespace back.healthchecks;
public class SqliteHealthCheck(IOptionsMonitor<DatabaseConfig> databaseConfig, IOptionsMonitor<HealthChecksConfigs> healthchecksConfig) : HealthCheck
{
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 override 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)
{
Details = details,
Severity = isHealthy ? HealthCheckSeverity.Info : HealthCheckSeverity.Critical
});
}
}

View File

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

View File

@@ -1,13 +0,0 @@
using DependencyInjector.Abstractions.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);
}

View File

@@ -1,56 +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);
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}

View File

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

View File

@@ -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");
}
}
}

View File

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

View File

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

View File

@@ -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");
});
});
}
}

View File

@@ -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");
});
});
}
}

View File

@@ -1,8 +0,0 @@
using Microsoft.EntityFrameworkCore;
namespace back.persistance.data.relations;
public interface IRelationEstablisher
{
void EstablishRelation(ModelBuilder modelBuilder);
}

View File

@@ -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");
});
});
}
}

View File

@@ -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();
// }
// }
//}

View File

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

View File

@@ -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);
// }
//}

View File

@@ -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");
});
});
}
}

View File

@@ -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);
// }
//}

View File

@@ -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();
});
}
}

View File

@@ -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");
});
});
}
}

View File

@@ -1,10 +0,0 @@
using back.DataModels;
using DependencyInjector.Abstractions.Lifetimes;
using MCVIngenieros.Transactional.Abstractions.Interfaces;
namespace back.persistance.data.repositories.Abstracts;
public interface IPermissionRepository : IRepository<Permission>, IScoped
{
Task SeedDefaultPermissions();
}

View File

@@ -1,10 +0,0 @@
using back.DataModels;
using DependencyInjector.Abstractions.Lifetimes;
using MCVIngenieros.Transactional.Abstractions.Interfaces;
namespace back.persistance.data.repositories.Abstracts;
public interface IPersonRepository : IRepository<Person>, IScoped
{
}

View File

@@ -1,8 +0,0 @@
using back.DataModels;
using DependencyInjector.Abstractions.Lifetimes;
using MCVIngenieros.Transactional.Abstractions.Interfaces;
namespace back.persistance.data.repositories.Abstracts;
public interface IPhotoRepository : IRepository<Photo>, IScoped
{ }

View File

@@ -1,10 +0,0 @@
using back.DataModels;
using DependencyInjector.Abstractions.Lifetimes;
using MCVIngenieros.Transactional.Abstractions.Interfaces;
namespace back.persistance.data.repositories.Abstracts;
public interface IRoleRepository : IRepository<Role>, IScoped
{
Task SeedDefaultRoles();
}

View File

@@ -1,14 +0,0 @@
using back.DataModels;
using DependencyInjector.Abstractions.Lifetimes;
using MCVIngenieros.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);
}

View File

@@ -1,34 +0,0 @@
using back.DataModels;
using back.persistance.data.repositories.Abstracts;
using MCVIngenieros.Transactional.Implementations.EntityFramework;
namespace back.persistance.data.repositories;
public class PermissionRepository(DataContext context) : ReadWriteRepository<Permission>(context), IPermissionRepository
{
// Implement methods specific to Photo repository if needed
public async Task SeedDefaultPermissions()
{
var defaultPermissions = new List<Permission>
{
Permission.ViewContentPermission,
Permission.LikeContentPermission,
Permission.EditContentPermission,
Permission.DeleteContentPermission,
Permission.CreateContentPermission,
Permission.EditUserPermission,
Permission.DeleteUserPermission,
Permission.DisableUserPermission,
Permission.CreateUserPermission,
Permission.EditWebConfigPermission
};
foreach (var permission in defaultPermissions)
{
if (!Entities.Any(p => p.Id == permission.Id))
{
Entities.Add(permission);
}
}
await SaveChanges();
}
}

View File

@@ -1,10 +0,0 @@
using back.DataModels;
using back.persistance.data.repositories.Abstracts;
using MCVIngenieros.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
}

View File

@@ -1,10 +0,0 @@
using back.DataModels;
using back.persistance.data.repositories.Abstracts;
using MCVIngenieros.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
}

View File

@@ -1,27 +0,0 @@
using back.DataModels;
using back.persistance.data.repositories.Abstracts;
using MCVIngenieros.Transactional.Implementations.EntityFramework;
namespace back.persistance.data.repositories;
public class RoleRepository(DataContext context) : ReadWriteRepository<Role>(context), IRoleRepository
{
// Implement methods specific to Photo repository if needed
public async Task SeedDefaultRoles()
{
var defaultRoles = new List<Role>
{
Role.AdminRole,
Role.UserRole,
Role.ContentManagerRole
};
foreach (var role in defaultRoles)
{
if (!Entities.Any(p => p.Id == role.Id))
{
Entities.Add(role);
}
}
await SaveChanges();
}
}

View File

@@ -1,75 +0,0 @@
using back.DataModels;
using back.persistance.data.repositories.Abstracts;
using MCVIngenieros.Transactional.Implementations.EntityFramework;
using Microsoft.EntityFrameworkCore;
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
.Include(u => u.Roles)
.ThenInclude(r => r.Permissions)
.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());
//}
}

View File

@@ -1,8 +0,0 @@
using Microsoft.EntityFrameworkCore;
namespace back.persistance.data.seeders;
public interface ISeeder
{
void Seed(ModelBuilder modelBuilder);
}

View File

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

View File

@@ -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" }
);
}
}

View File

@@ -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
// );
// }
//}

View File

@@ -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.");
}

View File

@@ -1,5 +0,0 @@
using System.Net;
namespace back.services.bussines;
public record HttpErrorMap(HttpStatusCode Code, string Description);

View File

@@ -1,15 +0,0 @@
using back.DataModels;
using back.DTO;
using DependencyInjector.Abstractions.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");
}

View File

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

View File

@@ -1,13 +0,0 @@
using back.DataModels;
using DependencyInjector.Abstractions.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);
}

View File

@@ -1,142 +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;
}
var loggedUser = await Login(user.Email!, decryptedPassword);
return loggedUser;
}
}

View File

@@ -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()}");
}
}

View File

@@ -1,16 +0,0 @@
using DependencyInjector.Abstractions.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);
}

View File

@@ -1,8 +0,0 @@
using DependencyInjector.Abstractions.Lifetimes;
namespace back.services.engine.ImageResizer;
public interface IImageResizer : ISingleton
{
Task<Stream> ResizeImage(IFormFile image, int v);
}

View File

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

View File

@@ -1,8 +0,0 @@
using DependencyInjector.Abstractions.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);
}

View File

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

View File

@@ -1,8 +0,0 @@
using DependencyInjector.Abstractions.Lifetimes;
namespace back.services.engine.SystemUser;
public interface ISystemUserGenerator: IScoped
{
Task GenerateAsync();
}

View File

@@ -1,60 +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 MCVIngenieros.Transactional.Abstractions.Interfaces;
using System.Text.Json;
namespace back.services.engine.SystemUser;
public class SystemUserGenerator(
ITransactionalService<DataContext> transactional,
JsonSerializerOptions jsonSerializerOptions,
IUserRepository userRepository,
IPersonRepository personRepository,
IRoleRepository roleRepository,
IPermissionRepository permissionRepository,
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 permissionRepository.SeedDefaultPermissions();
await roleRepository.SeedDefaultRoles();
await personRepository.Insert(Person.SystemPerson);
await userRepository.Insert(User.SystemUser);
});
}
else
{
await userRepository.Update(User.SystemUser);
await userRepository.SaveChanges();
}
}
}

View File

@@ -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}");
}
}
}

View File

@@ -1,9 +0,0 @@
using DependencyInjector.Abstractions.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);
}

View File

@@ -1,8 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace Presentation.Controllers;
public class AuthController : Controller
{
}

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