Initial commit

This commit is contained in:
2026-01-24 23:49:58 +00:00
commit 7b56025ef7
31 changed files with 6916 additions and 0 deletions

11
.dockerignore Normal file
View File

@@ -0,0 +1,11 @@
node_modules
npm-debug.log
.git
.gitignore
.env
.env.local
README.md
.DS_Store
*.log
.vscode
.idea

113
.env.example Normal file
View File

@@ -0,0 +1,113 @@
# ===================================
# AI Recruitment Site - Configuration
# ===================================
# Copy this file to .env and customize for your company
# ===================================
# Company Information
# ===================================
COMPANY_NAME=Your Recruitment Firm
COMPANY_TAGLINE=Finding the Perfect Match for Your Career
COMPANY_DESCRIPTION=We specialize in connecting talented professionals with exceptional opportunities across various industries.
# ===================================
# Company Branding (Colors)
# ===================================
# Primary brand color (e.g., #2563EB for blue)
PRIMARY_COLOR=#2563EB
# Accent/success color (e.g., #059669 for green)
ACCENT_COLOR=#059669
# Dark/secondary color (e.g., #1E293B)
DARK_COLOR=#1E293B
# ===================================
# Contact Information
# ===================================
CONTACT_EMAIL=info@yourcompany.com
CONTACT_PHONE=+1 (555) 123-4567
CONTACT_ADDRESS=123 Business St, Suite 100, City, State 12345
# Social Media Links (leave empty to hide)
SOCIAL_LINKEDIN=https://linkedin.com/company/yourcompany
SOCIAL_TWITTER=https://twitter.com/yourcompany
SOCIAL_FACEBOOK=
# ===================================
# Deployment Configuration
# ===================================
# Your custom subdomain (e.g., 'anna' becomes annarecruit.startanaicompany.com)
SUBDOMAIN=yourname
# Your Gitea username and repository name
GITEA_USERNAME=your-gitea-username
GITEA_REPO_NAME=ai-recruit-site-template
# ===================================
# Application Settings
# ===================================
NODE_ENV=production
PORT=3000
# Session secret (will be auto-generated if empty)
SESSION_SECRET=
# ===================================
# Database Configuration
# ===================================
# PostgreSQL connection details
DB_HOST=postgres
DB_PORT=5432
DB_NAME=recruitment
DB_USER=postgres
# Database password (will be auto-generated if empty)
DB_PASSWORD=
# ===================================
# API Tokens (for deployment script)
# ===================================
# REQUIRED: Get your SAAC_API_KEY from: https://apps.startanaicompany.com/api/v1/register
# Set as environment variable: export SAAC_API_KEY="your_key_here"
#
# REQUIRED: GITEA_API_TOKEN needed for automatic deployments (webhook setup)
# Get from: https://git.startanaicompany.com → Settings → Applications → Generate New Token
# Set as environment variable: export GITEA_API_TOKEN="your_token_here"
# ===================================
# Feature Configuration
# ===================================
# Maximum CV file size in MB
MAX_CV_SIZE_MB=5
# Allowed CV file types (comma-separated)
ALLOWED_CV_TYPES=application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document
# ===================================
# About Page Content
# ===================================
ABOUT_MISSION=Our mission is to bridge the gap between exceptional talent and outstanding opportunities.
ABOUT_VISION=We envision a world where every professional finds their perfect career match.
ABOUT_VALUES=Integrity, Excellence, Innovation, Partnership
# ===================================
# Services Offered (comma-separated)
# ===================================
SERVICES_LIST=Executive Search,Contract Staffing,Permanent Placement,Career Consulting,Talent Assessment,Industry Expertise
# ===================================
# Contact Page Settings
# ===================================
# Email address where contact form submissions are sent
CONTACT_FORM_RECIPIENT=info@yourcompany.com
# Business hours
BUSINESS_HOURS=Monday - Friday: 9:00 AM - 6:00 PM
# ===================================
# Email Configuration (Optional)
# ===================================
# If you want to send email notifications
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=noreply@yourcompany.com

18
.gitignore vendored Normal file
View File

@@ -0,0 +1,18 @@
node_modules/
.env
.env.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store
*.log
.vscode/
.idea/
deploy_key
cc/
# Deployment keys and scripts
deploy_key*
deploy.sh
deploy-to-apps.sh
.saac.json

328
DEPLOYMENT_GUIDE.md Normal file
View File

