32 KiB
Copper Tone Technologies API Documentation
This document provides comprehensive documentation for all backend API endpoints and frontend integration patterns.
Table of Contents
Overview
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Frontend (Vue 3 PWA) │
│ Port 8090 │
└─────────────────────────────────────────────────────────────────┘
│
Nginx Reverse Proxy
│
┌─────────────────────┼─────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ /api/auth/* │ │ /api/work/* │ │ /api/payment/*│
│ Auth Service │ │ Work Mgmt │ │ Payment │
│ Port 8082 │ │ Port 8083 │ │ Port 8084 │
└───────────────┘ └───────────────┘ └───────────────┘
│ │ │
└─────────────────────┼─────────────────────┘
│
┌─────────────────┐
│ PostgreSQL │
│ Port 5432 │
└─────────────────┘
Additional Services:
┌───────────────┐ ┌───────────────┐
│ /api/blog/* │ │ /api/ipfs/* │
│ Blog Service │ │ IPFS/P2P │
│ Port 8085 │ │ Port 8086 │
└───────────────┘ └───────────────┘
Base URLs
| Environment | Base URL |
|---|---|
| Development | http://localhost:8090/api |
| Production | https://coppertone.tech/api |
Response Format
All API responses are JSON. Successful responses return the requested data directly. Error responses follow this format:
{
"error": "Error message description"
}
Or for validation errors:
{
"field": "fieldName",
"message": "Validation error message"
}
Authentication
JWT Token
All protected endpoints require a JWT token in the Authorization header:
Authorization: Bearer <token>
Token Payload
{
"user_id": 1,
"userId": 1,
"email": "user@example.com",
"roles": ["CLIENT", "STAFF", "ADMIN"],
"exp": 1700000000
}
Roles
| Role | Description | Permissions |
|---|---|---|
CLIENT |
Customer/end-user | View own projects, invoices; manage own identities |
STAFF |
Internal team member | Create/edit work items, invoices, blogs |
ADMIN |
Administrator | Full access including user management |
Getting a Token
Email/Password Login:
curl -X POST http://localhost:8090/api/auth/login-email-password \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"Password123"}'
Blockchain Login:
curl -X POST http://localhost:8090/api/auth/login-blockchain \
-H "Content-Type: application/json" \
-d '{
"address":"0x...",
"signature":"0x...",
"message":"Sign this message to login"
}'
Backend Services
Auth Service (Port 8082)
Base path: /api/auth
Public Endpoints
POST /register-email-password
Register a new user with email and password.
Request:
{
"email": "user@example.com",
"password": "SecurePass123",
"name": "John Doe"
}
Password Requirements:
- 8-72 characters
- At least 1 uppercase letter
- At least 1 lowercase letter
- At least 1 number
Response (201):
{
"message": "User registered successfully",
"userId": 1
}
Notes:
- First registered user automatically gets ADMIN role
- All subsequent users get CLIENT role
POST /register-blockchain
Register with Ethereum wallet.
Request:
{
"address": "0x1234...",
"signature": "0xabcd...",
"message": "Sign this message to register",
"name": "John Doe"
}
Response (201):
{
"message": "User registered successfully",
"userId": 1
}
POST /login-email-password
Login with email and password.
Request:
{
"email": "user@example.com",
"password": "SecurePass123"
}
Response (200):
{
"token": "eyJhbGciOiJIUzI1NiIs..."
}
POST /login-blockchain
Login with Ethereum signature.
Request:
{
"address": "0x1234...",
"signature": "0xabcd...",
"message": "Sign this message to login"
}
Response (200):
{
"token": "eyJhbGciOiJIUzI1NiIs..."
}
GET /healthz
Health check endpoint.
Response (200):
ok
Protected Endpoints
GET /profile
Get current user's profile.
Headers: Authorization: Bearer <token>
Response (200):
{
"id": 1,
"name": "John Doe",
"email": "user@example.com",
"roles": ["CLIENT"],
"createdAt": "2024-01-01T00:00:00Z"
}
GET /identities
Get user's linked identities.
Headers: Authorization: Bearer <token>
Response (200):
[
{
"id": 1,
"userId": 1,
"type": "email_password",
"identifier": "user@example.com",
"isPrimaryLogin": true,
"createdAt": "2024-01-01T00:00:00Z"
}
]
POST /link-identity
Link a new identity to account.
Headers: Authorization: Bearer <token>
Request (email):
{
"type": "email_password",
"email": "another@example.com",
"password": "SecurePass123"
}
Request (blockchain):
{
"type": "blockchain_address",
"address": "0x1234...",
"signature": "0xabcd...",
"message": "Sign to link identity"
}
Response (201):
{
"message": "Identity linked successfully"
}
POST /unlink-identity
Remove an identity from account.
Headers: Authorization: Bearer <token>
Request:
{
"identityId": 2
}
Response (200):
{
"message": "Identity unlinked successfully"
}
Constraints:
- Cannot remove last identity
- If removing primary, another identity is auto-promoted
Admin Endpoints
GET /admin/users
Get all users (ADMIN only).
Headers: Authorization: Bearer <token>
Response (200):
[
{
"id": 1,
"name": "John Doe",
"email": "user@example.com",
"roles": ["ADMIN"],
"createdAt": "2024-01-01T00:00:00Z"
}
]
POST /admin/users/promote-role
Grant a role to a user (ADMIN only).
Headers: Authorization: Bearer <token>
Request:
{
"userId": 2,
"role": "STAFF"
}
Response (200):
{
"message": "Successfully granted STAFF role to user 2"
}
POST /admin/users/demote-role
Remove a role from a user (ADMIN only).
Headers: Authorization: Bearer <token>
Request:
{
"userId": 2,
"role": "STAFF"
}
Response (200):
{
"message": "Successfully removed STAFF role from user 2"
}
Constraints:
- Cannot remove user's last role
- Admin cannot remove own ADMIN role
Work Management Service (Port 8083)
Base path: /api/work
Project Endpoints
GET /projects
List all projects.
Headers: Authorization: Bearer <token>
Authorization:
- STAFF/ADMIN: See all projects
- CLIENT: See only their own projects
Response (200):
[
{
"id": 1,
"name": "Website Redesign",
"description": "Complete website overhaul",
"status": "IN_PROGRESS",
"clientId": 2,
"ipfsMetadataCid": null,
"startDate": "2024-01-01",
"endDate": "2024-03-01",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-15T00:00:00Z"
}
]
POST /projects
Create a new project (STAFF/ADMIN).
Headers: Authorization: Bearer <token>
Request:
{
"name": "New Project",
"description": "Project description",
"status": "PLANNING",
"clientId": 2,
"startDate": "2024-02-01",
"endDate": "2024-04-01"
}
Status Values: PLANNING, IN_PROGRESS, ON_HOLD, COMPLETED, ARCHIVED
Response (201): Created project object
GET /projects/{id}
Get project by ID.
Headers: Authorization: Bearer <token>
Response (200): Project object
PUT /projects/{id}
Update a project (STAFF/ADMIN).
Headers: Authorization: Bearer <token>
Request: Same as POST
Response (200): Updated project object
DELETE /projects/{id}
Delete a project (STAFF/ADMIN only).
Headers: Authorization: Bearer <token>
Response: 204 No Content
Task Endpoints
GET /tasks
List tasks.
Headers: Authorization: Bearer <token>
Query Parameters:
project_id(optional): Filter by project
Response (200):
[
{
"id": 1,
"projectId": 1,
"title": "Design mockups",
"description": "Create UI mockups",
"status": "IN_PROGRESS",
"assigneeId": 3,
"dueDate": "2024-01-15",
"completedAt": null,
"priority": 1,
"estimatedHours": 8,
"actualHours": null,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-10T00:00:00Z"
}
]
POST /tasks
Create a task (STAFF/ADMIN).
Headers: Authorization: Bearer <token>
Request:
{
"projectId": 1,
"title": "New Task",
"description": "Task description",
"status": "TODO",
"assigneeId": 3,
"dueDate": "2024-02-01",
"priority": 2,
"estimatedHours": 4
}
Status Values: TODO, IN_PROGRESS, COMPLETED
Response (201): Created task object
GET /tasks/{id}
Get task by ID.
Response (200): Task object
PUT /tasks/{id}
Update a task (STAFF/ADMIN).
Note: Setting status to "COMPLETED" auto-sets completedAt
Response (200): Updated task object
DELETE /tasks/{id}
Delete a task (STAFF/ADMIN).
Response: 204 No Content
Work Order Endpoints
GET /workorders
List work orders (STAFF/ADMIN).
Headers: Authorization: Bearer <token>
Query Parameters:
project_id(optional): Filter by project
Response (200):
[
{
"id": 1,
"projectId": 1,
"title": "Initial Setup",
"description": "Server configuration",
"orderNumber": "WO-2024-001",
"ipfsDocumentCid": null,
"createdBy": 3,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z"
}
]
POST /workorders
Create work order (STAFF/ADMIN).
Request:
{
"projectId": 1,
"title": "Work Order Title",
"description": "Description",
"orderNumber": "WO-2024-002"
}
Response (201): Created work order object
GET /workorders/{id}
Get work order by ID (STAFF/ADMIN).
PUT /workorders/{id}
Update work order (STAFF/ADMIN).
DELETE /workorders/{id}
Delete work order (STAFF/ADMIN).
Payment Service (Port 8084)
Base path: /api/payment
Invoice Endpoints
GET /invoices
List invoices.
Headers: Authorization: Bearer <token>
Query Parameters:
client_id(optional, STAFF/ADMIN only): Filter by client
Authorization:
- STAFF/ADMIN: See all or filtered
- CLIENT: See only their own invoices
Response (200):
[
{
"id": 1,
"invoiceNumber": "INV-2024-001",
"projectId": 1,
"clientId": 2,
"amount": 1500.00,
"currency": "USD",
"status": "ISSUED",
"dueDate": "2024-02-01",
"issuedDate": "2024-01-01",
"paidDate": null,
"blockchainTxHash": null,
"ipfsDocumentCid": null,
"notes": "Initial deposit",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z"
}
]
POST /invoices
Create invoice (STAFF/ADMIN).
Request:
{
"invoiceNumber": "INV-2024-002",
"projectId": 1,
"clientId": 2,
"amount": 2500.00,
"currency": "USD",
"status": "DRAFT",
"dueDate": "2024-03-01",
"notes": "Final payment"
}
Status Values: DRAFT, ISSUED, PAID, CANCELLED, OVERDUE
Response (201): Created invoice object
GET /invoices/{id}
Get invoice by ID.
Authorization: Owner (client) or STAFF/ADMIN
PUT /invoices/{id}
Update invoice (STAFF/ADMIN).
DELETE /invoices/{id}
Delete invoice (STAFF/ADMIN).
Payment Endpoints
GET /payments
List payments.
Query Parameters:
invoice_id(optional): Filter by invoice
Response (200):
[
{
"id": 1,
"invoiceId": 1,
"amount": 1500.00,
"currency": "USD",
"paymentMethod": "CREDIT_CARD",
"status": "COMPLETED",
"transactionId": "pi_xxx",
"blockchainTxHash": null,
"blockchainNetwork": null,
"paymentProcessor": "stripe",
"processorFee": 45.00,
"processedAt": "2024-01-15T10:30:00Z",
"createdAt": "2024-01-15T10:25:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
]
POST /payments
Record a payment (STAFF/ADMIN).
Request:
{
"invoiceId": 1,
"amount": 1500.00,
"currency": "USD",
"paymentMethod": "BANK_TRANSFER",
"status": "COMPLETED",
"transactionId": "TXN123",
"processedAt": "2024-01-15T10:30:00Z"
}
Payment Methods: CREDIT_CARD, BANK_TRANSFER, CRYPTO, CHECK, CASH
Status Values: PENDING, COMPLETED, FAILED, REFUNDED
GET /payments/{id}
Get payment by ID.
Stripe Integration
POST /invoices/create-payment-intent
Create Stripe payment intent.
Request:
{
"invoiceId": 1
}
Response (200):
{
"clientSecret": "pi_xxx_secret_xxx",
"paymentId": 1
}
Frontend Usage:
const { clientSecret } = await invoicesStore.createPaymentIntent(invoiceId)
// Use clientSecret with Stripe.js to complete payment
POST /webhooks/stripe
Stripe webhook handler (no auth, signature verified).
Handled Events:
payment_intent.succeeded→ Payment COMPLETED, Invoice PAIDpayment_intent.payment_failed→ Payment FAILEDcharge.refunded→ Payment REFUNDED
Blog Service (Port 8085)
Base path: /api/blog
The blog service supports two types of blogs:
- SITE blogs: Official content created by STAFF/ADMIN with approval workflow
- USER blogs: Community content created by any authenticated user
Blog Status Lifecycle
SITE Blogs (STAFF): DRAFT → PENDING_REVIEW → APPROVED/REJECTED → PUBLISHED → ARCHIVED
SITE Blogs (ADMIN): DRAFT → PUBLISHED (can skip review) → ARCHIVED
USER Blogs: PUBLISHED immediately (community content)
Blog Object
{
"id": 1,
"slug": "getting-started",
"title": "Getting Started with Our Services",
"author": "John Doe",
"authorId": 1,
"date": "2024-01-01T00:00:00Z",
"tags": ["guide", "introduction"],
"short_description": "Learn how to get started...",
"content": "# Full markdown content...",
"status": "PUBLISHED",
"blogType": "SITE",
"verified": true,
"verifiedBy": 1,
"verifiedAt": "2024-01-01T00:00:00Z",
"reviewedBy": 1,
"reviewedAt": "2024-01-01T00:00:00Z",
"reviewNotes": "Approved - great content!",
"publishedAt": "2024-01-01T00:00:00Z",
"promotedAt": null,
"promotedBy": null,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
Public Endpoints (Site Blogs)
GET /blogs
List published SITE blogs (official content).
Response (200): Array of blog objects with blogType="SITE" and status="PUBLISHED"
GET /blogs/{slug}
Get a published SITE blog by slug.
Response (200): Blog object Response (404): Blog not found
Public Endpoints (Community Blogs)
GET /community/blogs
List published USER blogs (community content).
Response (200): Array of blog objects with blogType="USER" and status="PUBLISHED"
GET /community/blogs/{slug}
Get a published community blog by slug.
Response (200): Blog object Response (404): Community blog not found
User Endpoints (Authenticated)
GET /community/my-blogs
List current user's community blogs (all statuses).
Auth: Required (any role)
Response (200): Array of user's blog objects
POST /community/blogs
Create a new community blog (published immediately).
Auth: Required (any role)
Request:
{
"slug": "my-tutorial",
"title": "My Tutorial",
"author": "Optional Author Name",
"tags": ["tutorial", "guide"],
"short_description": "Brief description...",
"content": "# Full content in markdown..."
}
Note: Slug is prefixed with "community-" automatically.
Response (201): Created blog object
PUT /community/blogs/{slug}
Update own community blog.
Auth: Required (author only)
Response (200): Success message Response (403): Not your blog
DELETE /community/blogs/{slug}
Delete own community blog.
Auth: Required (author only)
Response (200): Success message Response (403): Not your blog
STAFF/ADMIN Endpoints (Site Blogs)
GET /admin/blogs
List all SITE blogs with optional filters.
Auth: Required (STAFF sees own, ADMIN sees all)
Query Parameters:
status: Filter by status (DRAFT, PENDING_REVIEW, APPROVED, PUBLISHED, REJECTED, ARCHIVED)author_id: Filter by author (ADMIN only)
Response (200): Array of blog objects
GET /admin/blogs/pending
List SITE blogs pending review.
Auth: Required (ADMIN only)
Response (200): Array of blogs with status="PENDING_REVIEW"
POST /admin/blogs
Create a new SITE blog.
Auth: Required (STAFF, ADMIN)
Request:
{
"slug": "new-post",
"title": "New Blog Post",
"author": "Jane Doe",
"date": "2024-01-01T00:00:00Z",
"tags": ["news", "update"],
"short_description": "Brief description...",
"content": "# Full content in markdown..."
}
Response (201): Created blog object (status=DRAFT)
PUT /admin/blogs/{slug}
Update a SITE blog.
Auth: Required (STAFF can update own DRAFT/REJECTED, ADMIN can update any)
Response (200): Success message
POST /admin/blogs/{slug}/submit
Submit blog for admin review.
Auth: Required (STAFF, ADMIN)
Response (200): Success message, status changes to PENDING_REVIEW
POST /admin/blogs/{slug}/review
Review a pending blog.
Auth: Required (ADMIN only)
Request:
{
"action": "approve|reject|request_changes",
"notes": "Feedback for author (required for reject/request_changes)"
}
Response (200): Success message
POST /admin/blogs/{slug}/publish
Publish an approved blog.
Auth: Required (ADMIN only)
Response (200): Success message, status changes to PUBLISHED
POST /admin/blogs/{slug}/unpublish
Unpublish a blog (archive it).
Auth: Required (ADMIN only)
Response (200): Success message, status changes to ARCHIVED
DELETE /admin/blogs/{slug}
Delete a SITE blog.
Auth: Required (ADMIN only)
Response (200): Success message
ADMIN Community Management Endpoints
GET /admin/community/blogs
List all community blogs.
Auth: Required (ADMIN only)
Query Parameters:
status: Filter by status
Response (200): Array of blog objects with blogType="USER"
POST /admin/community/blogs/{slug}/verify
Verify a community blog/tutorial (adds verified badge).
Auth: Required (ADMIN only)
Response (200): Success message
POST /admin/community/blogs/{slug}/unverify
Remove verification from a community blog.
Auth: Required (ADMIN only)
Response (200): Success message
POST /admin/community/blogs/{slug}/promote
Promote a community blog to SITE blog.
Auth: Required (ADMIN only)
Response (200): Success message, blogType changes to SITE
POST /admin/community/blogs/{slug}/archive
Archive a community blog (hide from public).
Auth: Required (ADMIN only)
Response (200): Success message, status changes to ARCHIVED
DELETE /admin/community/blogs/{slug}
Delete any community blog.
Auth: Required (ADMIN only)
Response (200): Success message
IPFS/P2P Service (Port 8086)
Base path: /api/ipfs
All endpoints are public (no authentication).
GET /health
Service health check.
Response (200):
{
"status": "healthy",
"peerId": "12D3KooW...",
"connectedPeers": 3,
"listenAddresses": [
"/ip4/10.89.0.3/tcp/4002/ws",
"/ip4/127.0.0.1/tcp/4002/ws"
]
}
GET /peer-info
Get this node's peer info.
Response (200):
{
"peerId": "12D3KooW...",
"addresses": [
"/ip4/10.89.0.3/tcp/4001/p2p/12D3KooW...",
"/ip4/10.89.0.3/tcp/4002/ws/p2p/12D3KooW..."
]
}
GET /peers
List connected peers.
Response (200):
{
"count": 3,
"peers": ["12D3KooWA...", "12D3KooWB...", "12D3KooWC..."]
}
POST /connect
Connect to a peer.
Request:
{
"peerAddr": "/ip4/192.168.1.100/tcp/4001/p2p/12D3KooW..."
}
Response (200):
{
"status": "connected",
"peerId": "12D3KooW..."
}
Forum Service (Port 8087)
Base path: /api/forum
The forum service provides Q&A functionality where any user can ask questions and anyone can answer.
Question Object
{
"id": 1,
"title": "How do I configure IPFS?",
"content": "I'm trying to set up IPFS for my project...",
"authorId": 1,
"authorName": "JohnDoe",
"tags": ["ipfs", "configuration"],
"upvotes": 10,
"downvotes": 2,
"answerCount": 3,
"viewCount": 150,
"acceptedAnswerId": 5,
"status": "ANSWERED",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z"
}
Answer Object
{
"id": 5,
"questionId": 1,
"content": "You need to configure the bootstrap nodes...",
"authorId": 2,
"authorName": "JaneDoe",
"upvotes": 15,
"downvotes": 0,
"isAccepted": true,
"isVerified": true,
"verifiedBy": 1,
"verifiedAt": "2024-01-02T00:00:00Z",
"createdAt": "2024-01-01T12:00:00Z",
"updatedAt": "2024-01-01T12:00:00Z"
}
Public Endpoints
GET /questions
List questions with optional filtering.
Query Parameters:
tag: Filter by tagstatus: Filter by status (OPEN, ANSWERED, CLOSED)sort: Sort by "newest" (default), "votes", or "unanswered"limit: Max results (default 20, max 100)offset: Pagination offset
Response (200): Array of question objects
GET /questions/{id}
Get a question with all its answers.
Response (200):
{
"question": { /* question object */ },
"answers": [ /* array of answer objects */ ]
}
Authenticated Endpoints
POST /questions
Create a new question.
Auth: Required (any role)
Request:
{
"title": "How do I configure IPFS?",
"content": "I'm trying to set up IPFS...",
"tags": ["ipfs", "configuration"]
}
Response (201): Created question object
PUT /questions/{id}
Update a question.
Auth: Required (author or ADMIN)
Response (200): Success message
DELETE /questions/{id}
Delete a question.
Auth: Required (author or ADMIN)
Response (200): Success message
POST /questions/{id}/answers
Create an answer to a question.
Auth: Required (any role)
Request:
{
"content": "You need to configure the bootstrap nodes..."
}
Response (201): Created answer object
PUT /answers/{id}
Update an answer.
Auth: Required (author or ADMIN)
Response (200): Success message
DELETE /answers/{id}
Delete an answer.
Auth: Required (author or ADMIN)
Response (200): Success message
POST /questions/{id}/vote
Vote on a question.
Auth: Required (any role)
Request:
{
"voteType": 1
}
Note: voteType is 1 for upvote, -1 for downvote
Response (200): Success message
POST /answers/{id}/vote
Vote on an answer.
Auth: Required (any role)
Request:
{
"voteType": 1
}
Response (200): Success message
POST /answers/{id}/accept
Accept an answer (question author only).
Auth: Required (question author)
Response (200): Success message
Admin Endpoints
POST /answers/{id}/verify
Verify an answer as correct.
Auth: Required (ADMIN only)
Response (200): Success message
POST /questions/{id}/close
Close a question (prevents new answers).
Auth: Required (ADMIN only)
Response (200): Success message
Frontend Stores
The frontend uses Pinia stores to interact with the API. All stores are located in frontend/src/stores/.
Auth Store (auth.ts)
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
// State
authStore.user // Current user object
authStore.token // JWT token
authStore.loading // Loading state
authStore.error // Error message
// Computed
authStore.isAuthenticated // boolean
authStore.isAdmin // boolean
authStore.isStaff // boolean
authStore.isClient // boolean
// Actions
await authStore.loginEmailPassword(email, password)
await authStore.loginBlockchain(address, signature, message)
await authStore.registerEmailPassword(email, password, name)
await authStore.registerBlockchain(address, signature, message, name)
await authStore.fetchProfile()
authStore.logout()
Projects Store (projects.ts)
import { useProjectsStore } from '@/stores/projects'
const projectsStore = useProjectsStore()
// State
projectsStore.projects // Project[]
projectsStore.currentProject // Project | null
projectsStore.loading // boolean
projectsStore.error // string | null
// Computed
projectsStore.activeProjects // Projects not completed/archived
projectsStore.completedProjects // Completed projects
projectsStore.inProgressProjects // In-progress projects
projectsStore.planningProjects // Planning projects
projectsStore.projectCount // Total count
projectsStore.projectsByStatus // Record<string, Project[]>
projectsStore.projectsByClient // Record<number, Project[]>
// Actions
await projectsStore.fetchProjects()
await projectsStore.fetchProject(id)
await projectsStore.createProject({ name, description, status, clientId })
await projectsStore.updateProject(id, { name, description, status })
await projectsStore.deleteProject(id)
Tasks Store (tasks.ts)
import { useTasksStore } from '@/stores/tasks'
const tasksStore = useTasksStore()
// State
tasksStore.tasks // Task[]
tasksStore.currentTask // Task | null
tasksStore.loading // boolean
tasksStore.error // string | null
// Computed
tasksStore.completedTasks // Completed tasks
tasksStore.pendingTasks // Not completed tasks
tasksStore.inProgressTasks // In-progress tasks
tasksStore.todoTasks // Todo tasks
tasksStore.taskCount // Total count
tasksStore.tasksByProject // Record<number, Task[]>
tasksStore.tasksByStatus // Record<string, Task[]>
// Actions
await tasksStore.fetchTasks(projectId?) // Optional filter by project
await tasksStore.fetchTask(id)
await tasksStore.createTask({ projectId, title, description, status })
await tasksStore.updateTask(id, { title, status })
await tasksStore.deleteTask(id)
Invoices Store (invoices.ts)
import { useInvoicesStore } from '@/stores/invoices'
const invoicesStore = useInvoicesStore()
// State
invoicesStore.invoices // Invoice[]
invoicesStore.currentInvoice // Invoice | null
invoicesStore.payments // Payment[]
invoicesStore.loading // boolean
invoicesStore.error // string | null
// Actions
await invoicesStore.fetchInvoices(clientId?)
await invoicesStore.fetchInvoice(id)
await invoicesStore.createInvoice({ invoiceNumber, clientId, amount, ... })
await invoicesStore.updateInvoice(id, { amount, status, ... })
await invoicesStore.deleteInvoice(id)
await invoicesStore.fetchPayments(invoiceId?)
await invoicesStore.createPaymentIntent(invoiceId) // Returns { clientSecret, paymentId }
await invoicesStore.createStripePayment(invoiceId, amount) // Returns clientSecret
await invoicesStore.recordPayment({ invoiceId, amount, paymentMethod, transactionHash? })
Users Store (users.ts)
import { useUsersStore } from '@/stores/users'
const usersStore = useUsersStore()
// State
usersStore.users // User[]
usersStore.loading // boolean
usersStore.error // string | null
// Actions
await usersStore.fetchUsers()
await usersStore.promoteRole(userId, role)
await usersStore.demoteRole(userId, role)
Error Handling
HTTP Status Codes
| Code | Meaning |
|---|---|
| 200 | Success (GET, PUT) |
| 201 | Created (POST) |
| 204 | No Content (DELETE) |
| 400 | Bad Request - Invalid input |
| 401 | Unauthorized - Missing/invalid token |
| 403 | Forbidden - Insufficient permissions |
| 404 | Not Found |
| 409 | Conflict - Duplicate value |
| 500 | Server Error |
Frontend Error Handling
import { useToast } from '@/composables/useToast'
const toast = useToast()
try {
await projectsStore.createProject(newProject)
toast.success('Project created successfully')
} catch (err) {
toast.error(projectsStore.error || 'Failed to create project')
}
Environment Variables
Backend Services
| Variable | Required | Default | Description |
|---|---|---|---|
JWT_SECRET |
Yes | - | JWT signing key (min 32 chars) |
DB_HOST |
Yes | - | PostgreSQL host |
DB_USER |
Yes | - | Database username |
DB_PASSWORD |
Yes | - | Database password |
DB_NAME |
Yes | - | Database name |
DB_SSL_MODE |
No | require |
SSL mode (disable, require, verify-ca, verify-full) |
CORS_ALLOW_ORIGIN |
No | http://localhost:8090 |
Allowed CORS origin |
STRIPE_SECRET_KEY |
No | - | Stripe secret key (payment-service) |
STRIPE_WEBHOOK_SECRET |
No | - | Stripe webhook secret |
Frontend
| Variable | Default | Description |
|---|---|---|
VITE_API_BASE_URL |
/api |
API base URL (usually not needed with nginx proxy) |
Summary
| Service | Port | Endpoints | Auth Required |
|---|---|---|---|
| Auth | 8082 | 12 | 5 public, 5 protected, 2 admin |
| Work Management | 8083 | 15 | All protected |
| Payment | 8084 | 10 | 1 public (webhook), rest protected |
| Blog | 8085 | 6 | 3 public (read), 3 protected (write) |
| IPFS/P2P | 8086 | 4 | All public |
| Total | - | 47 | - |