Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 68 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,74 @@
{
"permissions": {
"allow": [
"Bash(git:*)"
"Bash(git:*)",
"Bash(docker compose -f infra/docker/docker-compose.yml up -d --force-recreate 2>&1)",
"Bash(sudo systemctl restart nginx && curl -I -H \"Host: code-host.online\" http://localhost:80 2>&1)",
"Bash(sleep 3 && docker exec codehost-api env | grep -E 'GITHUB_CLIENT|GOOGLE_CLIENT' 2>&1)",
"Bash(curl -s -o /dev/null -w \"%{http_code} redirect:%{redirect_url}\" -H \"Host: api.code-host.online\" http://localhost:80/auth/github 2>&1 && echo && curl -s -o /dev/null -w \"%{http_code} redirect:%{redirect_url}\" -H \"Host: api.code-host.online\" http://localhost:80/auth/google 2>&1)",
"Bash(curl -s -I -H \"Host: api.code-host.online\" http://localhost:80/api/auth/github 2>&1 | head -5 && echo \"---\" && curl -s -H \"Host: api.code-host.online\" http://localhost:80/auth/github 2>&1 | head -5 && echo \"---\" && curl -s -H \"Host: api.code-host.online\" http://localhost:4000/auth/github 2>&1 | head -5)",
"Bash(curl -s http://localhost:4000/ 2>&1 | head -3 && echo \"---\" && curl -s http://localhost:4000/auth/github 2>&1 && echo \"---\" && curl -s http://localhost:4000/auth/google 2>&1)",
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:4000/auth/oauth/github && echo && curl -s -o /dev/null -w \"%{http_code}\" http://localhost:4000/auth/oauth/google)",
"Bash(docker exec codehost-api env | grep -E 'API_URL|APP_URL' 2>&1)",
"Bash(docker exec codehost-api env | grep DATABASE_URL && echo \"---\" && docker logs codehost-api 2>&1 | grep -i -A5 'PrismaClientInitializationError' | tail -15)",
"Bash(sleep 3 && docker exec codehost-api env | grep DATABASE_URL)",
"Bash(docker exec codehost-db psql -U codehost -d codehost -c \"\\\\dt\" 2>&1 | head -20)",
"Bash(grep -A 30 -B 5 \"newName\\\\|subdomain\\\\|rename\" /home/ubuntu/web/CodeHost/apps/web/src/app/dashboard/project/[id]/page.tsx | head -100)",
"Bash(grep -rn \"subdomain\\\\|rename\" /home/ubuntu/web/CodeHost --include=\"*.md\" --include=\"*.json\" 2>/dev/null | head -20)",
"Bash(/tmp/summary.txt:*)",
"Read(//tmp/**)",
"Bash(npx tsc --noEmit --project apps/api/tsconfig.json 2>&1 | head -30)",
"Bash(npx -w @codehost/api tsc --noEmit 2>&1 | head -30)",
"Bash(find /home/ubuntu/web/CodeHost -name \"config*\" -o -name \"constant*\" | grep -E \"\\\\.\\(ts|js|json\\)$\")",
"Bash(grep -r \"MEMORY_LIMIT\\\\|CPU_LIMIT\\\\|TIMEOUT\" /home/ubuntu/web/CodeHost --include=\"*.ts\" --include=\"*.tsx\" 2>/dev/null | head -20)",
"Bash(grep -r \"Dockerfile\\\\|node:20\\\\|python:3\" /home/ubuntu/web/CodeHost --include=\"*.ts\" --include=\"*.tsx\" --include=\"*.md\" 2>/dev/null | head -20)",
"Bash(docker compose -f infra/docker/docker-compose.yml up -d --build 2>&1)",
"Bash(docker ps --format \"table {{.Names}}\\\\t{{.Status}}\\\\t{{.Ports}}\" | grep codehost)",
"Bash(docker exec codehost-web env | grep -E 'PORT|HOSTNAME' 2>&1)",
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3000 && echo \" \\(web:3000\\)\" ; curl -s -o /dev/null -w \"%{http_code}\" http://localhost:4000 && echo \" \\(api:4000\\)\" ; curl -s -o /dev/null -w \"%{http_code}\" -H \"Host: code-host.online\" http://localhost:9000 && echo \" \\(traefik->web\\)\" ; curl -s -o /dev/null -w \"%{http_code}\" -H \"Host: api.code-host.online\" http://localhost:9000 && echo \" \\(traefik->api\\)\")",
"Read(//etc/nginx/sites-enabled/**)",
"Read(//etc/nginx/conf.d/**)",
"Bash(nginx -T)",
"Bash(curl -s -I -H \"Host: code-host.online\" http://localhost:80 2>&1 | head -10)",
"Bash(curl -s -I -H \"Host: api.code-host.online\" http://localhost:80 2>&1 | head -10)",
"Bash(docker ps --format \"{{.Names}}\\\\t{{.Networks}}\" | grep codehost)",
"Bash(docker inspect codehost-run-5f184bfe-2b82-4926-b34a-b022b49a55e3 --format '{{json .Config.Labels}}' 2>&1 | python3 -m json.tool | grep -i traefik)",
"Bash(curl -s -I -H \"Host: dfghj.code-host.online\" http://localhost:9000 2>&1 | head -5)",
"Bash(curl -s -I -H \"Host: dfghj.code-host.online\" http://localhost:80 2>&1 | head -5)",
"Bash(docker network ls | grep -E \"default|codehost\")",
"Bash(docker exec codehost-proxy wget -q -O- --timeout=3 http://172.21.0.7:80 2>&1 | head -5)",
"Bash(docker exec codehost-run-5f184bfe-2b82-4926-b34a-b022b49a55e3 netstat -tlnp 2>/dev/null || docker exec codehost-run-5f184bfe-2b82-4926-b34a-b022b49a55e3 ss -tlnp 2>/dev/null || docker exec codehost-run-5f184bfe-2b82-4926-b34a-b022b49a55e3 cat /etc/nginx/conf.d/default.conf 2>&1)",
"Bash(docker exec codehost-run-5f184bfe-2b82-4926-b34a-b022b49a55e3 env | grep PORT)",
"Bash(docker exec codehost-run-5f184bfe-2b82-4926-b34a-b022b49a55e3 cat /etc/nginx/conf.d/default.conf 2>&1)",
"Bash(docker image inspect codehost-project-5f184bfe-2b82-4926-b34a-b022b49a55e3 --format '{{json .Config.ExposedPorts}}' 2>&1 | head -5)",
"Bash(docker image inspect codehost-project-5f184bfe-2b82-4926-b34a-b022b49a55e3:0de4f8de-040f-42ce-8991-7ef11fcbcdd6 --format '{{json .Config.ExposedPorts}}' 2>&1)",
"Bash(docker exec codehost-api ls /app/storage/projects/5f184bfe-2b82-4926-b34a-b022b49a55e3/source/ 2>&1)",
"Bash(docker exec codehost-api cat /app/storage/projects/5f184bfe-2b82-4926-b34a-b022b49a55e3/source/Dockerfile 2>&1)",
"Bash(docker exec codehost-api cat /app/storage/projects/5f184bfe-2b82-4926-b34a-b022b49a55e3/source/nginx.conf 2>&1)",
"Bash(docker compose -f infra/docker/docker-compose.yml up -d --build api 2>&1)",
"Bash(docker exec codehost-api ls /app/storage/projects/ 2>&1)",
"Bash(docker exec codehost-api find /app -name \"storage\" -type d 2>&1 | head -5)",
"Bash(docker inspect codehost-api --format '{{json .Mounts}}' 2>&1 | python3 -m json.tool)",
"Bash(docker exec codehost-db psql -U codehost -d codehost -c 'SELECT \"id\", \"name\", \"status\", \"containerId\" FROM \"Project\";' 2>&1)",
"Bash(docker ps --format \"{{.Names}}\" | grep codehost-run)",
"Bash(docker exec codehost-run-59ebf542-5ffa-4d74-9585-282722f51b91 netstat -tlnp 2>/dev/null | grep -v \"127.0.0.11\")",
"Bash(docker inspect codehost-run-59ebf542-5ffa-4d74-9585-282722f51b91 --format '{{index .Config.Labels \"traefik.http.services.codehost-run-59ebf542-5ffa-4d74-9585-282722f51b91.loadbalancer.server.port\"}}' 2>&1)",
"Bash(docker inspect codehost-run-59ebf542-5ffa-4d74-9585-282722f51b91 --format '{{index .Config.Labels \"traefik.http.routers.codehost-run-59ebf542-5ffa-4d74-9585-282722f51b91-subdomain.rule\"}}' 2>&1)",
"Bash(curl -s -I -H \"Host: portfolio.code-host.online\" http://localhost:9000 2>&1 | head -5)",
"Bash(docker ps -a --format \"{{.Names}}\\\\t{{.Status}}\" | grep codehost-run)",
"Bash(curl -s -I -H \"Host: test.code-host.online\" http://localhost:9000 2>&1 | head -5)",
"Bash(curl -s -I -H \"Host: code-host.online\" http://localhost:80 2>&1 | head -5)",
"Bash(curl -s -I -H \"Host: portfolio.code-host.online\" http://localhost:80 2>&1 | head -5)",
"Bash(curl -s -o /dev/null -w \"%{http_code}\" -H \"Host: api.code-host.online\" http://localhost:80/auth/me && echo)",
"Bash(docker exec codehost-db psql -U codehost -d codehost -c \"UPDATE \\\\\"Project\\\\\" SET status = 'running', \\\\\"containerId\\\\\" = \\(SELECT \\\\\"Id\\\\\" FROM \\(SELECT split_part\\(names, '/', 2\\) as \\\\\"Id\\\\\" FROM \\(SELECT 'dummy' as names\\) t\\) s LIMIT 0\\) WHERE id = '59ebf542-5ffa-4d74-9585-282722f51b91' AND status = 'building';\" 2>&1)",
"Bash(docker rm codehost-run-b31b48bd-588b-4945-b091-8b96010d9330 codehost-run-9c8e19d7-29af-48c2-b8fa-20094fd1ccdf codehost-run-6f792b86-a780-45cb-a236-649b1eed9bf7 2>&1)",
"Bash(find /home/ubuntu/web/CodeHost -type f \\\\\\( -name \"*billing*\" -o -name \"*payment*\" -o -name \"*stripe*\" \\\\\\) 2>/dev/null | head -20)",
"Bash(find /home/ubuntu/web/CodeHost -name \".env*\" -o -name \"*.env\" 2>/dev/null | head -10)",
"Bash(cd /home/ubuntu/web/CodeHost/apps/api && npm install razorpay)",
"Bash(cd /home/ubuntu/web/CodeHost && npx prisma migrate dev --name add-billing --schema packages/database/prisma/schema.prisma 2>&1)",
"Bash(cd /home/ubuntu/web/CodeHost && npx prisma migrate diff --from-schema-datamodel packages/database/prisma/schema.prisma --to-schema-datamodel packages/database/prisma/schema.prisma --script 2>&1 | head -5)",
"Bash(npx prisma migrate dev --name add-billing --schema packages/database/prisma/schema.prisma --create-only 2>&1)",
"Bash(npx prisma generate --schema packages/database/prisma/schema.prisma 2>&1)"
]
}
}
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- **Safe by Default**: Sandboxed container isolation with strict resource limits (128MB RAM).
- **Admin Console**: Enhanced panel for monitoring platform health, users, and detailed deployment stats.
- **Multi-Provider Auth**: Sign in with email/password, Google, or GitHub. Email verification included.
- **Prepaid Billing**: Comprehensive system with Razorpay integration, wallet credits, and tiered resource allocation.

