Initial commit: Full project structure

- Backend: NestJS with Docker
- Frontend: Next.js with Docker
- Nginx configuration for reverse proxy
- PostgreSQL setup
- Docker compose for orchestration
- Development environment configuration
This commit is contained in:
2026-04-23 15:33:11 +03:30
commit 26bd35ae3c
94 changed files with 5627 additions and 0 deletions

View File

@@ -0,0 +1,191 @@
-- CreateEnum
CREATE TYPE "LinkStatus" AS ENUM ('PENDING', 'ACTIVE', 'REJECTED', 'BLOCKED');
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"passwordHash" TEXT,
"googleId" TEXT,
"facebookId" TEXT,
"name" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "organization_types" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
CONSTRAINT "organization_types_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "organizations" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"email" TEXT NOT NULL,
"phone" TEXT,
"address" TEXT,
"typeId" TEXT NOT NULL,
"ownerId" TEXT NOT NULL,
"planId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "organizations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "plans" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"maxUsers" INTEGER NOT NULL,
"price" DOUBLE PRECISION NOT NULL,
"features" JSONB NOT NULL,
CONSTRAINT "plans_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "memberships" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
"isOwner" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "memberships_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "permissions" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"featureId" TEXT,
CONSTRAINT "permissions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "membership_permissions" (
"membershipId" TEXT NOT NULL,
"permissionId" TEXT NOT NULL,
CONSTRAINT "membership_permissions_pkey" PRIMARY KEY ("membershipId","permissionId")
);
-- CreateTable
CREATE TABLE "features" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"organizationTypeId" TEXT,
CONSTRAINT "features_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "organization_links" (
"id" TEXT NOT NULL,
"organizationAId" TEXT NOT NULL,
"organizationBId" TEXT NOT NULL,
"status" "LinkStatus" NOT NULL DEFAULT 'PENDING',
"sharedDataTypes" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "organization_links_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sessions" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"token" TEXT NOT NULL,
"refreshToken" TEXT,
"expiresAt" TIMESTAMP(3) NOT NULL,
"userAgent" TEXT,
"ipAddress" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE UNIQUE INDEX "users_googleId_key" ON "users"("googleId");
-- CreateIndex
CREATE UNIQUE INDEX "users_facebookId_key" ON "users"("facebookId");
-- CreateIndex
CREATE UNIQUE INDEX "organization_types_name_key" ON "organization_types"("name");
-- CreateIndex
CREATE UNIQUE INDEX "organizations_email_key" ON "organizations"("email");
-- CreateIndex
CREATE UNIQUE INDEX "plans_name_key" ON "plans"("name");
-- CreateIndex
CREATE UNIQUE INDEX "memberships_userId_organizationId_key" ON "memberships"("userId", "organizationId");
-- CreateIndex
CREATE UNIQUE INDEX "permissions_name_key" ON "permissions"("name");
-- CreateIndex
CREATE UNIQUE INDEX "features_name_key" ON "features"("name");
-- CreateIndex
CREATE UNIQUE INDEX "organization_links_organizationAId_organizationBId_key" ON "organization_links"("organizationAId", "organizationBId");
-- CreateIndex
CREATE UNIQUE INDEX "sessions_token_key" ON "sessions"("token");
-- CreateIndex
CREATE UNIQUE INDEX "sessions_refreshToken_key" ON "sessions"("refreshToken");
-- AddForeignKey
ALTER TABLE "organizations" ADD CONSTRAINT "organizations_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "organization_types"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "organizations" ADD CONSTRAINT "organizations_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "organizations" ADD CONSTRAINT "organizations_planId_fkey" FOREIGN KEY ("planId") REFERENCES "plans"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "memberships" ADD CONSTRAINT "memberships_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "memberships" ADD CONSTRAINT "memberships_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "permissions" ADD CONSTRAINT "permissions_featureId_fkey" FOREIGN KEY ("featureId") REFERENCES "features"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "membership_permissions" ADD CONSTRAINT "membership_permissions_membershipId_fkey" FOREIGN KEY ("membershipId") REFERENCES "memberships"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "membership_permissions" ADD CONSTRAINT "membership_permissions_permissionId_fkey" FOREIGN KEY ("permissionId") REFERENCES "permissions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "features" ADD CONSTRAINT "features_organizationTypeId_fkey" FOREIGN KEY ("organizationTypeId") REFERENCES "organization_types"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "organization_links" ADD CONSTRAINT "organization_links_organizationAId_fkey" FOREIGN KEY ("organizationAId") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "organization_links" ADD CONSTRAINT "organization_links_organizationBId_fkey" FOREIGN KEY ("organizationBId") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View File

@@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global() // makes it available everywhere without re-importing
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

View File

@@ -0,0 +1,22 @@
// backend/src/prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
constructor() {
super();
}
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}

View File

