Compare commits
21 Commits
df3e602015
...
work-in-pr
| Author | SHA1 | Date | |
|---|---|---|---|
| 07458c1541 | |||
| a9bfdc460d | |||
| 258554f9d4 | |||
| 6731fb5d3a | |||
| 5aaddbca40 | |||
| 1263e28c00 | |||
| 4873f39192 | |||
| cb6948aa14 | |||
| f9960b4fc9 | |||
| 2e4c16621d | |||
| 60ce08ee86 | |||
| 0f3652c1a1 | |||
| 63738ad027 | |||
| 6177eec2bf | |||
| b51b8b4185 | |||
| d222e33667 | |||
| fcd80cd30f | |||
| 43bcf449fd | |||
| 20f8a14bfb | |||
| 121757546a | |||
| b6eb692c27 |
2
.github/workflows/backend-ci.yml
vendored
2
.github/workflows/backend-ci.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
- name: dotnet build and publish
|
||||
run: |
|
||||
cd backend
|
||||
dotnet publish --configuration Release --artifacts-path ./publish/ backend.sln
|
||||
dotnet publish --configuration Release --artifacts-path ./publish/ Socialize.slnx
|
||||
|
||||
# Deploy to Azure WebApp
|
||||
- name: Deploy to Azure WebApp
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -19,10 +19,21 @@
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# .NET
|
||||
bin/
|
||||
obj/
|
||||
TestResults/
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
dist/
|
||||
.vite/
|
||||
|
||||
# Local environment files
|
||||
*.local
|
||||
.env.local
|
||||
.env.*.local
|
||||
App_Data/
|
||||
|
||||
# Local SSL certificates
|
||||
*.pem
|
||||
|
||||
238
AGENTS.md
238
AGENTS.md
@@ -1,103 +1,74 @@
|
||||
# AGENTS.md
|
||||
# AGENTS
|
||||
|
||||
## Purpose
|
||||
This document is a working guide for coding agents in this repository. It captures the current architecture, conventions, and safe execution workflow for making reliable changes.
|
||||
This repository is designed for human + AI agent collaboration.
|
||||
|
||||
## Documentation-First Workflow
|
||||
Agents must treat repository documentation as the source of truth. Conversation history is secondary and may be incomplete, stale, or contradictory.
|
||||
## Read Order
|
||||
|
||||
Before making any substantial code change, agents must read the relevant docs first. At minimum, inspect:
|
||||
- `AGENTS.md`
|
||||
- `docs/LLM_DEVELOPMENT_WORKFLOW.md`
|
||||
- `docs/PRODUCT.md` when product behavior, UX, or user workflow may change
|
||||
- `docs/ARCHITECTURE.md` when structure, module boundaries, routing, data flow, or integration points may change
|
||||
- `docs/CONVENTIONS.md` when adding or modifying code patterns
|
||||
- `docs/DECISIONS.md` before revisiting architecture or product decisions
|
||||
- the active `docs/tasks/TASK-*.md` file when one exists
|
||||
Before meaningful code changes, read:
|
||||
|
||||
If one of these files does not exist yet, do not invent broad behavior from chat history. State what is missing and proceed only with the narrowest safe interpretation of the current task.
|
||||
1. `README.md`
|
||||
2. `docs/AGENTIC_WORKFLOW.md`
|
||||
3. `docs/ARCHITECTURE.md`
|
||||
4. `docs/DEVELOPMENT_WORKFLOW.md`
|
||||
5. `docs/PRODUCT.md`
|
||||
6. `docs/CONVENTIONS.md`
|
||||
7. Relevant file in `docs/FEATURES/`
|
||||
8. Relevant file in `docs/TASKS/`
|
||||
|
||||
For non-trivial work, follow this sequence:
|
||||
1. Read the relevant docs and existing code.
|
||||
2. Restate the task in a short summary.
|
||||
3. Identify backend impact, frontend impact, data impact, and documentation impact.
|
||||
4. List files likely to change.
|
||||
5. Surface ambiguities or risky assumptions.
|
||||
6. Propose a minimal implementation plan.
|
||||
7. Implement only the approved or clearly requested scope.
|
||||
8. Validate with the relevant commands before finishing when possible.
|
||||
## Core Rules
|
||||
|
||||
Do not use a long chat thread as the durable memory for the project. Durable decisions, conventions, and task requirements belong in repository docs.
|
||||
|
||||
## Pair Working Mode
|
||||
- Work as a pair with the repository owner, not as an isolated implementer.
|
||||
- Before substantial changes, read the relevant docs first, then restate the task briefly and inspect the existing code.
|
||||
- Surface assumptions, tradeoffs, and blockers early instead of silently picking risky directions.
|
||||
- Prefer small, reviewable increments when the product direction is still being shaped.
|
||||
- When requirements are exploratory, help turn them into concrete workflows, domain language, and next implementation steps.
|
||||
- Do not rewrite broad areas of the codebase without clear justification from the current task.
|
||||
- Preserve user changes in the worktree and treat uncommitted files as active collaboration unless told otherwise.
|
||||
- When creating commits, use the Conventional Commits format, for example `docs: update product planning`.
|
||||
- Do not invent architecture.
|
||||
- Work from docs, feature specs, and task files instead of long chat history.
|
||||
- Keep backend code under `backend/src/Socialize.Api`.
|
||||
- The solution file is `backend/Socialize.slnx`.
|
||||
- Backend feature code currently follows FastEndpoints module folders under `Modules/<Feature>`.
|
||||
- Frontend feature-owned code belongs under `frontend/src/features/<feature>`.
|
||||
- Frontend runtime config must flow through `frontend/src/config.js`.
|
||||
- If backend contracts change, run `./scripts/update-openapi.sh` when the backend is running.
|
||||
- Dev servers use HTTP and bind to `0.0.0.0` for LAN access.
|
||||
- Avoid broad refactors unless the task explicitly asks for one.
|
||||
|
||||
## Repository Layout
|
||||
- `backend/`: ASP.NET Core (`net10.0`) API using FastEndpoints, EF Core (PostgreSQL), ASP.NET Identity, and modular bounded contexts for workflow data.
|
||||
- `frontend/`: Vue 3 + Vite + Vuetify + Pinia + Vue Router + Tailwind CSS SPA.
|
||||
- `.github/workflows/`: build/deploy pipelines for backend (Azure Web App) and frontend (Azure Static Web Apps).
|
||||
|
||||
- `backend/src/Socialize.Api/`: ASP.NET Core `net10.0` API using FastEndpoints, EF Core, PostgreSQL, ASP.NET Identity, and workflow modules.
|
||||
- `backend/tests/Socialize.Tests/`: backend test project scaffold.
|
||||
- `frontend/`: Vue 3 + Vite + Vuetify + Pinia SPA.
|
||||
- `docs/FEATURES/`: product and technical feature specs.
|
||||
- `docs/TASKS/`: implementation tickets for coding agents.
|
||||
- `docs/PROMPTS/`: reusable agent prompt templates.
|
||||
- `docs/DECISIONS/`: architecture and product decision records.
|
||||
- `shared/openapi/`: backend OpenAPI schema snapshots.
|
||||
- `scripts/`: root developer workflow commands.
|
||||
- `deploy/caddy/`: Caddy reverse proxy config for Docker Compose.
|
||||
|
||||
## Local Runbook
|
||||
### Backend
|
||||
- Prereqs: .NET 10 SDK, Docker, PostgreSQL container.
|
||||
- Start database:
|
||||
- `cd backend`
|
||||
- `./scripts/start-infrastructure.sh`
|
||||
- Run API:
|
||||
- `dotnet run --project Socialize.Api.csproj` (from `backend/`)
|
||||
- Local API URL:
|
||||
- `http://localhost:5000`
|
||||
- Swagger/OpenAPI UI in dev:
|
||||
- `/api`
|
||||
|
||||
### Frontend
|
||||
- Prereqs: Node/npm.
|
||||
- Runtime configuration:
|
||||
- frontend app config is loaded from `.env.development` and `.env.production`
|
||||
- `frontend/src/config.js` is the single frontend source of truth for runtime env access
|
||||
- Commands:
|
||||
- `cd frontend && npm install`
|
||||
- `npm run dev`
|
||||
- `npm run build`
|
||||
- Local dev server:
|
||||
- `http://localhost:5173`
|
||||
Start infrastructure:
|
||||
|
||||
## Backend Architecture
|
||||
### Composition Root
|
||||
- Entry point: `backend/Program.cs`.
|
||||
- Registers:
|
||||
- Web services/auth (`backend/DependencyInjection.cs`)
|
||||
- Infrastructure services (`backend/Infrastructure/DependencyInjection.cs`)
|
||||
- Modules: Identity, Workspaces, Clients, Projects, ContentItems, Assets, Comments, Approvals, Notifications.
|
||||
- Each module has:
|
||||
- `Add{Module}Module(...)` to register DbContext/services.
|
||||
- `Use{Module}ModuleAsync()` to auto-run migrations at startup.
|
||||
```bash
|
||||
./scripts/start-infrastructure.sh
|
||||
```
|
||||
|
||||
### API Style
|
||||
- FastEndpoints-based handlers.
|
||||
- Pattern: request/response records + optional FluentValidation validator + handler class.
|
||||
- Tagging via `Options(o => o.WithTags("..."))`.
|
||||
- File upload handlers call `AllowFileUploads()`.
|
||||
Run backend:
|
||||
|
||||
### Data Boundaries
|
||||
- Separate DbContext per module:
|
||||
- Identity, Workspaces, Clients, Projects, ContentItems, Assets, Comments, Approvals, Notifications.
|
||||
- Migrations are module-scoped under each `Modules/*/Migrations` folder.
|
||||
```bash
|
||||
./scripts/dev-backend.sh
|
||||
```
|
||||
|
||||
### Auth/Security
|
||||
- JWT is generated manually in `Infrastructure/Security/GenerateJwtToken.cs`.
|
||||
- Refresh-token flow is implemented in Identity handlers (`/api/users/login`, `/api/users/refresh`).
|
||||
- User claim helpers live in `Infrastructure/Security/ClaimsPrincipalExtensions.cs`.
|
||||
- Role-gated frontend routes currently use `Administrator` and `Manager` checks for settings access.
|
||||
Run frontend:
|
||||
|
||||
```bash
|
||||
./scripts/dev-frontend.sh
|
||||
```
|
||||
|
||||
Update OpenAPI:
|
||||
|
||||
```bash
|
||||
./scripts/update-openapi.sh
|
||||
```
|
||||
|
||||
## Current Domain Modules
|
||||
|
||||
### Current Domain Modules
|
||||
- `Identity`: authentication, refresh tokens, email verification, password reset, social login.
|
||||
- `Workspaces`: workspace membership, workspace settings, access scoping.
|
||||
- `Clients`: client records and primary contacts tied to workspaces.
|
||||
@@ -107,83 +78,46 @@ Do not use a long chat thread as the durable memory for the project. Durable dec
|
||||
- `Comments`: discussion threads on reviewable work.
|
||||
- `Approvals`: review decisions and workflow state transitions.
|
||||
- `Notifications`: activity feed and unread workflow notifications.
|
||||
- `Feedback`: product feedback reports, screenshots, comments, activity, and developer review workflows.
|
||||
|
||||
## Frontend Architecture
|
||||
### Bootstrap
|
||||
- `frontend/src/main.js` wires Vue app + Pinia + Vuetify + Router + i18n + Google OAuth + Toasts.
|
||||
- `frontend/src/config.js` is the app-facing runtime configuration module. Do not scatter `import.meta.env` reads across the app.
|
||||
## Task Discipline
|
||||
|
||||
### Routing
|
||||
- Defined in `frontend/src/router/router.js`.
|
||||
- Route guards enforce:
|
||||
- `meta.requiresAuth`
|
||||
- `meta.notAuthenticated`
|
||||
- optional `meta.roles`
|
||||
- Primary authenticated app routes live under `/app/*`.
|
||||
Agents should work from task files in `docs/TASKS/`.
|
||||
|
||||
### State Management
|
||||
- Pinia stores:
|
||||
- `authStore`: token lifecycle + refresh concurrency guard.
|
||||
- `workspaceStore`: active workspace context.
|
||||
- `clientsStore`: client list and creation flows.
|
||||
- `projectsStore`: project list and creation flows.
|
||||
- `contentItemsStore` and `contentItemDetailStore`: content item listing/detail flows.
|
||||
- `reviewQueueStore`: pending review work.
|
||||
- `notificationsStore`: workflow notifications.
|
||||
- `userProfileStore`: current user profile and account edits.
|
||||
A good task:
|
||||
|
||||
### API Client
|
||||
- Axios client in `frontend/src/plugins/api.js`.
|
||||
- Injects bearer token, proactively refreshes near expiry, retries once on 401.
|
||||
- has a clear goal
|
||||
- names the relevant feature spec
|
||||
- has a small scope
|
||||
- lists likely files
|
||||
- lists validation commands
|
||||
|
||||
## High-Value Domains
|
||||
- Identity and social login (`backend/Modules/Identity/*`, `frontend/src/views/auth/*`).
|
||||
- Workspace-scoped operations and role checks (`backend/Modules/Workspaces/*`, `frontend/src/stores/workspaceStore.js`, `frontend/src/router/router.js`).
|
||||
- Client and project workflow (`backend/Modules/Clients/*`, `backend/Modules/Projects/*`, `frontend/src/views/app/ClientsView.vue`, `frontend/src/views/app/ProjectsView.vue`).
|
||||
- Content review lifecycle (`backend/Modules/ContentItems/*`, `backend/Modules/Assets/*`, `backend/Modules/Comments/*`, `backend/Modules/Approvals/*`, `frontend/src/views/app/ContentItemsView.vue`, `frontend/src/views/app/ContentItemDetailView.vue`, `frontend/src/views/app/ReviewQueueView.vue`).
|
||||
- Notifications and workflow awareness (`backend/Modules/Notifications/*`, `frontend/src/stores/notificationsStore.js`).
|
||||
If no task exists, create one before implementing a meaningful feature.
|
||||
|
||||
## Task-Driven Development With Agents
|
||||
Use `docs/tasks/TASK-*.md` files as LLM-friendly implementation tickets. A task file should be self-contained enough for a fresh agent to understand the desired change without relying on a long conversation.
|
||||
## Validation
|
||||
|
||||
A good task file defines:
|
||||
- objective and product context
|
||||
- scope and out of scope
|
||||
- backend requirements, API contract, validation, data, authorization
|
||||
- frontend requirements, route/screen, components, state, API integration, UX states
|
||||
- files likely involved
|
||||
- acceptance criteria
|
||||
- validation plan
|
||||
- risks and open questions
|
||||
Backend:
|
||||
|
||||
Features are fullstack by default unless the task explicitly says otherwise. Do not assume a feature is backend-only. For user-facing work, define both backend and frontend behavior before implementation.
|
||||
```bash
|
||||
dotnet build backend/Socialize.slnx
|
||||
dotnet test backend/Socialize.slnx
|
||||
```
|
||||
|
||||
When an adjacent issue is discovered outside the task scope, do not fix it opportunistically. Report it as a suggested backlog item or add it to `docs/BACKLOG.md` if explicitly asked.
|
||||
Frontend:
|
||||
|
||||
## Agent Working Rules For This Repo
|
||||
1. Keep module boundaries intact. Do not couple DbContexts across modules.
|
||||
2. When adding endpoints, follow existing FastEndpoints pattern with validator + explicit route + tag.
|
||||
3. If schema changes are needed, generate migration in the matching module only.
|
||||
4. Preserve token refresh behavior in frontend client/store; avoid introducing parallel refresh races.
|
||||
5. Keep frontend runtime configuration centralized in `frontend/src/config.js` and `.env.*`; do not introduce ad hoc env fallbacks.
|
||||
6. Preserve workspace scoping and route-role checks when editing app flows.
|
||||
7. Do not commit secrets. Existing appsettings and env files include sensitive-looking values; treat them as legacy and avoid propagating.
|
||||
8. For non-trivial features, prefer a `docs/tasks/TASK-*.md` file before implementation.
|
||||
9. Treat frontend behavior as part of the feature definition: route, components, Pinia store usage, API integration, loading/error/success states, and navigation must be explicit or derived from existing patterns.
|
||||
10. If requirements conflict with repository docs, stop and surface the conflict instead of silently choosing one.
|
||||
```bash
|
||||
cd frontend
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Validation Checklist Before Finishing
|
||||
- Backend:
|
||||
- `cd backend && dotnet build Socialize.Api.csproj`
|
||||
- run affected endpoint flows if change touches handlers/auth/workspace scoping/data writes
|
||||
- Frontend:
|
||||
- `cd frontend && npm run build`
|
||||
- validate affected route/store interactions in browser when UI behavior changed
|
||||
- If migrations were changed:
|
||||
- ensure module context name/output directory remain consistent with `backend/scripts/add-migration.sh`.
|
||||
Contract changes:
|
||||
|
||||
## Notes / Known Sharp Edges
|
||||
- Frontend config should come through `.env.development` / `.env.production` and `frontend/src/config.js`; avoid direct `import.meta.env` reads in feature code.
|
||||
- Backend development now runs on HTTP locally (`http://localhost:5000`), while HTTPS redirection stays enabled outside development.
|
||||
- `frontend/.env.development` is currently checked in and points `VITE_API_URL` to `http://192.168.1.2:5000`; verify whether changes should target `localhost` or the LAN host before editing.
|
||||
- Some style/formatting is inconsistent across JS/Vue/C# files; minimize churn to touched lines.
|
||||
```bash
|
||||
./scripts/update-openapi.sh
|
||||
```
|
||||
|
||||
## Sharp Edges
|
||||
|
||||
- Existing checked-in env and appsettings files may include legacy sensitive-looking values; do not propagate those values into new docs or templates.
|
||||
- The frontend is still JavaScript, not the TypeScript starter app generated by the bootstrap script. New OpenAPI scaffolding exists, but migrating app code to generated typed API calls should happen by task.
|
||||
- Feature-owned frontend route views and stores now live under `frontend/src/features/*`; keep future feature work there.
|
||||
|
||||
125
README.md
125
README.md
@@ -1,88 +1,101 @@
|
||||
# Socialize
|
||||
|
||||
Socialize is a workflow application for social media content review, revision, approval, and publication readiness.
|
||||
Socialize is a workspace-based workflow application for social media content review, revision, approval, and publication readiness.
|
||||
|
||||
It is not a public social network. The current product direction is a workspace-based review tool for internal teams, providers, and client approvers.
|
||||
It is not a public social network. The product is for internal teams, providers, and client approvers coordinating content work before publication.
|
||||
|
||||
## Repository Structure
|
||||
## Monorepo
|
||||
|
||||
- `backend/`: ASP.NET Core `net10.0` API with FastEndpoints, EF Core, PostgreSQL, and modular bounded contexts.
|
||||
- `frontend/`: Vue 3 + Vite + Vuetify + Pinia SPA.
|
||||
- `docs/`: product, planning, and archived project documentation.
|
||||
|
||||
## Current Backend Modules
|
||||
|
||||
- `Identity`
|
||||
- `Workspaces`
|
||||
- `Clients`
|
||||
- `Projects`
|
||||
- `ContentItems`
|
||||
- `Assets`
|
||||
- `Comments`
|
||||
- `Approvals`
|
||||
- `Notifications`
|
||||
- Backend: .NET 10 Web API in `backend/src/Socialize.Api`
|
||||
- Backend tests: `backend/tests/Socialize.Tests`
|
||||
- Frontend: Vue 3 + Vite + Vuetify + Pinia in `frontend`
|
||||
- API contract: OpenAPI snapshot in `shared/openapi`
|
||||
- Deployment: Docker Compose + Caddy
|
||||
- Agentic workflow: specs, task files, and prompt templates under `docs`
|
||||
|
||||
## Local Development
|
||||
|
||||
### Backend
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- .NET 10 SDK
|
||||
- Docker
|
||||
|
||||
Start infrastructure:
|
||||
Terminal 1:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
./scripts/start-infrastructure.sh
|
||||
./scripts/dev-backend.sh
|
||||
```
|
||||
|
||||
Run the API:
|
||||
Terminal 2:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
dotnet run --project Socialize.Api.csproj
|
||||
./scripts/dev-frontend.sh
|
||||
```
|
||||
|
||||
Local backend URL:
|
||||
Frontend:
|
||||
|
||||
- `http://localhost:5000`
|
||||
- Swagger UI: `http://localhost:5000/api`
|
||||
```txt
|
||||
http://localhost:5173
|
||||
http://<this-machine-lan-ip>:5173
|
||||
```
|
||||
|
||||
### Frontend
|
||||
Backend:
|
||||
|
||||
Prerequisites:
|
||||
```txt
|
||||
http://localhost:5080
|
||||
http://<this-machine-lan-ip>:5080
|
||||
```
|
||||
|
||||
- Node.js / npm
|
||||
Swagger UI:
|
||||
|
||||
The frontend reads runtime values from:
|
||||
```txt
|
||||
http://localhost:5080/api
|
||||
```
|
||||
|
||||
- `frontend/.env.development`
|
||||
- `frontend/.env.production`
|
||||
- `frontend/src/config.js`
|
||||
## Update Frontend API Types
|
||||
|
||||
Run the frontend:
|
||||
The backend must be running first.
|
||||
|
||||
```bash
|
||||
./scripts/update-openapi.sh
|
||||
```
|
||||
|
||||
This writes:
|
||||
|
||||
```txt
|
||||
shared/openapi/openapi.json
|
||||
frontend/src/api/schema.d.ts
|
||||
```
|
||||
|
||||
## Docker Compose
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
Then open:
|
||||
|
||||
```txt
|
||||
http://localhost:8080
|
||||
http://<this-machine-lan-ip>:8080
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```bash
|
||||
dotnet build backend/Socialize.slnx
|
||||
dotnet test backend/Socialize.slnx
|
||||
```
|
||||
|
||||
## Frontend Build
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
npm run build
|
||||
```
|
||||
|
||||
Local frontend URL:
|
||||
## Agentic Workflow
|
||||
|
||||
- `http://localhost:5173`
|
||||
Start here:
|
||||
|
||||
## Validation
|
||||
```txt
|
||||
docs/AGENTIC_WORKFLOW.md
|
||||
```
|
||||
|
||||
- Backend: `cd backend && dotnet build Socialize.Api.csproj`
|
||||
- Frontend: `cd frontend && npm run build`
|
||||
|
||||
## Docs
|
||||
|
||||
- [docs/README.md](/home/jbourdon/repos/social-media/docs/README.md)
|
||||
- [docs/product/vision.md](/home/jbourdon/repos/social-media/docs/product/vision.md)
|
||||
- [docs/product/glossary.md](/home/jbourdon/repos/social-media/docs/product/glossary.md)
|
||||
- [docs/constraints.md](/home/jbourdon/repos/social-media/docs/constraints.md)
|
||||
- [AGENTS.md](/home/jbourdon/repos/social-media/AGENTS.md)
|
||||
Use feature specs, task files, and prompt templates instead of asking agents to work from vague chat history.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# PROMPT TEMPLATES
|
||||
I need you to help me write a feature. First, we need to define it, so you will ask me questions one-by-one to make sure we have a shared understanding of the scope
|
||||
and expectating. - The feature we want is a way for your clients to report bugs/suggestions/requests from within our app. It should not be intrusive. It should allow
|
||||
them to take a screen capture, put annotation, describe their request and/or issue. Then, as a dev, i will want to collect and review them.
|
||||
|
||||
## Purpose
|
||||
This document standardizes how we interact with AI coding agents (Codex, Claude, etc).
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Socialize.Modules.Approvals.Data;
|
||||
using Socialize.Modules.Assets.Data;
|
||||
using Socialize.Modules.Clients.Data;
|
||||
using Socialize.Modules.Comments.Data;
|
||||
using Socialize.Modules.ContentItems.Data;
|
||||
using Socialize.Modules.Identity.Data;
|
||||
using Socialize.Modules.Notifications.Data;
|
||||
using Socialize.Modules.Projects.Data;
|
||||
using Socialize.Modules.Workspaces.Data;
|
||||
|
||||
namespace Socialize.Data;
|
||||
|
||||
public class AppDbContext(
|
||||
DbContextOptions<AppDbContext> options)
|
||||
: IdentityDbContext<User, Role, Guid>(options)
|
||||
{
|
||||
public DbSet<Workspace> Workspaces => Set<Workspace>();
|
||||
public DbSet<WorkspaceInvite> WorkspaceInvites => Set<WorkspaceInvite>();
|
||||
public DbSet<Client> Clients => Set<Client>();
|
||||
public DbSet<Project> Projects => Set<Project>();
|
||||
public DbSet<ContentItem> ContentItems => Set<ContentItem>();
|
||||
public DbSet<ContentItemRevision> ContentItemRevisions => Set<ContentItemRevision>();
|
||||
public DbSet<Asset> Assets => Set<Asset>();
|
||||
public DbSet<AssetRevision> AssetRevisions => Set<AssetRevision>();
|
||||
public DbSet<Comment> Comments => Set<Comment>();
|
||||
public DbSet<ApprovalRequest> ApprovalRequests => Set<ApprovalRequest>();
|
||||
public DbSet<ApprovalDecision> ApprovalDecisions => Set<ApprovalDecision>();
|
||||
public DbSet<NotificationEvent> NotificationEvents => Set<NotificationEvent>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<Workspace>(workspace =>
|
||||
{
|
||||
workspace.ToTable("Workspaces");
|
||||
workspace.HasKey(x => x.Id);
|
||||
workspace.Property(x => x.Name).HasMaxLength(256).IsRequired();
|
||||
workspace.Property(x => x.Slug).HasMaxLength(128).IsRequired();
|
||||
workspace.Property(x => x.TimeZone).HasMaxLength(128).IsRequired();
|
||||
workspace.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
workspace.HasIndex(x => x.Slug).IsUnique();
|
||||
workspace.HasIndex(x => x.OwnerUserId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<WorkspaceInvite>(workspaceInvite =>
|
||||
{
|
||||
workspaceInvite.ToTable("WorkspaceInvites");
|
||||
workspaceInvite.HasKey(x => x.Id);
|
||||
workspaceInvite.Property(x => x.Email).HasMaxLength(256).IsRequired();
|
||||
workspaceInvite.Property(x => x.Role).HasMaxLength(64).IsRequired();
|
||||
workspaceInvite.Property(x => x.Status).HasMaxLength(64).IsRequired();
|
||||
workspaceInvite.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
workspaceInvite.HasIndex(x => x.WorkspaceId);
|
||||
workspaceInvite.HasIndex(x => new { x.WorkspaceId, x.Email, x.Status });
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Client>(client =>
|
||||
{
|
||||
client.ToTable("Clients");
|
||||
client.HasKey(x => x.Id);
|
||||
client.Property(x => x.Name).HasMaxLength(256).IsRequired();
|
||||
client.Property(x => x.Status).HasMaxLength(64).IsRequired();
|
||||
client.Property(x => x.PortraitUrl).HasMaxLength(2048);
|
||||
client.Property(x => x.PrimaryContactName).HasMaxLength(256);
|
||||
client.Property(x => x.PrimaryContactEmail).HasMaxLength(256);
|
||||
client.Property(x => x.PrimaryContactPortraitUrl).HasMaxLength(2048);
|
||||
client.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
client.HasIndex(x => new { x.WorkspaceId, x.Name }).IsUnique();
|
||||
client.HasIndex(x => x.WorkspaceId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Project>(project =>
|
||||
{
|
||||
project.ToTable("Projects");
|
||||
project.HasKey(x => x.Id);
|
||||
project.Property(x => x.Name).HasMaxLength(256).IsRequired();
|
||||
project.Property(x => x.Description).HasMaxLength(4000);
|
||||
project.Property(x => x.Notes).HasMaxLength(4000);
|
||||
project.Property(x => x.Status).HasMaxLength(64).IsRequired();
|
||||
project.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
project.HasIndex(x => new { x.ClientId, x.Name }).IsUnique();
|
||||
project.HasIndex(x => x.WorkspaceId);
|
||||
project.HasIndex(x => x.ClientId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ContentItem>(contentItem =>
|
||||
{
|
||||
contentItem.ToTable("ContentItems");
|
||||
contentItem.HasKey(x => x.Id);
|
||||
contentItem.Property(x => x.Title).HasMaxLength(256).IsRequired();
|
||||
contentItem.Property(x => x.PublicationMessage).HasMaxLength(4000).IsRequired();
|
||||
contentItem.Property(x => x.PublicationTargets).HasMaxLength(512).IsRequired();
|
||||
contentItem.Property(x => x.Hashtags).HasMaxLength(1024);
|
||||
contentItem.Property(x => x.Status).HasMaxLength(64).IsRequired();
|
||||
contentItem.Property(x => x.CurrentRevisionLabel).HasMaxLength(32).IsRequired();
|
||||
contentItem.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
contentItem.HasIndex(x => x.WorkspaceId);
|
||||
contentItem.HasIndex(x => x.ClientId);
|
||||
contentItem.HasIndex(x => x.ProjectId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ContentItemRevision>(revision =>
|
||||
{
|
||||
revision.ToTable("ContentItemRevisions");
|
||||
revision.HasKey(x => x.Id);
|
||||
revision.Property(x => x.RevisionLabel).HasMaxLength(32).IsRequired();
|
||||
revision.Property(x => x.Title).HasMaxLength(256).IsRequired();
|
||||
revision.Property(x => x.PublicationMessage).HasMaxLength(4000).IsRequired();
|
||||
revision.Property(x => x.PublicationTargets).HasMaxLength(512).IsRequired();
|
||||
revision.Property(x => x.Hashtags).HasMaxLength(1024);
|
||||
revision.Property(x => x.ChangeSummary).HasMaxLength(1024);
|
||||
revision.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
revision.HasIndex(x => x.ContentItemId);
|
||||
revision.HasIndex(x => new { x.ContentItemId, x.RevisionNumber }).IsUnique();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Asset>(asset =>
|
||||
{
|
||||
asset.ToTable("Assets");
|
||||
asset.HasKey(x => x.Id);
|
||||
asset.Property(x => x.AssetType).HasMaxLength(64).IsRequired();
|
||||
asset.Property(x => x.SourceType).HasMaxLength(64).IsRequired();
|
||||
asset.Property(x => x.DisplayName).HasMaxLength(256).IsRequired();
|
||||
asset.Property(x => x.GoogleDriveFileId).HasMaxLength(256);
|
||||
asset.Property(x => x.GoogleDriveLink).HasMaxLength(2048);
|
||||
asset.Property(x => x.PreviewUrl).HasMaxLength(2048);
|
||||
asset.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
asset.HasIndex(x => x.WorkspaceId);
|
||||
asset.HasIndex(x => x.ContentItemId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<AssetRevision>(revision =>
|
||||
{
|
||||
revision.ToTable("AssetRevisions");
|
||||
revision.HasKey(x => x.Id);
|
||||
revision.Property(x => x.SourceReference).HasMaxLength(2048).IsRequired();
|
||||
revision.Property(x => x.PreviewUrl).HasMaxLength(2048);
|
||||
revision.Property(x => x.Notes).HasMaxLength(1024);
|
||||
revision.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
revision.HasIndex(x => x.AssetId);
|
||||
revision.HasIndex(x => new { x.AssetId, x.RevisionNumber }).IsUnique();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Comment>(comment =>
|
||||
{
|
||||
comment.ToTable("Comments");
|
||||
comment.HasKey(x => x.Id);
|
||||
comment.Property(x => x.AuthorDisplayName).HasMaxLength(256).IsRequired();
|
||||
comment.Property(x => x.AuthorEmail).HasMaxLength(256).IsRequired();
|
||||
comment.Property(x => x.Body).HasMaxLength(4000).IsRequired();
|
||||
comment.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
comment.HasIndex(x => x.WorkspaceId);
|
||||
comment.HasIndex(x => x.ContentItemId);
|
||||
comment.HasIndex(x => x.ParentCommentId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ApprovalRequest>(approvalRequest =>
|
||||
{
|
||||
approvalRequest.ToTable("ApprovalRequests");
|
||||
approvalRequest.HasKey(x => x.Id);
|
||||
approvalRequest.Property(x => x.Stage).HasMaxLength(64).IsRequired();
|
||||
approvalRequest.Property(x => x.ReviewerName).HasMaxLength(256).IsRequired();
|
||||
approvalRequest.Property(x => x.ReviewerEmail).HasMaxLength(256).IsRequired();
|
||||
approvalRequest.Property(x => x.State).HasMaxLength(64).IsRequired();
|
||||
approvalRequest.Property(x => x.AccessToken).HasMaxLength(64).IsRequired();
|
||||
approvalRequest.Property(x => x.SentAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
approvalRequest.HasIndex(x => x.WorkspaceId);
|
||||
approvalRequest.HasIndex(x => x.ContentItemId);
|
||||
approvalRequest.HasIndex(x => x.ReviewerEmail);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ApprovalDecision>(approvalDecision =>
|
||||
{
|
||||
approvalDecision.ToTable("ApprovalDecisions");
|
||||
approvalDecision.HasKey(x => x.Id);
|
||||
approvalDecision.Property(x => x.Decision).HasMaxLength(64).IsRequired();
|
||||
approvalDecision.Property(x => x.Comment).HasMaxLength(2048);
|
||||
approvalDecision.Property(x => x.DecidedByName).HasMaxLength(256).IsRequired();
|
||||
approvalDecision.Property(x => x.DecidedByEmail).HasMaxLength(256).IsRequired();
|
||||
approvalDecision.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
approvalDecision.HasIndex(x => x.ApprovalRequestId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<NotificationEvent>(notificationEvent =>
|
||||
{
|
||||
notificationEvent.ToTable("NotificationEvents");
|
||||
notificationEvent.HasKey(x => x.Id);
|
||||
notificationEvent.Property(x => x.EventType).HasMaxLength(128).IsRequired();
|
||||
notificationEvent.Property(x => x.EntityType).HasMaxLength(128).IsRequired();
|
||||
notificationEvent.Property(x => x.Message).HasMaxLength(1024).IsRequired();
|
||||
notificationEvent.Property(x => x.RecipientEmail).HasMaxLength(256);
|
||||
notificationEvent.Property(x => x.MetadataJson).HasMaxLength(4000);
|
||||
notificationEvent.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
notificationEvent.HasIndex(x => x.WorkspaceId);
|
||||
notificationEvent.HasIndex(x => x.ContentItemId);
|
||||
notificationEvent.HasIndex(x => x.RecipientUserId);
|
||||
notificationEvent.HasIndex(x => x.CreatedAt);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
global using FluentValidation;
|
||||
global using FastEndpoints;
|
||||
global using JetBrains.Annotations;
|
||||
global using Microsoft.EntityFrameworkCore;
|
||||
global using Socialize.Data;
|
||||
global using Socialize.Modules.Approvals.Data;
|
||||
global using Socialize.Modules.Assets.Data;
|
||||
global using Socialize.Modules.Clients.Data;
|
||||
global using Socialize.Modules.Comments.Data;
|
||||
global using Socialize.Modules.ContentItems.Data;
|
||||
global using Socialize.Modules.Identity.Data;
|
||||
global using Socialize.Modules.Notifications.Data;
|
||||
global using Socialize.Modules.Projects.Data;
|
||||
global using Socialize.Modules.Workspaces.Data;
|
||||
@@ -1,154 +0,0 @@
|
||||
using Azure;
|
||||
using Azure.Storage.Blobs;
|
||||
using Azure.Storage.Blobs.Models;
|
||||
using Socialize.Infrastructure.BlobStorage.Contracts;
|
||||
|
||||
namespace Socialize.Infrastructure.BlobStorage.Services;
|
||||
|
||||
public class AzureBlobStorage : IBlobStorage
|
||||
{
|
||||
private const long MaxUploadSize = 10 * 1024 * 1024; // 10 MB in bytes
|
||||
|
||||
private readonly BlobServiceClient _blobServiceClient;
|
||||
private readonly ILogger<AzureBlobStorage> _logger;
|
||||
|
||||
public AzureBlobStorage(IConfiguration configuration, ILogger<AzureBlobStorage> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
string? connectionString = configuration.GetConnectionString("AzureBlob");
|
||||
_blobServiceClient = new BlobServiceClient(connectionString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upload a file to microsoft azure blob storage.
|
||||
/// </summary>
|
||||
/// <param name="containerName">The name of the container where the file is stored.</param>
|
||||
/// <param name="blobName">The blob name (path within the container, include the file name).</param>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="contentType">The content type.</param>
|
||||
/// <param name="ct">The cancellation token</param>
|
||||
/// <returns></returns>
|
||||
public async Task<string> UploadFileAsync(
|
||||
string containerName,
|
||||
string blobName,
|
||||
Stream stream,
|
||||
string contentType,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Read the file stream into a memory stream to determine the length
|
||||
// WATCH FOR MEMORY USAGE USING THE MEMORY STREAM.
|
||||
stream.Position = 0;
|
||||
|
||||
// Check if the file size exceeds the maximum upload size
|
||||
if (stream.Length > MaxUploadSize)
|
||||
{
|
||||
_logger.LogError(
|
||||
$"Blob storage: File size exceeds the maximum allowed size of {MaxUploadSize} bytes.");
|
||||
throw new InvalidOperationException(
|
||||
$"Blob storage: File size exceeds the maximum allowed size of {MaxUploadSize} bytes.");
|
||||
}
|
||||
|
||||
// Validate content type
|
||||
if (!ContentTypes.IsAllowed(contentType, stream))
|
||||
{
|
||||
_logger.LogError(
|
||||
$"Blob storage: Unsupported file type {contentType}.");
|
||||
throw new InvalidOperationException("Unsupported file type.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Get a reference to a container
|
||||
BlobContainerClient? containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
|
||||
|
||||
// Create the container if it does not exist
|
||||
await containerClient.CreateIfNotExistsAsync(
|
||||
PublicAccessType.Blob,
|
||||
cancellationToken: ct);
|
||||
|
||||
// Get a reference to a blob
|
||||
BlobClient? blobClient = containerClient.GetBlobClient(blobName);
|
||||
|
||||
// Define the BlobHttpHeaders to include the content type
|
||||
BlobHttpHeaders blobHttpHeaders = new() { ContentType = contentType };
|
||||
|
||||
// Upload the file
|
||||
Response<BlobContentInfo>? response = await blobClient.UploadAsync(
|
||||
stream,
|
||||
new BlobUploadOptions { HttpHeaders = blobHttpHeaders },
|
||||
ct);
|
||||
|
||||
string fileUri = blobClient.Uri.ToString();
|
||||
|
||||
_logger.LogInformation(
|
||||
"""
|
||||
Blob storage: Status [ {ResponseStatus} ]
|
||||
Uploaded [ {BlobName} ] to the container [ {ContainerName} ]
|
||||
with contentType [ {ContentType} ]
|
||||
with a length of [ {StreamLength} bytes ]
|
||||
with the uri [ {FileUri} ]
|
||||
""",
|
||||
response.GetRawResponse().Status.ToString(),
|
||||
blobName,
|
||||
containerName,
|
||||
contentType,
|
||||
stream.Length,
|
||||
fileUri
|
||||
);
|
||||
|
||||
// Return the URI of the uploaded blob
|
||||
return fileUri;
|
||||
}
|
||||
catch (RequestFailedException ex)
|
||||
{
|
||||
_logger.LogError($"Blob storage: Azure Storage request failed: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"Blob storage: An error occurred: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Download a file to microsoft's azure blob storage.
|
||||
/// </summary>
|
||||
/// <param name="blobName">The blob name (path within the container).</param>
|
||||
/// <param name="containerName">The name of the container where the file is stored. (users)</param>
|
||||
/// <param name="ct">The cancellation token for the request</param>
|
||||
/// <returns></returns>
|
||||
public async Task<MemoryStream> DownloadFileAsync(
|
||||
string containerName,
|
||||
string blobName,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get a reference to a container
|
||||
BlobContainerClient? containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
|
||||
|
||||
// Get a reference to a blob
|
||||
BlobClient? blobClient = containerClient.GetBlobClient(blobName);
|
||||
|
||||
// Download the blob to a stream
|
||||
BlobDownloadInfo download = await blobClient.DownloadAsync(ct);
|
||||
|
||||
MemoryStream memoryStream = new();
|
||||
await download.Content.CopyToAsync(memoryStream, ct);
|
||||
memoryStream.Position = 0; // Ensure the stream is at the beginning
|
||||
|
||||
return memoryStream;
|
||||
}
|
||||
catch (RequestFailedException ex)
|
||||
{
|
||||
_logger.LogError($"Azure Storage request failed: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"An error occurred: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using Socialize.Infrastructure.BlobStorage.Contracts;
|
||||
using Socialize.Infrastructure.BlobStorage.Services;
|
||||
using Socialize.Infrastructure.Configuration;
|
||||
using Socialize.Infrastructure.Emailer.Configuration;
|
||||
using Socialize.Infrastructure.Emailer.Contracts;
|
||||
using Socialize.Infrastructure.Emailer.Services;
|
||||
using Socialize.Infrastructure.Payments.Stripe.Configuration;
|
||||
|
||||
namespace Socialize.Infrastructure;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static WebApplicationBuilder AddInfrastructureModule(
|
||||
this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.Configure<WebsiteOptions>(
|
||||
builder.Configuration.GetRequiredSection(WebsiteOptions.SectionName));
|
||||
|
||||
builder.Services.AddTransient<IBlobStorage, AzureBlobStorage>();
|
||||
builder.Services.Configure<StripeOptions>(
|
||||
builder.Configuration.GetSection(StripeOptions.ConfigurationSection));
|
||||
|
||||
builder.Services.Configure<EmailerOptions>(
|
||||
builder.Configuration.GetSection(EmailerOptions.ConfigurationSection));
|
||||
builder.Services.AddTransient<IEmailSender, ResendEmailSender>();
|
||||
//builder.Services.AddTransient<IEmailSender, EmailSender>();
|
||||
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
using Azure.Identity;
|
||||
using Socialize;
|
||||
using Socialize.Data;
|
||||
using Socialize.Infrastructure;
|
||||
using Socialize.Infrastructure.Development;
|
||||
using Socialize.Modules.Approvals;
|
||||
using Socialize.Modules.Assets;
|
||||
using Socialize.Modules.Clients;
|
||||
using Socialize.Modules.Comments;
|
||||
using Socialize.Modules.ContentItems;
|
||||
using Socialize.Modules.Identity;
|
||||
using Socialize.Modules.Notifications;
|
||||
using Socialize.Modules.Projects;
|
||||
using Socialize.Modules.Workspaces;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using NSwag;
|
||||
using NSwag.Generation.AspNetCore.Processors;
|
||||
using NSwag.Generation.Processors.Security;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
if (!builder.Environment.IsDevelopment())
|
||||
{
|
||||
string? vaultUri = Environment.GetEnvironmentVariable("VaultUri");
|
||||
|
||||
if (vaultUri is null)
|
||||
{
|
||||
throw new InvalidOperationException("Missing VaultUri configuration setting");
|
||||
}
|
||||
|
||||
builder.Configuration.AddAzureKeyVault(new Uri(vaultUri), new DefaultAzureCredential());
|
||||
}
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy(
|
||||
"AllowAll",
|
||||
policy =>
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
});
|
||||
});
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddWebServices();
|
||||
builder.Services.AddAuthorizationAndAuthentication(builder.Configuration);
|
||||
|
||||
builder.Services.AddOpenApiDocument((
|
||||
configure,
|
||||
_) =>
|
||||
{
|
||||
configure.Title = "Socialize API";
|
||||
|
||||
// Add JWT
|
||||
configure.AddSecurity(
|
||||
"JWT",
|
||||
[],
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Type = OpenApiSecuritySchemeType.ApiKey,
|
||||
Name = "Authorization",
|
||||
In = OpenApiSecurityApiKeyLocation.Header,
|
||||
Description = "Type into the textbox: Bearer {your JWT token}."
|
||||
});
|
||||
|
||||
configure.OperationProcessors.Add(new AspNetCoreOperationTagsProcessor());
|
||||
configure.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT"));
|
||||
});
|
||||
|
||||
string postgresConnectionString = builder.Configuration.GetConnectionString("PostgresConnection")
|
||||
?? throw new InvalidOperationException(
|
||||
"Missing ConnectionStrings:PostgresConnection");
|
||||
|
||||
builder.Services.AddFastEndpoints();
|
||||
builder.Services.AddAppData(postgresConnectionString);
|
||||
builder.AddInfrastructureModule();
|
||||
builder.AddIdentityModule();
|
||||
builder.AddWorkspaceModule();
|
||||
builder.AddClientsModule();
|
||||
builder.AddProjectsModule();
|
||||
builder.AddContentItemsModule();
|
||||
builder.AddAssetsModule();
|
||||
builder.AddCommentsModule();
|
||||
builder.AddApprovalsModule();
|
||||
builder.AddNotificationsModule();
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
app.UseForwardedHeaders(
|
||||
new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedProto }
|
||||
);
|
||||
|
||||
app.UseCors("AllowAll");
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
// Initialize and seed the db.
|
||||
await app.UseAppDataAsync();
|
||||
await app.UseIdentityModuleAsync();
|
||||
await app.UseDevelopmentSeedAsync();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHealthChecks("/health");
|
||||
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseHttpsRedirection();
|
||||
}
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseOpenApi();
|
||||
app.UseSwaggerUi(options => options.Path = "/api");
|
||||
}
|
||||
|
||||
app.UseFastEndpoints();
|
||||
|
||||
app.Run();
|
||||
11
backend/Socialize.slnx
Normal file
11
backend/Socialize.slnx
Normal file
@@ -0,0 +1,11 @@
|
||||
<Solution>
|
||||
<Configurations>
|
||||
<Platform Name="Any CPU" />
|
||||
<Platform Name="x64" />
|
||||
<Platform Name="x86" />
|
||||
</Configurations>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/Socialize.Tests/Socialize.Tests.csproj" />
|
||||
</Folder>
|
||||
<Project Path="src/Socialize.Api/Socialize.Api.csproj" />
|
||||
</Solution>
|
||||
@@ -1,16 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Socialize.Api", "Socialize.Api.csproj", "{D790B528-6968-4CCD-A25D-A108A82CBDAC}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{D790B528-6968-4CCD-A25D-A108A82CBDAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D790B528-6968-4CCD-A25D-A108A82CBDAC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D790B528-6968-4CCD-A25D-A108A82CBDAC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D790B528-6968-4CCD-A25D-A108A82CBDAC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -13,5 +13,7 @@ fi
|
||||
dotnet ef migrations add \
|
||||
--context "Socialize.Modules.${MODULE_NAME}.Data.${MODULE_NAME}DbContext" \
|
||||
--configuration Debug \
|
||||
--project "src/Socialize.Api/Socialize.Api.csproj" \
|
||||
--startup-project "src/Socialize.Api/Socialize.Api.csproj" \
|
||||
--output-dir "Modules/${MODULE_NAME}/Migrations" \
|
||||
"$MIGRATION_NAME"
|
||||
|
||||
@@ -16,6 +16,8 @@ UPDATE_COMMAND=(
|
||||
dotnet ef database update
|
||||
--context "Socialize.Modules.${MODULE_NAME}.Data.${MODULE_NAME}DbContext"
|
||||
--configuration Debug
|
||||
--project "src/Socialize.Api/Socialize.Api.csproj"
|
||||
--startup-project "src/Socialize.Api/Socialize.Api.csproj"
|
||||
)
|
||||
|
||||
if [ -n "$TARGET_MIGRATION" ]; then
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Socialize.Common.Domain;
|
||||
namespace Socialize.Api.Common.Domain;
|
||||
|
||||
public abstract class Entity
|
||||
{
|
||||
52
backend/src/Socialize.Api/Data/AppDbContext.cs
Normal file
52
backend/src/Socialize.Api/Data/AppDbContext.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Socialize.Api.Modules.Approvals.Data;
|
||||
using Socialize.Api.Modules.Assets.Data;
|
||||
using Socialize.Api.Modules.Clients.Data;
|
||||
using Socialize.Api.Modules.Comments.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Feedback.Data;
|
||||
using Socialize.Api.Modules.Identity.Data;
|
||||
using Socialize.Api.Modules.Notifications.Data;
|
||||
using Socialize.Api.Modules.Projects.Data;
|
||||
using Socialize.Api.Modules.Workspaces.Data;
|
||||
|
||||
namespace Socialize.Api.Data;
|
||||
|
||||
public class AppDbContext(
|
||||
DbContextOptions<AppDbContext> options)
|
||||
: IdentityDbContext<User, Role, Guid>(options)
|
||||
{
|
||||
public DbSet<Workspace> Workspaces => Set<Workspace>();
|
||||
public DbSet<WorkspaceInvite> WorkspaceInvites => Set<WorkspaceInvite>();
|
||||
public DbSet<Client> Clients => Set<Client>();
|
||||
public DbSet<Project> Projects => Set<Project>();
|
||||
public DbSet<ContentItem> ContentItems => Set<ContentItem>();
|
||||
public DbSet<ContentItemRevision> ContentItemRevisions => Set<ContentItemRevision>();
|
||||
public DbSet<Asset> Assets => Set<Asset>();
|
||||
public DbSet<AssetRevision> AssetRevisions => Set<AssetRevision>();
|
||||
public DbSet<Comment> Comments => Set<Comment>();
|
||||
public DbSet<ApprovalRequest> ApprovalRequests => Set<ApprovalRequest>();
|
||||
public DbSet<ApprovalDecision> ApprovalDecisions => Set<ApprovalDecision>();
|
||||
public DbSet<NotificationEvent> NotificationEvents => Set<NotificationEvent>();
|
||||
public DbSet<FeedbackReport> FeedbackReports => Set<FeedbackReport>();
|
||||
public DbSet<FeedbackTag> FeedbackTags => Set<FeedbackTag>();
|
||||
public DbSet<FeedbackScreenshot> FeedbackScreenshots => Set<FeedbackScreenshot>();
|
||||
public DbSet<FeedbackComment> FeedbackComments => Set<FeedbackComment>();
|
||||
public DbSet<FeedbackActivityEntry> FeedbackActivityEntries => Set<FeedbackActivityEntry>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
|
||||
builder.ConfigureWorkspacesModule();
|
||||
builder.ConfigureClientsModule();
|
||||
builder.ConfigureProjectsModule();
|
||||
builder.ConfigureContentItemsModule();
|
||||
builder.ConfigureAssetsModule();
|
||||
builder.ConfigureCommentsModule();
|
||||
builder.ConfigureApprovalsModule();
|
||||
builder.ConfigureNotificationsModule();
|
||||
builder.ConfigureFeedbackModule();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text;
|
||||
using Socialize.Data;
|
||||
using Socialize.Infrastructure.Security;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Facebook;
|
||||
using Microsoft.AspNetCore.Authentication.Google;
|
||||
@@ -49,7 +50,7 @@ public static class DependencyInjection
|
||||
{
|
||||
using IServiceScope scope = app.ApplicationServices.CreateScope();
|
||||
await using AppDbContext context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await context.Database.EnsureCreatedAsync(cancellationToken);
|
||||
await context.Database.MigrateAsync(cancellationToken);
|
||||
|
||||
return app;
|
||||
}
|
||||
25
backend/src/Socialize.Api/Dockerfile
Normal file
25
backend/src/Socialize.Api/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
COPY backend/Socialize.slnx backend/
|
||||
COPY backend/src/Socialize.Api/Socialize.Api.csproj backend/src/Socialize.Api/
|
||||
COPY backend/tests/Socialize.Tests/Socialize.Tests.csproj backend/tests/Socialize.Tests/
|
||||
|
||||
RUN dotnet restore backend/Socialize.slnx
|
||||
|
||||
COPY backend/ backend/
|
||||
|
||||
RUN dotnet publish backend/src/Socialize.Api/Socialize.Api.csproj \
|
||||
-c Release \
|
||||
-o /app/publish \
|
||||
--no-restore
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
ENV ASPNETCORE_URLS=http://0.0.0.0:8080
|
||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["dotnet", "Socialize.Api.dll"]
|
||||
2
backend/src/Socialize.Api/GlobalUsings.cs
Normal file
2
backend/src/Socialize.Api/GlobalUsings.cs
Normal file
@@ -0,0 +1,2 @@
|
||||
global using FluentValidation;
|
||||
global using JetBrains.Annotations;
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Socialize.Api.Infrastructure.BlobStorage.Configuration;
|
||||
|
||||
public sealed class LocalBlobStorageOptions
|
||||
{
|
||||
public const string SectionName = "LocalBlobStorage";
|
||||
|
||||
public string RootPath { get; set; } = "App_Data/blob-storage";
|
||||
|
||||
public string RequestPath { get; set; } = "/api/storage";
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Socialize.Infrastructure.BlobStorage.Contracts;
|
||||
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||
|
||||
public static class CommonFileNames
|
||||
{
|
||||
@@ -1,8 +1,10 @@
|
||||
namespace Socialize.Infrastructure.BlobStorage.Contracts;
|
||||
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||
|
||||
internal static class ContainerNames
|
||||
{
|
||||
public const string Users = "users";
|
||||
public const string Clients = "clients";
|
||||
public const string Workspaces = "workspaces";
|
||||
public const string Creators = "creators";
|
||||
public const string Feedback = "feedback";
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Socialize.Infrastructure.BlobStorage.Contracts;
|
||||
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||
|
||||
public static class ContentTypes
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Socialize.Infrastructure.BlobStorage.Contracts;
|
||||
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||
|
||||
public interface IBlobStorage
|
||||
{
|
||||
@@ -1,8 +1,9 @@
|
||||
namespace Socialize.Infrastructure.BlobStorage.Contracts;
|
||||
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||
|
||||
public static class SubDirectoryNames
|
||||
{
|
||||
public const string Profile = "profile";
|
||||
public const string Contents = "contents";
|
||||
public const string Albums = "albums";
|
||||
public const string FeedbackScreenshots = "screenshots";
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Socialize.Api.Infrastructure.BlobStorage.Configuration;
|
||||
using Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||
|
||||
namespace Socialize.Api.Infrastructure.BlobStorage.Services;
|
||||
|
||||
public sealed class LocalBlobStorage(
|
||||
IWebHostEnvironment environment,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IOptions<LocalBlobStorageOptions> options,
|
||||
ILogger<LocalBlobStorage> logger)
|
||||
: IBlobStorage
|
||||
{
|
||||
private const long MaxUploadSize = 10 * 1024 * 1024;
|
||||
private const string ContentTypeMetadataSuffix = ".content-type";
|
||||
|
||||
private readonly LocalBlobStorageOptions _options = options.Value;
|
||||
|
||||
public async Task<string> UploadFileAsync(
|
||||
string containerName,
|
||||
string blobName,
|
||||
Stream stream,
|
||||
string contentType,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
stream.Position = 0;
|
||||
|
||||
if (stream.Length > MaxUploadSize)
|
||||
{
|
||||
logger.LogError("Blob storage: File size exceeds the maximum allowed size of {MaxUploadSize} bytes.", MaxUploadSize);
|
||||
throw new InvalidOperationException($"Blob storage: File size exceeds the maximum allowed size of {MaxUploadSize} bytes.");
|
||||
}
|
||||
|
||||
if (!ContentTypes.IsAllowed(contentType, stream))
|
||||
{
|
||||
logger.LogError("Blob storage: Unsupported file type {ContentType}.", contentType);
|
||||
throw new InvalidOperationException("Unsupported file type.");
|
||||
}
|
||||
|
||||
string relativePath = GetSafeRelativePath(containerName, blobName);
|
||||
string filePath = Path.Combine(GetRootPath(), relativePath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(filePath) ?? GetRootPath());
|
||||
|
||||
await using FileStream fileStream = File.Create(filePath);
|
||||
await stream.CopyToAsync(fileStream, ct);
|
||||
await File.WriteAllTextAsync(GetContentTypeMetadataPath(filePath), contentType, ct);
|
||||
|
||||
string fileUri = BuildPublicUrl(relativePath);
|
||||
logger.LogInformation(
|
||||
"Blob storage: Uploaded [{BlobName}] to local container [{ContainerName}] with contentType [{ContentType}] and uri [{FileUri}]",
|
||||
blobName,
|
||||
containerName,
|
||||
contentType,
|
||||
fileUri);
|
||||
|
||||
return fileUri;
|
||||
}
|
||||
|
||||
public async Task<MemoryStream> DownloadFileAsync(
|
||||
string containerName,
|
||||
string blobName,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
string filePath = Path.Combine(GetRootPath(), GetSafeRelativePath(containerName, blobName));
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException("Blob storage: Local file was not found.", blobName);
|
||||
}
|
||||
|
||||
MemoryStream memoryStream = new();
|
||||
await using FileStream fileStream = File.OpenRead(filePath);
|
||||
await fileStream.CopyToAsync(memoryStream, ct);
|
||||
memoryStream.Position = 0;
|
||||
|
||||
return memoryStream;
|
||||
}
|
||||
|
||||
internal string GetRootPath()
|
||||
{
|
||||
if (Path.IsPathRooted(_options.RootPath))
|
||||
{
|
||||
return Path.GetFullPath(_options.RootPath);
|
||||
}
|
||||
|
||||
return Path.GetFullPath(Path.Combine(environment.ContentRootPath, _options.RootPath));
|
||||
}
|
||||
|
||||
internal static string? ReadContentType(string filePath)
|
||||
{
|
||||
string metadataPath = GetContentTypeMetadataPath(filePath);
|
||||
return File.Exists(metadataPath)
|
||||
? File.ReadAllText(metadataPath)
|
||||
: null;
|
||||
}
|
||||
|
||||
private static string GetContentTypeMetadataPath(string filePath)
|
||||
{
|
||||
return $"{filePath}{ContentTypeMetadataSuffix}";
|
||||
}
|
||||
|
||||
private static string GetSafeRelativePath(string containerName, string blobName)
|
||||
{
|
||||
if (Path.IsPathRooted(containerName) || Path.IsPathRooted(blobName))
|
||||
{
|
||||
throw new InvalidOperationException("Blob storage: Blob paths must be relative.");
|
||||
}
|
||||
|
||||
string[] pathParts = [containerName, .. blobName.Split([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar])];
|
||||
if (pathParts.Any(part => part is "" or "." or ".."))
|
||||
{
|
||||
throw new InvalidOperationException("Blob storage: Blob paths must not contain relative path segments.");
|
||||
}
|
||||
|
||||
return Path.Combine(pathParts);
|
||||
}
|
||||
|
||||
private string BuildPublicUrl(string relativePath)
|
||||
{
|
||||
HttpRequest? request = httpContextAccessor.HttpContext?.Request;
|
||||
string requestPath = NormalizeRequestPath(_options.RequestPath);
|
||||
string urlPath = $"{requestPath}/{relativePath.Replace(Path.DirectorySeparatorChar, '/')}";
|
||||
|
||||
if (request is null)
|
||||
{
|
||||
return urlPath;
|
||||
}
|
||||
|
||||
return $"{request.Scheme}://{request.Host}{request.PathBase}{urlPath}";
|
||||
}
|
||||
|
||||
internal static string NormalizeRequestPath(string requestPath)
|
||||
{
|
||||
string normalized = string.IsNullOrWhiteSpace(requestPath)
|
||||
? "/api/storage"
|
||||
: requestPath.Trim();
|
||||
|
||||
return normalized.StartsWith("/", StringComparison.Ordinal)
|
||||
? normalized.TrimEnd('/')
|
||||
: $"/{normalized.TrimEnd('/')}";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Socialize.Infrastructure.Configuration;
|
||||
namespace Socialize.Api.Infrastructure.Configuration;
|
||||
|
||||
public class WebsiteOptions
|
||||
{
|
||||
@@ -0,0 +1,36 @@
|
||||
using Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||
using Socialize.Api.Infrastructure.BlobStorage.Services;
|
||||
using Socialize.Api.Infrastructure.BlobStorage.Configuration;
|
||||
using Socialize.Api.Infrastructure.Configuration;
|
||||
using Socialize.Api.Infrastructure.Emailer.Configuration;
|
||||
using Socialize.Api.Infrastructure.Emailer.Contracts;
|
||||
using Socialize.Api.Infrastructure.Emailer.Services;
|
||||
using Socialize.Api.Infrastructure.Payments.Stripe.Configuration;
|
||||
|
||||
namespace Socialize.Api.Infrastructure;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static WebApplicationBuilder AddInfrastructureModule(
|
||||
this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.Configure<WebsiteOptions>(
|
||||
builder.Configuration.GetRequiredSection(WebsiteOptions.SectionName));
|
||||
|
||||
builder.Services.Configure<LocalBlobStorageOptions>(
|
||||
builder.Configuration.GetSection(LocalBlobStorageOptions.SectionName));
|
||||
builder.Services.AddTransient<LocalBlobStorage>();
|
||||
builder.Services.AddTransient<IBlobStorage>(services => services.GetRequiredService<LocalBlobStorage>());
|
||||
builder.Services.Configure<StripeOptions>(
|
||||
builder.Configuration.GetSection(StripeOptions.ConfigurationSection));
|
||||
|
||||
builder.Services.Configure<EmailerOptions>(
|
||||
builder.Configuration.GetSection(EmailerOptions.ConfigurationSection));
|
||||
builder.Services.AddTransient<IEmailSender, ResendEmailSender>();
|
||||
//builder.Services.AddTransient<IEmailSender, EmailSender>();
|
||||
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,21 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Security.Claims;
|
||||
using Socialize.Infrastructure.Security;
|
||||
using Socialize.Modules.Identity.Contracts;
|
||||
using Socialize.Modules.Identity.Data;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Identity.Contracts;
|
||||
using Socialize.Api.Modules.Identity.Data;
|
||||
using Socialize.Api.Modules.Assets.Data;
|
||||
using Socialize.Api.Modules.Approvals.Data;
|
||||
using Socialize.Api.Modules.Comments.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Clients.Data;
|
||||
using Socialize.Api.Modules.Notifications.Data;
|
||||
using Socialize.Api.Modules.Projects.Data;
|
||||
using Socialize.Api.Modules.Workspaces.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Socialize.Infrastructure.Development;
|
||||
namespace Socialize.Api.Infrastructure.Development;
|
||||
|
||||
public static class DevelopmentSeedExtensions
|
||||
{
|
||||
@@ -41,8 +51,6 @@ public static class DevelopmentSeedExtensions
|
||||
UserManager userManager = scope.ServiceProvider.GetRequiredService<UserManager>();
|
||||
AppDbContext dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
await RemoveLegacyDevUserAsync(userManager);
|
||||
|
||||
User manager = await EnsureUserAsync(
|
||||
userManager,
|
||||
id: Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
|
||||
@@ -94,6 +102,21 @@ public static class DevelopmentSeedExtensions
|
||||
new Claim(KnownClaims.ProjectScope, ScopedProjectId.ToString()),
|
||||
]);
|
||||
|
||||
User dev = await EnsureUserAsync(
|
||||
userManager,
|
||||
id: Guid.Parse("dddddddd-dddd-dddd-dddd-dddddddddddd"),
|
||||
username: "dev",
|
||||
email: "dev@socialize.local",
|
||||
password: "dev",
|
||||
alias: "Socialize Dev",
|
||||
firstname: "Jo",
|
||||
lastname: "Bumble",
|
||||
portraitUrl: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=200&q=80",
|
||||
roles: [KnownRoles.Developer, KnownRoles.Administrator, KnownRoles.Manager, KnownRoles.WorkspaceMember],
|
||||
claims:
|
||||
[
|
||||
]);
|
||||
|
||||
await EnsureWorkspaceDataAsync(
|
||||
manager.Id,
|
||||
clientUser.Id,
|
||||
@@ -104,19 +127,6 @@ public static class DevelopmentSeedExtensions
|
||||
return app;
|
||||
}
|
||||
|
||||
private static async Task RemoveLegacyDevUserAsync(UserManager userManager)
|
||||
{
|
||||
User? legacyUser = await userManager.FindByNameAsync("dev")
|
||||
?? await userManager.FindByEmailAsync("dev@socialize.local");
|
||||
|
||||
if (legacyUser is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await userManager.DeleteAsync(legacyUser);
|
||||
}
|
||||
|
||||
private static async Task<User> EnsureUserAsync(
|
||||
UserManager userManager,
|
||||
Guid id,
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Socialize.Infrastructure.Development;
|
||||
namespace Socialize.Api.Infrastructure.Development;
|
||||
|
||||
public record DevelopmentSeedOptions
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Socialize.Infrastructure.Emailer.Configuration;
|
||||
namespace Socialize.Api.Infrastructure.Emailer.Configuration;
|
||||
|
||||
public class EmailerOptions
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Socialize.Infrastructure.Emailer.Contracts;
|
||||
namespace Socialize.Api.Infrastructure.Emailer.Contracts;
|
||||
|
||||
public interface IEmailSender
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using Socialize.Infrastructure.Emailer.Contracts;
|
||||
using Socialize.Api.Infrastructure.Emailer.Contracts;
|
||||
|
||||
namespace Socialize.Infrastructure.Emailer.Services;
|
||||
namespace Socialize.Api.Infrastructure.Emailer.Services;
|
||||
|
||||
public class LoggerEmailSender(ILogger<IEmailSender> logger)
|
||||
: IEmailSender
|
||||
@@ -1,9 +1,9 @@
|
||||
using Socialize.Infrastructure.Emailer.Configuration;
|
||||
using Socialize.Infrastructure.Emailer.Contracts;
|
||||
using Socialize.Api.Infrastructure.Emailer.Configuration;
|
||||
using Socialize.Api.Infrastructure.Emailer.Contracts;
|
||||
using Microsoft.Extensions.Options;
|
||||
using PostmarkDotNet;
|
||||
|
||||
namespace Socialize.Infrastructure.Emailer.Services;
|
||||
namespace Socialize.Api.Infrastructure.Emailer.Services;
|
||||
|
||||
public class PostmarkEmailSender : IEmailSender
|
||||
{
|
||||
@@ -1,11 +1,11 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Socialize.Infrastructure.Emailer.Configuration;
|
||||
using Socialize.Infrastructure.Emailer.Contracts;
|
||||
using Socialize.Api.Infrastructure.Emailer.Configuration;
|
||||
using Socialize.Api.Infrastructure.Emailer.Contracts;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Socialize.Infrastructure.Emailer.Services;
|
||||
namespace Socialize.Api.Infrastructure.Emailer.Services;
|
||||
|
||||
public class ResendEmailSender : IEmailSender
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Socialize.Infrastructure.Payments.Stripe.Configuration;
|
||||
namespace Socialize.Api.Infrastructure.Payments.Stripe.Configuration;
|
||||
|
||||
public class StripeOptions
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Security.Claims;
|
||||
using Socialize.Modules.Identity.Contracts;
|
||||
using Socialize.Api.Modules.Identity.Contracts;
|
||||
|
||||
namespace Socialize.Infrastructure.Security;
|
||||
namespace Socialize.Api.Infrastructure.Security;
|
||||
|
||||
public sealed class AccessScopeService
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Socialize.Infrastructure.Security;
|
||||
namespace Socialize.Api.Infrastructure.Security;
|
||||
|
||||
public static class ClaimsPrincipalExtensions
|
||||
{
|
||||
@@ -3,7 +3,7 @@ using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Socialize.Infrastructure.Security;
|
||||
namespace Socialize.Api.Infrastructure.Security;
|
||||
|
||||
public static class JwtTokenHelper
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Socialize.Infrastructure.Security;
|
||||
namespace Socialize.Api.Infrastructure.Security;
|
||||
|
||||
public static class KnownClaims
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Socialize.Infrastructure.Security;
|
||||
namespace Socialize.Api.Infrastructure.Security;
|
||||
|
||||
public class MissingClaimException(
|
||||
string claimName)
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Socialize.Infrastructure.Security;
|
||||
namespace Socialize.Api.Infrastructure.Security;
|
||||
|
||||
// If we need to add special characters we can alternate between 2 pools.
|
||||
public static class PasswordGenerator
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Socialize.Infrastructure.Security;
|
||||
namespace Socialize.Api.Infrastructure.Security;
|
||||
|
||||
public static class RefreshTokenGenerator
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Socialize.Infrastructure.YouTube;
|
||||
namespace Socialize.Api.Infrastructure.YouTube;
|
||||
|
||||
public static class YouTubeUrlHelper
|
||||
{
|
||||
@@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Socialize.Data;
|
||||
using Socialize.Api.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Socialize.Api.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Socialize.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260430054500_AddWorkspaceLogo")]
|
||||
public partial class AddWorkspaceLogo : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "LogoUrl",
|
||||
table: "Workspaces",
|
||||
type: "character varying(2048)",
|
||||
maxLength: 2048,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LogoUrl",
|
||||
table: "Workspaces");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,21 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Socialize.Data;
|
||||
using Socialize.Api.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Socialize.Api.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
partial class AppDbContextModelSnapshot : ModelSnapshot
|
||||
[Migration("20260430072517_AddFeedbackFoundation")]
|
||||
partial class AddFeedbackFoundation
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
@@ -125,7 +128,7 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Approvals.Data.ApprovalDecision", b =>
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalDecision", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -168,7 +171,7 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("ApprovalDecisions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Approvals.Data.ApprovalRequest", b =>
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalRequest", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -230,7 +233,7 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("ApprovalRequests", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Assets.Data.Asset", b =>
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Assets.Data.Asset", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -286,7 +289,7 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("Assets", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Assets.Data.AssetRevision", b =>
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Assets.Data.AssetRevision", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -329,7 +332,7 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("AssetRevisions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Clients.Data.Client", b =>
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Clients.Data.Client", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -379,7 +382,7 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("Clients", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Comments.Data.Comment", b =>
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Comments.Data.Comment", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -434,7 +437,7 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("Comments", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.ContentItems.Data.ContentItem", b =>
|
||||
modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -500,7 +503,7 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("ContentItems", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.ContentItems.Data.ContentItemRevision", b =>
|
||||
modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItemRevision", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -558,7 +561,150 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("ContentItemRevisions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Identity.Data.Role", b =>
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackReport", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AppVersion")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("BrowserUserAgent")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("CancellationReason")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<DateTimeOffset?>("CancelledAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("CancelledByUserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ClientId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ClientName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<Guid?>("ContentItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ContentItemTitle")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8000)
|
||||
.HasColumnType("character varying(8000)");
|
||||
|
||||
b.Property<DateTimeOffset>("LastActivityAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ProjectName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ReporterDisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ReporterEmail")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<Guid>("ReporterUserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("SubmittedPath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<int?>("ViewportHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("ViewportWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("WorkspaceName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("LastActivityAt");
|
||||
|
||||
b.HasIndex("ReporterUserId");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("Type");
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.ToTable("FeedbackReports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackTag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("FeedbackReportId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName");
|
||||
|
||||
b.HasIndex("FeedbackReportId", "NormalizedName")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("FeedbackTags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Identity.Data.Role", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -585,7 +731,7 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Identity.Data.User", b =>
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Identity.Data.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -688,7 +834,7 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Notifications.Data.NotificationEvent", b =>
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Notifications.Data.NotificationEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -750,7 +896,7 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("NotificationEvents", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Projects.Data.Project", b =>
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Projects.Data.Project", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -803,7 +949,7 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("Projects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Workspaces.Data.Workspace", b =>
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -814,6 +960,10 @@ namespace Socialize.Api.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("LogoUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
@@ -842,7 +992,7 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("Workspaces", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Workspaces.Data.WorkspaceInvite", b =>
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.WorkspaceInvite", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -885,7 +1035,7 @@ namespace Socialize.Api.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("Socialize.Modules.Identity.Data.Role", null)
|
||||
b.HasOne("Socialize.Api.Modules.Identity.Data.Role", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -894,7 +1044,7 @@ namespace Socialize.Api.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("Socialize.Modules.Identity.Data.User", null)
|
||||
b.HasOne("Socialize.Api.Modules.Identity.Data.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -903,7 +1053,7 @@ namespace Socialize.Api.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("Socialize.Modules.Identity.Data.User", null)
|
||||
b.HasOne("Socialize.Api.Modules.Identity.Data.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -912,13 +1062,13 @@ namespace Socialize.Api.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("Socialize.Modules.Identity.Data.Role", null)
|
||||
b.HasOne("Socialize.Api.Modules.Identity.Data.Role", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Socialize.Modules.Identity.Data.User", null)
|
||||
b.HasOne("Socialize.Api.Modules.Identity.Data.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -927,12 +1077,28 @@ namespace Socialize.Api.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("Socialize.Modules.Identity.Data.User", null)
|
||||
b.HasOne("Socialize.Api.Modules.Identity.Data.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackTag", b =>
|
||||
{
|
||||
b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport")
|
||||
.WithMany("Tags")
|
||||
.HasForeignKey("FeedbackReportId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("FeedbackReport");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackReport", b =>
|
||||
{
|
||||
b.Navigation("Tags");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Socialize.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddFeedbackFoundation : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "FeedbackReports",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Type = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(8000)", maxLength: 8000, nullable: false),
|
||||
ReporterUserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ReporterDisplayName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
ReporterEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
SubmittedPath = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
|
||||
BrowserUserAgent = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||
ViewportWidth = table.Column<int>(type: "integer", nullable: true),
|
||||
ViewportHeight = table.Column<int>(type: "integer", nullable: true),
|
||||
AppVersion = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||
WorkspaceId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
WorkspaceName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
ClientId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
ClientName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
ProjectId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
ProjectName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
ContentItemId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
ContentItemTitle = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||
LastActivityAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
CancelledAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
CancelledByUserId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
CancellationReason = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_FeedbackReports", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "FeedbackTags",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
FeedbackReportId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
NormalizedName = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_FeedbackTags", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_FeedbackTags_FeedbackReports_FeedbackReportId",
|
||||
column: x => x.FeedbackReportId,
|
||||
principalTable: "FeedbackReports",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackReports_LastActivityAt",
|
||||
table: "FeedbackReports",
|
||||
column: "LastActivityAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackReports_ReporterUserId",
|
||||
table: "FeedbackReports",
|
||||
column: "ReporterUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackReports_Status",
|
||||
table: "FeedbackReports",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackReports_Type",
|
||||
table: "FeedbackReports",
|
||||
column: "Type");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackReports_WorkspaceId",
|
||||
table: "FeedbackReports",
|
||||
column: "WorkspaceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackTags_FeedbackReportId_NormalizedName",
|
||||
table: "FeedbackTags",
|
||||
columns: new[] { "FeedbackReportId", "NormalizedName" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackTags_NormalizedName",
|
||||
table: "FeedbackTags",
|
||||
column: "NormalizedName");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "FeedbackTags");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "FeedbackReports");
|
||||
}
|
||||
}
|
||||
}
|
||||
1163
backend/src/Socialize.Api/Migrations/20260430171123_AddFeedbackScreenshots.Designer.cs
generated
Normal file
1163
backend/src/Socialize.Api/Migrations/20260430171123_AddFeedbackScreenshots.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Socialize.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddFeedbackScreenshots : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "FeedbackScreenshots",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
FeedbackReportId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
FileName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
ContentType = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
SizeBytes = table.Column<long>(type: "bigint", nullable: false),
|
||||
BlobContainerName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
BlobName = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_FeedbackScreenshots", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_FeedbackScreenshots_FeedbackReports_FeedbackReportId",
|
||||
column: x => x.FeedbackReportId,
|
||||
principalTable: "FeedbackReports",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackScreenshots_FeedbackReportId",
|
||||
table: "FeedbackScreenshots",
|
||||
column: "FeedbackReportId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "FeedbackScreenshots");
|
||||
}
|
||||
}
|
||||
}
|
||||
1292
backend/src/Socialize.Api/Migrations/20260430171959_AddFeedbackCommentsActivity.Designer.cs
generated
Normal file
1292
backend/src/Socialize.Api/Migrations/20260430171959_AddFeedbackCommentsActivity.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Socialize.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddFeedbackCommentsActivity : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "FeedbackActivityEntries",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
FeedbackReportId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ActorUserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ActorDisplayName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
ActorEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
ActivityType = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
FromValue = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
|
||||
ToValue = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
|
||||
Note = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_FeedbackActivityEntries", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_FeedbackActivityEntries_FeedbackReports_FeedbackReportId",
|
||||
column: x => x.FeedbackReportId,
|
||||
principalTable: "FeedbackReports",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "FeedbackComments",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
FeedbackReportId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
AuthorUserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
AuthorDisplayName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
AuthorEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
AuthorRole = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
Body = table.Column<string>(type: "character varying(8000)", maxLength: 8000, nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_FeedbackComments", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_FeedbackComments_FeedbackReports_FeedbackReportId",
|
||||
column: x => x.FeedbackReportId,
|
||||
principalTable: "FeedbackReports",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackActivityEntries_ActorUserId",
|
||||
table: "FeedbackActivityEntries",
|
||||
column: "ActorUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackActivityEntries_CreatedAt",
|
||||
table: "FeedbackActivityEntries",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackActivityEntries_FeedbackReportId",
|
||||
table: "FeedbackActivityEntries",
|
||||
column: "FeedbackReportId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackComments_AuthorUserId",
|
||||
table: "FeedbackComments",
|
||||
column: "AuthorUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackComments_CreatedAt",
|
||||
table: "FeedbackComments",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackComments_FeedbackReportId",
|
||||
table: "FeedbackComments",
|
||||
column: "FeedbackReportId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "FeedbackActivityEntries");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "FeedbackComments");
|
||||
}
|
||||
}
|
||||
}
|
||||
1289
backend/src/Socialize.Api/Migrations/AppDbContextModelSnapshot.cs
Normal file
1289
backend/src/Socialize.Api/Migrations/AppDbContextModelSnapshot.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
namespace Socialize.Modules.Approvals.Data;
|
||||
namespace Socialize.Api.Modules.Approvals.Data;
|
||||
|
||||
public class ApprovalDecision
|
||||
{
|
||||
@@ -0,0 +1,42 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Socialize.Api.Modules.Approvals.Data;
|
||||
|
||||
public static class ApprovalModelConfiguration
|
||||
{
|
||||
public static ModelBuilder ConfigureApprovalsModule(this ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<ApprovalRequest>(approvalRequest =>
|
||||
{
|
||||
approvalRequest.ToTable("ApprovalRequests");
|
||||
approvalRequest.HasKey(x => x.Id);
|
||||
approvalRequest.Property(x => x.Stage).HasMaxLength(64).IsRequired();
|
||||
approvalRequest.Property(x => x.ReviewerName).HasMaxLength(256).IsRequired();
|
||||
approvalRequest.Property(x => x.ReviewerEmail).HasMaxLength(256).IsRequired();
|
||||
approvalRequest.Property(x => x.State).HasMaxLength(64).IsRequired();
|
||||
approvalRequest.Property(x => x.AccessToken).HasMaxLength(64).IsRequired();
|
||||
approvalRequest.Property(x => x.SentAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
approvalRequest.HasIndex(x => x.WorkspaceId);
|
||||
approvalRequest.HasIndex(x => x.ContentItemId);
|
||||
approvalRequest.HasIndex(x => x.ReviewerEmail);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ApprovalDecision>(approvalDecision =>
|
||||
{
|
||||
approvalDecision.ToTable("ApprovalDecisions");
|
||||
approvalDecision.HasKey(x => x.Id);
|
||||
approvalDecision.Property(x => x.Decision).HasMaxLength(64).IsRequired();
|
||||
approvalDecision.Property(x => x.Comment).HasMaxLength(2048);
|
||||
approvalDecision.Property(x => x.DecidedByName).HasMaxLength(256).IsRequired();
|
||||
approvalDecision.Property(x => x.DecidedByEmail).HasMaxLength(256).IsRequired();
|
||||
approvalDecision.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
approvalDecision.HasIndex(x => x.ApprovalRequestId);
|
||||
});
|
||||
|
||||
return modelBuilder;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Socialize.Modules.Approvals.Data;
|
||||
namespace Socialize.Api.Modules.Approvals.Data;
|
||||
|
||||
public class ApprovalRequest
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using Socialize.Modules.Approvals.Data;
|
||||
using Socialize.Api.Modules.Approvals.Data;
|
||||
|
||||
namespace Socialize.Modules.Approvals;
|
||||
namespace Socialize.Api.Modules.Approvals;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
@@ -1,8 +1,12 @@
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Security.Cryptography;
|
||||
using Socialize.Infrastructure.Security;
|
||||
using Socialize.Modules.Notifications.Contracts;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Approvals.Data;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
|
||||
namespace Socialize.Modules.Approvals.Handlers;
|
||||
namespace Socialize.Api.Modules.Approvals.Handlers;
|
||||
|
||||
public record CreateApprovalRequestRequest(
|
||||
Guid WorkspaceId,
|
||||
@@ -39,7 +43,8 @@ public class CreateApprovalRequestHandler(
|
||||
|
||||
public override async Task HandleAsync(CreateApprovalRequestRequest request, CancellationToken ct)
|
||||
{
|
||||
ContentItem? contentItem = await dbContext.ContentItems
|
||||
var contentItem = await dbContext
|
||||
.ContentItems
|
||||
.SingleOrDefaultAsync(
|
||||
candidate => candidate.Id == request.ContentItemId && candidate.WorkspaceId == request.WorkspaceId,
|
||||
ct);
|
||||
@@ -57,7 +62,7 @@ public class CreateApprovalRequestHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
ApprovalRequest approval = new()
|
||||
var approval = new ApprovalRequest()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
WorkspaceId = request.WorkspaceId,
|
||||
@@ -1,6 +1,11 @@
|
||||
using Socialize.Infrastructure.Security;
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Modules.Approvals.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
|
||||
namespace Socialize.Modules.Approvals.Handlers;
|
||||
namespace Socialize.Api.Modules.Approvals.Handlers;
|
||||
|
||||
public record GetApprovalsRequest(Guid ContentItemId);
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
using Socialize.Infrastructure.Security;
|
||||
using Socialize.Modules.Notifications.Contracts;
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Approvals.Data;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
|
||||
namespace Socialize.Modules.Approvals.Handlers;
|
||||
namespace Socialize.Api.Modules.Approvals.Handlers;
|
||||
|
||||
public record SubmitApprovalDecisionRequest(
|
||||
string Decision,
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Socialize.Modules.Assets.Data;
|
||||
namespace Socialize.Api.Modules.Assets.Data;
|
||||
|
||||
public class Asset
|
||||
{
|
||||
@@ -0,0 +1,42 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Socialize.Api.Modules.Assets.Data;
|
||||
|
||||
public static class AssetModelConfiguration
|
||||
{
|
||||
public static ModelBuilder ConfigureAssetsModule(this ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Asset>(asset =>
|
||||
{
|
||||
asset.ToTable("Assets");
|
||||
asset.HasKey(x => x.Id);
|
||||
asset.Property(x => x.AssetType).HasMaxLength(64).IsRequired();
|
||||
asset.Property(x => x.SourceType).HasMaxLength(64).IsRequired();
|
||||
asset.Property(x => x.DisplayName).HasMaxLength(256).IsRequired();
|
||||
asset.Property(x => x.GoogleDriveFileId).HasMaxLength(256);
|
||||
asset.Property(x => x.GoogleDriveLink).HasMaxLength(2048);
|
||||
asset.Property(x => x.PreviewUrl).HasMaxLength(2048);
|
||||
asset.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
asset.HasIndex(x => x.WorkspaceId);
|
||||
asset.HasIndex(x => x.ContentItemId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<AssetRevision>(revision =>
|
||||
{
|
||||
revision.ToTable("AssetRevisions");
|
||||
revision.HasKey(x => x.Id);
|
||||
revision.Property(x => x.SourceReference).HasMaxLength(2048).IsRequired();
|
||||
revision.Property(x => x.PreviewUrl).HasMaxLength(2048);
|
||||
revision.Property(x => x.Notes).HasMaxLength(1024);
|
||||
revision.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
revision.HasIndex(x => x.AssetId);
|
||||
revision.HasIndex(x => new { x.AssetId, x.RevisionNumber }).IsUnique();
|
||||
});
|
||||
|
||||
return modelBuilder;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Socialize.Modules.Assets.Data;
|
||||
namespace Socialize.Api.Modules.Assets.Data;
|
||||
|
||||
public class AssetRevision
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using Socialize.Modules.Assets.Data;
|
||||
using Socialize.Api.Modules.Assets.Data;
|
||||
|
||||
namespace Socialize.Modules.Assets;
|
||||
namespace Socialize.Api.Modules.Assets;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
@@ -1,7 +1,12 @@
|
||||
using Socialize.Infrastructure.Security;
|
||||
using Socialize.Modules.Notifications.Contracts;
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Assets.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
|
||||
namespace Socialize.Modules.Assets.Handlers;
|
||||
namespace Socialize.Api.Modules.Assets.Handlers;
|
||||
|
||||
public record CreateAssetRevisionRequest(
|
||||
string SourceReference,
|
||||
@@ -1,7 +1,12 @@
|
||||
using Socialize.Infrastructure.Security;
|
||||
using Socialize.Modules.Notifications.Contracts;
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Assets.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
|
||||
namespace Socialize.Modules.Assets.Handlers;
|
||||
namespace Socialize.Api.Modules.Assets.Handlers;
|
||||
|
||||
public record CreateGoogleDriveAssetRequest(
|
||||
Guid WorkspaceId,
|
||||
@@ -1,5 +1,9 @@
|
||||
using Socialize.Infrastructure.Security;
|
||||
namespace Socialize.Modules.Assets.Handlers;
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
|
||||
namespace Socialize.Api.Modules.Assets.Handlers;
|
||||
|
||||
public record GetAssetsRequest(Guid ContentItemId);
|
||||
|
||||
@@ -40,7 +44,7 @@ public class GetAssetsHandler(
|
||||
|
||||
public override async Task HandleAsync(GetAssetsRequest request, CancellationToken ct)
|
||||
{
|
||||
ContentItem? item = await dbContext.ContentItems
|
||||
var item = await dbContext.ContentItems
|
||||
.SingleOrDefaultAsync(candidate => candidate.Id == request.ContentItemId, ct);
|
||||
if (item is null)
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Socialize.Modules.Clients.Data;
|
||||
namespace Socialize.Api.Modules.Clients.Data;
|
||||
|
||||
public class Client
|
||||
{
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Socialize.Api.Modules.Clients.Data;
|
||||
|
||||
public static class ClientModelConfiguration
|
||||
{
|
||||
public static ModelBuilder ConfigureClientsModule(this ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Client>(client =>
|
||||
{
|
||||
client.ToTable("Clients");
|
||||
client.HasKey(x => x.Id);
|
||||
client.Property(x => x.Name).HasMaxLength(256).IsRequired();
|
||||
client.Property(x => x.Status).HasMaxLength(64).IsRequired();
|
||||
client.Property(x => x.PortraitUrl).HasMaxLength(2048);
|
||||
client.Property(x => x.PrimaryContactName).HasMaxLength(256);
|
||||
client.Property(x => x.PrimaryContactEmail).HasMaxLength(256);
|
||||
client.Property(x => x.PrimaryContactPortraitUrl).HasMaxLength(2048);
|
||||
client.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
client.HasIndex(x => new { x.WorkspaceId, x.Name }).IsUnique();
|
||||
client.HasIndex(x => x.WorkspaceId);
|
||||
});
|
||||
|
||||
return modelBuilder;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using Socialize.Modules.Clients.Data;
|
||||
using Socialize.Api.Modules.Clients.Data;
|
||||
|
||||
namespace Socialize.Modules.Clients;
|
||||
namespace Socialize.Api.Modules.Clients;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
@@ -1,8 +1,11 @@
|
||||
using Socialize.Infrastructure.BlobStorage.Contracts;
|
||||
using Socialize.Infrastructure.Security;
|
||||
using Socialize.Modules.Clients.Data;
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Clients.Data;
|
||||
|
||||
namespace Socialize.Modules.Clients.Handlers;
|
||||
namespace Socialize.Api.Modules.Clients.Handlers;
|
||||
|
||||
public record ChangeClientPortraitRequest(
|
||||
IFormFile File);
|
||||
@@ -1,5 +1,10 @@
|
||||
using Socialize.Infrastructure.Security;
|
||||
namespace Socialize.Modules.Clients.Handlers;
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Modules.Clients.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
|
||||
namespace Socialize.Api.Modules.Clients.Handlers;
|
||||
|
||||
public record CreateClientRequest(
|
||||
Guid WorkspaceId,
|
||||
@@ -1,7 +1,10 @@
|
||||
using Socialize.Infrastructure.Security;
|
||||
using Socialize.Modules.Clients.Data;
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Clients.Data;
|
||||
|
||||
namespace Socialize.Modules.Clients.Handlers;
|
||||
namespace Socialize.Api.Modules.Clients.Handlers;
|
||||
|
||||
public record GetClientsRequest(Guid? WorkspaceId);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using Socialize.Infrastructure.Security;
|
||||
using Socialize.Modules.Clients.Data;
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Clients.Data;
|
||||
|
||||
namespace Socialize.Modules.Clients.Handlers;
|
||||
namespace Socialize.Api.Modules.Clients.Handlers;
|
||||
|
||||
public record UpdateClientRequest(
|
||||
string Name,
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Socialize.Modules.Comments.Data;
|
||||
namespace Socialize.Api.Modules.Comments.Data;
|
||||
|
||||
public class Comment
|
||||
{
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Socialize.Api.Modules.Comments.Data;
|
||||
|
||||
public static class CommentModelConfiguration
|
||||
{
|
||||
public static ModelBuilder ConfigureCommentsModule(this ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Comment>(comment =>
|
||||
{
|
||||
comment.ToTable("Comments");
|
||||
comment.HasKey(x => x.Id);
|
||||
comment.Property(x => x.AuthorDisplayName).HasMaxLength(256).IsRequired();
|
||||
comment.Property(x => x.AuthorEmail).HasMaxLength(256).IsRequired();
|
||||
comment.Property(x => x.Body).HasMaxLength(4000).IsRequired();
|
||||
comment.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
comment.HasIndex(x => x.WorkspaceId);
|
||||
comment.HasIndex(x => x.ContentItemId);
|
||||
comment.HasIndex(x => x.ParentCommentId);
|
||||
});
|
||||
|
||||
return modelBuilder;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using Socialize.Modules.Comments.Data;
|
||||
using Socialize.Api.Modules.Comments.Data;
|
||||
|
||||
namespace Socialize.Modules.Comments;
|
||||
namespace Socialize.Api.Modules.Comments;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
@@ -1,7 +1,12 @@
|
||||
using Socialize.Infrastructure.Security;
|
||||
using Socialize.Modules.Notifications.Contracts;
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Comments.Data;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
|
||||
namespace Socialize.Modules.Comments.Handlers;
|
||||
namespace Socialize.Api.Modules.Comments.Handlers;
|
||||
|
||||
public record CreateCommentRequest(
|
||||
Guid WorkspaceId,
|
||||
@@ -1,6 +1,11 @@
|
||||
using Socialize.Infrastructure.Security;
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Modules.Comments.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
|
||||
namespace Socialize.Modules.Comments.Handlers;
|
||||
namespace Socialize.Api.Modules.Comments.Handlers;
|
||||
|
||||
public record GetCommentsRequest(Guid ContentItemId);
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
using Socialize.Infrastructure.Security;
|
||||
using Socialize.Modules.Notifications.Contracts;
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Comments.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
|
||||
namespace Socialize.Modules.Comments.Handlers;
|
||||
namespace Socialize.Api.Modules.Comments.Handlers;
|
||||
|
||||
public class ResolveCommentHandler(
|
||||
AppDbContext dbContext,
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Socialize.Modules.ContentItems.Data;
|
||||
namespace Socialize.Api.Modules.ContentItems.Data;
|
||||
|
||||
public class ContentItem
|
||||
{
|
||||
@@ -0,0 +1,46 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Socialize.Api.Modules.ContentItems.Data;
|
||||
|
||||
public static class ContentItemModelConfiguration
|
||||
{
|
||||
public static ModelBuilder ConfigureContentItemsModule(this ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<ContentItem>(contentItem =>
|
||||
{
|
||||
contentItem.ToTable("ContentItems");
|
||||
contentItem.HasKey(x => x.Id);
|
||||
contentItem.Property(x => x.Title).HasMaxLength(256).IsRequired();
|
||||
contentItem.Property(x => x.PublicationMessage).HasMaxLength(4000).IsRequired();
|
||||
contentItem.Property(x => x.PublicationTargets).HasMaxLength(512).IsRequired();
|
||||
contentItem.Property(x => x.Hashtags).HasMaxLength(1024);
|
||||
contentItem.Property(x => x.Status).HasMaxLength(64).IsRequired();
|
||||
contentItem.Property(x => x.CurrentRevisionLabel).HasMaxLength(32).IsRequired();
|
||||
contentItem.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
contentItem.HasIndex(x => x.WorkspaceId);
|
||||
contentItem.HasIndex(x => x.ClientId);
|
||||
contentItem.HasIndex(x => x.ProjectId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ContentItemRevision>(revision =>
|
||||
{
|
||||
revision.ToTable("ContentItemRevisions");
|
||||
revision.HasKey(x => x.Id);
|
||||
revision.Property(x => x.RevisionLabel).HasMaxLength(32).IsRequired();
|
||||
revision.Property(x => x.Title).HasMaxLength(256).IsRequired();
|
||||
revision.Property(x => x.PublicationMessage).HasMaxLength(4000).IsRequired();
|
||||
revision.Property(x => x.PublicationTargets).HasMaxLength(512).IsRequired();
|
||||
revision.Property(x => x.Hashtags).HasMaxLength(1024);
|
||||
revision.Property(x => x.ChangeSummary).HasMaxLength(1024);
|
||||
revision.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
revision.HasIndex(x => x.ContentItemId);
|
||||
revision.HasIndex(x => new { x.ContentItemId, x.RevisionNumber }).IsUnique();
|
||||
});
|
||||
|
||||
return modelBuilder;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Socialize.Modules.ContentItems.Data;
|
||||
namespace Socialize.Api.Modules.ContentItems.Data;
|
||||
|
||||
public class ContentItemRevision
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using Socialize.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
|
||||
namespace Socialize.Modules.ContentItems;
|
||||
namespace Socialize.Api.Modules.ContentItems;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
@@ -1,7 +1,12 @@
|
||||
using Socialize.Infrastructure.Security;
|
||||
using Socialize.Modules.Notifications.Contracts;
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Workspaces.Data;
|
||||
|
||||
namespace Socialize.Modules.ContentItems.Handlers;
|
||||
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||
|
||||
public record CreateContentItemRequest(
|
||||
Guid WorkspaceId,
|
||||
@@ -1,7 +1,11 @@
|
||||
using Socialize.Infrastructure.Security;
|
||||
using Socialize.Modules.Notifications.Contracts;
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
|
||||
namespace Socialize.Modules.ContentItems.Handlers;
|
||||
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||
|
||||
public record CreateContentItemRevisionRequest(
|
||||
string Title,
|
||||
@@ -1,7 +1,10 @@
|
||||
using Socialize.Infrastructure.Security;
|
||||
using Socialize.Modules.ContentItems.Data;
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
|
||||
namespace Socialize.Modules.ContentItems.Handlers;
|
||||
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||
|
||||
public record ContentItemDetailDto(
|
||||
Guid Id,
|
||||
@@ -1,5 +1,10 @@
|
||||
using Socialize.Infrastructure.Security;
|
||||
namespace Socialize.Modules.ContentItems.Handlers;
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
|
||||
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||
|
||||
public record ContentItemRevisionDto(
|
||||
Guid Id,
|
||||
@@ -1,7 +1,10 @@
|
||||
using Socialize.Infrastructure.Security;
|
||||
using Socialize.Modules.ContentItems.Data;
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
|
||||
namespace Socialize.Modules.ContentItems.Handlers;
|
||||
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||
|
||||
public record GetContentItemsRequest(Guid? WorkspaceId, Guid? ClientId, Guid? ProjectId);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
using Socialize.Infrastructure.Security;
|
||||
using Socialize.Modules.ContentItems.Data;
|
||||
using Socialize.Modules.Notifications.Contracts;
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
|
||||
namespace Socialize.Modules.ContentItems.Handlers;
|
||||
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||
|
||||
public record UpdateContentItemStatusRequest(string Status);
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
using Socialize.Api.Modules.Feedback.Data;
|
||||
|
||||
namespace Socialize.Api.Modules.Feedback.Contracts;
|
||||
|
||||
public record FeedbackContextDto(
|
||||
Guid? WorkspaceId,
|
||||
string? WorkspaceName,
|
||||
Guid? ClientId,
|
||||
string? ClientName,
|
||||
Guid? ProjectId,
|
||||
string? ProjectName,
|
||||
Guid? ContentItemId,
|
||||
string? ContentItemTitle);
|
||||
|
||||
public record FeedbackMetadataDto(
|
||||
string SubmittedPath,
|
||||
string? BrowserUserAgent,
|
||||
int? ViewportWidth,
|
||||
int? ViewportHeight,
|
||||
string? AppVersion);
|
||||
|
||||
public record FeedbackScreenshotDto(
|
||||
Guid Id,
|
||||
string FileName,
|
||||
string ContentType,
|
||||
long SizeBytes,
|
||||
string DownloadPath,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
public record FeedbackReportDto(
|
||||
Guid Id,
|
||||
string Type,
|
||||
string Status,
|
||||
string Description,
|
||||
Guid ReporterUserId,
|
||||
string ReporterDisplayName,
|
||||
string ReporterEmail,
|
||||
FeedbackMetadataDto Metadata,
|
||||
FeedbackContextDto Context,
|
||||
FeedbackScreenshotDto? Screenshot,
|
||||
IReadOnlyCollection<string> Tags,
|
||||
IReadOnlyCollection<FeedbackTimelineItemDto> Timeline,
|
||||
DateTimeOffset CreatedAt,
|
||||
DateTimeOffset LastActivityAt,
|
||||
DateTimeOffset? CancelledAt,
|
||||
string? CancellationReason);
|
||||
|
||||
public record FeedbackTimelineItemDto(
|
||||
Guid Id,
|
||||
string Kind,
|
||||
Guid ActorUserId,
|
||||
string ActorDisplayName,
|
||||
string ActorEmail,
|
||||
string? ActorRole,
|
||||
string? Body,
|
||||
string? ActivityType,
|
||||
string? FromValue,
|
||||
string? ToValue,
|
||||
string? Note,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
public static class FeedbackDtoMapper
|
||||
{
|
||||
public static FeedbackReportDto ToDto(this FeedbackReport report)
|
||||
{
|
||||
return new FeedbackReportDto(
|
||||
report.Id,
|
||||
ToDisplayString(report.Type),
|
||||
ToDisplayString(report.Status),
|
||||
report.Description,
|
||||
report.ReporterUserId,
|
||||
report.ReporterDisplayName,
|
||||
report.ReporterEmail,
|
||||
new FeedbackMetadataDto(
|
||||
report.SubmittedPath,
|
||||
report.BrowserUserAgent,
|
||||
report.ViewportWidth,
|
||||
report.ViewportHeight,
|
||||
report.AppVersion),
|
||||
new FeedbackContextDto(
|
||||
report.WorkspaceId,
|
||||
report.WorkspaceName,
|
||||
report.ClientId,
|
||||
report.ClientName,
|
||||
report.ProjectId,
|
||||
report.ProjectName,
|
||||
report.ContentItemId,
|
||||
report.ContentItemTitle),
|
||||
report.Screenshot is null
|
||||
? null
|
||||
: new FeedbackScreenshotDto(
|
||||
report.Screenshot.Id,
|
||||
report.Screenshot.FileName,
|
||||
report.Screenshot.ContentType,
|
||||
report.Screenshot.SizeBytes,
|
||||
$"/api/feedback/{report.Id}/screenshot",
|
||||
report.Screenshot.CreatedAt),
|
||||
report.Tags.OrderBy(tag => tag.Name).Select(tag => tag.Name).ToArray(),
|
||||
report.Comments
|
||||
.Select(comment => comment.ToTimelineDto())
|
||||
.Concat(report.ActivityEntries.Select(activity => activity.ToTimelineDto()))
|
||||
.OrderBy(item => item.CreatedAt)
|
||||
.ThenBy(item => item.Kind)
|
||||
.ToArray(),
|
||||
report.CreatedAt,
|
||||
report.LastActivityAt,
|
||||
report.CancelledAt,
|
||||
report.CancellationReason);
|
||||
}
|
||||
|
||||
private static string ToDisplayString(FeedbackType type)
|
||||
{
|
||||
return type.ToString();
|
||||
}
|
||||
|
||||
private static string ToDisplayString(FeedbackStatus status)
|
||||
{
|
||||
return status == FeedbackStatus.WontDo ? "Won't Do" : status.ToString();
|
||||
}
|
||||
|
||||
public static FeedbackTimelineItemDto ToTimelineDto(this FeedbackComment comment)
|
||||
{
|
||||
return new FeedbackTimelineItemDto(
|
||||
comment.Id,
|
||||
"Comment",
|
||||
comment.AuthorUserId,
|
||||
comment.AuthorDisplayName,
|
||||
comment.AuthorEmail,
|
||||
comment.AuthorRole,
|
||||
comment.Body,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
comment.CreatedAt);
|
||||
}
|
||||
|
||||
public static FeedbackTimelineItemDto ToTimelineDto(this FeedbackActivityEntry activity)
|
||||
{
|
||||
return new FeedbackTimelineItemDto(
|
||||
activity.Id,
|
||||
"Activity",
|
||||
activity.ActorUserId,
|
||||
activity.ActorDisplayName,
|
||||
activity.ActorEmail,
|
||||
null,
|
||||
null,
|
||||
activity.ActivityType,
|
||||
activity.FromValue,
|
||||
activity.ToValue,
|
||||
activity.Note,
|
||||
activity.CreatedAt);
|
||||
}
|
||||
|
||||
public static string ToFeedbackDisplayString(this FeedbackType type)
|
||||
{
|
||||
return ToDisplayString(type);
|
||||
}
|
||||
|
||||
public static string ToFeedbackDisplayString(this FeedbackStatus status)
|
||||
{
|
||||
return ToDisplayString(status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Socialize.Api.Modules.Feedback.Data;
|
||||
|
||||
public class FeedbackActivityEntry
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid FeedbackReportId { get; set; }
|
||||
public Guid ActorUserId { get; set; }
|
||||
public string ActorDisplayName { get; set; } = string.Empty;
|
||||
public string ActorEmail { get; set; } = string.Empty;
|
||||
public string ActivityType { get; set; } = string.Empty;
|
||||
public string? FromValue { get; set; }
|
||||
public string? ToValue { get; set; }
|
||||
public string? Note { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
|
||||
public FeedbackReport? FeedbackReport { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Socialize.Api.Modules.Feedback.Data;
|
||||
|
||||
public class FeedbackComment
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid FeedbackReportId { get; set; }
|
||||
public Guid AuthorUserId { get; set; }
|
||||
public string AuthorDisplayName { get; set; } = string.Empty;
|
||||
public string AuthorEmail { get; set; } = string.Empty;
|
||||
public string AuthorRole { get; set; } = string.Empty;
|
||||
public string Body { get; set; } = string.Empty;
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
|
||||
public FeedbackReport? FeedbackReport { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Socialize.Api.Modules.Feedback.Data;
|
||||
|
||||
public static class FeedbackModelConfiguration
|
||||
{
|
||||
public static ModelBuilder ConfigureFeedbackModule(this ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<FeedbackReport>(feedback =>
|
||||
{
|
||||
feedback.ToTable("FeedbackReports");
|
||||
feedback.HasKey(x => x.Id);
|
||||
feedback.Property(x => x.Type).HasConversion<string>().HasMaxLength(32).IsRequired();
|
||||
feedback.Property(x => x.Status).HasConversion<string>().HasMaxLength(32).IsRequired();
|
||||
feedback.Property(x => x.Description).HasMaxLength(8000).IsRequired();
|
||||
feedback.Property(x => x.ReporterDisplayName).HasMaxLength(256).IsRequired();
|
||||
feedback.Property(x => x.ReporterEmail).HasMaxLength(256).IsRequired();
|
||||
feedback.Property(x => x.SubmittedPath).HasMaxLength(2048).IsRequired();
|
||||
feedback.Property(x => x.BrowserUserAgent).HasMaxLength(1024);
|
||||
feedback.Property(x => x.AppVersion).HasMaxLength(128);
|
||||
feedback.Property(x => x.WorkspaceName).HasMaxLength(256);
|
||||
feedback.Property(x => x.ClientName).HasMaxLength(256);
|
||||
feedback.Property(x => x.ProjectName).HasMaxLength(256);
|
||||
feedback.Property(x => x.ContentItemTitle).HasMaxLength(256);
|
||||
feedback.Property(x => x.CancellationReason).HasMaxLength(2000);
|
||||
feedback.Property(x => x.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
feedback.HasIndex(x => x.ReporterUserId);
|
||||
feedback.HasIndex(x => x.Status);
|
||||
feedback.HasIndex(x => x.Type);
|
||||
feedback.HasIndex(x => x.WorkspaceId);
|
||||
feedback.HasIndex(x => x.LastActivityAt);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<FeedbackTag>(tag =>
|
||||
{
|
||||
tag.ToTable("FeedbackTags");
|
||||
tag.HasKey(x => x.Id);
|
||||
tag.Property(x => x.Name).HasMaxLength(64).IsRequired();
|
||||
tag.Property(x => x.NormalizedName).HasMaxLength(64).IsRequired();
|
||||
tag.HasIndex(x => x.NormalizedName);
|
||||
tag.HasIndex(x => new { x.FeedbackReportId, x.NormalizedName }).IsUnique();
|
||||
tag.HasOne(x => x.FeedbackReport)
|
||||
.WithMany(x => x.Tags)
|
||||
.HasForeignKey(x => x.FeedbackReportId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<FeedbackScreenshot>(screenshot =>
|
||||
{
|
||||
screenshot.ToTable("FeedbackScreenshots");
|
||||
screenshot.HasKey(x => x.Id);
|
||||
screenshot.Property(x => x.FileName).HasMaxLength(256).IsRequired();
|
||||
screenshot.Property(x => x.ContentType).HasMaxLength(128).IsRequired();
|
||||
screenshot.Property(x => x.BlobContainerName).HasMaxLength(128).IsRequired();
|
||||
screenshot.Property(x => x.BlobName).HasMaxLength(512).IsRequired();
|
||||
screenshot.Property(x => x.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
screenshot.HasIndex(x => x.FeedbackReportId).IsUnique();
|
||||
screenshot.HasOne(x => x.FeedbackReport)
|
||||
.WithOne(x => x.Screenshot)
|
||||
.HasForeignKey<FeedbackScreenshot>(x => x.FeedbackReportId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<FeedbackComment>(comment =>
|
||||
{
|
||||
comment.ToTable("FeedbackComments");
|
||||
comment.HasKey(x => x.Id);
|
||||
comment.Property(x => x.AuthorDisplayName).HasMaxLength(256).IsRequired();
|
||||
comment.Property(x => x.AuthorEmail).HasMaxLength(256).IsRequired();
|
||||
comment.Property(x => x.AuthorRole).HasMaxLength(32).IsRequired();
|
||||
comment.Property(x => x.Body).HasMaxLength(8000).IsRequired();
|
||||
comment.Property(x => x.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
comment.HasIndex(x => x.FeedbackReportId);
|
||||
comment.HasIndex(x => x.AuthorUserId);
|
||||
comment.HasIndex(x => x.CreatedAt);
|
||||
comment.HasOne(x => x.FeedbackReport)
|
||||
.WithMany(x => x.Comments)
|
||||
.HasForeignKey(x => x.FeedbackReportId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<FeedbackActivityEntry>(activity =>
|
||||
{
|
||||
activity.ToTable("FeedbackActivityEntries");
|
||||
activity.HasKey(x => x.Id);
|
||||
activity.Property(x => x.ActorDisplayName).HasMaxLength(256).IsRequired();
|
||||
activity.Property(x => x.ActorEmail).HasMaxLength(256).IsRequired();
|
||||
activity.Property(x => x.ActivityType).HasMaxLength(64).IsRequired();
|
||||
activity.Property(x => x.FromValue).HasMaxLength(512);
|
||||
activity.Property(x => x.ToValue).HasMaxLength(512);
|
||||
activity.Property(x => x.Note).HasMaxLength(2000);
|
||||
activity.Property(x => x.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
activity.HasIndex(x => x.FeedbackReportId);
|
||||
activity.HasIndex(x => x.ActorUserId);
|
||||
activity.HasIndex(x => x.CreatedAt);
|
||||
activity.HasOne(x => x.FeedbackReport)
|
||||
.WithMany(x => x.ActivityEntries)
|
||||
.HasForeignKey(x => x.FeedbackReportId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
return modelBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace Socialize.Api.Modules.Feedback.Data;
|
||||
|
||||
public class FeedbackReport
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public FeedbackType Type { get; set; }
|
||||
public FeedbackStatus Status { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public Guid ReporterUserId { get; set; }
|
||||
public string ReporterDisplayName { get; set; } = string.Empty;
|
||||
public string ReporterEmail { get; set; } = string.Empty;
|
||||
public string SubmittedPath { get; set; } = string.Empty;
|
||||
public string? BrowserUserAgent { get; set; }
|
||||
public int? ViewportWidth { get; set; }
|
||||
public int? ViewportHeight { get; set; }
|
||||
public string? AppVersion { get; set; }
|
||||
public Guid? WorkspaceId { get; set; }
|
||||
public string? WorkspaceName { get; set; }
|
||||
public Guid? ClientId { get; set; }
|
||||
public string? ClientName { get; set; }
|
||||
public Guid? ProjectId { get; set; }
|
||||
public string? ProjectName { get; set; }
|
||||
public Guid? ContentItemId { get; set; }
|
||||
public string? ContentItemTitle { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
public DateTimeOffset LastActivityAt { get; set; }
|
||||
public DateTimeOffset? CancelledAt { get; set; }
|
||||
public Guid? CancelledByUserId { get; set; }
|
||||
public string? CancellationReason { get; set; }
|
||||
public ICollection<FeedbackTag> Tags { get; } = new List<FeedbackTag>();
|
||||
public ICollection<FeedbackComment> Comments { get; } = new List<FeedbackComment>();
|
||||
public ICollection<FeedbackActivityEntry> ActivityEntries { get; } = new List<FeedbackActivityEntry>();
|
||||
public FeedbackScreenshot? Screenshot { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Socialize.Api.Modules.Feedback.Data;
|
||||
|
||||
public class FeedbackScreenshot
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid FeedbackReportId { get; set; }
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
public string ContentType { get; set; } = string.Empty;
|
||||
public long SizeBytes { get; set; }
|
||||
public string BlobContainerName { get; set; } = string.Empty;
|
||||
public string BlobName { get; set; } = string.Empty;
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
public FeedbackReport? FeedbackReport { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Socialize.Api.Modules.Feedback.Data;
|
||||
|
||||
public enum FeedbackStatus
|
||||
{
|
||||
New,
|
||||
Planned,
|
||||
Resolved,
|
||||
WontDo,
|
||||
Cancelled,
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user