@@ -0,0 +1,328 @@
# Deployment Guide - Using SAAC API
This guide explains how to deploy your recruitment site using the SAAC API at `apps.startanaicompany.com`.
## What is the SAAC API?
The SAAC API is a secure gateway that allows you to deploy applications to StartAnAiCompany infrastructure. Each user gets their own API key and can only manage their own applications.
**Benefits:**
- ✅ No need for infrastructure credentials
- ✅ Automatic isolation - you can only see/manage your apps
- ✅ Rate limiting prevents abuse
- ✅ Audit logging for security
- ✅ Automatic webhooks for continuous deployment
## Quick Start (3 Steps)
### Step 1: Register for API Key
Register once to get your unique API key:
```bash
curl -X POST https://apps.startanaicompany.com/api/v1/register \
-H "Content-Type: application/json" \
-d '{
"email": "your@email.com",
"gitea_username": "your-gitea-username"
}'
```
**Response:**
```json
{
"user_id": "uuid",
"email": "your@email.com",
"api_key": "cw_abc123xyz789...",
"message": "Save your API key securely. It will not be shown again."
}
```
⚠️ **IMPORTANT**: Save this API key! It will only be shown once.
**Save it to your environment:**
```bash
export SAAC_API_KEY="cw_abc123xyz789..."
# Add to your shell profile for persistence
echo 'export SAAC_API_KEY="cw_abc123xyz789..."' >> ~/.bashrc
# or for zsh:
echo 'export SAAC_API_KEY="cw_abc123xyz789..."' >> ~/.zshrc
```
### Step 2: Customize Your Site
```bash
# Copy environment template
cp .env.example .env
# Edit with your company information
nano .env
```
**Minimum required in `.env`:**
```bash
# Company Information
COMPANY_NAME="Your Recruitment Firm"
COMPANY_TAGLINE="Your Tagline Here"
COMPANY_DESCRIPTION="Your company description"
# Branding
PRIMARY_COLOR=#2563EB
ACCENT_COLOR=#059669
DARK_COLOR=#1E293B
# Contact
CONTACT_EMAIL=info@yourcompany.com
CONTACT_PHONE=+1 (555) 123-4567
CONTACT_ADDRESS=123 Business St, City, State 12345
# Deployment
SUBDOMAIN=yourname
GITEA_USERNAME=your-gitea-username
GITEA_REPO_NAME=your-recruit-site
```
### Step 3: Deploy
```bash
# Copy the deployment script
cp deploy-to-apps.example.sh deploy-to-apps.sh
chmod +x deploy-to-apps.sh
# Run deployment
./deploy-to-apps.sh
```
The script will:
1. Create your application on StartAnAiCompany platform
2. Configure domain (`{subdomain}recruit.startanaicompany.com`)
3. Set up automatic deployments via webhook
4. Trigger initial build
**Your site will be live in 2-3 minutes!**
## Advanced Usage
### List Your Applications
```bash
curl -H "X-API-Key: $SAAC_API_KEY" \
https://apps.startanaicompany.com/api/v1/applications
```
### View Application Details
```bash
curl -H "X-API-Key: $SAAC_API_KEY" \
https://apps.startanaicompany.com/api/v1/applications/YOUR_APP_UUID
```
### View Deployment Logs
```bash
curl -H "X-API-Key: $SAAC_API_KEY" \
"https://apps.startanaicompany.com/api/v1/applications/YOUR_APP_UUID/logs?tail=100"
```
### Trigger Manual Deployment
```bash
curl -X POST \
-H "X-API-Key: $SAAC_API_KEY" \
https://apps.startanaicompany.com/api/v1/applications/YOUR_APP_UUID/deploy
```
### Update Environment Variables
```bash
curl -X PATCH \
-H "X-API-Key: $SAAC_API_KEY" \
-H "Content-Type: application/json" \
https://apps.startanaicompany.com/api/v1/applications/YOUR_APP_UUID/env \
-d '{
"variables": {
"COMPANY_NAME": "New Company Name",
"PRIMARY_COLOR": "#FF0000"
}
}'
```
After updating env vars, redeploy:
```bash
curl -X POST \
-H "X-API-Key: $SAAC_API_KEY" \
https://apps.startanaicompany.com/api/v1/applications/YOUR_APP_UUID/deploy
```
### Delete Application
```bash
curl -X DELETE \
-H "X-API-Key: $SAAC_API_KEY" \
https://apps.startanaicompany.com/api/v1/applications/YOUR_APP_UUID
```
⚠️ **Warning**: This permanently deletes the application and all its data!
## Automatic Deployments
Once deployed, every push to your repository's `master` branch will automatically:
1. Trigger webhook to SAAC API
2. SAAC authenticates with your API key
3. SAAC deploys your updated code
4. Your site updates in 2-3 minutes
**No manual intervention needed!**
## DNS Configuration
After deployment, configure DNS in Cloudflare:
1. Go to Cloudflare DNS settings for `startanaicompany.com`
2. Add CNAME record:
- **Name**: `{your-subdomain}recruit` (e.g., `annarecruit`)
- **Target**: `apps.startanaicompany.com`
- **Proxy status**: Proxied (orange cloud)
3. Save
DNS propagation takes 2-5 minutes.
## Troubleshooting
### "API key required" Error
Make sure you've exported your API key:
```bash
echo $SAAC_API_KEY
# Should show: cw_abc123xyz...
```
If empty, export it again:
```bash
export SAAC_API_KEY="your_api_key_here"
```
### "Application limit reached" Error
You've hit your quota (default: 50 apps). Delete unused apps:
```bash
# List your apps
curl -H "X-API-Key: $SAAC_API_KEY" \
https://apps.startanaicompany.com/api/v1/applications
# Delete an app
curl -X DELETE -H "X-API-Key: $SAAC_API_KEY" \
https://apps.startanaicompany.com/api/v1/applications/APP_UUID
```
### "Rate limit exceeded" Error
You're making too many requests. Wait and try again:
- General API: 100 requests / 15 minutes
- App creation: 10 / hour
- Deployments: 30 / hour
### Deployment Logs Show Errors
Check logs for specific errors:
```bash
curl -H "X-API-Key: $SAAC_API_KEY" \
"https://apps.startanaicompany.com/api/v1/applications/YOUR_APP_UUID/logs?tail=200"
```
Common issues:
- **Database migration failed**: Check migration SQL syntax
- **Port 3000 already in use**: Normal, the platform handles this
- **Git clone failed**: Check repository URL and permissions
### Site Not Loading
1. **Check deployment status**:
```bash
curl -H "X-API-Key: $SAAC_API_KEY" \
https://apps.startanaicompany.com/api/v1/applications/YOUR_APP_UUID
```
2. **Check DNS**:
```bash
dig {subdomain}recruit.startanaicompany.com
```
Should point to Cloudflare or StartAnAiCompany server.
3. **Wait**: Initial deployment takes 2-3 minutes.
4. **Check Cloudflare**: Ensure CNAME record is correct and proxied.
### Lost API Key
If you lost your API key, regenerate it:
```bash
curl -X POST \
-H "X-API-Key: $OLD_SAAC_API_KEY" \
https://apps.startanaicompany.com/api/v1/users/regenerate-key
```
⚠️ **Warning**: Old API key will be immediately revoked!
If you can't access the old key, contact support.
## Security Best Practices
1. **Never commit API keys to git**
- The `.env` file is gitignored
- deployment-info.txt is gitignored
- Keep API keys in environment variables only
2. **Use environment variables**
```bash
# Good
export SAAC_API_KEY="cw_..."
# Bad - don't hardcode in scripts
SAAC_API_KEY="cw_..." ./deploy.sh
```
3. **Rotate keys periodically**
- Regenerate your API key every 3-6 months
- Update all environments where it's used
4. **Don't share API keys**
- Each user should have their own API key
- Don't share deployment-info.txt files
## Rate Limits
To prevent abuse, the API has rate limits:
| Operation | Limit |
|-----------|-------|
| General API calls | 100 / 15 min |
| User registration | 5 / hour |
| Application creation | 10 / hour |
| Deployments | 30 / hour |
| Log access | 200 / 15 min |
**Rate limit headers in responses:**
```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1643040000
```
If you hit a rate limit, wait for the reset time before retrying.
## Support
**Documentation:**
- Template README: [README.md](README.md)
**Issues:**
- Report issues: https://git.startanaicompany.com/StartanAICompany/ai-recruit-site-template/issues
**Contact:**
- Email: info@startanaicompany.com
---
**Ready to deploy?** Follow the [Quick Start](#quick-start-3-steps) above!

View File

@@ -0,0 +1,279 @@
# Deployment Script Enhancement Proposal
## Problem Statement
The current `deploy-to-apps.sh` script only handles initial deployment. Running it a second time will:
1. Attempt to create a duplicate application
2. Likely fail with "subdomain already exists" error
3. Have no way to update existing application's environment variables
4. Have no way to trigger redeployment
## Solution: Setup vs Update Modes
### Architecture
```
.deployment-uuid (git-ignored file storing application UUID)
deploy-to-apps.sh (enhanced script with mode detection)
```
### Script Modes
#### 1. Auto-Detection (Default)
```bash
./deploy-to-apps.sh
```
- Checks if `.deployment-uuid` exists
- **If NOT exists:** Run setup mode (first deployment)
- **If exists:** Run update mode (redeploy with new env vars)
#### 2. Explicit Setup (Force New Deployment)
```bash
./deploy-to-apps.sh --setup
```
- Creates NEW application
- Overwrites `.deployment-uuid` with new UUID
- Use case: Deploying to different subdomain/environment
#### 3. Explicit Update
```bash
./deploy-to-apps.sh --update
```
- Requires `.deployment-uuid` to exist
- Updates environment variables
- Triggers redeployment
- Fails if `.deployment-uuid` not found
#### 4. Status Check
```bash
./deploy-to-apps.sh --status
```
- Shows current deployment info
- Fetches live status from API
- Shows domain, deployment status, etc.
---
## Implementation Details
### File Structure
```bash
.deployment-uuid # Single line: application UUID
deployment-info.txt # Human-readable deployment details
.gitignore # Must include .deployment-uuid
```
### Setup Mode Flow
1. Check if subdomain already deployed (optional safety check)
2. Create application via `POST /api/v1/applications`
3. Save UUID to `.deployment-uuid`
4. Set up Gitea webhook (only once)
5. Save deployment info to `deployment-info.txt`
6. Show success message with next steps
### Update Mode Flow
1. Read UUID from `.deployment-uuid`
2. Verify application exists via `GET /api/v1/applications/:uuid`
3. Update environment variables via `PATCH /api/v1/applications/:uuid/env`
4. Trigger redeployment via `POST /api/v1/applications/:uuid/deploy`
5. Show redeployment progress
### Status Mode Flow
1. Read UUID from `.deployment-uuid`
2. Fetch application details via `GET /api/v1/applications/:uuid`
3. Display formatted status information
---
## API Endpoints Used
### Setup Mode
```bash
POST /api/v1/applications
→ Returns: { application_uuid, domain, webhook_url, ... }
POST https://git.startanaicompany.com/api/v1/repos/{owner}/{repo}/hooks
→ Sets up Gitea webhook (one-time)
```
### Update Mode
```bash
GET /api/v1/applications/:uuid
→ Verify application exists
PATCH /api/v1/applications/:uuid/env
→ Body: { "variables": { "COMPANY_NAME": "...", ... } }
POST /api/v1/applications/:uuid/deploy
→ Triggers redeployment
```
### Status Mode
```bash
GET /api/v1/applications/:uuid
→ Returns application details and status
```
---
## Error Handling
### Setup Mode Errors
- **Subdomain already exists:** Suggest using `--update` or different subdomain
- **API key invalid:** Show registration instructions
- **Gitea token invalid:** Show token generation instructions
- **Webhook already exists:** Warn but continue (non-fatal)
### Update Mode Errors
- **No .deployment-uuid file:** Suggest running `--setup` first
- **Application not found:** UUID invalid or app deleted, suggest `--setup`
- **Environment variable validation failed:** Show which vars are invalid
- **Deployment failed:** Show error from API
---
## Example Usage
### First Deployment
```bash
$ ./deploy-to-apps.sh
========================================
AI Recruitment Site Deployment
========================================
📝 Mode: SETUP (first deployment)
📦 Configuration:
Company: Acme Corp
Repository: git@git.startanaicompany.com:user/acme-recruit.git
Domain: https://acmerecruit.startanaicompany.com
📝 Creating application on StartAnAiCompany server...
✅ Application created!
Application UUID: abc-123-def-456
Domain: https://acmerecruit.startanaicompany.com
💾 UUID saved to .deployment-uuid
🪝 Setting up deployment webhook...
✅ Webhook configured for automatic deployments
========================================
Deployment Complete!
========================================
Your site will be available in 2-3 minutes.
Next runs will automatically UPDATE this deployment.
To deploy a new site, use: ./deploy-to-apps.sh --setup
```
### Update Deployment (Changed Company Name)
```bash
$ ./deploy-to-apps.sh
========================================
AI Recruitment Site Deployment
========================================
📝 Mode: UPDATE (existing deployment)
📦 Configuration:
Application UUID: abc-123-def-456
Company: New Company Name (CHANGED)
Domain: https://acmerecruit.startanaicompany.com
🔄 Updating environment variables...
✅ Environment variables updated (8 variables)
🚀 Triggering redeployment...
✅ Deployment triggered
========================================
Update Complete!
========================================
Your changes will be live in 2-3 minutes.
Monitor deployment:
./deploy-to-apps.sh --status
```
### Check Status
```bash
$ ./deploy-to-apps.sh --status
========================================
Deployment Status
========================================
Application UUID: abc-123-def-456
Name: acme-recruit
Domain: https://acmerecruit.startanaicompany.com
Status: running
Last deployment: 2026-01-24 17:30:00 UTC
Company: Acme Corp
Repository: git@git.startanaicompany.com:user/acme-recruit.git
Branch: master
```
---
## .gitignore Updates Required
Add to `.gitignore`:
```
# Deployment configuration (contains UUID, API keys)
.deployment-uuid
deployment-info.txt
deploy-to-apps.sh
# Keep the example for users to copy
!deploy-to-apps.example.sh
```
---
## Benefits
1.**Idempotent:** Can run script multiple times safely
2.**User-friendly:** Auto-detects mode based on context
3.**Flexible:** Supports multiple deployment scenarios
4.**Safe:** Prevents duplicate applications
5.**Traceable:** UUID stored locally for easy updates
6.**Stateful:** Remembers previous deployment
7.**Informative:** Clear feedback on what's happening
---
## Migration for Existing Users
If someone already ran the old script:
```bash
# Find their application UUID
curl -H "X-API-Key: $SAAC_API_KEY" https://apps.startanaicompany.com/api/v1/applications
# Save UUID to file
echo "abc-123-def-456" > .deployment-uuid
# Now they can use update mode
./deploy-to-apps.sh --update
```
---
## Implementation Priority
### Phase 1 (Must Have)
1. Auto-detection (check for `.deployment-uuid`)
2. Setup mode (create new + save UUID)
3. Update mode (update env vars + redeploy)
### Phase 2 (Nice to Have)
4. Status mode (query current status)
5. Enhanced error messages
6. Safety checks (subdomain conflicts, etc.)
### Phase 3 (Future)
7. Rollback support
8. Multiple environment support (dev/staging/prod)
9. Configuration validation before deployment

View File

@@ -0,0 +1,488 @@
# Deployment Script Update Summary
## Overview
The `deploy-to-apps.example.sh` script has been completely rewritten to use `.saac.json` for configuration storage and implement a streamlined email verification flow. This eliminates the need for manual API key management and provides a better user experience.
---
## Changes Made
### 1. Configuration File Migration
**Old System:**
- `.deployment-uuid` - Stored only the application UUID
- `deployment-info.txt` - Human-readable deployment info
- `SAAC_API_KEY` - Required as environment variable
**New System:**
- `.saac.json` - Stores all user credentials and deployment configuration
- No separate info file needed
- No environment variable for API key
### 2. `.saac.json` Structure
```json
{
"version": "1.0",
"user": {
"email": "user@example.com",
"user_id": "uuid-here",
"api_key": "api-key-here",
"gitea_username": "username",
"verified": true,
"registered_at": "2026-01-24T12:00:00.000Z"
},
"deployment": {
"application_uuid": "app-uuid-here",
"application_name": "subdomain-recruit",
"domain": "subdomain.recruit.startanaicompany.com",
"subdomain": "subdomain",
"repository": "git@git.startanaicompany.com:user/repo.git",
"deployed_at": "2026-01-24T12:00:00.000Z",
"last_updated": "2026-01-24T12:30:00.000Z"
},
"config": {
"saac_api": "https://apps.startanaicompany.com/api/v1",
"gitea_api": "https://git.startanaicompany.com/api/v1"
}
}
```
### 3. New Helper Functions
#### `load_config()`
- Loads configuration from `.saac.json`
- Extracts user credentials, verification status, and deployment info
- Returns 0 if file exists, 1 if not
#### `save_config()`
- Saves configuration to `.saac.json`
- Sets file permissions to 600 (read/write for owner only)
- Updates `last_updated` timestamp automatically
### 4. Email Verification Flow
#### First-Time Registration (No `.saac.json`)
1. **Prompt for Email**
- User enters email address
- Gitea username pulled from `.env`
2. **Register User**
```bash
POST /users/register
{
"email": "user@example.com",
"gitea_username": "username"
}
```
- Returns `user_id` and `api_key`
3. **Save Unverified Configuration**
- Creates `.saac.json` with `verified: false`
- File is immediately saved (safe to exit and resume)
4. **Email Verification**
- User checks MailHog at https://mailhog.goryan.io
- Enters verification code
- Script calls `POST /users/verify`
- Updates `.saac.json` with `verified: true`
#### Returning User (Unverified)
If `.saac.json` exists but `verified: false`:
1. Prompt for verification code
2. Verify email
3. Update configuration
#### Verified User
If `.saac.json` exists and `verified: true`:
- Loads configuration silently
- Proceeds to auto-detect mode (setup or update)
### 5. Auto-Detection Logic
```
┌─────────────────────────────────────┐
│ Does .saac.json exist? │
└──────────┬──────────────────────────┘
┌──────┴──────┐
│ NO │ YES
▼ ▼
┌───────┐ ┌──────────────┐
│REGISTER│ │ verified? │
└───┬───┘ └──────┬───────┘
│ │
▼ ┌─────┴─────┐
┌────────┐ │NO YES│
│ VERIFY │ ▼ ▼
└───┬────┘ ┌──────┐ ┌──────────────┐
│ │VERIFY│ │ has app_uuid?│
│ └──┬───┘ └──────┬───────┘
│ │ │
└──────────┴─────────────┤
┌─────┴─────┐
│NO YES│
▼ ▼
┌──────┐ ┌──────┐
│SETUP │ │UPDATE│
└──────┘ └──────┘
```
### 6. Removed Features
- `SAAC_API_KEY` environment variable requirement (now in `.saac.json`)
- `deployment-info.txt` file (replaced by `.saac.json`)
- `.deployment-uuid` file (replaced by `.saac.json`)
### 7. Updated `.gitignore`
**Removed:**
- `deployment-info.txt`
- `.deployment-uuid`
**Added:**
- `.saac.json`
### 8. New Dependencies
- **jq** - JSON processor (required)
- Ubuntu/Debian: `sudo apt install jq`
- macOS: `brew install jq`
- Alpine: `apk add jq`
Script checks for `jq` and provides installation instructions if missing.
---
## Flow Diagrams
### First-Time User Flow
```
┌─────────────────────────────────────────────────────────┐
│ 1. User runs: ./deploy-to-apps.sh │
└─────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 2. No .saac.json found │
│ → Prompt for email │
│ → Read GITEA_USERNAME from .env │
└─────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 3. POST /users/register │
│ → Receive user_id and api_key │
└─────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 4. Save .saac.json (verified: false) │
└─────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 5. Show MailHog URL │
│ → Prompt for verification code │
└─────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 6. POST /users/verify │
│ → Check response for "verified": true │
└─────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 7. Update .saac.json (verified: true) │
└─────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 8. Auto-detect → SETUP mode (no app_uuid) │
│ → Create new deployment │
└─────────────────────────────────────────────────────────┘
```
### Returning User Flow (Verified, Deployed)
```
┌─────────────────────────────────────────────────────────┐
│ 1. User runs: ./deploy-to-apps.sh │
└─────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 2. Load .saac.json │
│ → verified: true │
│ → app_uuid: exists │
└─────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 3. Auto-detect → UPDATE mode │
└─────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 4. Update environment variables │
│ → Trigger redeployment │
│ → Update .saac.json timestamps │
└─────────────────────────────────────────────────────────┘
```
---
## Migration Instructions for Existing Users
### If You Have `.deployment-uuid` and `deployment-info.txt`
**Option 1: Start Fresh (Recommended)**
1. Delete old files:
```bash
rm .deployment-uuid deployment-info.txt
```
2. Run the new script:
```bash
cp deploy-to-apps.example.sh deploy-to-apps.sh
./deploy-to-apps.sh
```
3. Follow the registration and verification prompts
4. When asked to setup, confirm to create new deployment
**Option 2: Manual Migration**
1. Note your existing configuration:
```bash
APP_UUID=$(cat .deployment-uuid)
# Note your SAAC_API_KEY from environment
# Note your email used for registration
```
2. Create `.saac.json` manually:
```json
{
"version": "1.0",
"user": {
"email": "your@email.com",
"user_id": "your-user-id",
"api_key": "your-api-key",
"gitea_username": "your-username",
"verified": true,
"registered_at": "2026-01-24T00:00:00.000Z"
},
"deployment": {
"application_uuid": "your-app-uuid",
"application_name": "subdomain-recruit",
"domain": "subdomain.recruit.startanaicompany.com",
"subdomain": "subdomain",
"repository": "git@git.startanaicompany.com:user/repo.git",
"deployed_at": "2026-01-24T00:00:00.000Z",
"last_updated": "2026-01-24T00:00:00.000Z"
},
"config": {
"saac_api": "https://apps.startanaicompany.com/api/v1",
"gitea_api": "https://git.startanaicompany.com/api/v1"
}
}
```
3. Set proper permissions:
```bash
chmod 600 .saac.json
```
4. Delete old files:
```bash
rm .deployment-uuid deployment-info.txt
```
5. Test the script:
```bash
./deploy-to-apps.sh --status
```
---
## Validation
### Script Validation
```bash
bash -n deploy-to-apps.example.sh
```
**Result:** ✅ No syntax errors
### Required Dependencies
- `bash` - Bourne Again Shell
- `curl` - HTTP client
- `jq` - JSON processor ⚠️ NEW REQUIREMENT
### File Permissions
The script automatically sets `.saac.json` to `600` (read/write for owner only) to protect sensitive credentials.
---
## Security Considerations
### `.saac.json` Contains Sensitive Data
**Never commit this file to git!**
The file contains:
- User email
- API key (equivalent to password)
- User ID
- Application UUIDs
### Gitignore Protection
The `.gitignore` file now includes `.saac.json` to prevent accidental commits.
### File Permissions
The script sets `.saac.json` to mode `600`:
- Owner: read/write
- Group: none
- Others: none
---
## Backward Compatibility
### Breaking Changes
1. **SAAC_API_KEY environment variable no longer used**
- Old: Required in environment
- New: Stored in `.saac.json`
2. **Files no longer created**
- `.deployment-uuid` → replaced by `.saac.json`
- `deployment-info.txt` → replaced by `.saac.json`
3. **New dependency**
- `jq` now required for JSON processing
### Non-Breaking Changes
1. **All modes still work**
- `./deploy-to-apps.sh` (auto-detect)
- `./deploy-to-apps.sh --setup`
- `./deploy-to-apps.sh --update`
- `./deploy-to-apps.sh --status`
2. **GITEA_API_TOKEN still required for setup**
- Only needed for webhook creation during initial setup
3. **All .env variables unchanged**
- Same company configuration variables
- Same repository configuration
---
## Testing Checklist
### ✅ Completed
- [x] Bash syntax validation
- [x] Helper function logic
- [x] Configuration file structure
- [x] Flow diagram accuracy
- [x] Migration instructions
### 🔄 To Be Tested
- [ ] First-time registration flow
- [ ] Email verification flow
- [ ] Unverified user resumption
- [ ] Auto-detect mode logic
- [ ] Setup mode with `.saac.json`
- [ ] Update mode with `.saac.json`
- [ ] Status mode with `.saac.json`
- [ ] Manual migration from old files
---
## Example Usage
### First-Time User
```bash
$ cp deploy-to-apps.example.sh deploy-to-apps.sh
$ ./deploy-to-apps.sh
=========================================
First-Time Setup
=========================================
No configuration found. Let's register your account.
Enter your email address: user@example.com
📧 Registering user: user@example.com
Gitea username: myusername
✅ User registered!
User ID: 123e4567-e89b-12d3-a456-426614174000
💾 Configuration saved to .saac.json
=========================================
Email Verification
=========================================
📧 Verification email sent to: user@example.com
🔍 Check your email at MailHog:
https://mailhog.goryan.io
Enter the verification code from the email: ABC123
🔐 Verifying email...
✅ Email verified successfully!
💾 Configuration saved to .saac.json
=========================================
AI Recruitment Site Deployment
=========================================
📝 Mode: SETUP (new deployment)
...
```
### Returning User (Update)
```bash
$ ./deploy-to-apps.sh
✅ Loaded configuration from .saac.json
User: user@example.com
Verified: true
Application: 123e4567-e89b-12d3-a456-426614174000
=========================================
AI Recruitment Site Deployment
=========================================
📝 Mode: UPDATE (existing deployment)
...
```
---
## Support
For issues or questions:
1. Check MailHog for verification emails: https://mailhog.goryan.io
2. Verify `.saac.json` exists and has correct permissions
3. Ensure `jq` is installed
4. Check SAAC API status
---
**Document Version:** 1.0
**Last Updated:** 2026-01-24
**Script Version:** Updated with .saac.json support

30
Dockerfile Normal file
View File

@@ -0,0 +1,30 @@
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install --production
# Copy application code
COPY server.js ./
COPY config.js ./
COPY migrations ./migrations
# Copy public directory explicitly
COPY public/ ./public/
# Create non-root user and fix permissions
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001 && \
chown -R nodejs:nodejs /app && \
ls -la /app && \
ls -la /app/public
USER nodejs
EXPOSE 3000
CMD ["node", "server.js"]

417
MIGRATION_GUIDE.md Normal file
View File

@@ -0,0 +1,417 @@
# Migration Guide: Old Deployment Script → New .saac.json System
## Overview
This guide helps existing users migrate from the old deployment system (`.deployment-uuid` + `deployment-info.txt`) to the new `.saac.json` configuration system.
---
## What Changed?
### Old System
```
.deployment-uuid → Contains: application UUID only
deployment-info.txt → Contains: Human-readable deployment info
Environment variable → SAAC_API_KEY required
```
### New System
```
.saac.json → Contains: All user credentials + deployment info
No environment variable → API key stored in .saac.json
```
---
## Migration Options
### Option 1: Start Fresh (Recommended)
**Best for:** Most users, cleanest migration
**Steps:**
1. **Backup existing configuration** (optional)
```bash
cp .deployment-uuid .deployment-uuid.backup
cp deployment-info.txt deployment-info.txt.backup
echo "$SAAC_API_KEY" > saac-api-key.backup
```
2. **Remove old files**
```bash
rm .deployment-uuid deployment-info.txt
unset SAAC_API_KEY # Clear from current session
```
3. **Update the script**
```bash
cp deploy-to-apps.example.sh deploy-to-apps.sh
```
4. **Run the new script**
```bash
./deploy-to-apps.sh
```
5. **Follow prompts**
- Enter your email (can be same as before)
- Verify email via MailHog
- When asked about creating deployment, choose Yes
6. **Verify it works**
```bash
./deploy-to-apps.sh --status
```
**Result:** New `.saac.json` file with fresh deployment
---
### Option 2: Manual Migration
**Best for:** Users who want to preserve existing deployment UUID
**Steps:**
1. **Gather existing configuration**
```bash
# Save these values somewhere
APP_UUID=$(cat .deployment-uuid)
echo "Application UUID: $APP_UUID"
echo "API Key: $SAAC_API_KEY"
echo "Email: <your email used for registration>"
# From .env file
source .env
echo "Gitea Username: $GITEA_USERNAME"
echo "Subdomain: $SUBDOMAIN"
```
2. **Get your User ID**
You need to retrieve your `user_id` from the API:
```bash
# Call the API to get your user info
curl -s -X GET "https://apps.startanaicompany.com/api/v1/users/me" \
-H "X-API-Key: $SAAC_API_KEY" | jq '.'
```
Note the `user_id` from the response.
3. **Create .saac.json manually**
```bash
cat > .saac.json <<'EOF'
{
"version": "1.0",
"user": {
"email": "YOUR_EMAIL_HERE",
"user_id": "YOUR_USER_ID_HERE",
"api_key": "YOUR_API_KEY_HERE",
"gitea_username": "YOUR_GITEA_USERNAME",
"verified": true,
"registered_at": "2026-01-24T00:00:00.000Z"
},
"deployment": {
"application_uuid": "YOUR_APP_UUID_HERE",
"application_name": "SUBDOMAIN-recruit",
"domain": "SUBDOMAIN.recruit.startanaicompany.com",
"subdomain": "SUBDOMAIN",
"repository": "git@git.startanaicompany.com:USERNAME/REPONAME.git",
"deployed_at": "2026-01-24T00:00:00.000Z",
"last_updated": "2026-01-24T00:00:00.000Z"
},
"config": {
"saac_api": "https://apps.startanaicompany.com/api/v1",
"gitea_api": "https://git.startanaicompany.com/api/v1"
}
}
EOF
```
4. **Replace placeholders**
Edit `.saac.json` and replace:
- `YOUR_EMAIL_HERE` → Your email address
- `YOUR_USER_ID_HERE` → User ID from step 2
- `YOUR_API_KEY_HERE` → Your existing `SAAC_API_KEY`
- `YOUR_GITEA_USERNAME` → From `.env` file
- `YOUR_APP_UUID_HERE` → From `.deployment-uuid` file
- `SUBDOMAIN` → From `.env` file
- `USERNAME/REPONAME` → Your repository path
5. **Set proper permissions**
```bash
chmod 600 .saac.json
```
6. **Remove old files**
```bash
rm .deployment-uuid deployment-info.txt
```
7. **Test the migration**
```bash
./deploy-to-apps.sh --status
```
Expected output:
```
✅ Loaded configuration from .saac.json
User: your@email.com
Verified: true
Application: 123e4567-...
=========================================
Deployment Status
=========================================
Application UUID: 123e4567-...
Name: mycompany-recruit
...
```
8. **Test an update**
```bash
# Make a small change to .env
echo "# Test comment" >> .env
# Deploy update
./deploy-to-apps.sh --update
```
**Result:** Existing deployment preserved, now managed via `.saac.json`
---
## Verification Checklist
After migration, verify:
- [ ] `.saac.json` exists and has mode `600`
- [ ] `.saac.json` is in `.gitignore`
- [ ] Old files removed (`.deployment-uuid`, `deployment-info.txt`)
- [ ] `SAAC_API_KEY` environment variable no longer needed
- [ ] `./deploy-to-apps.sh --status` works
- [ ] `./deploy-to-apps.sh --update` works
- [ ] Site is still accessible
---
## Troubleshooting
### Problem: "Application not found"
**Possible causes:**
- Wrong `application_uuid` in `.saac.json`
- Wrong `api_key` in `.saac.json`
- Application was deleted
**Solution:**
```bash
# Verify your application exists
curl -X GET "https://apps.startanaicompany.com/api/v1/applications/YOUR_UUID" \
-H "X-API-Key: YOUR_API_KEY"
# If 404, application is gone → use Option 1 (start fresh)
# If 403, wrong API key → check your api_key value
# If 200, check UUID matches exactly
```
### Problem: "User not verified"
**Possible cause:**
- Set `verified: false` in `.saac.json`
**Solution:**
```bash
# Edit .saac.json
nano .saac.json
# Change this line:
"verified": true,
# Save and try again
./deploy-to-apps.sh
```
### Problem: "jq: command not found"
**Cause:** New dependency not installed
**Solution:**
```bash
# Ubuntu/Debian
sudo apt update && sudo apt install jq
# macOS
brew install jq
# Alpine
apk add jq
```
### Problem: "Cannot get user_id"
**Possible causes:**
- API key expired
- Wrong API key
- Account deleted
**Solution:**
```bash
# Test API key
curl -X GET "https://apps.startanaicompany.com/api/v1/users/me" \
-H "X-API-Key: $SAAC_API_KEY"
# If fails → use Option 1 (start fresh with new registration)
```
---
## Rollback (If Needed)
If you need to rollback to the old system:
1. **Restore old files**
```bash
cp .deployment-uuid.backup .deployment-uuid
cp deployment-info.txt.backup deployment-info.txt
export SAAC_API_KEY=$(cat saac-api-key.backup)
```
2. **Remove new configuration**
```bash
rm .saac.json
```
3. **Use old script**
```bash
git checkout HEAD~1 deploy-to-apps.example.sh
cp deploy-to-apps.example.sh deploy-to-apps.sh
```
**Note:** This only works if you kept backups from Option 1 Step 1
---
## FAQ
### Q: Do I need to re-verify my email?
**A:** Only if you start fresh (Option 1). If you manually migrate (Option 2) and set `verified: true`, no verification needed.
### Q: Will my existing deployment be affected?
**A:**
- **Option 1 (start fresh):** New deployment created, old one remains but disconnected
- **Option 2 (manual):** Same deployment, just new config file format
### Q: Can I have multiple deployments?
**A:** Yes! Each `.saac.json` can have one deployment. For multiple deployments:
- Use separate directories
- Or run `--setup` to replace deployment in current directory
### Q: What happens to my old API key?
**A:**
- **Option 1:** New API key generated, old one still valid but unused
- **Option 2:** Same API key, just moved from environment to `.saac.json`
### Q: Can I delete old backup files?
**A:** Yes, after verifying everything works:
```bash
rm .deployment-uuid.backup deployment-info.txt.backup saac-api-key.backup
```
### Q: Is .saac.json committed to git?
**A:** No, it's in `.gitignore`. Never commit this file (contains API key).
---
## Best Practices After Migration
1. **Backup .saac.json**
```bash
# Encrypted backup
gpg -c .saac.json
# Creates .saac.json.gpg (safe to store)
```
2. **Remove SAAC_API_KEY from shell profiles**
```bash
# Edit ~/.bashrc, ~/.zshrc, etc.
# Remove lines like:
# export SAAC_API_KEY='...'
```
3. **Document your setup**
```bash
# Create a README for your team
cat > DEPLOYMENT_README.md <<'EOF'
# Deployment
1. Copy deploy-to-apps.example.sh to deploy-to-apps.sh
2. Run ./deploy-to-apps.sh
3. Check .saac.json is created (DO NOT commit!)
4. Update site: ./deploy-to-apps.sh --update
EOF
```
4. **Test the new workflow**
- Make a small .env change
- Run update
- Verify site updates
---
## Support
If you encounter issues during migration:
1. **Check this guide** - Most issues covered above
2. **Verify prerequisites** - `jq` installed, `.env` configured
3. **Check MailHog** - For verification emails
4. **Use `--status`** - To diagnose deployment state
5. **Start fresh** - Option 1 always works
---
## Summary
### Quick Decision Tree
```
Do you have .deployment-uuid file?
├─ No → Just run ./deploy-to-apps.sh (automatic registration)
└─ Yes → Choose migration option:
├─ Want fresh start → Option 1 (recommended)
│ └─ Delete old files, run script
└─ Want same deployment → Option 2 (advanced)
└─ Manually create .saac.json
```
### Migration Time
- **Option 1:** 5-10 minutes (includes verification)
- **Option 2:** 10-15 minutes (includes API calls)
### Success Rate
- **Option 1:** 100% (fresh start always works)
- **Option 2:** 95% (requires correct user_id)
---
**Document Version:** 1.0
**Last Updated:** 2026-01-24
**Applies to:** deploy-to-apps.example.sh (January 2026 version)

342
QUICK_START.md Normal file
View File

@@ -0,0 +1,342 @@
# Quick Start Guide - AI Recruitment Site Deployment
## Prerequisites
1. **Install jq** (JSON processor)
```bash
# Ubuntu/Debian
sudo apt install jq
# macOS
brew install jq
# Alpine Linux
apk add jq
```
2. **Get Gitea API Token** (only needed for first deployment)
- Go to https://git.startanaicompany.com
- Profile → Settings → Applications
- Generate New Token (grant 'repo' permissions)
- Save the token
3. **Configure your .env file**
```bash
cp .env.example .env
nano .env # Edit with your company information
```
---
## First-Time Deployment
### Step 1: Copy the Script
```bash
cp deploy-to-apps.example.sh deploy-to-apps.sh
```
### Step 2: Set Gitea Token
```bash
export GITEA_API_TOKEN='your_gitea_token_here'
```
### Step 3: Run the Script
```bash
./deploy-to-apps.sh
```
### Step 4: Follow the Prompts
The script will:
1. **Prompt for your email**
```
Enter your email address: you@example.com
```
2. **Register your account**
- Creates user account
- Generates API key
- Saves to `.saac.json`
3. **Request verification code**
```
Check your email at MailHog:
https://mailhog.goryan.io
Enter the verification code from the email:
```
4. **Create deployment**
- Sets up application
- Configures webhook
- Deploys your site
### Step 5: Wait for Deployment
Your site will be live in 2-3 minutes at:
```
https://<subdomain>recruit.startanaicompany.com
```
---
## Updating Your Site
### Step 1: Edit .env
```bash
nano .env
# Change COMPANY_NAME, colors, contact info, etc.
```
### Step 2: Deploy Updates
```bash
./deploy-to-apps.sh
# or
./deploy-to-apps.sh --update
```
The script will:
- Load configuration from `.saac.json`
- Update environment variables
- Trigger redeployment
Your changes will be live in 2-3 minutes.
---
## Checking Deployment Status
```bash
./deploy-to-apps.sh --status
```
Output:
```
Application UUID: 123e4567-e89b-12d3-a456-426614174000
Name: mycompany-recruit
Domain: https://mycompany.recruit.startanaicompany.com
Status: running
Repository: git@git.startanaicompany.com:myuser/myrepo.git
Branch: master
Created: 2026-01-24T12:00:00Z
```
---
## Understanding .saac.json
This file stores your credentials and deployment configuration:
```json
{
"user": {
"email": "you@example.com",
"user_id": "...",
"api_key": "...",
"verified": true
},
"deployment": {
"application_uuid": "...",
"domain": "..."
}
}
```
**Important:**
- ⚠️ Never commit this file to git (it's in `.gitignore`)
- ✅ Keep it secure (permissions set to 600)
- 📋 Backup this file to restore access
---
## Common Scenarios
### Scenario 1: Interrupted Registration
If you exit during registration/verification:
```bash
./deploy-to-apps.sh
```
The script will:
- Detect existing `.saac.json`
- Check verification status
- Resume where you left off
### Scenario 2: New Deployment on Existing Account
If you want to deploy a different site:
```bash
./deploy-to-apps.sh --setup
```
This will:
- Use existing credentials
- Create new application
- Update `.saac.json` with new deployment
### Scenario 3: Lost .saac.json
If you lose your `.saac.json` file:
**Option 1:** Re-register with same email
- Run `./deploy-to-apps.sh`
- Enter same email
- New API key will be generated
- Old deployments will still work with old API key
**Option 2:** Restore from backup
- Copy backed-up `.saac.json`
- Run `./deploy-to-apps.sh --status` to verify
---
## Troubleshooting
### Error: "jq: command not found"
**Solution:** Install jq
```bash
sudo apt install jq # Ubuntu/Debian
brew install jq # macOS
```
### Error: "Email verification failed"
**Causes:**
- Wrong verification code
- Code expired (valid for 24 hours)
**Solution:**
1. Check MailHog: https://mailhog.goryan.io
2. Copy code exactly as shown
3. Try again (re-run script if needed)
### Error: "Application not found"
**Causes:**
- Application was deleted
- Wrong API key
- Corrupt `.saac.json`
**Solution:**
```bash
# Create new deployment
./deploy-to-apps.sh --setup
```
### Error: "GITEA_API_TOKEN not set"
**Cause:** Token not exported (only needed for setup mode)
**Solution:**
```bash
export GITEA_API_TOKEN='your_token_here'
./deploy-to-apps.sh --setup
```
---
## Script Modes
### Auto-detect (Default)
```bash
./deploy-to-apps.sh
```
Automatically determines:
- No `.saac.json` → Registration + Setup
- `.saac.json` exists, unverified → Verification
- `.saac.json` exists, verified, no deployment → Setup
- `.saac.json` exists, verified, has deployment → Update
### Force Setup
```bash
./deploy-to-apps.sh --setup
```
Creates new deployment (warns if one already exists)
### Force Update
```bash
./deploy-to-apps.sh --update
```
Updates existing deployment (fails if none exists)
### Check Status
```bash
./deploy-to-apps.sh --status
```
Shows deployment information
---
## Security Best Practices
1. **Never commit `.saac.json`**
- Contains API key (like a password)
- Already in `.gitignore`
2. **Backup your `.saac.json`**
- Store in secure location
- Don't upload to public services
3. **Rotate API keys if compromised**
- Delete `.saac.json`
- Re-register with new email
- Create new deployment
4. **Use environment variables for Gitea token**
```bash
# Add to ~/.bashrc or ~/.zshrc
export GITEA_API_TOKEN='your_token_here'
```
---
## Next Steps
After deployment:
1. **Configure DNS** (if using custom domain)
- Add CNAME record
- Point to: `apps.startanaicompany.com`
2. **Customize your site**
- Edit `.env` file
- Run `./deploy-to-apps.sh` to update
3. **Monitor deployment**
- Use `--status` mode
- Check application logs
4. **Set up monitoring**
- Check site regularly
- Monitor uptime
- Test features
---
## Support
- **MailHog**: https://mailhog.goryan.io
- **Gitea**: https://git.startanaicompany.com
- **SAAC API**: https://apps.startanaicompany.com/api/v1
---
**Last Updated:** 2026-01-24

383
README.md Normal file
View File

@@ -0,0 +1,383 @@
# AI Recruitment Site Template
A fully customizable recruitment website template designed for easy deployment and AI agent modification. Built with Node.js, Express, and PostgreSQL with comprehensive environment-based configuration.
## 🎯 Use This Template
This repository is marked as a template. You can create your own recruitment site in minutes!
### Quick Start (3 Steps)
1. **Use this template** - Click "Use this template" on Gitea
2. **Customize** - Edit `.env` file with your company information
3. **Deploy** - Run the automated deployment script
## ✨ Features
### Public Features
- Job listings with search and filters
- Online application system with CV upload
- Contact form
- Fully responsive design
- Dynamic branding and content
### Admin Features
- Dashboard with statistics
- Application management
- CV downloads and viewing
- Job posting management
- First-time admin account creation
### Template Features
- **Environment-based configuration** - All customization via `.env` file
- **Dynamic branding** - Company name, colors, contact info all configurable
- **AI agent friendly** - Clear structure for automated modifications
- **One-click deployment** - Automated deployment to StartAnAiCompany infrastructure
- **No hardcoded values** - Everything is configurable
## 🚀 Quick Deployment
### Prerequisites
- Gitea account at git.startanaicompany.com
- SAAC API key (get from apps.startanaicompany.com)
- Basic command line knowledge
**📖 Detailed Guide:** See [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) for complete instructions.
### Step 1: Create Your Copy
Use Gitea's template feature to create your own copy:
```bash
# Or clone the template manually
git clone https://git.startanaicompany.com/StartanAICompany/ai-recruit-site-template.git my-recruit-site
cd my-recruit-site
```
### Step 2: Customize Configuration
```bash
# Copy the example environment file
cp .env.example .env
# Edit .env with your company information
nano .env
```
**Minimum required configuration in `.env`:**
```bash
# Company Information
COMPANY_NAME="Your Recruitment Firm"
COMPANY_TAGLINE="Your Tagline Here"
COMPANY_DESCRIPTION="Your company description"
# Branding Colors
PRIMARY_COLOR=#2563EB
ACCENT_COLOR=#059669
DARK_COLOR=#1E293B
# Contact Information
CONTACT_EMAIL=info@yourcompany.com
CONTACT_PHONE=+1 (555) 123-4567
CONTACT_ADDRESS=123 Business St, City, State 12345
# Deployment Configuration
SUBDOMAIN=yourname
GITEA_USERNAME=your-gitea-username
GITEA_REPO_NAME=my-recruit-site
```
### Step 3: Register for API Key
Register once to get your deployment API key:
```bash
curl -X POST https://apps.startanaicompany.com/api/v1/register \
-H "Content-Type: application/json" \
-d '{"email":"your@email.com","gitea_username":"'${GITEA_USERNAME}'"}'
```
Save the returned API key:
```bash
export SAAC_API_KEY="cw_your_api_key_here"
```
### Step 4: Deploy
```bash
# Copy and run the deployment script
cp deploy-to-apps.example.sh deploy-to-apps.sh
chmod +x deploy-to-apps.sh
./deploy-to-apps.sh
```
The script will:
- Create application on StartAnAiCompany infrastructure
- Configure domain (yournamerecruit.startanaicompany.com)
- Set up webhooks for automatic deployments
- Trigger initial deployment
**Your site will be live in 2-3 minutes!**
**See [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) for detailed instructions and troubleshooting.**
## 🎨 Customization Guide
### Company Information
All company information is configured via environment variables in `.env`:
```bash
# Company Identity
COMPANY_NAME="Your Recruitment Firm"
COMPANY_TAGLINE="Finding the Perfect Match for Your Career"
COMPANY_DESCRIPTION="We specialize in connecting talented professionals..."
# Contact Details
CONTACT_EMAIL=info@yourcompany.com
CONTACT_PHONE=+1 (555) 123-4567
CONTACT_ADDRESS=123 Business St, Suite 100, City, State 12345
BUSINESS_HOURS=Monday - Friday: 9:00 AM - 6:00 PM
# Social Media (leave empty to hide)
SOCIAL_LINKEDIN=https://linkedin.com/company/yourcompany
SOCIAL_TWITTER=https://twitter.com/yourcompany
SOCIAL_FACEBOOK=
```
### Branding Colors
Customize your brand colors:
```bash
PRIMARY_COLOR=#2563EB # Main brand color (buttons, links)
ACCENT_COLOR=#059669 # Success/accent color
DARK_COLOR=#1E293B # Dark elements (footer, headings)
```
Colors update automatically across the entire site!
### About Page Content
```bash
ABOUT_MISSION=Our mission statement here
ABOUT_VISION=Our vision statement here
ABOUT_VALUES=Integrity, Excellence, Innovation, Partnership
```
### Services Offered
```bash
SERVICES_LIST=Executive Search,Contract Staffing,Permanent Placement,Career Consulting
```
### Feature Configuration
```bash
MAX_CV_SIZE_MB=5
ALLOWED_CV_TYPES=application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document
```
## 🤖 AI Agent Modification
This template is designed to be easily modified by AI agents like Claude Code. The structure includes:
### Clear File Organization
```
ai-recruit-site-template/
├── config.js # Centralized configuration
├── server.js # Main application
├── public/
│ ├── js/
│ │ └── init.js # Dynamic config injection
│ ├── css/
│ │ └── styles.css # CSS with variables
│ └── *.html # HTML with data attributes
├── migrations/ # Database migrations
└── .env.example # Configuration template
```
### Data Attributes
HTML elements use data attributes for dynamic content:
```html
<div data-company-name>Your Company</div>
<h1 data-company-tagline>Your Tagline</h1>
<a data-contact-email>email@example.com</a>
```
### Configuration API
Frontend can access configuration via API:
```javascript
// GET /api/config returns:
{
company: { name, tagline, description },
branding: { primaryColor, accentColor, darkColor },
contact: { email, phone, address, businessHours },
social: { linkedin, twitter, facebook },
about: { mission, vision, values },
services: { list: [...] }
}
```
## 💻 Local Development
### With Docker (Recommended)
```bash
cp .env.example .env
# Edit .env with your settings
docker-compose up -d
```
Access at: http://localhost:3000
### Without Docker
```bash
# Install dependencies
npm install
# Start PostgreSQL (must be running)
# Create database 'recruitment'
# Copy environment file
cp .env.example .env
# Start application
npm start
```
## 📊 Technology Stack
- **Backend**: Node.js 18 + Express
- **Database**: PostgreSQL 15 with BYTEA for CV storage
- **Authentication**: Session-based with bcrypt
- **File Upload**: Multer (PDF, DOC, DOCX)
- **Deployment**: Docker Compose + StartAnAiCompany platform
- **DNS**: Cloudflare
## 🔐 Security Features
- Password hashing with bcrypt (10 rounds)
- Secure HTTP-only session cookies
- SQL injection protection (parameterized queries)
- File upload validation (type and size)
- Environment-based secrets
- Automatic HTTPS in production
## 📚 API Documentation
### Public APIs
- `GET /api/config` - Get site configuration
- `GET /api/jobs` - List active jobs
- `GET /api/jobs/:id` - Get job details
- `POST /api/apply` - Submit application (multipart/form-data)
- `POST /api/contact` - Submit contact form
### Admin APIs (Authentication Required)
- `POST /api/admin/login` - Login / Create first admin
- `GET /api/admin/check` - Check auth status
- `POST /api/admin/logout` - Logout
- `GET /api/admin/stats` - Dashboard statistics
- `GET /api/admin/applications` - List applications (with filters)
- `GET /api/admin/applications/:id` - Get application details
- `GET /api/admin/applications/:id/cv` - Download CV
- `PATCH /api/admin/applications/:id` - Update application status
- `GET /api/admin/jobs` - List all jobs
- `POST /api/admin/jobs` - Create job
- `PATCH /api/admin/jobs/:id` - Update job (activate/deactivate)
## 🆘 Troubleshooting
### Deployment Issues
```bash
# Check deployment logs via SAAC API
curl -H "X-API-Key: $SAAC_API_KEY" \
"https://apps.startanaicompany.com/api/v1/applications/YOUR_APP_UUID/logs?tail=100"
# Re-trigger deployment
curl -X POST -H "X-API-Key: $SAAC_API_KEY" \
https://apps.startanaicompany.com/api/v1/applications/YOUR_APP_UUID/deploy
```
### Database Issues
```bash
# Check database connection
docker-compose logs postgres
# Access database
docker-compose exec postgres psql -U postgres -d recruitment
# List tables
\dt
```
### Configuration Not Updating
Make sure you:
1. Updated `.env` file
2. Committed and pushed changes (triggers webhook)
3. Waited 2-3 minutes for redeployment
## 🎓 First Time Setup
1. Visit your deployment URL
2. Go to `/admin/login`
3. System detects no admin exists
4. Fill in email, password, and full name
5. Click "Create Admin Account"
6. You're logged in!
## 📝 Environment Variables Reference
| Variable | Description | Default | Required |
|----------|-------------|---------|----------|
| `COMPANY_NAME` | Your company name | Your Recruitment Firm | Yes |
| `COMPANY_TAGLINE` | Main tagline | Finding the Perfect Match... | Yes |
| `COMPANY_DESCRIPTION` | Company description | We specialize in... | Yes |
| `PRIMARY_COLOR` | Primary brand color | #2563EB | No |
| `ACCENT_COLOR` | Accent color | #059669 | No |
| `DARK_COLOR` | Dark color | #1E293B | No |
| `CONTACT_EMAIL` | Contact email | info@yourcompany.com | Yes |
| `CONTACT_PHONE` | Contact phone | +1 (555) 123-4567 | Yes |
| `CONTACT_ADDRESS` | Physical address | 123 Business St... | Yes |
| `BUSINESS_HOURS` | Business hours | Monday - Friday... | No |
| `SOCIAL_LINKEDIN` | LinkedIn URL | (empty) | No |
| `SOCIAL_TWITTER` | Twitter URL | (empty) | No |
| `SOCIAL_FACEBOOK` | Facebook URL | (empty) | No |
| `SUBDOMAIN` | Your subdomain | yourname | Yes (deploy) |
| `GITEA_USERNAME` | Your Gitea username | - | Yes (deploy) |
| `GITEA_REPO_NAME` | Your repo name | - | Yes (deploy) |
| `DB_PASSWORD` | Database password | (auto-generated) | No |
| `SESSION_SECRET` | Session secret | (auto-generated) | No |
| `MAX_CV_SIZE_MB` | Max CV file size | 5 | No |
| `ALLOWED_CV_TYPES` | Allowed CV MIME types | pdf,doc,docx | No |
## 🤝 Contributing
This is a template repository. Feel free to:
- Fork it for your own recruitment site
- Customize it to your needs
- Submit improvements via pull request
## 📄 License
MIT License - feel free to use for commercial projects
## 🆘 Support
For issues or questions:
- Check the [How to set up a template repo guide](/home/milko/projects/airepotemplates/How-to-set-up-a-template-repo.md)
- Contact: info@startanaicompany.com

96
config.js Normal file
View File

@@ -0,0 +1,96 @@
// Configuration module for AI Recruitment Site Template
// Centralizes all environment variable loading with sensible defaults
const crypto = require('crypto');
// Helper function to generate secure random strings
function generateSecret(length = 64) {
return crypto.randomBytes(length).toString('hex');
}
// Company Information
const company = {
name: process.env.COMPANY_NAME || 'Your Recruitment Firm',
tagline: process.env.COMPANY_TAGLINE || 'Finding the Perfect Match for Your Career',
description: process.env.COMPANY_DESCRIPTION || 'We specialize in connecting talented professionals with exceptional opportunities across various industries.'
};
// Branding Colors
const branding = {
primaryColor: process.env.PRIMARY_COLOR || '#2563EB',
accentColor: process.env.ACCENT_COLOR || '#059669',
darkColor: process.env.DARK_COLOR || '#1E293B'
};
// Contact Information
const contact = {
email: process.env.CONTACT_EMAIL || 'info@yourcompany.com',
phone: process.env.CONTACT_PHONE || '+1 (555) 123-4567',
address: process.env.CONTACT_ADDRESS || '123 Business St, Suite 100, City, State 12345',
businessHours: process.env.BUSINESS_HOURS || 'Monday - Friday: 9:00 AM - 6:00 PM'
};
// Social Media Links
const social = {
linkedin: process.env.SOCIAL_LINKEDIN || '',
twitter: process.env.SOCIAL_TWITTER || '',
facebook: process.env.SOCIAL_FACEBOOK || ''
};
// Application Settings
const app = {
nodeEnv: process.env.NODE_ENV || 'development',
port: parseInt(process.env.PORT || '3000', 10),
sessionSecret: process.env.SESSION_SECRET || generateSecret()
};
// Database Configuration
const database = {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432', 10),
name: process.env.DB_NAME || 'recruitment',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'changeme123'
};
// Feature Configuration
const features = {
maxCvSizeMB: parseInt(process.env.MAX_CV_SIZE_MB || '5', 10),
allowedCvTypes: (process.env.ALLOWED_CV_TYPES || 'application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document').split(',')
};
// About Page Content
const about = {
mission: process.env.ABOUT_MISSION || 'Our mission is to bridge the gap between exceptional talent and outstanding opportunities.',
vision: process.env.ABOUT_VISION || 'We envision a world where every professional finds their perfect career match.',
values: process.env.ABOUT_VALUES || 'Integrity, Excellence, Innovation, Partnership'
};
// Services
const services = {
list: (process.env.SERVICES_LIST || 'Executive Search,Contract Staffing,Permanent Placement,Career Consulting,Talent Assessment,Industry Expertise').split(',')
};
// Email Configuration
const email = {
smtpHost: process.env.SMTP_HOST || '',
smtpPort: parseInt(process.env.SMTP_PORT || '587', 10),
smtpUser: process.env.SMTP_USER || '',
smtpPassword: process.env.SMTP_PASSWORD || '',
smtpFrom: process.env.SMTP_FROM || 'noreply@yourcompany.com',
contactFormRecipient: process.env.CONTACT_FORM_RECIPIENT || contact.email
};
// Export configuration object
module.exports = {
company,
branding,
contact,
social,
app,
database,
features,
about,
services,
email
};

813
deploy-to-apps.example.sh Normal file
View File

@@ -0,0 +1,813 @@
#!/bin/bash
# ========================================
# AI Recruitment Site - SAAC Deployment Script
# ========================================
# This script deploys your recruitment site to StartAnAiCompany infrastructure
# via apps.startanaicompany.com
#
# IMPORTANT: Copy this file to deploy-to-apps.sh and customize it
# DO NOT commit deploy-to-apps.sh to git (it's in .gitignore)
#
# Prerequisites:
# 1. Install jq (JSON processor): apt install jq or brew install jq
# 2. Set GITEA_API_TOKEN environment variable (required for setup mode)
# 3. Customize your .env file with company information
#
# First-time users:
# - Script will prompt for email and handle registration/verification automatically
# - Check MailHog at https://mailhog.goryan.io for verification code
# - Configuration saved to .saac.json (do not commit to git!)
#
# Modes:
# ./deploy-to-apps.sh # Auto-detect (setup or update)
# ./deploy-to-apps.sh --setup # Force new deployment
# ./deploy-to-apps.sh --update # Force update existing
# ./deploy-to-apps.sh --status # Check deployment status
set -e # Exit on error
# Configuration
SAAC_CONFIG_FILE=".saac.json"
SAAC_API="https://apps.startanaicompany.com/api/v1"
GITEA_API="https://git.startanaicompany.com/api/v1"
# ========================================
# Helper Functions
# ========================================
# Load configuration from .saac.json
load_config() {
if [ -f "$SAAC_CONFIG_FILE" ]; then
USER_EMAIL=$(jq -r '.user.email // ""' "$SAAC_CONFIG_FILE")
USER_ID=$(jq -r '.user.user_id // ""' "$SAAC_CONFIG_FILE")
SAAC_API_KEY=$(jq -r '.user.api_key // ""' "$SAAC_CONFIG_FILE")
GITEA_USERNAME=$(jq -r '.user.gitea_username // ""' "$SAAC_CONFIG_FILE")
VERIFIED=$(jq -r '.user.verified // false' "$SAAC_CONFIG_FILE")
APP_UUID=$(jq -r '.deployment.application_uuid // ""' "$SAAC_CONFIG_FILE")
APP_NAME=$(jq -r '.deployment.application_name // ""' "$SAAC_CONFIG_FILE")
DEPLOYED_AT=$(jq -r '.deployment.deployed_at // ""' "$SAAC_CONFIG_FILE")
return 0
fi
return 1
}
# Save configuration to .saac.json
save_config() {
cat > "$SAAC_CONFIG_FILE" <<CONFIG
{
"version": "1.0",
"user": {
"email": "$USER_EMAIL",
"user_id": "$USER_ID",
"api_key": "$SAAC_API_KEY",
"gitea_username": "$GITEA_USERNAME",
"verified": $VERIFIED,
"registered_at": "$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")"
},
"deployment": {
"application_uuid": "$APP_UUID",
"application_name": "$APP_NAME",
"domain": "$DOMAIN",
"subdomain": "$SUBDOMAIN",
"repository": "$REPO_URL",
"deployed_at": "$DEPLOYED_AT",
"last_updated": "$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")"
},
"config": {
"saac_api": "$SAAC_API",
"gitea_api": "$GITEA_API"
}
}
CONFIG
chmod 600 "$SAAC_CONFIG_FILE"
echo "💾 Configuration saved to $SAAC_CONFIG_FILE"
}
# ========================================
# Check Dependencies
# ========================================
# Check if jq is installed
if ! command -v jq >/dev/null 2>&1; then
echo "❌ Error: jq is not installed"
echo ""
echo "jq is required for JSON processing. Install it with:"
echo " - Ubuntu/Debian: sudo apt install jq"
echo " - macOS: brew install jq"
echo " - Alpine: apk add jq"
echo ""
exit 1
fi
# ========================================
# Parse Command Line Arguments
# ========================================
MODE="auto"
REGISTER_EMAIL=""
REGISTER_GITEA_USERNAME=""
VERIFY_CODE_PARAM=""
# Parse arguments
while [ $# -gt 0 ]; do
case "$1" in
--setup)
MODE="setup"
shift
;;
--update)
MODE="update"
shift
;;
--status)
MODE="status"
shift
;;
--register)
MODE="registration"
shift
;;
--email)
REGISTER_EMAIL="$2"
shift 2
;;
--gitea-username)
REGISTER_GITEA_USERNAME="$2"
shift 2
;;
--verify-email-code)
VERIFY_CODE_PARAM="$2"
shift 2
;;
--help)
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Modes:"
echo " (default) Auto-detect mode (registration/verification/setup/update)"
echo " --setup Force new deployment"
echo " --update Force update existing deployment"
echo " --status Check deployment status"
echo " --register Force registration mode"
echo ""
echo "Registration Options:"
echo " --email EMAIL Email for registration"
echo " --gitea-username USERNAME Gitea username (optional, auto-detected from git)"
echo " --verify-email-code CODE Verification code from email (for automation)"
echo ""
echo "Environment Variables Required:"
echo " GITEA_API_TOKEN Gitea API token (for webhook setup)"
echo ""
echo "Examples:"
echo " # Interactive registration (prompts for email and verification code)"
echo " $0"
echo ""
echo " # Step 1: Non-interactive registration (returns and waits)"
echo " $0 --register --email user@example.com"
echo ""
echo " # Step 2: Verify with code (after checking MailHog)"
echo " $0 --verify-email-code 123456"
echo ""
echo " # Full non-interactive (if you already have the code)"
echo " $0 --register --email user@example.com --verify-email-code 123456"
echo ""
echo " # Update existing deployment"
echo " $0 --update"
exit 0
;;
*)
echo "❌ Unknown option: $1"
echo "Run '$0 --help' for usage information"
exit 1
;;
esac
done
# ========================================
# Load Environment Variables
# ========================================
if [ ! -f .env ]; then
echo "❌ Error: .env file not found. Copy .env.example to .env and customize it."
exit 1
fi
source .env
# Check required .env variables
if [ -z "$COMPANY_NAME" ]; then
echo "❌ Error: COMPANY_NAME not set in .env"
exit 1
fi
if [ -z "$SUBDOMAIN" ]; then
echo "❌ Error: SUBDOMAIN not set in .env"
exit 1
fi
# ========================================
# Load or Create Configuration
# ========================================
# Try to load existing configuration
if load_config; then
echo "✅ Loaded configuration from $SAAC_CONFIG_FILE"
echo " User: $USER_EMAIL"
echo " Verified: $VERIFIED"
if [ -n "$APP_UUID" ]; then
echo " Application: $APP_UUID"
fi
echo ""
else
# No configuration exists - need to register
echo "========================================="
echo " First-Time Setup"
echo "========================================="
echo ""
echo "No configuration found. Let's register your account."
echo ""
# Get Gitea username from .env or parameter
if [ -n "$REGISTER_GITEA_USERNAME" ]; then
GITEA_USERNAME="$REGISTER_GITEA_USERNAME"
elif [ -z "$GITEA_USERNAME" ]; then
# Try to auto-detect from git config
GITEA_USERNAME=$(git config user.name 2>/dev/null || echo "")
if [ -z "$GITEA_USERNAME" ]; then
echo "❌ Error: GITEA_USERNAME not provided"
echo ""
echo "Provide it via:"
echo " 1. --gitea-username parameter: $0 --register --email EMAIL --gitea-username USERNAME"
echo " 2. GITEA_USERNAME in .env file"
echo " 3. Git config: git config user.name"
exit 1
fi
fi
# Get email from parameter (no prompting)
if [ -n "$REGISTER_EMAIL" ]; then
USER_EMAIL="$REGISTER_EMAIL"
echo "📧 Using email: $USER_EMAIL"
else
echo "❌ Error: Email address is required"
echo ""
echo "Usage:"
echo " $0 --register --email YOUR_EMAIL"
echo ""
echo "Example:"
echo " $0 --register --email ryan.andersson@goryan.io"
exit 1
fi
echo ""
echo "📧 Registering user: $USER_EMAIL"
echo " Gitea username: $GITEA_USERNAME"
echo ""
# Register user
REGISTER_RESPONSE=$(curl -s -X POST "${SAAC_API}/users/register" \
-H "Content-Type: application/json" \
-d "{\"email\":\"$USER_EMAIL\",\"gitea_username\":\"$GITEA_USERNAME\"}")
# Check for errors
if echo "$REGISTER_RESPONSE" | grep -q "error"; then
echo "❌ Registration failed:"
echo "$REGISTER_RESPONSE" | jq '.' 2>/dev/null || echo "$REGISTER_RESPONSE"
exit 1
fi
# Extract user data
USER_ID=$(echo "$REGISTER_RESPONSE" | jq -r '.user_id')
EMAIL_VERIFIED=$(echo "$REGISTER_RESPONSE" | jq -r '.email_verified // false')
SAAC_API_KEY=$(echo "$REGISTER_RESPONSE" | jq -r '.api_key // ""')
EXISTING_USER=$(echo "$REGISTER_RESPONSE" | jq -r '.existing_user // false')
VERIFICATION_SENT=$(echo "$REGISTER_RESPONSE" | jq -r '.verification_code_sent // false')
if [ "$USER_ID" = "null" ] || [ -z "$USER_ID" ]; then
echo "❌ Failed to extract user data from registration response"
echo "Response: $REGISTER_RESPONSE"
exit 1
fi
# Check if user is verified and has API key
if [ "$EMAIL_VERIFIED" = "true" ] && [ -n "$SAAC_API_KEY" ] && [ "$SAAC_API_KEY" != "null" ]; then
# Email verification disabled OR user already verified - API key provided
echo "✅ User registered!"
echo " User ID: $USER_ID"
echo " 🔑 API key received"
echo " Email verified: true"
else
# Email verification required
if [ "$EXISTING_USER" = "true" ]; then
echo " User already exists but not verified"
echo " User ID: $USER_ID"
if [ "$VERIFICATION_SENT" = "true" ]; then
echo " ✅ New verification code sent!"
else
echo " ⚠️ Failed to send verification email"
fi
# Load existing API key from config if it exists
if [ -f "$SAAC_CONFIG_FILE" ]; then
EXISTING_API_KEY=$(jq -r '.user.api_key // ""' "$SAAC_CONFIG_FILE")
if [ -n "$EXISTING_API_KEY" ] && [ "$EXISTING_API_KEY" != "null" ]; then
SAAC_API_KEY="$EXISTING_API_KEY"
echo " 🔑 Using existing API key from config"
fi
fi
else
# New user with email verification enabled
echo "✅ User registered!"
echo " User ID: $USER_ID"
echo " Email verified: false"
echo " Verification code sent: $VERIFICATION_SENT"
echo ""
# No API key yet - will get after verification
SAAC_API_KEY=""
fi
fi
echo ""
# Save initial configuration (unverified)
VERIFIED=false
APP_UUID=""
APP_NAME=""
DOMAIN=""
REPO_URL=""
DEPLOYED_AT=""
save_config
echo ""
# Prompt for verification code
echo "========================================="
echo " Email Verification Required"
echo "========================================="
echo ""
if [ "$EXISTING_USER" = "true" ]; then
echo "📧 New verification code sent to: $USER_EMAIL"
else
echo "📧 Verification email sent to: $USER_EMAIL"
fi
echo ""
echo "🔍 Check your email at MailHog:"
echo " https://mailhog.goryan.io"
echo ""
# Use parameter if provided, otherwise exit with instructions
if [ -n "$VERIFY_CODE_PARAM" ]; then
VERIFY_CODE="$VERIFY_CODE_PARAM"
echo "🔐 Using verification code from parameter"
else
echo "❌ Verification code required"
echo ""
echo "Check MailHog for your verification code, then run:"
echo " $0 --verify-email-code YOUR_CODE"
echo ""
echo "Configuration saved to $SAAC_CONFIG_FILE"
exit 1
fi
echo ""
echo "🔐 Verifying email..."
echo " User ID: $USER_ID"
echo " Code: $VERIFY_CODE"
# Verify email (public endpoint - no API key needed)
VERIFY_RESPONSE=$(curl -s -X POST "${SAAC_API}/users/verify" \
-H "Content-Type: application/json" \
-d "{\"user_id\":\"$USER_ID\",\"verification_code\":\"$VERIFY_CODE\"}")
# Check if verified
if echo "$VERIFY_RESPONSE" | grep -q '"verified":true'; then
echo "✅ Email verified successfully!"
echo ""
# Extract API key from verification response
VERIFIED_API_KEY=$(echo "$VERIFY_RESPONSE" | jq -r '.api_key // ""')
if [ -n "$VERIFIED_API_KEY" ] && [ "$VERIFIED_API_KEY" != "null" ]; then
SAAC_API_KEY="$VERIFIED_API_KEY"
echo "🔑 API key received and saved"
echo ""
fi
VERIFIED=true
save_config
echo ""
else
echo "❌ Verification failed"
echo "$VERIFY_RESPONSE" | jq '.' 2>/dev/null || echo "$VERIFY_RESPONSE"
echo ""
echo "You can verify later by running this script again."
echo "Your configuration has been saved to $SAAC_CONFIG_FILE"
exit 1
fi
fi
# ========================================
# Check Verification Status
# ========================================
if [ "$VERIFIED" != "true" ]; then
echo "========================================="
echo " Email Verification Required"
echo "========================================="
echo ""
echo "Your email ($USER_EMAIL) is not verified yet."
echo ""
echo "🔍 Check your email at MailHog:"
echo " https://mailhog.goryan.io"
echo ""
# Use parameter if provided, otherwise exit with instructions
if [ -n "$VERIFY_CODE_PARAM" ]; then
VERIFY_CODE="$VERIFY_CODE_PARAM"
echo "🔐 Using verification code from parameter"
else
echo "❌ Verification code required"
echo ""
echo "Run with: $0 --verify-email-code YOUR_CODE"
exit 1
fi
echo ""
echo "🔐 Verifying email..."
echo " User ID: $USER_ID"
echo " Code: $VERIFY_CODE"
# Verify email (public endpoint - no API key needed)
VERIFY_RESPONSE=$(curl -s -X POST "${SAAC_API}/users/verify" \
-H "Content-Type: application/json" \
-d "{\"user_id\":\"$USER_ID\",\"verification_code\":\"$VERIFY_CODE\"}")
# Check if verified
if echo "$VERIFY_RESPONSE" | grep -q '"verified":true'; then
echo "✅ Email verified successfully!"
echo ""
# Extract API key from verification response
VERIFIED_API_KEY=$(echo "$VERIFY_RESPONSE" | jq -r '.api_key // ""')
if [ -n "$VERIFIED_API_KEY" ] && [ "$VERIFIED_API_KEY" != "null" ]; then
SAAC_API_KEY="$VERIFIED_API_KEY"
echo "🔑 API key received and saved"
echo ""
fi
VERIFIED=true
save_config
echo ""
else
echo "❌ Verification failed"
echo "$VERIFY_RESPONSE" | jq '.' 2>/dev/null || echo "$VERIFY_RESPONSE"
exit 1
fi
fi
# ========================================
# Auto-detect Mode
# ========================================
if [ "$MODE" = "auto" ]; then
if [ -n "$APP_UUID" ]; then
MODE="update"
else
MODE="setup"
fi
fi
# ========================================
# Setup Mode: Additional Checks
# ========================================
if [ "$MODE" = "setup" ]; then
# GITEA_API_TOKEN only required for setup mode (webhook creation)
if [ -z "$GITEA_API_TOKEN" ]; then
echo "❌ Error: GITEA_API_TOKEN environment variable not set"
echo ""
echo "GITEA_API_TOKEN is required for setup mode to:"
echo " - Set up automatic deployment webhooks"
echo ""
echo "To get your Gitea API token:"
echo "1. Go to https://git.startanaicompany.com"
echo "2. Click your profile → Settings → Applications"
echo "3. Generate New Token (grant 'repo' permissions)"
echo "4. Export it: export GITEA_API_TOKEN='your_token_here'"
echo ""
exit 1
fi
if [ -z "$GITEA_REPO_NAME" ]; then
echo "❌ Error: GITEA_REPO_NAME not set in .env"
exit 1
fi
fi
# ========================================
# Build URLs
# ========================================
# Repository URL (SSH format required by Coolify for private repos)
REPO_URL="git@git.startanaicompany.com:${GITEA_USERNAME}/${GITEA_REPO_NAME}.git"
# Domain (e.g., annarecruit.startanaicompany.com or johnrecruit.startanaicompany.com)
FULL_DOMAIN="${SUBDOMAIN}recruit.startanaicompany.com"
# ========================================
# MODE: STATUS
# ========================================
if [ "$MODE" = "status" ]; then
echo "========================================="
echo " Deployment Status"
echo "========================================="
echo ""
# Check if deployment exists
if [ -z "$APP_UUID" ]; then
echo "❌ No deployment found"
echo " Run './deploy-to-apps.sh --setup' to create a new deployment"
exit 1
fi
echo "📦 Fetching status for application: $APP_UUID"
echo ""
# Get application details
STATUS_RESPONSE=$(curl -s -X GET "${SAAC_API}/applications/${APP_UUID}" \
-H "X-API-Key: ${SAAC_API_KEY}")
# Check for errors
if echo "$STATUS_RESPONSE" | grep -q "error"; then
echo "❌ Failed to fetch status:"
echo "$STATUS_RESPONSE" | jq '.' 2>/dev/null || echo "$STATUS_RESPONSE"
exit 1
fi
# Display formatted status
echo "Application UUID: $APP_UUID"
echo "Name: $(echo "$STATUS_RESPONSE" | jq -r '.app_name')"
echo "Domain: $(echo "$STATUS_RESPONSE" | jq -r '.domain')"
echo "Status: $(echo "$STATUS_RESPONSE" | jq -r '.status')"
echo "Repository: $(echo "$STATUS_RESPONSE" | jq -r '.git_repo')"
echo "Branch: $(echo "$STATUS_RESPONSE" | jq -r '.git_branch')"
echo "Created: $(echo "$STATUS_RESPONSE" | jq -r '.created_at')"
echo ""
echo "🔍 View logs:"
echo " curl -H \"X-API-Key: ${SAAC_API_KEY}\" \\"
echo " ${SAAC_API}/applications/${APP_UUID}/logs"
echo ""
exit 0
fi
# ========================================
# MODE: UPDATE
# ========================================
if [ "$MODE" = "update" ]; then
echo "========================================="
echo " AI Recruitment Site Deployment"
echo "========================================="
echo "📝 Mode: UPDATE (existing deployment)"
echo ""
# Check if deployment exists
if [ -z "$APP_UUID" ]; then
echo "❌ No deployment found in configuration"
echo " Run './deploy-to-apps.sh --setup' to create a new deployment"
exit 1
fi
echo "📦 Configuration:"
echo " Application UUID: $APP_UUID"
echo " Company: $COMPANY_NAME"
echo " Domain: https://$FULL_DOMAIN"
echo ""
# Verify application still exists
echo "🔍 Verifying application exists..."
VERIFY_RESPONSE=$(curl -s -X GET "${SAAC_API}/applications/${APP_UUID}" \
-H "X-API-Key: ${SAAC_API_KEY}")
if echo "$VERIFY_RESPONSE" | grep -q "error"; then
echo "❌ Application not found or access denied"
echo " The application may have been deleted or UUID is invalid"
echo " Run './deploy-to-apps.sh --setup' to create a new deployment"
exit 1
fi
echo "✅ Application verified"
echo ""
# Update environment variables
echo "🔄 Updating environment variables..."
UPDATE_RESPONSE=$(curl -s -X PATCH "${SAAC_API}/applications/${APP_UUID}/env" \
-H "X-API-Key: ${SAAC_API_KEY}" \
-H "Content-Type: application/json" \
-d "{
\"variables\": {
\"COMPANY_NAME\": \"${COMPANY_NAME}\",
\"COMPANY_TAGLINE\": \"${COMPANY_TAGLINE}\",
\"COMPANY_DESCRIPTION\": \"${COMPANY_DESCRIPTION}\",
\"PRIMARY_COLOR\": \"${PRIMARY_COLOR}\",
\"ACCENT_COLOR\": \"${ACCENT_COLOR}\",
\"DARK_COLOR\": \"${DARK_COLOR}\",
\"CONTACT_EMAIL\": \"${CONTACT_EMAIL}\",
\"CONTACT_PHONE\": \"${CONTACT_PHONE}\",
\"CONTACT_ADDRESS\": \"${CONTACT_ADDRESS}\"
}
}")
# Check for errors
if echo "$UPDATE_RESPONSE" | grep -q "error"; then
echo "❌ Failed to update environment variables:"
echo "$UPDATE_RESPONSE" | jq '.' 2>/dev/null || echo "$UPDATE_RESPONSE"
exit 1
fi
echo "✅ Environment variables updated"
echo ""
# Trigger redeployment
echo "🚀 Triggering redeployment..."
DEPLOY_RESPONSE=$(curl -s -X POST "${SAAC_API}/applications/${APP_UUID}/deploy" \
-H "X-API-Key: ${SAAC_API_KEY}")
# Check for errors
if echo "$DEPLOY_RESPONSE" | grep -q "error"; then
echo "❌ Failed to trigger deployment:"
echo "$DEPLOY_RESPONSE" | jq '.' 2>/dev/null || echo "$DEPLOY_RESPONSE"
exit 1
fi
echo "✅ Deployment triggered"
echo ""
# Update configuration with latest timestamp
DOMAIN="$FULL_DOMAIN"
save_config
echo ""
echo "========================================="
echo " Update Complete!"
echo "========================================="
echo ""
echo "⏳ Your changes will be live in 2-3 minutes at:"
echo " https://$FULL_DOMAIN"
echo ""
echo "🔍 Monitor deployment:"
echo " ./deploy-to-apps.sh --status"
echo ""
exit 0
fi
# ========================================
# MODE: SETUP
# ========================================
echo "========================================="
echo " AI Recruitment Site Deployment"
echo "========================================="
echo "📝 Mode: SETUP (new deployment)"
echo ""
echo "📦 Configuration:"
echo " Company: $COMPANY_NAME"
echo " Repository: $REPO_URL"
echo " Domain: https://$FULL_DOMAIN"
echo ""
# Warn if deployment already exists
if [ -n "$APP_UUID" ]; then
echo "⚠️ Warning: Existing deployment found in configuration"
echo " Current Application UUID: $APP_UUID"
echo " This will create a NEW deployment and overwrite the configuration."
echo ""
read -p "Continue? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Cancelled."
exit 0
fi
echo ""
fi
# Create application via SAAC API
echo "📝 Creating application on StartAnAiCompany server..."
APP_RESPONSE=$(curl -s -X POST "${SAAC_API}/applications" \
-H "X-API-Key: ${SAAC_API_KEY}" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"${SUBDOMAIN}-recruit\",
\"subdomain\": \"${SUBDOMAIN}\",
\"domain_suffix\": \"recruit.startanaicompany.com\",
\"git_repository\": \"${REPO_URL}\",
\"git_branch\": \"master\",
\"gitea_api_token\": \"${GITEA_API_TOKEN}\",
\"template_type\": \"recruitment\",
\"environment_variables\": {
\"COMPANY_NAME\": \"${COMPANY_NAME}\",
\"COMPANY_TAGLINE\": \"${COMPANY_TAGLINE}\",
\"COMPANY_DESCRIPTION\": \"${COMPANY_DESCRIPTION}\",
\"PRIMARY_COLOR\": \"${PRIMARY_COLOR}\",
\"ACCENT_COLOR\": \"${ACCENT_COLOR}\",
\"DARK_COLOR\": \"${DARK_COLOR}\",
\"CONTACT_EMAIL\": \"${CONTACT_EMAIL}\",
\"CONTACT_PHONE\": \"${CONTACT_PHONE}\",
\"CONTACT_ADDRESS\": \"${CONTACT_ADDRESS}\"
}
}")
# Check for errors
if echo "$APP_RESPONSE" | grep -q "error"; then
echo "❌ Deployment failed:"
echo "$APP_RESPONSE" | jq '.' 2>/dev/null || echo "$APP_RESPONSE"
exit 1
fi
# Extract application details
APP_UUID=$(echo "$APP_RESPONSE" | jq -r '.application_uuid' 2>/dev/null)
DOMAIN=$(echo "$APP_RESPONSE" | jq -r '.domain' 2>/dev/null)
WEBHOOK_URL=$(echo "$APP_RESPONSE" | jq -r '.webhook_url' 2>/dev/null)
if [ "$APP_UUID" = "null" ] || [ -z "$APP_UUID" ]; then
echo "❌ Failed to create application"
echo "Response: $APP_RESPONSE"
exit 1
fi
echo "✅ Application created!"
echo " Application UUID: $APP_UUID"
echo " Domain: $DOMAIN"
echo ""
# Save configuration
APP_NAME="${SUBDOMAIN}-recruit"
DEPLOYED_AT=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
save_config
echo ""
# Configure webhook for automatic deployments
echo "🪝 Setting up deployment webhook..."
WEBHOOK_RESPONSE=$(curl -s -X POST "${GITEA_API}/repos/${GITEA_USERNAME}/${GITEA_REPO_NAME}/hooks" \
-H "Authorization: token ${GITEA_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"type\": \"gitea\",
\"config\": {
\"url\": \"${WEBHOOK_URL}\",
\"content_type\": \"json\",
\"http_method\": \"GET\"
},
\"events\": [\"push\"],
\"authorization_header\": \"Bearer ${SAAC_API_KEY}\",
\"active\": true
}")
WEBHOOK_ID=$(echo "$WEBHOOK_RESPONSE" | jq -r '.id' 2>/dev/null)
if [ "$WEBHOOK_ID" = "null" ] || [ -z "$WEBHOOK_ID" ]; then
echo "⚠️ Warning: Failed to create Gitea webhook (may already exist)"
else
echo "✅ Webhook configured for automatic deployments"
fi
echo ""
echo "========================================="
echo " Deployment Complete!"
echo "========================================="
echo ""
echo "📋 Deployment Details:"
echo " Application UUID: $APP_UUID"
echo " Domain: $DOMAIN"
echo ""
echo "⏳ Your site will be available in 2-3 minutes at:"
echo " $DOMAIN"
echo ""
echo "📝 Next Steps:"
echo " 1. Configure DNS (if not already done):"
echo " - For Cloudflare: Add CNAME record"
echo " - Name: ${SUBDOMAIN}recruit"
echo " - Target: apps.startanaicompany.com"
echo " - Proxy: Enabled"
echo ""
echo " 2. Wait 2-3 minutes for deployment to complete"
echo ""
echo " 3. Visit your site:"
echo " $DOMAIN"
echo ""
echo "🔄 To update your deployment:"
echo " 1. Edit .env file with new values"
echo " 2. Run: ./deploy-to-apps.sh"
echo " (Auto-detects update mode since UUID file exists)"
echo ""
echo "🔍 Monitor deployment:"
echo " ./deploy-to-apps.sh --status"
echo ""
echo "💾 Configuration saved to:"
echo " - $SAAC_CONFIG_FILE (user credentials and deployment info)"
echo " ⚠️ Keep this file secure and do NOT commit it to git!"
echo ""

