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:
@@ -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;
|
||||
3
backend/prisma/migrations/migration_lock.toml
Normal file
3
backend/prisma/migrations/migration_lock.toml
Normal 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"
|
||||
9
backend/prisma/prisma.module.ts
Normal file
9
backend/prisma/prisma.module.ts
Normal 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 {}
|
||||
22
backend/prisma/prisma.service.ts
Normal file
22
backend/prisma/prisma.service.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
172
backend/prisma/schema.prisma
Normal file
172
backend/prisma/schema.prisma
Normal 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
122
backend/prisma/seed.ts
Normal 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();
|
||||
});
|
||||
Reference in New Issue
Block a user