@@ -0,0 +1,172 @@
// backend/prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
email String @unique
passwordHash String?
googleId String? @unique
facebookId String? @unique
name String
memberships Membership[]
ownedOrganizations Organization[] @relation("OrganizationOwner")
sessions Session[] // 👈 ADD THIS - opposite relation for Session
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
}
model OrganizationType {
id String @id @default(uuid())
name String @unique // "CLINIC" or "LAB"
organizations Organization[]
features Feature[] // 👈 ADD THIS - opposite relation for Feature
@@map("organization_types")
}
model Organization {
id String @id @default(uuid())
name String
email String @unique
phone String?
address String?
typeId String
type OrganizationType @relation(fields: [typeId], references: [id])
ownerId String
owner User @relation("OrganizationOwner", fields: [ownerId], references: [id])
memberships Membership[]
planId String
plan Plan @relation(fields: [planId], references: [id])
sharedWithMe OrganizationLink[] @relation("OrganizationB")
sharedWithOthers OrganizationLink[] @relation("OrganizationA")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("organizations")
}
model Plan {
id String @id @default(uuid())
name String @unique // "Solo", "Small", "Medium", "Large", "Enterprise"
maxUsers Int // 1, 5, 10, 15, 999999 for unlimited
price Float
features Json // Store feature flags as JSON
organizations Organization[]
@@map("plans")
}
model Membership {
id String @id @default(uuid())
userId String
organizationId String
isOwner Boolean @default(false)
user User @relation(fields: [userId], references: [id])
organization Organization @relation(fields: [organizationId], references: [id])
permissions MembershipPermission[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, organizationId])
@@map("memberships")
}
model Permission {
id String @id @default(uuid())
name String @unique
description String?
memberships MembershipPermission[]
featureId String?
feature Feature? @relation(fields: [featureId], references: [id])
@@map("permissions")
}
model MembershipPermission {
membershipId String
permissionId String
membership Membership @relation(fields: [membershipId], references: [id])
permission Permission @relation(fields: [permissionId], references: [id])
@@id([membershipId, permissionId])
@@map("membership_permissions")
}
model Feature {
id String @id @default(uuid())
name String @unique
description String?
permissions Permission[]
organizationTypeId String?
organizationType OrganizationType? @relation(fields: [organizationTypeId], references: [id])
@@map("features")
}
model OrganizationLink {
id String @id @default(uuid())
organizationAId String
organizationBId String
status LinkStatus @default(PENDING)
sharedDataTypes Json
organizationA Organization @relation("OrganizationA", fields: [organizationAId], references: [id])
organizationB Organization @relation("OrganizationB", fields: [organizationBId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationAId, organizationBId])
@@map("organization_links")
}
model Session {
id String @id @default(uuid())
userId String
token String @unique
refreshToken String? @unique
expiresAt DateTime
userAgent String?
ipAddress String?
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("sessions")
}
enum LinkStatus {
PENDING
ACTIVE
REJECTED
BLOCKED
}

122
backend/prisma/seed.ts Normal file
View File

@@ -0,0 +1,122 @@
// backend/prisma/seed.ts
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt';
import { config } from 'dotenv';
import path from 'path';
// Load environment variables from the correct path
const envPath = path.join(__dirname, '..', '.env');
console.log('Loading .env from:', envPath);
config({ path: envPath });
// Verify DATABASE_URL is loaded
if (!process.env.DATABASE_URL) {
console.error('❌ DATABASE_URL is not set in environment');
console.log('Current directory:', process.cwd());
console.log('.env path:', envPath);
process.exit(1);
}
console.log('✅ DATABASE_URL found:', process.env.DATABASE_URL.substring(0, 30) + '...');
const prisma = new PrismaClient();
async function main() {
console.log('🌱 Starting seeding...');
// Test the connection
await prisma.$connect();
console.log('✅ Database connected successfully');
// Create organization types
const clinicType = await prisma.organizationType.upsert({
where: { name: 'CLINIC' },
update: {},
create: { name: 'CLINIC' },
});
console.log('✅ Created clinic type');
const labType = await prisma.organizationType.upsert({
where: { name: 'LAB' },
update: {},
create: { name: 'LAB' },
});
console.log('✅ Created lab type');
// Create plans
const plans = [
{ name: 'trial', maxUsers: 5, price: 0, features: {} },
{ name: 'Small', maxUsers: 5, price: 79, features: {} },
{ name: 'Medium', maxUsers: 10, price: 129, features: {} },
{ name: 'Large', maxUsers: 15, price: 179, features: {} },
{ name: 'Enterprise', maxUsers: 999999, price: 299, features: {} },
];
for (const plan of plans) {
await prisma.plan.upsert({
where: { name: plan.name },
update: {},
create: plan,
});
}
console.log('✅ Created plans');
// Create features and permissions
const features = [
{
name: 'Patient Management',
permissions: ['VIEW_PATIENTS', 'CREATE_PATIENTS', 'EDIT_PATIENTS', 'DELETE_PATIENTS']
},
{
name: 'Order Management',
permissions: ['VIEW_ORDERS', 'CREATE_ORDERS', 'EDIT_ORDERS', 'DELETE_ORDERS', 'TRACK_ORDERS']
},
{
name: 'Case Management',
permissions: ['VIEW_CASES', 'CREATE_CASES', 'EDIT_CASES', 'DELETE_CASES']
},
{
name: 'Reports',
permissions: ['VIEW_REPORTS', 'EXPORT_REPORTS']
},
{
name: 'Team Management',
permissions: ['INVITE_USERS', 'REMOVE_USERS', 'MANAGE_PERMISSIONS']
},
{
name: 'Billing',
permissions: ['VIEW_INVOICES', 'CREATE_INVOICES', 'MANAGE_PAYMENTS']
}
];
for (const feature of features) {
const createdFeature = await prisma.feature.upsert({
where: { name: feature.name },
update: {},
create: { name: feature.name },
});
for (const permissionName of feature.permissions) {
await prisma.permission.upsert({
where: { name: permissionName },
update: {},
create: {
name: permissionName,
featureId: createdFeature.id,
},
});
}
}
console.log('✅ Created features and permissions');
console.log('🌱 Seeding completed successfully!'); ``
}
main()
.catch((e) => {
console.error('❌ Seeding failed:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});