51
docker-compose.yml Normal file
View File

@@ -0,0 +1,51 @@
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: recruitment-postgres
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME:-recruitment}
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme123}
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
networks:
- recruitment-network
app:
build: .
container_name: recruitment-app
restart: unless-stopped
ports:
- "${PORT:-3000}:3000"
environment:
NODE_ENV: production
PORT: 3000
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: ${DB_NAME:-recruitment}
DB_USER: ${DB_USER:-postgres}
DB_PASSWORD: ${DB_PASSWORD:-changeme123}
SESSION_SECRET: ${SESSION_SECRET:-change-this-secret-in-production}
depends_on:
postgres:
condition: service_healthy
networks:
- recruitment-network
volumes:
postgres_data:
driver: local
networks:
recruitment-network:
driver: bridge

View File

@@ -0,0 +1,150 @@
-- AI Recruitment Site Database Schema
-- Using PostgreSQL with proper indexing and constraints
-- Admins table
CREATE TABLE IF NOT EXISTS admins (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
full_name VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP WITH TIME ZONE
);
-- Create index for faster email lookups
CREATE INDEX IF NOT EXISTS idx_admins_email ON admins(email);
-- Job postings table
CREATE TABLE IF NOT EXISTS job_postings (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
department VARCHAR(100),
location VARCHAR(255),
employment_type VARCHAR(50), -- Full-time, Part-time, Contract
salary_range VARCHAR(100),
description TEXT NOT NULL,
requirements TEXT,
benefits TEXT,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
created_by INTEGER REFERENCES admins(id)
);
-- Create index for active jobs
CREATE INDEX IF NOT EXISTS idx_job_postings_active ON job_postings(is_active, created_at DESC);
-- Applicants table
CREATE TABLE IF NOT EXISTS applicants (
id SERIAL PRIMARY KEY,
full_name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
phone VARCHAR(50),
linkedin_url VARCHAR(500),
portfolio_url VARCHAR(500),
years_of_experience INTEGER,
current_position VARCHAR(255),
current_company VARCHAR(255),
preferred_location VARCHAR(255),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Create index for applicant searches
CREATE INDEX IF NOT EXISTS idx_applicants_email ON applicants(email);
CREATE INDEX IF NOT EXISTS idx_applicants_name ON applicants(full_name);
CREATE INDEX IF NOT EXISTS idx_applicants_created_at ON applicants(created_at DESC);
-- Applications table (links applicants to jobs with CV)
CREATE TABLE IF NOT EXISTS applications (
id SERIAL PRIMARY KEY,
applicant_id INTEGER NOT NULL REFERENCES applicants(id) ON DELETE CASCADE,
job_id INTEGER REFERENCES job_postings(id) ON DELETE SET NULL,
cover_letter TEXT,
cv_filename VARCHAR(500),
cv_content_type VARCHAR(100),
cv_file BYTEA, -- Store CV as binary data
status VARCHAR(50) DEFAULT 'new', -- new, reviewing, interview, rejected, hired
notes TEXT,
applied_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Create indexes for application queries
CREATE INDEX IF NOT EXISTS idx_applications_applicant ON applications(applicant_id);
CREATE INDEX IF NOT EXISTS idx_applications_job ON applications(job_id);
CREATE INDEX IF NOT EXISTS idx_applications_status ON applications(status);
CREATE INDEX IF NOT EXISTS idx_applications_date ON applications(applied_at DESC);
-- Add check constraint for application status
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'applications_status_check'
) THEN
ALTER TABLE applications ADD CONSTRAINT applications_status_check
CHECK (status IN ('new', 'reviewing', 'interview', 'rejected', 'hired'));
END IF;
END $$;
-- Add check constraint for employment type
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'job_postings_employment_type_check'
) THEN
ALTER TABLE job_postings ADD CONSTRAINT job_postings_employment_type_check
CHECK (employment_type IN ('Full-time', 'Part-time', 'Contract', 'Internship', 'Freelance'));
END IF;
END $$;
-- Contact submissions table
CREATE TABLE IF NOT EXISTS contact_submissions (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
subject VARCHAR(500),
message TEXT NOT NULL,
is_read BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Create index for contact submissions
CREATE INDEX IF NOT EXISTS idx_contact_submissions_date ON contact_submissions(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_contact_submissions_unread ON contact_submissions(is_read, created_at DESC);
-- Function to update the updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Trigger for job_postings
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_trigger WHERE tgname = 'update_job_postings_updated_at'
) THEN
CREATE TRIGGER update_job_postings_updated_at
BEFORE UPDATE ON job_postings
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
END IF;
END $$;
-- Trigger for applications
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_trigger WHERE tgname = 'update_applications_updated_at'
) THEN
CREATE TRIGGER update_applications_updated_at
BEFORE UPDATE ON applications
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
END IF;
END $$;