---

Expand Down Expand Up @@ -85,6 +86,11 @@ SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password

# Payments (https://dashboard.razorpay.com/app/keys)
RAZORPAY_KEY_ID=
RAZORPAY_KEY_SECRET=
RAZORPAY_WEBHOOK_SECRET=
```

### 3. OAuth Provider Setup
Expand Down Expand Up @@ -146,14 +152,37 @@ sudo ufw allow 443/tcp

---

## Administrative Access
## Billing & Resource Tiers

| Tier | RAM | CPU | Storage | Credits/Month |
|------|-----|-----|---------|---------------|
| **Free** | 128MB | 0.5 | 1GB | ₹0 |
| **Basic** | 256MB | 1.0 | 2GB | ₹100 |
| **Pro** | 512MB | 2.0 | 5GB | ₹300 |
| **Business** | 1GB | 4.0 | 10GB | ₹800 |

- **Prepaid Model**: Buy credits (1 credit = ₹2), then select a tier for each project.
- **Auto-stop**: If your wallet balance is insufficient for a monthly charge, the project container is automatically stopped to avoid negative balances.
- **Payment Methods**: Seamless integration with Razorpay (UPI, Google Pay, Cards, Netbanking).

---

## Administrative Actions

To promote a user to Admin:

```bash
docker exec -it codehost-db psql -U ${DB_USER:-codehost} -d ${DB_NAME:-codehost} -c "UPDATE \"User\" SET role = 'ADMIN' WHERE email = 'your@email.com';"
```

### Wallet Management (Admin)
Admins can manually adjust user balances or grant credits via the database if necessary:

```bash
# Add 500 credits to a user
docker exec -it codehost-db psql -U ${DB_USER:-codehost} -d ${DB_NAME:-codehost} -c "UPDATE \"Wallet\" SET balance = balance + 500 WHERE \"userId\" = 'user-uuid';"
```

---

## Authentication Flow
Expand Down
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"jsonwebtoken": "^9.0.3",
"multer": "^2.0.2",
"nodemailer": "^8.0.3",
"razorpay": "^2.9.6",
"socket.io": "^4.8.3",
"tar-fs": "^3.1.1"
},
Expand Down
57 changes: 57 additions & 0 deletions apps/api/src/config/tiers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
export interface TierConfig {
name: string;
memory: number; // bytes
cpus: number; // cores
storage: number; // bytes
creditsPerMonth: number;
maxProjects: number;
label: string;
}

export const RESOURCE_TIERS: Record<string, TierConfig> = {
free: {
name: 'free',
memory: 128 * 1024 * 1024,
cpus: 0.5,
storage: 1 * 1024 * 1024 * 1024,
creditsPerMonth: 0,
maxProjects: 1,
label: 'Free',
},
basic: {
name: 'basic',
memory: 256 * 1024 * 1024,
cpus: 1,
storage: 2 * 1024 * 1024 * 1024,
creditsPerMonth: 50,
maxProjects: 3,
label: 'Basic',
},
pro: {
name: 'pro',
memory: 512 * 1024 * 1024,
cpus: 2,
storage: 5 * 1024 * 1024 * 1024,
creditsPerMonth: 150,
maxProjects: 5,
label: 'Pro',
},
business: {
name: 'business',
memory: 1024 * 1024 * 1024,
cpus: 4,
storage: 10 * 1024 * 1024 * 1024,
creditsPerMonth: 400,
maxProjects: 10,
label: 'Business',
},
};

// 1 credit = ₹2
export const CREDIT_PRICE_INR = 2;

export const CREDIT_PACKAGES = [
{ credits: 100, priceInr: 200, label: '100 Credits', savings: null },
{ credits: 300, priceInr: 500, label: '300 Credits', savings: '17% savings' },
{ credits: 600, priceInr: 900, label: '600 Credits', savings: '25% savings' },
];
64 changes: 64 additions & 0 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@ import projectsRouter from './routes/projects.js';
import deploymentsRouter from './routes/deployments.js';
import adminRouter from './routes/admin.js';
import filesRouter from './routes/files.js';
import billingRouter from './routes/billing.js';
import { RunnerService } from './services/runner.js';
import { RESOURCE_TIERS } from './config/tiers.js';
app.use('/auth', authRouter);
app.use('/auth/oauth', oauthRouter);
app.use('/projects', projectsRouter);
app.use('/deployments', deploymentsRouter);
app.use('/admin', adminRouter);
app.use('/files', filesRouter);
app.use('/billing', billingRouter);

// Public Stats (for landing page)
app.get('/stats/public', async (req, res) => {
Expand Down Expand Up @@ -91,6 +94,67 @@ setInterval(async () => {
}
}, 5000);

// Billing cron: check every 24h for monthly tier charges
setInterval(async () => {
try {
const paidProjects = await prisma.project.findMany({
where: { tier: { not: 'free' }, status: 'running' },
include: { user: true },
});

for (const project of paidProjects) {
const tierConfig = RESOURCE_TIERS[project.tier];
if (!tierConfig || tierConfig.creditsPerMonth === 0) continue;

const wallet = await prisma.wallet.findUnique({ where: { userId: project.userId } });
if (!wallet) continue;

// Check last tier_charge for this project
const lastCharge = await prisma.transaction.findFirst({
where: { walletId: wallet.id, type: 'tier_charge', projectId: project.id },
orderBy: { createdAt: 'desc' },
});

const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
if (lastCharge && lastCharge.createdAt > thirtyDaysAgo) continue;

// Deduct credits
if (wallet.balance >= tierConfig.creditsPerMonth) {
await prisma.$transaction(async (tx) => {
await tx.wallet.update({
where: { id: wallet.id },
data: { balance: { decrement: tierConfig.creditsPerMonth } },
});
await tx.transaction.create({
data: {
walletId: wallet.id,
amount: -tierConfig.creditsPerMonth,
type: 'tier_charge',
description: `Monthly charge for ${project.name} (${tierConfig.label} tier)`,
projectId: project.id,
},
});
});
logger.info(`Charged ${tierConfig.creditsPerMonth} credits for project ${project.id}`);
} else {
// Insufficient balance: stop container
try {
await RunnerService.stopContainer(project.id);
await prisma.project.update({
where: { id: project.id },
data: { status: 'stopped' },
});
logger.info(`Stopped project ${project.id} due to insufficient credits`);
} catch (err) {
logger.error({ error: err }, `Failed to stop project ${project.id} for billing`);
}
}
}
} catch (error) {
logger.error({ error }, 'Billing cron error');
}
}, 24 * 60 * 60 * 1000);

const PORT = env.PORT || 4000;

httpServer.listen(PORT, () => {
Expand Down
Loading