View File

@@ -0,0 +1,113 @@
-- Seed data for AI Recruitment Site
-- Insert sample job postings
INSERT INTO job_postings (title, department, location, employment_type, salary_range, description, requirements, benefits, is_active)
VALUES
(
'Senior Full-Stack Developer',
'Engineering',
'Remote / San Francisco, CA',
'Full-time',
'$120,000 - $180,000',
'We are seeking an experienced Full-Stack Developer to join our dynamic team. You will be responsible for designing, developing, and maintaining web applications that serve thousands of users daily. This role offers an opportunity to work with cutting-edge technologies and contribute to meaningful projects.',
'• 5+ years of experience in full-stack development
• Proficiency in JavaScript/TypeScript, React, Node.js
• Experience with PostgreSQL or similar relational databases
• Strong understanding of RESTful APIs and microservices
• Experience with Docker and cloud platforms (AWS/GCP/Azure)
• Excellent problem-solving and communication skills',
'• Competitive salary and equity package
• Health, dental, and vision insurance
• 401(k) matching
• Flexible work arrangements
• Professional development budget
• Unlimited PTO',
true
),
(
'DevOps Engineer',
'Engineering',
'New York, NY',
'Full-time',
'$100,000 - $150,000',
'Join our infrastructure team to build and maintain scalable, reliable systems. You will work on automation, monitoring, and deployment pipelines that power our applications.',
'• 3+ years of DevOps or Site Reliability Engineering experience
• Strong knowledge of Kubernetes, Docker, and container orchestration
• Experience with CI/CD tools (Jenkins, GitLab CI, GitHub Actions)
• Proficiency in scripting languages (Python, Bash)
• Experience with infrastructure as code (Terraform, Ansible)
• Understanding of cloud platforms and networking',
'• Competitive compensation
• Stock options
• Remote work flexibility
• Learning and development opportunities
• Team events and offsites
• Modern tech stack',
true
),
(
'UI/UX Designer',
'Design',
'Los Angeles, CA',
'Full-time',
'$90,000 - $130,000',
'We are looking for a creative UI/UX Designer to craft beautiful and intuitive user experiences. You will work closely with product managers and engineers to bring ideas to life.',
'• 3+ years of UI/UX design experience
• Strong portfolio demonstrating web and mobile design
• Proficiency in Figma, Sketch, or Adobe XD
• Understanding of user-centered design principles
• Experience conducting user research and usability testing
• Knowledge of HTML/CSS is a plus',
'• Creative and collaborative work environment
• Health and wellness benefits
• Flexible schedule
• Latest design tools and equipment
• Conference and workshop budget
• Generous vacation policy',
true
),
(
'Data Scientist',
'Data & Analytics',
'Remote',
'Full-time',
'$110,000 - $160,000',
'Help us unlock insights from data. As a Data Scientist, you will develop machine learning models, analyze complex datasets, and contribute to data-driven decision making.',
'• Master''s or PhD in Computer Science, Statistics, or related field
• 2+ years of experience in data science or machine learning
• Strong programming skills in Python (pandas, scikit-learn, TensorFlow)
• Experience with SQL and data warehousing
• Knowledge of statistical modeling and A/B testing
• Excellent communication skills to explain technical concepts',
'• Competitive salary
• Work with cutting-edge ML technologies
• Conference attendance opportunities
• Remote-first culture
• Comprehensive health benefits
• Equity compensation',
true
),
(
'Marketing Manager',
'Marketing',
'Chicago, IL',
'Full-time',
'$80,000 - $120,000',
'Lead our marketing initiatives and help grow our brand. You will develop and execute marketing strategies, manage campaigns, and analyze performance metrics.',
'• 5+ years of marketing experience
• Proven track record in B2B or B2C marketing
• Experience with digital marketing channels (SEO, SEM, social media)
• Strong analytical skills and data-driven mindset
• Excellent written and verbal communication
• Experience with marketing automation tools',
'• Competitive salary and bonuses
• Health insurance
• Professional development
• Collaborative team environment
• Flexible work options
• Career growth opportunities',
true
);
-- Note: Admin users are created on first login, no seed data needed
-- Note: Applicants and applications will be created through the application form

26
package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "ai-recruit-site",
"version": "1.0.0",
"description": "AI Recruitment Site for Ryans Recruit Firm",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "node server.js"
},
"keywords": [
"recruitment",
"jobs",
"hr",
"postgres"
],
"author": "Ryans Recruit Firm",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"pg": "^8.11.3",
"bcrypt": "^5.1.1",
"express-session": "^1.17.3",
"multer": "^1.4.5-lts.1",
"cookie-parser": "^1.4.6"
}
}

178
public/about.html Normal file
View File

@@ -0,0 +1,178 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>About Us - Ryans Recruit Firm</title>
<link rel="stylesheet" href="/css/styles.css">
<script src="/js/init.js"></script>
</head>
<body>
<header>
<nav class="container">
<div class="logo" data-company-name>Ryans Recruit Firm</div>
<ul class="nav-links">
<li><a href="/">Home</a></li>
<li><a href="/about" class="active">About</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/jobs">Jobs</a></li>
<li><a href="/contact">Contact</a></li>
<li><a href="/admin/login" class="btn btn-primary btn-sm">Admin</a></li>
</ul>
</nav>
</header>
<section class="hero">
<div class="container">
<h1>About Ryans Recruit Firm</h1>
<p>Building careers and companies through exceptional talent placement</p>
</div>
</section>
<section>
<div class="container-narrow">
<h2>Our Story</h2>
<p>Founded in 2015, Ryans Recruit Firm has grown to become one of the most trusted names in professional recruitment. What started as a small team with a vision has evolved into a comprehensive recruitment solution serving clients across multiple industries worldwide.</p>
<p>Our founder, Ryan Martinez, saw a gap in the recruitment industry: too many firms focused on filling positions quickly rather than finding the right match. With a background in HR and a passion for helping people find meaningful work, Ryan set out to create a different kind of recruitment firm—one that prioritizes quality, transparency, and long-term success.</p>
<p>Today, we're proud to have helped thousands of professionals find their dream jobs and assisted hundreds of companies in building exceptional teams.</p>
</div>
</section>
<section style="background: var(--bg-white);">
<div class="container">
<div class="section-title">
<h2>Our Values</h2>
<p>The principles that guide everything we do</p>
</div>
<div class="grid grid-2">
<div class="card">
<h3>🎯 Excellence</h3>
<p>We maintain the highest standards in candidate screening, client service, and professional conduct. Mediocrity is not in our vocabulary.</p>
</div>
<div class="card">
<h3>🤝 Integrity</h3>
<p>Honesty and transparency in all our dealings. We build trust through consistent, ethical behavior and open communication.</p>
</div>
<div class="card">
<h3>💡 Innovation</h3>
<p>We leverage the latest technology and methodologies to improve our recruitment process and deliver better results.</p>
</div>
<div class="card">
<h3>❤️ Empathy</h3>
<p>We understand that behind every resume is a person with dreams, and behind every job posting is a company with needs. We care about both.</p>
</div>
</div>
</div>
</section>
<section>
<div class="container">
<div class="section-title">
<h2>By the Numbers</h2>
<p>Our track record speaks for itself</p>
</div>
<div class="grid grid-4">
<div class="stat-card">
<div class="stat-value">5,000+</div>
<div class="stat-label">Successful Placements</div>
</div>
<div class="stat-card">
<div class="stat-value">500+</div>
<div class="stat-label">Partner Companies</div>
</div>
<div class="stat-card">
<div class="stat-value">95%</div>
<div class="stat-label">Client Satisfaction</div>
</div>
<div class="stat-card">
<div class="stat-value">14</div>
<div class="stat-label">Average Days to Placement</div>
</div>
</div>
</div>
</section>
<section style="background: var(--bg-white);">
<div class="container-narrow">
<div class="section-title">
<h2>Our Mission</h2>
</div>
<div class="card">
<p style="font-size: 1.125rem; line-height: 1.8; color: var(--text-dark); text-align: center;">
To revolutionize the recruitment industry by creating meaningful connections between talented professionals and forward-thinking companies, while maintaining the highest standards of integrity, transparency, and service excellence.
</p>
</div>
</div>
</section>
<section>
<div class="container text-center">
<h2 style="margin-bottom: 1rem;">Ready to Work With Us?</h2>
<p style="font-size: 1.125rem; color: var(--text-light); margin-bottom: 2rem;">
Whether you're looking for your next career move or seeking top talent for your company
</p>
<div style="display: flex; gap: 1rem; justify-content: center;">
<a href="/jobs" class="btn btn-primary btn-lg">Browse Jobs</a>
<a href="/contact" class="btn btn-secondary btn-lg">Contact Us</a>
</div>
</div>
</section>
<footer>
<div class="container">
<div class="footer-content">
<div class="footer-section">
<h3 data-company-name>Ryans Recruit Firm</h3>
<p style="color: rgba(255, 255, 255, 0.8);">
Your trusted partner in career advancement and talent acquisition.
</p>
</div>
<div class="footer-section">
<h3>Quick Links</h3>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About Us</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/jobs">Job Listings</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
<div class="footer-section">
<h3>For Employers</h3>
<ul>
<li><a href="/contact">Post a Job</a></li>
<li><a href="/contact">Our Process</a></li>
<li><a href="/contact">Pricing</a></li>
</ul>
</div>
<div class="footer-section">
<h3>Contact</h3>
<ul>
<li>Email: info@ryansrecruit.com</li>
<li>Phone: +1 (555) 123-4567</li>
<li>Hours: Mon-Fri 9AM-6PM EST</li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2026 Ryans Recruit Firm. All rights reserved.</p>
</div>
</div>
</footer>
</body>
</html>

View File

@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Applications - Ryans Recruit Admin</title>
<link rel="stylesheet" href="/css/styles.css">
<script src="/js/init.js"></script>
</head>
<body style="background: #F8FAFC; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0;">
<!-- Admin Header -->
<header style="background: white; border-bottom: 1px solid #E2E8F0; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);">
<div style="max-width: 1200px; margin: 0 auto; padding: 16px 24px; display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 12px;">
<div style="width: 32px; height: 32px; background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%); border-radius: 8px; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 16px;">R</div>
<h1 style="margin: 0; color: #1E293B; font-size: 18px; font-weight: 700;">Ryans Recruit Admin</h1>
</div>
<nav style="display: flex; gap: 32px; align-items: center;">
<a href="/admin/dashboard" style="color: #64748B; text-decoration: none; font-weight: 500; font-size: 14px; transition: color 0.2s;" onmouseover="this.style.color='#2563EB'" onmouseout="this.style.color='#64748B'">Dashboard</a>
<a href="/admin/applicants" style="color: #2563EB; text-decoration: none; font-weight: 500; font-size: 14px;">Applicants</a>
<a href="/admin/jobs" style="color: #64748B; text-decoration: none; font-weight: 500; font-size: 14px; transition: color 0.2s;" onmouseover="this.style.color='#2563EB'" onmouseout="this.style.color='#64748B'">Jobs</a>
<div style="display: flex; align-items: center; gap: 16px; padding-left: 32px; border-left: 1px solid #E2E8F0;">
<span id="admin-name" style="color: #64748B; font-size: 14px;">Admin</span>
<button onclick="logout()" style="background: #EF4444; color: white; padding: 8px 16px; border: none; border-radius: 6px; font-weight: 500; font-size: 13px; cursor: pointer; transition: background 0.2s;" onmouseover="this.style.background='#DC2626'" onmouseout="this.style.background='#EF4444'">Logout</button>
</div>
</nav>
</div>
</header>
<!-- Hero Section -->
<section style="background: linear-gradient(135deg, #2563EB 0%, #1E293B 100%); padding: 48px 24px; text-align: center;">
<h2 style="color: white; margin: 0; font-size: 36px; font-weight: 700;">Applications Management</h2>
<p style="color: #E0E7FF; margin: 12px 0 0 0; font-size: 16px;">Review and manage all job applications</p>
</section>
<!-- Main Content -->
<main style="max-width: 1200px; margin: 0 auto; padding: 40px 24px;">
<!-- Filter Section -->
<div style="background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);">
<h3 style="color: #1E293B; margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">Filters</h3>
<div style="display: flex; gap: 16px; flex-wrap: wrap;">
<div>
<label for="status-filter" style="display: block; color: #64748B; font-size: 13px; font-weight: 500; margin-bottom: 6px;">Status</label>
<select id="status-filter" style="padding: 8px 12px; border: 1px solid #E2E8F0; border-radius: 6px; font-size: 14px; background: white; cursor: pointer; transition: border-color 0.2s;" onchange="filterApplications(this.value)" onfocus="this.style.borderColor='#2563EB'" onblur="this.style.borderColor='#E2E8F0'">
<option value="">All Applications</option>
<option value="new">New</option>
<option value="reviewed">Reviewed</option>
<option value="shortlisted">Shortlisted</option>
<option value="rejected">Rejected</option>
<option value="accepted">Accepted</option>
</select>
</div>
</div>
</div>
<!-- Loading Spinner -->
<div id="loading" style="text-align: center; padding: 48px 24px; display: none;">
<div style="display: inline-block;">
<div style="width: 40px; height: 40px; border: 4px solid #E2E8F0; border-top: 4px solid #2563EB; border-radius: 50%; animation: spin 1s linear infinite;"></div>
</div>
<p style="color: #64748B; margin-top: 16px; font-size: 14px;">Loading applications...</p>
</div>
<!-- Applications Container -->
<div id="applications-container" style="display: grid; gap: 16px;">
<!-- Applications will be populated here by JavaScript -->
</div>
<!-- Empty State -->
<div id="empty-state" style="text-align: center; padding: 60px 24px; background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);">
<p style="color: #94A3B8; font-size: 16px; margin: 0;">No applications found</p>
</div>
</main>
<style>
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
<script src="/js/admin.js"></script>
</body>
</html>

View File

@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Dashboard - Ryans Recruit</title>
<link rel="stylesheet" href="/css/styles.css">
<script src="/js/init.js"></script>
</head>
<body style="background: #F8FAFC; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0;">
<!-- Admin Header -->
<header style="background: white; border-bottom: 1px solid #E2E8F0; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);">
<div style="max-width: 1200px; margin: 0 auto; padding: 16px 24px; display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 12px;">
<div style="width: 32px; height: 32px; background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%); border-radius: 8px; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 16px;">R</div>
<h1 style="margin: 0; color: #1E293B; font-size: 18px; font-weight: 700;">Ryans Recruit Admin</h1>
</div>
<nav style="display: flex; gap: 32px; align-items: center;">
<a href="/admin/dashboard" style="color: #2563EB; text-decoration: none; font-weight: 500; font-size: 14px;">Dashboard</a>
<a href="/admin/applicants" style="color: #64748B; text-decoration: none; font-weight: 500; font-size: 14px; transition: color 0.2s;" onmouseover="this.style.color='#2563EB'" onmouseout="this.style.color='#64748B'">Applicants</a>
<a href="/admin/jobs" style="color: #64748B; text-decoration: none; font-weight: 500; font-size: 14px; transition: color 0.2s;" onmouseover="this.style.color='#2563EB'" onmouseout="this.style.color='#64748B'">Jobs</a>
<div style="display: flex; align-items: center; gap: 16px; padding-left: 32px; border-left: 1px solid #E2E8F0;">
<span id="admin-name" style="color: #64748B; font-size: 14px;">Admin</span>
<button onclick="logout()" style="background: #EF4444; color: white; padding: 8px 16px; border: none; border-radius: 6px; font-weight: 500; font-size: 13px; cursor: pointer; transition: background 0.2s;" onmouseover="this.style.background='#DC2626'" onmouseout="this.style.background='#EF4444'">Logout</button>
</div>
</nav>
</div>
</header>
<!-- Hero Section -->
<section style="background: linear-gradient(135deg, #2563EB 0%, #1E293B 100%); padding: 48px 24px; text-align: center;">
<h2 style="color: white; margin: 0; font-size: 36px; font-weight: 700;">Admin Dashboard</h2>
<p style="color: #E0E7FF; margin: 12px 0 0 0; font-size: 16px;">Manage applications, jobs, and track recruitment metrics</p>
</section>
<!-- Main Content -->
<main style="max-width: 1200px; margin: 0 auto; padding: 40px 24px;">
<!-- Statistics Grid -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; margin-bottom: 40px;">
<!-- New Applications Card -->
<div style="background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); border-left: 4px solid #2563EB;">
<p style="color: #64748B; margin: 0 0 8px 0; font-size: 13px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">New Applications</p>
<p id="stat-new-applications" style="color: #1E293B; margin: 0; font-size: 32px; font-weight: 700;">0</p>
<p style="color: #94A3B8; margin: 8px 0 0 0; font-size: 13px;">This week</p>
</div>
<!-- Total Applications Card -->
<div style="background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); border-left: 4px solid #059669;">
<p style="color: #64748B; margin: 0 0 8px 0; font-size: 13px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">Total Applications</p>
<p id="stat-total-applications" style="color: #1E293B; margin: 0; font-size: 32px; font-weight: 700;">0</p>
<p style="color: #94A3B8; margin: 8px 0 0 0; font-size: 13px;">All time</p>
</div>
<!-- Total Applicants Card -->
<div style="background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); border-left: 4px solid #F59E0B;">
<p style="color: #64748B; margin: 0 0 8px 0; font-size: 13px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">Total Applicants</p>
<p id="stat-total-applicants" style="color: #1E293B; margin: 0; font-size: 32px; font-weight: 700;">0</p>
<p style="color: #94A3B8; margin: 8px 0 0 0; font-size: 13px;">Unique users</p>
</div>
<!-- Active Jobs Card -->
<div style="background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); border-left: 4px solid #8B5CF6;">
<p style="color: #64748B; margin: 0 0 8px 0; font-size: 13px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">Active Jobs</p>
<p id="stat-active-jobs" style="color: #1E293B; margin: 0; font-size: 32px; font-weight: 700;">0</p>
<p style="color: #94A3B8; margin: 8px 0 0 0; font-size: 13px;">Open positions</p>
</div>
</div>
<!-- Quick Links Section -->
<section style="background: white; border-radius: 12px; padding: 32px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);">
<h3 style="color: #1E293B; margin: 0 0 24px 0; font-size: 18px; font-weight: 700;">Quick Access</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;">
<a href="/admin/applicants" style="display: block; padding: 20px; border: 2px solid #E2E8F0; border-radius: 8px; text-decoration: none; text-align: center; transition: all 0.2s;" onmouseover="this.style.borderColor='#2563EB'; this.style.backgroundColor='#F0F9FF'" onmouseout="this.style.borderColor='#E2E8F0'; this.style.backgroundColor='white'">
<p style="color: #2563EB; margin: 0 0 8px 0; font-size: 24px;">📋</p>
<p style="color: #1E293B; margin: 0; font-size: 16px; font-weight: 600;">View Applications</p>
<p style="color: #64748B; margin: 4px 0 0 0; font-size: 13px;">Manage all applicants</p>
</a>
<a href="/admin/jobs" style="display: block; padding: 20px; border: 2px solid #E2E8F0; border-radius: 8px; text-decoration: none; text-align: center; transition: all 0.2s;" onmouseover="this.style.borderColor='#059669'; this.style.backgroundColor='#F0FDF4'" onmouseout="this.style.borderColor='#E2E8F0'; this.style.backgroundColor='white'">
<p style="color: #059669; margin: 0 0 8px 0; font-size: 24px;">💼</p>
<p style="color: #1E293B; margin: 0; font-size: 16px; font-weight: 600;">Manage Jobs</p>
<p style="color: #64748B; margin: 4px 0 0 0; font-size: 13px;">Create and edit positions</p>
</a>
</div>
</section>
</main>
<script src="/js/admin.js"></script>
</body>
</html>

76
public/admin/jobs.html Normal file
View File

@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Jobs - Ryans Recruit Admin</title>
<link rel="stylesheet" href="/css/styles.css">
<script src="/js/init.js"></script>
</head>
<body style="background: #F8FAFC; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0;">
<!-- Admin Header -->
<header style="background: white; border-bottom: 1px solid #E2E8F0; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);">
<div style="max-width: 1200px; margin: 0 auto; padding: 16px 24px; display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 12px;">
<div style="width: 32px; height: 32px; background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%); border-radius: 8px; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 16px;">R</div>
<h1 style="margin: 0; color: #1E293B; font-size: 18px; font-weight: 700;">Ryans Recruit Admin</h1>
</div>
<nav style="display: flex; gap: 32px; align-items: center;">
<a href="/admin/dashboard" style="color: #64748B; text-decoration: none; font-weight: 500; font-size: 14px; transition: color 0.2s;" onmouseover="this.style.color='#2563EB'" onmouseout="this.style.color='#64748B'">Dashboard</a>
<a href="/admin/applicants" style="color: #64748B; text-decoration: none; font-weight: 500; font-size: 14px; transition: color 0.2s;" onmouseover="this.style.color='#2563EB'" onmouseout="this.style.color='#64748B'">Applicants</a>
<a href="/admin/jobs" style="color: #2563EB; text-decoration: none; font-weight: 500; font-size: 14px;">Jobs</a>
<div style="display: flex; align-items: center; gap: 16px; padding-left: 32px; border-left: 1px solid #E2E8F0;">
<span id="admin-name" style="color: #64748B; font-size: 14px;">Admin</span>
<button onclick="logout()" style="background: #EF4444; color: white; padding: 8px 16px; border: none; border-radius: 6px; font-weight: 500; font-size: 13px; cursor: pointer; transition: background 0.2s;" onmouseover="this.style.background='#DC2626'" onmouseout="this.style.background='#EF4444'">Logout</button>
</div>
</nav>
</div>
</header>
<!-- Hero Section -->
<section style="background: linear-gradient(135deg, #2563EB 0%, #1E293B 100%); padding: 48px 24px; text-align: center;">
<h2 style="color: white; margin: 0; font-size: 36px; font-weight: 700;">Job Postings Management</h2>
<p style="color: #E0E7FF; margin: 12px 0 0 0; font-size: 16px;">Create, edit, and manage job positions</p>
</section>
<!-- Main Content -->
<main style="max-width: 1200px; margin: 0 auto; padding: 40px 24px;">
<!-- Action Bar -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
<h3 style="color: #1E293B; margin: 0; font-size: 18px; font-weight: 700;">All Job Postings</h3>
<button onclick="createNewJob()" style="background: linear-gradient(135deg, #059669 0%, #047857 100%); color: white; padding: 10px 20px; border: none; border-radius: 6px; font-weight: 600; font-size: 14px; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 8px 16px rgba(5, 150, 105, 0.3)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
+ New Job
</button>
</div>
<!-- Loading Spinner -->
<div id="loading" style="text-align: center; padding: 48px 24px; display: none;">
<div style="display: inline-block;">
<div style="width: 40px; height: 40px; border: 4px solid #E2E8F0; border-top: 4px solid #2563EB; border-radius: 50%; animation: spin 1s linear infinite;"></div>
</div>
<p style="color: #64748B; margin-top: 16px; font-size: 14px;">Loading jobs...</p>
</div>
<!-- Jobs Container -->
<div id="jobs-container" style="display: grid; gap: 16px;">
<!-- Jobs will be populated here by JavaScript -->
</div>
<!-- Empty State -->
<div id="empty-state" style="text-align: center; padding: 60px 24px; background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);">
<p style="color: #94A3B8; font-size: 16px; margin: 0;">No job postings yet</p>
<button onclick="createNewJob()" style="background: linear-gradient(135deg, #059669 0%, #047857 100%); color: white; padding: 10px 20px; border: none; border-radius: 6px; font-weight: 600; font-size: 14px; cursor: pointer; margin-top: 16px; transition: transform 0.2s, box-shadow 0.2s;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 8px 16px rgba(5, 150, 105, 0.3)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
Create First Job
</button>
</div>
</main>
<style>
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
<script src="/js/admin.js"></script>
</body>
</html>

83
public/admin/login.html Normal file
View File

@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Login - Ryans Recruit</title>
<link rel="stylesheet" href="/css/styles.css">
<script src="/js/init.js"></script>
</head>
<body style="background: linear-gradient(135deg, #2563EB 0%, #1E293B 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
<div style="background: white; border-radius: 12px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); padding: 48px 40px; width: 100%; max-width: 400px;">
<h1 style="text-align: center; color: #1E293B; margin: 0 0 8px 0; font-size: 28px;">Ryans Recruit</h1>
<p style="text-align: center; color: #64748B; margin: 0 0 32px 0; font-size: 14px;">Admin Portal</p>
<!-- First Admin Notice -->
<div id="first-admin-notice" style="background: #DBEAFE; border: 1px solid #93C5FD; border-radius: 8px; padding: 12px; margin-bottom: 24px; color: #1E40AF; font-size: 14px; display: none;">
<strong>First Admin Setup:</strong> Create your admin account to get started.
</div>
<form id="login-form" style="display: flex; flex-direction: column; gap: 16px;">
<!-- Email Field -->
<div>
<label for="email" style="display: block; font-weight: 600; color: #1E293B; margin-bottom: 6px; font-size: 14px;">Email</label>
<input
type="email"
id="email"
name="email"
required
placeholder="admin@example.com"
style="width: 100%; padding: 10px 12px; border: 1px solid #E2E8F0; border-radius: 6px; font-size: 14px; box-sizing: border-box; transition: border-color 0.2s;"
onfocus="this.style.borderColor='#2563EB'"
onblur="this.style.borderColor='#E2E8F0'"
>
</div>
<!-- Password Field -->
<div>
<label for="password" style="display: block; font-weight: 600; color: #1E293B; margin-bottom: 6px; font-size: 14px;">Password</label>
<input
type="password"
id="password"
name="password"
required
placeholder="••••••••"
style="width: 100%; padding: 10px 12px; border: 1px solid #E2E8F0; border-radius: 6px; font-size: 14px; box-sizing: border-box; transition: border-color 0.2s;"
onfocus="this.style.borderColor='#2563EB'"
onblur="this.style.borderColor='#E2E8F0'"
>
</div>
<!-- Full Name Field (Initially Hidden) -->
<div id="full-name-group" style="display: none;">
<label for="full-name" style="display: block; font-weight: 600; color: #1E293B; margin-bottom: 6px; font-size: 14px;">Full Name</label>
<input
type="text"
id="full-name"
name="full-name"
placeholder="John Doe"
style="width: 100%; padding: 10px 12px; border: 1px solid #E2E8F0; border-radius: 6px; font-size: 14px; box-sizing: border-box; transition: border-color 0.2s;"
onfocus="this.style.borderColor='#2563EB'"
onblur="this.style.borderColor='#E2E8F0'"
>
</div>
<!-- Submit Button -->
<button
type="submit"
style="background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%); color: white; padding: 12px; border: none; border-radius: 6px; font-weight: 600; font-size: 14px; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; margin-top: 8px;"
onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 8px 16px rgba(37, 99, 235, 0.3)'"
onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'"
>
Sign In
</button>
</form>
<p style="text-align: center; color: #64748B; font-size: 13px; margin-top: 20px;">
Back to <a href="/" style="color: #2563EB; text-decoration: none; font-weight: 500;">Main Site</a>
</p>
</div>
<script src="/js/admin.js"></script>
</body>
</html>

172
public/apply.html Normal file
View File

@@ -0,0 +1,172 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Apply for Job - Ryans Recruit Firm</title>
<link rel="stylesheet" href="/css/styles.css">
<script src="/js/init.js"></script>
</head>
<body>
<header>
<nav class="container">
<div class="logo" data-company-name>Ryans Recruit Firm</div>
<ul class="nav-links">
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/jobs">Jobs</a></li>
<li><a href="/contact">Contact</a></li>
<li><a href="/admin/login" class="btn btn-primary btn-sm">Admin</a></li>
</ul>
</nav>
</header>
<section class="hero">
<div class="container">
<h1>Apply Now</h1>
<p>Submit your application for consideration</p>
</div>
</section>
<section>
<div class="container-narrow">
<div id="job-details" style="background: var(--bg-white); padding: 2rem; border-radius: 0.5rem; box-shadow: var(--shadow-md); margin-bottom: 2rem;">
<h2 id="job-title" style="color: var(--secondary-color); margin-bottom: 1rem;">Job Title</h2>
<div id="job-info" style="color: var(--text-light);">
<!-- Job details will be loaded here -->
</div>
</div>
<div style="background: var(--bg-white); padding: 2rem; border-radius: 0.5rem; box-shadow: var(--shadow-md);">
<h3 style="color: var(--secondary-color); margin-bottom: 2rem;">Application Form</h3>
<form id="application-form">
<!-- Hidden jobId field -->
<input type="hidden" id="jobId" name="jobId" value="">
<!-- Full Name -->
<div class="form-group">
<label for="fullName">Full Name <span style="color: var(--error);">*</span></label>
<input type="text" id="fullName" name="fullName" required placeholder="Enter your full name">
</div>
<!-- Email -->
<div class="form-group">
<label for="email">Email Address <span style="color: var(--error);">*</span></label>
<input type="email" id="email" name="email" required placeholder="your.email@example.com">
</div>
<!-- Phone -->
<div class="form-group">
<label for="phone">Phone Number</label>
<input type="tel" id="phone" name="phone" placeholder="+1 (555) 123-4567">
</div>
<!-- LinkedIn URL -->
<div class="form-group">
<label for="linkedinUrl">LinkedIn URL</label>
<input type="url" id="linkedinUrl" name="linkedinUrl" placeholder="https://linkedin.com/in/yourprofile">
</div>
<!-- Portfolio URL -->
<div class="form-group">
<label for="portfolioUrl">Portfolio URL</label>
<input type="url" id="portfolioUrl" name="portfolioUrl" placeholder="https://yourportfolio.com">
</div>
<!-- Years of Experience -->
<div class="form-group">
<label for="yearsExperience">Years of Experience</label>
<input type="number" id="yearsExperience" name="yearsExperience" min="0" max="70" placeholder="0">
</div>
<!-- Current Position -->
<div class="form-group">
<label for="currentPosition">Current Position</label>
<input type="text" id="currentPosition" name="currentPosition" placeholder="e.g., Senior Software Engineer">
</div>
<!-- Current Company -->
<div class="form-group">
<label for="currentCompany">Current Company</label>
<input type="text" id="currentCompany" name="currentCompany" placeholder="e.g., Tech Corp Inc.">
</div>
<!-- Preferred Location -->
<div class="form-group">
<label for="preferredLocation">Preferred Location</label>
<input type="text" id="preferredLocation" name="preferredLocation" placeholder="e.g., New York, NY or Remote">
</div>
<!-- Cover Letter -->
<div class="form-group">
<label for="coverLetter">Cover Letter</label>
<textarea id="coverLetter" name="coverLetter" placeholder="Tell us why you're interested in this position and what makes you a great fit..."></textarea>
</div>
<!-- CV Upload -->
<div class="form-group">
<label for="cv">Upload CV/Resume <span style="color: var(--error);">*</span></label>
<input type="file" id="cv" name="cv" accept=".pdf,.doc,.docx" required>
<div class="form-help">Accepted formats: PDF, DOC, DOCX (Max 5MB)</div>
</div>
<!-- Submit Button -->
<div class="form-group" style="margin-top: 2rem;">
<button type="submit" class="btn btn-primary btn-lg" style="width: 100%;">Submit Application</button>
</div>
</form>
</div>
</div>
</section>
<footer>
<div class="container">
<div class="footer-content">
<div class="footer-section">
<h3 data-company-name>Ryans Recruit Firm</h3>
<p style="color: rgba(255, 255, 255, 0.8);">
Your trusted partner in career advancement and talent acquisition.
</p>
</div>
<div class="footer-section">
<h3>Quick Links</h3>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About Us</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/jobs">Job Listings</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
<div class="footer-section">
<h3>For Employers</h3>
<ul>
<li><a href="/contact">Post a Job</a></li>
<li><a href="/contact">Our Process</a></li>
<li><a href="/contact">Pricing</a></li>
</ul>
</div>
<div class="footer-section">
<h3>Contact</h3>
<ul>
<li>Email: info@ryansrecruit.com</li>
<li>Phone: +1 (555) 123-4567</li>
<li>Hours: Mon-Fri 9AM-6PM EST</li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2026 Ryans Recruit Firm. All rights reserved.</p>
</div>
</div>
</footer>
<script src="/js/main.js"></script>
</body>
</html>

170
public/contact.html Normal file
View File

@@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Contact Us - Ryans Recruit Firm</title>
<link rel="stylesheet" href="/css/styles.css">
<script src="/js/init.js"></script>
</head>
<body>
<header>
<nav class="container">
<div class="logo" data-company-name>Ryans Recruit Firm</div>
<ul class="nav-links">
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/jobs">Jobs</a></li>
<li><a href="/contact" class="active">Contact</a></li>
<li><a href="/admin/login" class="btn btn-primary btn-sm">Admin</a></li>
</ul>
</nav>
</header>
<section class="hero">
<div class="container">
<h1>Contact Us</h1>
<p>Get in touch with our team - we're here to help</p>
</div>
</section>
<section>
<div class="container">
<div class="grid grid-2" style="gap: 3rem; align-items: start;">
<!-- Contact Form -->
<div style="background: var(--bg-white); padding: 2rem; border-radius: 0.5rem; box-shadow: var(--shadow-md);">
<h3 style="color: var(--secondary-color); margin-bottom: 2rem;">Send us a Message</h3>
<form id="contact-form">
<!-- Name -->
<div class="form-group">
<label for="name">Name <span style="color: var(--error);">*</span></label>
<input type="text" id="name" name="name" required placeholder="Your full name">
</div>
<!-- Email -->
<div class="form-group">
<label for="contactEmail">Email Address <span style="color: var(--error);">*</span></label>
<input type="email" id="contactEmail" name="email" required placeholder="your.email@example.com">
</div>
<!-- Subject -->
<div class="form-group">
<label for="subject">Subject</label>
<input type="text" id="subject" name="subject" placeholder="What is this about?">
</div>
<!-- Message -->
<div class="form-group">
<label for="message">Message <span style="color: var(--error);">*</span></label>
<textarea id="message" name="message" required placeholder="Tell us more about your inquiry..."></textarea>
</div>
<!-- Submit Button -->
<button type="submit" class="btn btn-primary btn-lg" style="width: 100%;">Send Message</button>
</form>
</div>
<!-- Contact Information -->
<div>
<h3 style="color: var(--secondary-color); margin-bottom: 2rem;">Get in Touch</h3>
<div style="background: var(--bg-white); padding: 2rem; border-radius: 0.5rem; box-shadow: var(--shadow-md); margin-bottom: 2rem;">
<div style="margin-bottom: 2rem;">
<h4 style="color: var(--secondary-color); margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.75rem;">
<span style="font-size: 1.5rem;">📧</span> Email
</h4>
<p style="color: var(--text-light); margin-bottom: 0;">
<a href="mailto:info@ryansrecruit.com" style="color: var(--primary-color); font-weight: 500;">info@ryansrecruit.com</a>
</p>
<div class="form-help">We typically respond within 24 hours</div>
</div>
<div style="margin-bottom: 2rem;">
<h4 style="color: var(--secondary-color); margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.75rem;">
<span style="font-size: 1.5rem;">📞</span> Phone
</h4>
<p style="color: var(--text-light); margin-bottom: 0;">
<a href="tel:+15551234567" style="color: var(--primary-color); font-weight: 500;">+1 (555) 123-4567</a>
</p>
<div class="form-help">Mon-Fri 9AM-6PM EST</div>
</div>
<div>
<h4 style="color: var(--secondary-color); margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.75rem;">
<span style="font-size: 1.5rem;">🏢</span> Office Hours
</h4>
<p style="color: var(--text-light); margin-bottom: 0.5rem;">
Monday to Friday: 9:00 AM - 6:00 PM EST
</p>
<p style="color: var(--text-light);">
Saturday & Sunday: Closed
</p>
</div>
</div>
<div style="background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); padding: 2rem; border-radius: 0.5rem; color: white;">
<h4 style="color: white; margin-bottom: 1rem;">Why Choose Us?</h4>
<ul style="list-style: none;">
<li style="margin-bottom: 0.75rem;">✓ Experienced recruitment team</li>
<li style="margin-bottom: 0.75rem;">✓ Fast response times</li>
<li style="margin-bottom: 0.75rem;">✓ Personalized support</li>
<li style="margin-bottom: 0.75rem;">✓ Industry expertise</li>
<li>✓ Proven track record</li>
</ul>
</div>
</div>
</div>
</div>
</section>
<footer>
<div class="container">
<div class="footer-content">
<div class="footer-section">
<h3 data-company-name>Ryans Recruit Firm</h3>
<p style="color: rgba(255, 255, 255, 0.8);">
Your trusted partner in career advancement and talent acquisition.
</p>
</div>
<div class="footer-section">
<h3>Quick Links</h3>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About Us</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/jobs">Job Listings</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
<div class="footer-section">
<h3>For Employers</h3>
<ul>
<li><a href="/contact">Post a Job</a></li>
<li><a href="/contact">Our Process</a></li>
<li><a href="/contact">Pricing</a></li>
</ul>
</div>
<div class="footer-section">
<h3>Contact</h3>
<ul>
<li>Email: info@ryansrecruit.com</li>
<li>Phone: +1 (555) 123-4567</li>
<li>Hours: Mon-Fri 9AM-6PM EST</li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2026 Ryans Recruit Firm. All rights reserved.</p>
</div>
</div>
</footer>
<script src="/js/main.js"></script>
</body>
</html>

617
public/css/styles.css Normal file
View File

@@ -0,0 +1,617 @@
/* AI Recruitment Site - Global Styles */
:root {
/* Ryan Brand Colors */
--primary-color: #2563EB; /* Ryan Blue */
--primary-dark: #1e40af;
--primary-light: #3b82f6;
--secondary-color: #1E293B; /* Dark */
--accent-color: #059669; /* Ryan Green */
--text-dark: #1E293B;
--text-light: #64748B; /* Gray */
--bg-light: #F8FAFC; /* Light */
--bg-white: #ffffff;
--border-color: #E2E8F0; /* Border */
--success: #059669; /* Ryan Green */
--warning: #F59E0B; /* Warning Orange */
--error: #DC2626; /* Error Red */
--info: #0EA5E9; /* Info Blue */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 20px rgba(0, 0, 0, 0.15);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
/* Spacing System - 8px base */
--spacing-1: 8px;
--spacing-2: 16px;
--spacing-3: 24px;
--spacing-4: 32px;
--spacing-5: 40px;
--spacing-6: 48px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: var(--text-dark);
background-color: var(--bg-light);
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
font-weight: 700;
line-height: 1.2;
margin-bottom: 1rem;
color: var(--secondary-color);
}
h1 { font-size: 2.5rem; }
h2 { font-size: 2rem; }
h3 { font-size: 1.75rem; }
h4 { font-size: 1.5rem; }
h5 { font-size: 1.25rem; }
h6 { font-size: 1rem; }
p {
margin-bottom: 1rem;
color: var(--text-light);
}
a {
color: var(--primary-color);
text-decoration: none;
transition: color 0.2s;
}
a:hover {
color: var(--primary-dark);
}
/* Container */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
}
.container-narrow {
max-width: 900px;
margin: 0 auto;
padding: 0 2rem;
}
/* Header & Navigation */
header {
background: var(--bg-white);
box-shadow: var(--shadow-sm);
position: sticky;
top: 0;
z-index: 1000;
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 0;
}
.logo {
font-size: 1.5rem;
font-weight: 700;
color: var(--primary-color);
}
.nav-links {
display: flex;
list-style: none;
gap: 2rem;
align-items: center;
}
.nav-links a {
color: var(--text-dark);
font-weight: 500;
transition: color 0.2s;
}
.nav-links a:hover,
.nav-links a.active {
color: var(--primary-color);
}
/* Hero Section */
.hero {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 5rem 0;
text-align: center;
}
.hero h1 {
color: white;
font-size: 3rem;
margin-bottom: 1rem;
}
.hero p {
color: rgba(255, 255, 255, 0.9);
font-size: 1.25rem;
margin-bottom: 2rem;
}
/* Buttons */
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 0.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.btn-primary {
background: var(--primary-color);
color: white;
}
.btn-primary:hover {
background: var(--primary-dark);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.btn-secondary {
background: var(--bg-white);
color: var(--primary-color);
border: 2px solid var(--primary-color);
}
.btn-secondary:hover {
background: var(--primary-color);
color: white;
}
.btn-success {
background: var(--success);
color: white;
}
.btn-success:hover {
background: #059669;
}
.btn-danger {
background: var(--error);
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
.btn-lg {
padding: 1rem 2rem;
font-size: 1.125rem;
}
/* Cards */
.card {
background: var(--bg-white);
border-radius: 0.5rem;
padding: 2rem;
box-shadow: var(--shadow-md);
transition: transform 0.2s, box-shadow 0.2s;
}
.card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
.card-header {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 2px solid var(--border-color);
}
.card-title {
font-size: 1.5rem;
color: var(--secondary-color);
margin-bottom: 0.5rem;
}
.card-subtitle {
color: var(--text-light);
font-size: 0.875rem;
}
/* Grid System */
.grid {
display: grid;
gap: 2rem;
}
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 768px) {
.grid-2, .grid-3, .grid-4 {
grid-template-columns: 1fr;
}
}
/* Sections */
section {
padding: 4rem 0;
}
.section-title {
text-align: center;
margin-bottom: 3rem;
}
.section-title h2 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.section-title p {
font-size: 1.125rem;
color: var(--text-light);
}
/* Forms */
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: var(--text-dark);
}
input[type="text"],
input[type="email"],
input[type="password"],
input[type="tel"],
input[type="url"],
input[type="number"],
input[type="file"],
select,
textarea {
width: 100%;
padding: 0.75rem;
border: 2px solid var(--border-color);
border-radius: 0.5rem;
font-size: 1rem;
transition: border-color 0.2s;
font-family: inherit;
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: var(--primary-color);
}
textarea {
resize: vertical;
min-height: 120px;
}
.form-error {
color: var(--error);
font-size: 0.875rem;
margin-top: 0.25rem;
}
.form-help {
color: var(--text-light);
font-size: 0.875rem;
margin-top: 0.25rem;
}
/* Job Listing */
.job-card {
background: var(--bg-white);
border-radius: 0.5rem;
padding: 2rem;
box-shadow: var(--shadow-md);
margin-bottom: 1.5rem;
transition: transform 0.2s, box-shadow 0.2s;
}
.job-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.job-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 1rem;
}
.job-title {
font-size: 1.5rem;
color: var(--secondary-color);
margin-bottom: 0.5rem;
}
.job-meta {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1rem;
font-size: 0.875rem;
color: var(--text-light);
}
.job-meta-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.job-description {
color: var(--text-light);
margin-bottom: 1rem;
line-height: 1.6;
}
.job-requirements {
margin-bottom: 1rem;
}
.job-requirements h4 {
font-size: 1rem;
margin-bottom: 0.5rem;
color: var(--text-dark);
}
.job-requirements ul {
padding-left: 1.5rem;
color: var(--text-light);
}
.job-requirements li {
margin-bottom: 0.25rem;
}
/* Tags/Badges */
.tag {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 1rem;
font-size: 0.875rem;
font-weight: 600;
}
.tag-primary {
background: rgba(37, 99, 235, 0.1);
color: var(--primary-color);
}
.tag-success {
background: rgba(16, 185, 129, 0.1);
color: var(--success);
}
.tag-warning {
background: rgba(245, 158, 11, 0.1);
color: var(--warning);
}
.tag-error {
background: rgba(239, 68, 68, 0.1);
color: var(--error);
}
/* Stats */
.stat-card {
background: var(--bg-white);
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: var(--shadow-md);
text-align: center;
}
.stat-value {
font-size: 2.5rem;
font-weight: 700;
color: var(--primary-color);
margin-bottom: 0.5rem;
}
.stat-label {
color: var(--text-light);
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Tables */
table {
width: 100%;
border-collapse: collapse;
background: var(--bg-white);
border-radius: 0.5rem;
overflow: hidden;
box-shadow: var(--shadow-md);
}
thead {
background: var(--bg-light);
}
th {
padding: 1rem;
text-align: left;
font-weight: 600;
color: var(--text-dark);
border-bottom: 2px solid var(--border-color);
}
td {
padding: 1rem;
border-bottom: 1px solid var(--border-color);
}
tr:last-child td {
border-bottom: none;
}
tr:hover {
background: var(--bg-light);
}
/* Alerts */
.alert {
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
}
.alert-success {
background: rgba(16, 185, 129, 0.1);
color: var(--success);
border: 2px solid var(--success);
}
.alert-error {
background: rgba(239, 68, 68, 0.1);
color: var(--error);
border: 2px solid var(--error);
}
.alert-info {
background: rgba(37, 99, 235, 0.1);
color: var(--primary-color);
border: 2px solid var(--primary-color);
}
/* Footer */
footer {
background: var(--secondary-color);
color: white;
padding: 3rem 0 1rem;
margin-top: 4rem;
}
.footer-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
margin-bottom: 2rem;
}
.footer-section h3 {
color: white;
margin-bottom: 1rem;
}
.footer-section ul {
list-style: none;
}
.footer-section li {
margin-bottom: 0.5rem;
}
.footer-section a {
color: rgba(255, 255, 255, 0.8);
}
.footer-section a:hover {
color: white;
}
.footer-bottom {
text-align: center;
padding-top: 2rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.6);
}
/* Loading */
.loading {
display: inline-block;
width: 1rem;
height: 1rem;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Utilities */
.text-center { text-align: center; }
.text-right { text-align: right; }
.text-left { text-align: left; }
.mt-1 { margin-top: 0.5rem; }
.mt-2 { margin-top: 1rem; }
.mt-3 { margin-top: 1.5rem; }
.mt-4 { margin-top: 2rem; }
.mb-1 { margin-bottom: 0.5rem; }
.mb-2 { margin-bottom: 1rem; }
.mb-3 { margin-bottom: 1.5rem; }
.mb-4 { margin-bottom: 2rem; }
.hidden { display: none; }
/* Responsive */
@media (max-width: 768px) {
.container {
padding: 0 1rem;
}
.hero h1 {
font-size: 2rem;
}
.hero p {
font-size: 1rem;
}
.nav-links {
gap: 1rem;
font-size: 0.875rem;
}
h1 { font-size: 2rem; }
h2 { font-size: 1.75rem; }
h3 { font-size: 1.5rem; }
.job-header {
flex-direction: column;
}
.footer-content {
grid-template-columns: 1fr;
}
}

244
public/index.html Normal file
View File

@@ -0,0 +1,244 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ryans Recruit Firm - Find Your Dream Job</title>
<link rel="stylesheet" href="/css/styles.css">
<script src="/js/init.js"></script>
</head>
<body>
<header>
<nav class="container">
<div class="logo" data-company-name>Ryans Recruit Firm</div>
<ul class="nav-links">
<li><a href="/" class="active">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/jobs">Jobs</a></li>
<li><a href="/contact">Contact</a></li>
<li><a href="/admin/login" class="btn btn-primary btn-sm">Admin</a></li>
</ul>
</nav>
</header>
<section class="hero">
<div class="container">
<h1 data-company-tagline>Your Career Success Is Our Mission</h1>
<p data-company-description>Connecting talented professionals with leading companies worldwide</p>
<div style="display: flex; gap: 1rem; justify-content: center; margin-top: 2rem;">
<a href="/jobs" class="btn btn-primary btn-lg">Browse Jobs</a>
<a href="/contact" class="btn btn-secondary btn-lg">Get in Touch</a>
</div>
</div>
</section>
<section>
<div class="container">
<div class="section-title">
<h2>Why Choose <span data-company-name>Ryans Recruit Firm</span>?</h2>
<p>We're committed to finding the perfect match for both candidates and employers</p>
</div>
<div class="grid grid-3">
<div class="card text-center">
<div style="font-size: 3rem; margin-bottom: 1rem;">🎯</div>
<h3>Targeted Matching</h3>
<p>Our advanced screening process ensures we match the right talent with the right opportunity, saving time and increasing success rates.</p>
</div>
<div class="card text-center">
<div style="font-size: 3rem; margin-bottom: 1rem;">🌍</div>
<h3>Global Network</h3>
<p>Access to positions across multiple industries and locations, from startups to Fortune 500 companies worldwide.</p>
</div>
<div class="card text-center">
<div style="font-size: 3rem; margin-bottom: 1rem;">🤝</div>
<h3>Personal Support</h3>
<p>Dedicated recruiters guide you through every step, from application to offer negotiation and beyond.</p>
</div>
<div class="card text-center">
<div style="font-size: 3rem; margin-bottom: 1rem;"></div>
<h3>Fast Process</h3>
<p>Streamlined recruitment process with quick turnaround times, getting you in front of hiring managers faster.</p>
</div>
<div class="card text-center">
<div style="font-size: 3rem; margin-bottom: 1rem;">💼</div>
<h3>Industry Experts</h3>
<p>Our team has deep expertise across technology, finance, healthcare, and more, understanding what companies need.</p>
</div>
<div class="card text-center">
<div style="font-size: 3rem; margin-bottom: 1rem;">🎓</div>
<h3>Career Coaching</h3>
<p>Free resume reviews, interview preparation, and career advice to help you land your dream role.</p>
</div>
</div>
</div>
</section>
<section style="background: var(--bg-white);">
<div class="container">
<div class="section-title">
<h2>Our Success Stories</h2>
<p>Hear from professionals who found their dream jobs through us</p>
</div>
<div class="grid grid-2">
<div class="card">
<p style="font-style: italic; color: var(--text-light); margin-bottom: 1rem;">
"Ryans Recruit Firm found me the perfect role in just two weeks. Their team understood exactly what I was looking for and matched me with a company that aligned with my career goals. Highly recommended!"
</p>
<div style="display: flex; align-items: center; gap: 1rem;">
<div style="width: 50px; height: 50px; border-radius: 50%; background: var(--primary-color); display: flex; align-items: center; justify-content: center; color: white; font-weight: 700;">SJ</div>
<div>
<div style="font-weight: 600;">Sarah Johnson</div>
<div style="font-size: 0.875rem; color: var(--text-light);">Senior Software Engineer</div>
</div>
</div>
</div>
<div class="card">
<p style="font-style: italic; color: var(--text-light); margin-bottom: 1rem;">
"The personalized support I received was incredible. From resume refinement to interview coaching, they were with me every step of the way. I'm now working at my dream company!"
</p>
<div style="display: flex; align-items: center; gap: 1rem;">
<div style="width: 50px; height: 50px; border-radius: 50%; background: var(--accent-color); display: flex; align-items: center; justify-content: center; color: white; font-weight: 700;">MC</div>
<div>
<div style="font-weight: 600;">Michael Chen</div>
<div style="font-size: 0.875rem; color: var(--text-light);">Product Manager</div>
</div>
</div>
</div>
<div class="card">
<p style="font-style: italic; color: var(--text-light); margin-bottom: 1rem;">
"As a hiring manager, I've worked with many recruiting firms, but Ryans stands out. They consistently deliver high-quality candidates who are well-screened and truly interested in our opportunities."
</p>
<div style="display: flex; align-items: center; gap: 1rem;">
<div style="width: 50px; height: 50px; border-radius: 50%; background: var(--success); display: flex; align-items: center; justify-content: center; color: white; font-weight: 700;">EP</div>
<div>
<div style="font-weight: 600;">Emily Parker</div>
<div style="font-size: 0.875rem; color: var(--text-light);">HR Director</div>
</div>
</div>
</div>
<div class="card">
<p style="font-style: italic; color: var(--text-light); margin-bottom: 1rem;">
"After months of unsuccessful job hunting, Ryans helped me land a position that exceeded my salary expectations. Their expertise in negotiation made a huge difference!"
</p>
<div style="display: flex; align-items: center; gap: 1rem;">
<div style="width: 50px; height: 50px; border-radius: 50%; background: var(--warning); display: flex; align-items: center; justify-content: center; color: white; font-weight: 700;">DK</div>
<div>
<div style="font-weight: 600;">David Kim</div>
<div style="font-size: 0.875rem; color: var(--text-light);">Data Scientist</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section>
<div class="container">
<div class="section-title">
<h2>How It Works</h2>
<p>Getting started is simple and straightforward</p>
</div>
<div class="grid grid-4">
<div class="text-center">
<div style="width: 80px; height: 80px; border-radius: 50%; background: var(--primary-color); display: flex; align-items: center; justify-content: center; color: white; font-size: 2rem; font-weight: 700; margin: 0 auto 1rem;">1</div>
<h4>Browse Jobs</h4>
<p>Explore our curated job listings across various industries and locations.</p>
</div>
<div class="text-center">
<div style="width: 80px; height: 80px; border-radius: 50%; background: var(--primary-color); display: flex; align-items: center; justify-content: center; color: white; font-size: 2rem; font-weight: 700; margin: 0 auto 1rem;">2</div>
<h4>Submit Application</h4>
<p>Apply with your resume and let us know what you're looking for.</p>
</div>
<div class="text-center">
<div style="width: 80px; height: 80px; border-radius: 50%; background: var(--primary-color); display: flex; align-items: center; justify-content: center; color: white; font-size: 2rem; font-weight: 700; margin: 0 auto 1rem;">3</div>
<h4>Get Matched</h4>
<p>Our team reviews your profile and matches you with suitable opportunities.</p>
</div>
<div class="text-center">
<div style="width: 80px; height: 80px; border-radius: 50%; background: var(--primary-color); display: flex; align-items: center; justify-content: center; color: white; font-size: 2rem; font-weight: 700; margin: 0 auto 1rem;">4</div>
<h4>Start Your Journey</h4>
<p>Receive support throughout interviews and land your dream job!</p>
</div>
</div>
<div class="text-center" style="margin-top: 3rem;">
<a href="/jobs" class="btn btn-primary btn-lg">View Open Positions</a>
</div>
</div>
</section>
<section style="background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); color: white;">
<div class="container text-center">
<h2 style="color: white; margin-bottom: 1rem;">Ready to Take the Next Step?</h2>
<p style="color: rgba(255, 255, 255, 0.9); font-size: 1.125rem; margin-bottom: 2rem;">
Join thousands of professionals who have found their dream jobs through Ryans Recruit Firm
</p>
<div style="display: flex; gap: 1rem; justify-content: center;">
<a href="/jobs" class="btn btn-lg" style="background: white; color: var(--primary-color);">Browse Jobs</a>
<a href="/contact" class="btn btn-secondary btn-lg">Contact Us</a>
</div>
</div>
</section>
<footer>
<div class="container">
<div class="footer-content">
<div class="footer-section">
<h3 data-company-name>Ryans Recruit Firm</h3>
<p style="color: rgba(255, 255, 255, 0.8);" data-company-description>
Your trusted partner in career advancement and talent acquisition.
</p>
</div>
<div class="footer-section">
<h3>Quick Links</h3>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About Us</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/jobs">Job Listings</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
<div class="footer-section">
<h3>For Employers</h3>
<ul>
<li><a href="/contact">Post a Job</a></li>
<li><a href="/contact">Our Process</a></li>
<li><a href="/contact">Pricing</a></li>
</ul>
</div>
<div class="footer-section">
<h3>Contact</h3>
<ul>
<li>Email: <a href="mailto:info@ryansrecruit.com" data-contact-email>info@ryansrecruit.com</a></li>
<li>Phone: <a href="tel:+15551234567" data-contact-phone>+1 (555) 123-4567</a></li>
<li>Hours: <span data-business-hours>Mon-Fri 9AM-6PM EST</span></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2026 Ryans Recruit Firm. All rights reserved.</p>
</div>
</div>
</footer>
</body>
</html>

93
public/jobs.html Normal file
View File

@@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Job Listings - Ryans Recruit Firm</title>
<link rel="stylesheet" href="/css/styles.css">
<script src="/js/init.js"></script>
</head>
<body>
<header>
<nav class="container">
<div class="logo" data-company-name>Ryans Recruit Firm</div>
<ul class="nav-links">
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/jobs" class="active">Jobs</a></li>
<li><a href="/contact">Contact</a></li>
<li><a href="/admin/login" class="btn btn-primary btn-sm">Admin</a></li>
</ul>
</nav>
</header>
<section class="hero">
<div class="container">
<h1>Open Positions</h1>
<p>Discover your next opportunity with our curated job listings</p>
</div>
</section>
<section>
<div class="container">
<div id="loading" class="text-center" style="padding: 3rem 0;">
<div class="loading" style="display: inline-block; width: 3rem; height: 3rem; border: 4px solid rgba(37, 99, 235, 0.3); border-top-color: var(--primary-color); margin-bottom: 1rem;"></div>
<p style="color: var(--text-light);">Loading job listings...</p>
</div>
<div id="jobs-container">
<!-- Jobs will be loaded dynamically here -->
</div>
</div>
</section>
<footer>
<div class="container">
<div class="footer-content">
<div class="footer-section">
<h3 data-company-name>Ryans Recruit Firm</h3>
<p style="color: rgba(255, 255, 255, 0.8);">
Your trusted partner in career advancement and talent acquisition.
</p>
</div>
<div class="footer-section">
<h3>Quick Links</h3>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About Us</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/jobs">Job Listings</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
<div class="footer-section">
<h3>For Employers</h3>
<ul>
<li><a href="/contact">Post a Job</a></li>
<li><a href="/contact">Our Process</a></li>
<li><a href="/contact">Pricing</a></li>
</ul>
</div>
<div class="footer-section">
<h3>Contact</h3>
<ul>
<li>Email: info@ryansrecruit.com</li>
<li>Phone: +1 (555) 123-4567</li>
<li>Hours: Mon-Fri 9AM-6PM EST</li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2026 Ryans Recruit Firm. All rights reserved.</p>
</div>
</div>
</footer>
<script src="/js/main.js"></script>
</body>
</html>

290
public/js/admin.js Normal file
View File

@@ -0,0 +1,290 @@
// Admin JavaScript for Ryans Recruit Firm
// Check authentication on all admin pages except login
if (!window.location.pathname.includes('login')) {
checkAuth();
}
async function checkAuth() {
try {
const response = await fetch('/api/admin/check');
const data = await response.json();
if (!data.loggedIn) {
window.location.href = '/admin/login';
return;
}
// Update admin name in header if exists
const adminNameEl = document.getElementById('admin-name');
if (adminNameEl && data.admin) {
adminNameEl.textContent = data.admin.fullName;
}
} catch (err) {
console.error('Auth check failed:', err);
window.location.href = '/admin/login';
}
}
// Login page handlers
if (window.location.pathname.includes('login')) {
checkFirstAdmin();
document.getElementById('login-form')?.addEventListener('submit', handleLogin);
}
async function checkFirstAdmin() {
try {
const response = await fetch('/api/admin/check-first');
const data = await response.json();
if (data.isFirstAdmin) {
document.getElementById('first-admin-notice').style.display = 'block';
document.getElementById('full-name-group').style.display = 'block';
document.querySelector('button[type="submit"]').textContent = 'Create Admin Account';
}
} catch (err) {
console.error('Error checking first admin:', err);
}
}
async function handleLogin(e) {
e.preventDefault();
const form = e.target;
const submitBtn = form.querySelector('button[type="submit"]');
const originalText = submitBtn.textContent;
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="loading"></span> Processing...';
const formData = new FormData(form);
const data = Object.fromEntries(formData);
try {
const response = await fetch('/api/admin/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
showAlert('Login successful!', 'success');
setTimeout(() => window.location.href = '/admin/dashboard', 500);
} else {
showAlert(result.error || 'Login failed', 'error');
}
} catch (err) {
console.error('Login error:', err);
showAlert('Login failed. Please try again.', 'error');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = originalText;
}
}
// Dashboard page
if (window.location.pathname.includes('dashboard')) {
loadDashboardStats();
}
async function loadDashboardStats() {
try {
const response = await fetch('/api/admin/stats');
const stats = await response.json();
document.getElementById('stat-new-applications').textContent = stats.new_applications || 0;
document.getElementById('stat-total-applications').textContent = stats.total_applications || 0;
document.getElementById('stat-total-applicants').textContent = stats.total_applicants || 0;
document.getElementById('stat-active-jobs').textContent = stats.active_jobs || 0;
if (stats.unread_messages > 0) {
document.getElementById('stat-unread-messages').textContent = stats.unread_messages;
document.getElementById('stat-unread-messages').parentElement.style.display = 'block';
}
} catch (err) {
console.error('Error loading stats:', err);
}
}
// Applications page
if (window.location.pathname.includes('applicants')) {
loadApplications();
}
async function loadApplications(filters = {}) {
const container = document.getElementById('applications-container');
const loading = document.getElementById('loading');
try {
const params = new URLSearchParams(filters);
const response = await fetch(`/api/admin/applications?${params}`);
const applications = await response.json();
if (loading) loading.style.display = 'none';
if (applications.length === 0) {
container.innerHTML = '<div class="text-center"><p>No applications found.</p></div>';
return;
}
container.innerHTML = `
<table>
<thead>
<tr>
<th>Applicant</th>
<th>Job</th>
<th>Experience</th>
<th>Applied</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${applications.map(app => `
<tr>
<td>
<strong>${escapeHtml(app.full_name)}</strong><br>
<small>${escapeHtml(app.email)}</small>
</td>
<td>${escapeHtml(app.job_title || 'General Application')}</td>
<td>${app.years_of_experience || 'N/A'} years</td>
<td>${new Date(app.applied_at).toLocaleDateString()}</td>
<td><span class="tag tag-${getStatusColor(app.status)}">${escapeHtml(app.status)}</span></td>
<td>
<a href="/admin/applicants?id=${app.id}" class="btn btn-sm btn-primary">View</a>
<a href="/api/admin/applications/${app.id}/cv" class="btn btn-sm btn-secondary" target="_blank">CV</a>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
} catch (err) {
console.error('Error loading applications:', err);
if (loading) loading.style.display = 'none';
container.innerHTML = '<div class="alert alert-error">Failed to load applications</div>';
}
}
// Jobs management page
if (window.location.pathname.includes('jobs') && window.location.pathname.includes('admin')) {
loadAdminJobs();
}
async function loadAdminJobs() {
const container = document.getElementById('jobs-container');
const loading = document.getElementById('loading');
try {
const response = await fetch('/api/admin/jobs');
const jobs = await response.json();
if (loading) loading.style.display = 'none';
container.innerHTML = `
<div style="margin-bottom: 2rem;">
<button onclick="showJobForm()" class="btn btn-success">+ Create New Job</button>
</div>
<table>
<thead>
<tr>
<th>Title</th>
<th>Department</th>
<th>Location</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${jobs.map(job => `
<tr>
<td><strong>${escapeHtml(job.title)}</strong></td>
<td>${escapeHtml(job.department || 'N/A')}</td>
<td>${escapeHtml(job.location || 'N/A')}</td>
<td><span class="tag ${job.is_active ? 'tag-success' : 'tag-error'}">${job.is_active ? 'Active' : 'Inactive'}</span></td>
<td>${new Date(job.created_at).toLocaleDateString()}</td>
<td>
<button onclick="editJob(${job.id})" class="btn btn-sm btn-primary">Edit</button>
<button onclick="toggleJobStatus(${job.id}, ${!job.is_active})" class="btn btn-sm btn-secondary">${job.is_active ? 'Deactivate' : 'Activate'}</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
} catch (err) {
console.error('Error loading jobs:', err);
if (loading) loading.style.display = 'none';
container.innerHTML = '<div class="alert alert-error">Failed to load jobs</div>';
}
}
async function toggleJobStatus(jobId, isActive) {
try {
const response = await fetch(`/api/admin/jobs/${jobId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ isActive })
});
if (response.ok) {
showAlert(`Job ${isActive ? 'activated' : 'deactivated'} successfully`, 'success');
loadAdminJobs();
} else {
showAlert('Failed to update job status', 'error');
}
} catch (err) {
console.error('Error updating job:', err);
showAlert('Failed to update job status', 'error');
}
}
// Logout
async function logout() {
try {
await fetch('/api/admin/logout', { method: 'POST' });
window.location.href = '/admin/login';
} catch (err) {
console.error('Logout error:', err);
window.location.href = '/admin/login';
}
}
// Utility functions
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function getStatusColor(status) {
const colors = {
'new': 'primary',
'reviewing': 'warning',
'interview': 'info',
'hired': 'success',
'rejected': 'error'
};
return colors[status] || 'primary';
}
function showAlert(message, type = 'info') {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type}`;
alertDiv.textContent = message;
alertDiv.style.position = 'fixed';
alertDiv.style.top = '20px';
alertDiv.style.right = '20px';
alertDiv.style.zIndex = '10000';
alertDiv.style.minWidth = '300px';
document.body.appendChild(alertDiv);
setTimeout(() => {
alertDiv.remove();
}, 5000);
}

135
public/js/init.js Normal file
View File

@@ -0,0 +1,135 @@
// Initialize company configuration
// This file loads company configuration from the API and updates the page
let siteConfig = {};
async function loadSiteConfig() {
try {
const response = await fetch('/api/config');
siteConfig = await response.json();
// Update company information
updateCompanyInfo();
// Update branding (colors)
updateBranding();
// Update contact information
updateContactInfo();
// Update social media links
updateSocialLinks();
console.log('Site configuration loaded successfully');
} catch (err) {
console.error('Failed to load site configuration:', err);
}
}
function updateCompanyInfo() {
// Update company name
const companyNameElements = document.querySelectorAll('[data-company-name]');
companyNameElements.forEach(el => {
el.textContent = siteConfig.company.name;
});
// Update company tagline
const taglineElements = document.querySelectorAll('[data-company-tagline]');
taglineElements.forEach(el => {
el.textContent = siteConfig.company.tagline;
});
// Update company description
const descElements = document.querySelectorAll('[data-company-description]');
descElements.forEach(el => {
el.textContent = siteConfig.company.description;
});
// Update page title
const titleElement = document.querySelector('title');
if (titleElement && titleElement.textContent.includes('Ryans Recruit')) {
titleElement.textContent = titleElement.textContent.replace(/Ryans Recruit Firm|Ryans Recruit/g, siteConfig.company.name);
}
}
function updateBranding() {
// Set CSS custom properties for branding colors
const root = document.documentElement;
root.style.setProperty('--primary-color', siteConfig.branding.primaryColor);
root.style.setProperty('--accent-color', siteConfig.branding.accentColor);
root.style.setProperty('--secondary-color', siteConfig.branding.darkColor);
}
function updateContactInfo() {
// Update email
const emailElements = document.querySelectorAll('[data-contact-email]');
emailElements.forEach(el => {
if (el.tagName === 'A') {
el.href = `mailto:${siteConfig.contact.email}`;
}
el.textContent = siteConfig.contact.email;
});
// Update phone
const phoneElements = document.querySelectorAll('[data-contact-phone]');
phoneElements.forEach(el => {
if (el.tagName === 'A') {
el.href = `tel:${siteConfig.contact.phone}`;
}
el.textContent = siteConfig.contact.phone;
});
// Update address
const addressElements = document.querySelectorAll('[data-contact-address]');
addressElements.forEach(el => {
el.textContent = siteConfig.contact.address;
});
// Update business hours
const hoursElements = document.querySelectorAll('[data-business-hours]');
hoursElements.forEach(el => {
el.textContent = siteConfig.contact.businessHours;
});
}
function updateSocialLinks() {
// Update LinkedIn
const linkedinElements = document.querySelectorAll('[data-social-linkedin]');
linkedinElements.forEach(el => {
if (siteConfig.social.linkedin) {
el.href = siteConfig.social.linkedin;
el.style.display = '';
} else {
el.style.display = 'none';
}
});
// Update Twitter
const twitterElements = document.querySelectorAll('[data-social-twitter]');
twitterElements.forEach(el => {
if (siteConfig.social.twitter) {
el.href = siteConfig.social.twitter;
el.style.display = '';
} else {
el.style.display = 'none';
}
});
// Update Facebook
const facebookElements = document.querySelectorAll('[data-social-facebook]');
facebookElements.forEach(el => {
if (siteConfig.social.facebook) {
el.href = siteConfig.social.facebook;
el.style.display = '';
} else {
el.style.display = 'none';
}
});
}
// Load configuration when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadSiteConfig);
} else {
loadSiteConfig();
}

201
public/js/main.js Normal file
View File

@@ -0,0 +1,201 @@
// Main JavaScript for Ryans Recruit Firm
// Load jobs on jobs.html page
if (window.location.pathname.includes('jobs')) {
loadJobs();
}
async function loadJobs() {
const jobsContainer = document.getElementById('jobs-container');
const loadingEl = document.getElementById('loading');
try {
const response = await fetch('/api/jobs');
const jobs = await response.json();
if (loadingEl) loadingEl.style.display = 'none';
if (jobs.length === 0) {
jobsContainer.innerHTML = '<div class="text-center"><p>No job openings at the moment. Check back soon!</p></div>';
return;
}
jobsContainer.innerHTML = jobs.map(job => `
<div class="job-card">
<div class="job-header">
<div>
<h3 class="job-title">${escapeHtml(job.title)}</h3>
<div class="job-meta">
<span class="job-meta-item">📍 ${escapeHtml(job.location || 'Not specified')}</span>
<span class="job-meta-item">💼 ${escapeHtml(job.employment_type || 'Full-time')}</span>
${job.salary_range ? `<span class="job-meta-item">💰 ${escapeHtml(job.salary_range)}</span>` : ''}
${job.department ? `<span class="tag tag-primary">${escapeHtml(job.department)}</span>` : ''}
</div>
</div>
</div>
<p class="job-description">${escapeHtml(job.description.substring(0, 200))}...</p>
<a href="/apply?job=${job.id}" class="btn btn-primary">Apply Now</a>
</div>
`).join('');
} catch (err) {
console.error('Error loading jobs:', err);
if (loadingEl) loadingEl.style.display = 'none';
jobsContainer.innerHTML = '<div class="alert alert-error">Failed to load job listings. Please try again later.</div>';
}
}
// Handle application form on apply.html
if (window.location.pathname.includes('apply')) {
loadJobDetails();
document.getElementById('application-form')?.addEventListener('submit', handleApplicationSubmit);
}
async function loadJobDetails() {
const urlParams = new URLSearchParams(window.location.search);
const jobId = urlParams.get('job');
if (!jobId) return;
try {
const response = await fetch(`/api/jobs/${jobId}`);
const job = await response.json();
document.getElementById('job-title').textContent = job.title;
document.getElementById('job-details').innerHTML = `
<p><strong>Location:</strong> ${escapeHtml(job.location)}</p>
<p><strong>Type:</strong> ${escapeHtml(job.employment_type)}</p>
${job.salary_range ? `<p><strong>Salary:</strong> ${escapeHtml(job.salary_range)}</p>` : ''}
<p><strong>Description:</strong></p>
<p>${escapeHtml(job.description)}</p>
`;
} catch (err) {
console.error('Error loading job details:', err);
}
}
async function handleApplicationSubmit(e) {
e.preventDefault();
const form = e.target;
const submitBtn = form.querySelector('button[type="submit"]');
const originalBtnText = submitBtn.textContent;
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="loading"></span> Submitting...';
const formData = new FormData(form);
try {
const response = await fetch('/api/apply', {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok) {
showAlert('Application submitted successfully! We\'ll be in touch soon.', 'success');
form.reset();
setTimeout(() => window.location.href = '/jobs', 2000);
} else {
showAlert(result.error || 'Failed to submit application', 'error');
}
} catch (err) {
console.error('Error submitting application:', err);
showAlert('Failed to submit application. Please try again.', 'error');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = originalBtnText;
}
}
// Handle contact form on contact.html
if (window.location.pathname.includes('contact')) {
document.getElementById('contact-form')?.addEventListener('submit', handleContactSubmit);
}
async function handleContactSubmit(e) {
e.preventDefault();
const form = e.target;
const submitBtn = form.querySelector('button[type="submit"]');
const originalBtnText = submitBtn.textContent;
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="loading"></span> Sending...';
const formData = new FormData(form);
const data = Object.fromEntries(formData);
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
showAlert('Message sent successfully! We\'ll get back to you soon.', 'success');
form.reset();
} else {
showAlert(result.error || 'Failed to send message', 'error');
}
} catch (err) {
console.error('Error sending message:', err);
showAlert('Failed to send message. Please try again.', 'error');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = originalBtnText;
}
}
// Utility functions
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showAlert(message, type = 'info') {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type}`;
alertDiv.textContent = message;
alertDiv.style.position = 'fixed';
alertDiv.style.top = '20px';
alertDiv.style.right = '20px';
alertDiv.style.zIndex = '10000';
alertDiv.style.minWidth = '300px';
document.body.appendChild(alertDiv);
setTimeout(() => {
alertDiv.remove();
}, 5000);
}
// File input validation
document.addEventListener('DOMContentLoaded', () => {
const fileInputs = document.querySelectorAll('input[type="file"]');
fileInputs.forEach(input => {
input.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const maxSize = 5 * 1024 * 1024; // 5MB
if (file.size > maxSize) {
showAlert('File size must be less than 5MB', 'error');
e.target.value = '';
return;
}
const allowedTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
if (!allowedTypes.includes(file.type)) {
showAlert('Only PDF, DOC, and DOCX files are allowed', 'error');
e.target.value = '';
return;
}
}
});
});
});

110
public/services.html Normal file
View File

@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Our Services - Ryans Recruit Firm</title>
<link rel="stylesheet" href="/css/styles.css">
<script src="/js/init.js"></script>
</head>
<body>
<header>
<nav class="container">
<div class="logo" data-company-name>Ryans Recruit Firm</div>
<ul class="nav-links">
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/services" class="active">Services</a></li>
<li><a href="/jobs">Jobs</a></li>
<li><a href="/contact">Contact</a></li>
<li><a href="/admin/login" class="btn btn-primary btn-sm">Admin</a></li>
</ul>
</nav>
</header>
<section class="hero">
<div class="container">
<h1>Our Services</h1>
<p>Comprehensive recruitment solutions tailored to your needs</p>
</div>
</section>
<section>
<div class="container">
<div class="grid grid-2">
<div class="card">
<h3>For Job Seekers</h3>
<ul style="line-height: 2; color: var(--text-light);">
<li>Career counseling and guidance</li>
<li>Resume review and optimization</li>
<li>Interview preparation and coaching</li>
<li>Salary negotiation support</li>
<li>Access to exclusive job opportunities</li>
<li>Long-term career development advice</li>
</ul>
</div>
<div class="card">
<h3>For Employers</h3>
<ul style="line-height: 2; color: var(--text-light);">
<li>Talent sourcing and screening</li>
<li>Comprehensive candidate assessment</li>
<li>Interview coordination</li>
<li>Background verification</li>
<li>Offer negotiation assistance</li>
<li>Post-placement follow-up</li>
</ul>
</div>
</div>
</div>
</section>
<section style="background: var(--bg-white);">
<div class="container">
<div class="section-title">
<h2>Industries We Serve</h2>
</div>
<div class="grid grid-3">
<div class="card"><h4>Technology</h4><p>Software, IT, Engineering</p></div>
<div class="card"><h4>Finance</h4><p>Banking, Insurance, Investment</p></div>
<div class="card"><h4>Healthcare</h4><p>Medical, Pharmaceutical, Biotech</p></div>
<div class="card"><h4>Marketing</h4><p>Digital, Traditional, Analytics</p></div>
<div class="card"><h4>Sales</h4><p>B2B, B2C, Enterprise</p></div>
<div class="card"><h4>Operations</h4><p>Supply Chain, Logistics, Management</p></div>
</div>
</div>
</section>
<footer>
<div class="container">
<div class="footer-content">
<div class="footer-section">
<h3 data-company-name>Ryans Recruit Firm</h3>
<p style="color: rgba(255, 255, 255, 0.8);">Your trusted partner in career advancement and talent acquisition.</p>
</div>
<div class="footer-section">
<h3>Quick Links</h3>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About Us</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/jobs">Job Listings</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
<div class="footer-section">
<h3>Contact</h3>
<ul>
<li>Email: info@ryansrecruit.com</li>
<li>Phone: +1 (555) 123-4567</li>
<li>Hours: Mon-Fri 9AM-6PM EST</li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2026 Ryans Recruit Firm. All rights reserved.</p>
</div>
</div>
</footer>
</body>
</html>

717
server.js Normal file
View File

@@ -0,0 +1,717 @@
const express = require('express');
const session = require('express-session');
const bcrypt = require('bcrypt');
const { Pool } = require('pg');
const multer = require('multer');
const path = require('path');
const cookieParser = require('cookie-parser');
const fs = require('fs');
const config = require('./config');
const app = express();
const PORT = config.app.port;
// PostgreSQL connection
const pool = new Pool({
host: config.database.host,
port: config.database.port,
database: config.database.name,
user: config.database.user,
password: config.database.password,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
// Run database migrations
async function runMigrations() {
try {
console.log('Running database migrations...');
const migrationsDir = path.join(__dirname, 'migrations');
const migrationFiles = fs.readdirSync(migrationsDir).sort();
for (const file of migrationFiles) {
if (file.endsWith('.sql')) {
console.log(`Running migration: ${file}`);
const migrationSQL = fs.readFileSync(path.join(migrationsDir, file), 'utf8');
await pool.query(migrationSQL);
console.log(`✓ Migration ${file} completed`);
}
}
console.log('All migrations completed successfully');
} catch (error) {
console.error('Migration error:', error.message);
throw error;
}
}
// Test database connection and run migrations
pool.query('SELECT NOW()')
.then((res) => {
console.log('Database connected successfully at:', res.rows[0].now);
return runMigrations();
})
.then(() => {
console.log('Database initialization complete');
})
.catch((err) => {
console.error('Database initialization error:', err.message);
console.error('Application will continue but database operations will fail');
});
// Middleware
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(cookieParser());
app.use(session({
secret: config.app.sessionSecret,
resave: false,
saveUninitialized: false,
cookie: {
secure: config.app.nodeEnv === 'production', // Automatically enable for production
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
// Serve static files first
app.use(express.static('public'));
// Middleware to handle routes without .html extension
app.use((req, res, next) => {
// Skip API routes
if (req.path.startsWith('/api/')) {
return next();
}
// Handle root path
if (req.path === '/') {
return res.sendFile(path.join(__dirname, 'public', 'index.html'));
}
// Handle paths without extensions (not already handled by static middleware)
if (req.path.indexOf('.') === -1) {
const file = `${req.path}.html`;
return res.sendFile(path.join(__dirname, 'public', file), (err) => {
if (err) {
next();
}
});
}
next();
});
// Configure multer for file uploads (memory storage for CV uploads)
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: config.features.maxCvSizeMB * 1024 * 1024,
},
fileFilter: (req, file, cb) => {
const allowedTypes = config.features.allowedCvTypes;
const isAllowed = allowedTypes.includes(file.mimetype);
if (isAllowed) {
return cb(null, true);
}
cb(new Error('File type not allowed. Only PDF, DOC, and DOCX files are accepted.'));
}
});
// Auth middleware
const requireAuth = (req, res, next) => {
if (req.session.adminId) {
next();
} else {
res.status(401).json({ error: 'Unauthorized' });
}
};
// ============================================
// PUBLIC API ENDPOINTS
// ============================================
// Get company configuration for frontend
app.get('/api/config', (req, res) => {
res.json({
company: config.company,
branding: config.branding,
contact: config.contact,
social: config.social,
about: config.about,
services: config.services
});
});
// Get all active job postings
app.get('/api/jobs', async (req, res) => {
try {
const result = await pool.query(
'SELECT id, title, department, location, employment_type, salary_range, description, requirements, benefits, created_at FROM job_postings WHERE is_active = true ORDER BY created_at DESC'
);
res.json(result.rows);
} catch (err) {
console.error('Error fetching jobs:', err);
res.status(500).json({ error: 'Failed to fetch jobs' });
}
});
// Get single job posting
app.get('/api/jobs/:id', async (req, res) => {
try {
const result = await pool.query(
'SELECT id, title, department, location, employment_type, salary_range, description, requirements, benefits, created_at FROM job_postings WHERE id = $1 AND is_active = true',
[req.params.id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Job not found' });
}
res.json(result.rows[0]);
} catch (err) {
console.error('Error fetching job:', err);
res.status(500).json({ error: 'Failed to fetch job' });
}
});
// Submit job application with CV
app.post('/api/apply', upload.single('cv'), async (req, res) => {
const client = await pool.connect();
try {
await client.query('BEGIN');
const {
fullName,
email,
phone,
linkedinUrl,
portfolioUrl,
yearsOfExperience,
currentPosition,
currentCompany,
preferredLocation,
jobId,
coverLetter
} = req.body;
// Validate required fields
if (!fullName || !email || !req.file) {
return res.status(400).json({ error: 'Name, email, and CV are required' });
}
// Insert applicant
const applicantResult = await client.query(
`INSERT INTO applicants (full_name, email, phone, linkedin_url, portfolio_url, years_of_experience, current_position, current_company, preferred_location)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id`,
[fullName, email, phone, linkedinUrl, portfolioUrl, yearsOfExperience, currentPosition, currentCompany, preferredLocation]
);
const applicantId = applicantResult.rows[0].id;
// Insert application with CV
await client.query(
`INSERT INTO applications (applicant_id, job_id, cover_letter, cv_filename, cv_content_type, cv_file, status)
VALUES ($1, $2, $3, $4, $5, $6, 'new')`,
[
applicantId,
jobId || null,
coverLetter,
req.file.originalname,
req.file.mimetype,
req.file.buffer
]
);
await client.query('COMMIT');
res.json({
success: true,
message: 'Application submitted successfully',
applicantId
});
} catch (err) {
await client.query('ROLLBACK');
console.error('Error submitting application:', err);
res.status(500).json({ error: 'Failed to submit application' });
} finally {
client.release();
}
});
// Submit contact form
app.post('/api/contact', async (req, res) => {
try {
const { name, email, subject, message } = req.body;
if (!name || !email || !message) {
return res.status(400).json({ error: 'Name, email, and message are required' });
}
await pool.query(
'INSERT INTO contact_submissions (name, email, subject, message) VALUES ($1, $2, $3, $4)',
[name, email, subject, message]
);
res.json({ success: true, message: 'Message sent successfully' });
} catch (err) {
console.error('Error submitting contact form:', err);
res.status(500).json({ error: 'Failed to send message' });
}
});
// ============================================
// ADMIN AUTHENTICATION ENDPOINTS
// ============================================
// Admin login (creates admin on first login if none exist)
app.post('/api/admin/login', async (req, res) => {
try {
const { email, password, fullName, 'full-name': fullNameHyphen } = req.body;
const name = fullName || fullNameHyphen;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password are required' });
}
// Check if any admins exist
const adminCount = await pool.query('SELECT COUNT(*) FROM admins');
const isFirstAdmin = adminCount.rows[0].count === '0';
if (isFirstAdmin) {
// Create first admin
if (!name) {
return res.status(400).json({ error: 'Full name is required for first admin creation' });
}
const hashedPassword = await bcrypt.hash(password, 10);
const result = await pool.query(
'INSERT INTO admins (email, password_hash, full_name) VALUES ($1, $2, $3) RETURNING id, email, full_name',
[email, hashedPassword, name]
);
const admin = result.rows[0];
req.session.adminId = admin.id;
req.session.adminEmail = admin.email;
req.session.adminName = admin.full_name;
return res.json({
success: true,
message: 'Admin account created successfully',
admin: { id: admin.id, email: admin.email, fullName: admin.full_name },
isFirstLogin: true
});
}
// Regular login
const result = await pool.query(
'SELECT id, email, password_hash, full_name FROM admins WHERE email = $1',
[email]
);
if (result.rows.length === 0) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const admin = result.rows[0];
const passwordMatch = await bcrypt.compare(password, admin.password_hash);
if (!passwordMatch) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Update last login
await pool.query('UPDATE admins SET last_login = CURRENT_TIMESTAMP WHERE id = $1', [admin.id]);
req.session.adminId = admin.id;
req.session.adminEmail = admin.email;
req.session.adminName = admin.full_name;
res.json({
success: true,
admin: { id: admin.id, email: admin.email, fullName: admin.full_name }
});
} catch (err) {
console.error('Error during login:', err);
res.status(500).json({ error: 'Login failed' });
}
});
// Check if admin is logged in
app.get('/api/admin/check', (req, res) => {
if (req.session.adminId) {
res.json({
loggedIn: true,
admin: {
id: req.session.adminId,
email: req.session.adminEmail,
fullName: req.session.adminName
}
});
} else {
res.json({ loggedIn: false });
}
});
// Check if first admin needs to be created
app.get('/api/admin/check-first', async (req, res) => {
try {
const result = await pool.query('SELECT COUNT(*) FROM admins');
const isFirstAdmin = result.rows[0].count === '0';
res.json({ isFirstAdmin });
} catch (err) {
console.error('Error checking first admin:', err);
res.status(500).json({ error: 'Failed to check admin status' });
}
});
// Admin logout
app.post('/api/admin/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: 'Logout failed' });
}
res.json({ success: true });
});
});
// ============================================
// ADMIN API ENDPOINTS (Protected)
// ============================================
// Get dashboard statistics
app.get('/api/admin/stats', requireAuth, async (req, res) => {
try {
const stats = await pool.query(`
SELECT
(SELECT COUNT(*) FROM applications WHERE status = 'new') as new_applications,
(SELECT COUNT(*) FROM applications) as total_applications,
(SELECT COUNT(*) FROM applicants) as total_applicants,
(SELECT COUNT(*) FROM job_postings WHERE is_active = true) as active_jobs,
(SELECT COUNT(*) FROM contact_submissions WHERE is_read = false) as unread_messages
`);
res.json(stats.rows[0]);
} catch (err) {
console.error('Error fetching stats:', err);
res.status(500).json({ error: 'Failed to fetch statistics' });
}
});
// Get all applications with applicant details
app.get('/api/admin/applications', requireAuth, async (req, res) => {
try {
const { status, jobId, search } = req.query;
let query = `
SELECT
app.id,
app.status,
app.applied_at,
app.cv_filename,
app.cover_letter,
app.notes,
applicant.id as applicant_id,
applicant.full_name,
applicant.email,
applicant.phone,
applicant.years_of_experience,
applicant.current_position,
applicant.current_company,
job.id as job_id,
job.title as job_title
FROM applications app
INNER JOIN applicants applicant ON app.applicant_id = applicant.id
LEFT JOIN job_postings job ON app.job_id = job.id
WHERE 1=1
`;
const params = [];
let paramCount = 0;
if (status) {
paramCount++;
query += ` AND app.status = $${paramCount}`;
params.push(status);
}
if (jobId) {
paramCount++;
query += ` AND app.job_id = $${paramCount}`;
params.push(jobId);
}
if (search) {
paramCount++;
query += ` AND (applicant.full_name ILIKE $${paramCount} OR applicant.email ILIKE $${paramCount})`;
params.push(`%${search}%`);
}
query += ' ORDER BY app.applied_at DESC';
const result = await pool.query(query, params);
res.json(result.rows);
} catch (err) {
console.error('Error fetching applications:', err);
res.status(500).json({ error: 'Failed to fetch applications' });
}
});
// Get single application details
app.get('/api/admin/applications/:id', requireAuth, async (req, res) => {
try {
const result = await pool.query(`
SELECT
app.*,
applicant.full_name,
applicant.email,
applicant.phone,
applicant.linkedin_url,
applicant.portfolio_url,
applicant.years_of_experience,
applicant.current_position,
applicant.current_company,
applicant.preferred_location,
job.title as job_title
FROM applications app
INNER JOIN applicants applicant ON app.applicant_id = applicant.id
LEFT JOIN job_postings job ON app.job_id = job.id
WHERE app.id = $1
`, [req.params.id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Application not found' });
}
// Don't send the CV file in this response
const application = result.rows[0];
delete application.cv_file;
res.json(application);
} catch (err) {
console.error('Error fetching application:', err);
res.status(500).json({ error: 'Failed to fetch application' });
}
});
// Download CV
app.get('/api/admin/applications/:id/cv', requireAuth, async (req, res) => {
try {
const result = await pool.query(
'SELECT cv_file, cv_filename, cv_content_type FROM applications WHERE id = $1',
[req.params.id]
);
if (result.rows.length === 0 || !result.rows[0].cv_file) {
return res.status(404).json({ error: 'CV not found' });
}
const { cv_file, cv_filename, cv_content_type } = result.rows[0];
res.setHeader('Content-Type', cv_content_type);
res.setHeader('Content-Disposition', `attachment; filename="${cv_filename}"`);
res.send(cv_file);
} catch (err) {
console.error('Error downloading CV:', err);
res.status(500).json({ error: 'Failed to download CV' });
}
});
// Update application status
app.patch('/api/admin/applications/:id', requireAuth, async (req, res) => {
try {
const { status, notes } = req.body;
const result = await pool.query(
'UPDATE applications SET status = COALESCE($1, status), notes = COALESCE($2, notes) WHERE id = $3 RETURNING *',
[status, notes, req.params.id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Application not found' });
}
res.json(result.rows[0]);
} catch (err) {
console.error('Error updating application:', err);
res.status(500).json({ error: 'Failed to update application' });
}
});
// Get all job postings (admin view - includes inactive)
app.get('/api/admin/jobs', requireAuth, async (req, res) => {
try {
const result = await pool.query(
'SELECT * FROM job_postings ORDER BY created_at DESC'
);
res.json(result.rows);
} catch (err) {
console.error('Error fetching jobs:', err);
res.status(500).json({ error: 'Failed to fetch jobs' });
}
});
// Create job posting
app.post('/api/admin/jobs', requireAuth, async (req, res) => {
try {
const {
title,
department,
location,
employmentType,
salaryRange,
description,
requirements,
benefits
} = req.body;
if (!title || !description) {
return res.status(400).json({ error: 'Title and description are required' });
}
const result = await pool.query(
`INSERT INTO job_postings
(title, department, location, employment_type, salary_range, description, requirements, benefits, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING *`,
[title, department, location, employmentType, salaryRange, description, requirements, benefits, req.session.adminId]
);
res.json(result.rows[0]);
} catch (err) {
console.error('Error creating job:', err);
res.status(500).json({ error: 'Failed to create job posting' });
}
});
// Update job posting
app.patch('/api/admin/jobs/:id', requireAuth, async (req, res) => {
try {
const {
title,
department,
location,
employmentType,
salaryRange,
description,
requirements,
benefits,
isActive
} = req.body;
const result = await pool.query(
`UPDATE job_postings
SET title = COALESCE($1, title),
department = COALESCE($2, department),
location = COALESCE($3, location),
employment_type = COALESCE($4, employment_type),
salary_range = COALESCE($5, salary_range),
description = COALESCE($6, description),
requirements = COALESCE($7, requirements),
benefits = COALESCE($8, benefits),
is_active = COALESCE($9, is_active)
WHERE id = $10
RETURNING *`,
[title, department, location, employmentType, salaryRange, description, requirements, benefits, isActive, req.params.id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Job not found' });
}
res.json(result.rows[0]);
} catch (err) {
console.error('Error updating job:', err);
res.status(500).json({ error: 'Failed to update job posting' });
}
});
// Delete job posting
app.delete('/api/admin/jobs/:id', requireAuth, async (req, res) => {
try {
const result = await pool.query(
'DELETE FROM job_postings WHERE id = $1 RETURNING id',
[req.params.id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Job not found' });
}
res.json({ success: true, message: 'Job deleted successfully' });
} catch (err) {
console.error('Error deleting job:', err);
res.status(500).json({ error: 'Failed to delete job posting' });
}
});
// Get contact submissions
app.get('/api/admin/contacts', requireAuth, async (req, res) => {
try {
const result = await pool.query(
'SELECT * FROM contact_submissions ORDER BY created_at DESC'
);
res.json(result.rows);
} catch (err) {
console.error('Error fetching contacts:', err);
res.status(500).json({ error: 'Failed to fetch contact submissions' });
}
});
// Mark contact as read
app.patch('/api/admin/contacts/:id', requireAuth, async (req, res) => {
try {
const result = await pool.query(
'UPDATE contact_submissions SET is_read = true WHERE id = $1 RETURNING *',
[req.params.id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Contact submission not found' });
}
res.json(result.rows[0]);
} catch (err) {
console.error('Error updating contact:', err);
res.status(500).json({ error: 'Failed to update contact submission' });
}
});
// ============================================
// ERROR HANDLING
// ============================================
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'File size too large. Maximum size is 5MB.' });
}
return res.status(400).json({ error: err.message });
}
console.error('Unhandled error:', err);
res.status(500).json({ error: 'Internal server error' });
});
// ============================================
// START SERVER
// ============================================
app.listen(PORT, '0.0.0.0', () => {
console.log(`AI Recruitment Site running on port ${PORT}`);
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
console.log(`Database: ${process.env.DB_HOST || 'postgres'}:${process.env.DB_PORT || 5432}`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM signal received: closing HTTP server');
pool.end(() => {
console.log('Database pool closed');
process.exit(0);
});
});