This is the backend for the Ghostvox app. It is a RESTful API built using Go, hosted on Fly.io with a PostgreSQL database. The API handles storage and retrieval of polls and their associated data for the Ghostvox app.
- Route:
POST /api/v1/auth/register
- Request:
{ "email": "john@example.com", "first_name": "John", "last_name": "Smith", "password": "yourpassword", "provider": "", "provider_id": "", "role": "user" }
- Response (201 Created):
- Cookies Set:
accessToken
: A short-lived JWT stored as an HTTP-only cookie and also returned in the Authorization header as Bearer .refreshToken
: A long-lived refresh token stored as an HTTP-only cookie.
- Response Body:
{ "message": "User created successfully" }
- Cookies Set:
- Error Response:
{ "errors": { "field": "Error message for this specific field" } }
- Route:
POST /api/v1/auth/login
- Request:
{ "email": "john@example.com", "password": "yourpassword" }
- Response (201 Created):
- Cookies Set:
accessToken
: A short-lived JWT stored as an HTTP-only cookie and also returned in the Authorization header as Bearer .refreshToken
: A long-lived refresh token stored as an HTTP-only cookie.
- Response Body:
{ "message": "User created successfully" }
- Cookies Set:
- Error Response:
{ "errors": { "email": "Invalid credentials" } }
- Route:
POST /api/v1/auth/refresh
- Request: No body needed (uses HTTP-only cookie)
- Response (201 Created):
- Cookies Set:
- New
accessToken
andrefreshToken
are issued as HTTP-only cookies.
- New
- Response Body:
{ "message": "User created successfully" }
- Cookies Set:
- Route:
POST /api/v1/auth/logout
- Request: No body needed (uses HTTP-only cookie)
- Response (200 OK):
- Cookies: Clears authentication cookies
- Response Body:
{ "message": "User logged out successfully" }
- Route:
GET /api/v1/auth/google/login
- Response: Redirects to Google authentication
- Route:
GET /api/v1/auth/google/callback
- Response (201 Created):
- Cookies Set:
accessToken
andrefreshToken
are issued as HTTP-only cookies.
- Response Body:
{ "message": "User created successfully" }
- Cookies Set:
- Route:
GET /api/v1/admin/users
- Response (200 OK):
[ { "id": "1", "email": "john@example.com", "first_name": "John", "last_name": "Smith", "role": "user", "created_at": "2023-01-01T00:00:00Z", "updated_at": "2023-01-01T00:00:00Z" }, { "id": "2", "email": "jane@example.com", "first_name": "Jane", "last_name": "Doe", "role": "admin", "created_at": "2023-01-01T00:00:00Z", "updated_at": "2023-01-01T00:00:00Z" } ]
- Route:
GET /api/v1/admin/users/{id}
- Response (200 OK):
{ "id": "1", "email": "john@example.com", "first_name": "John", "last_name": "Smith", "role": "user", "created_at": "2023-01-01T00:00:00Z", "updated_at": "2023-01-01T00:00:00Z" }
- Route:
PUT /api/v1/users/{id}
- Request:
{ "email": "john@example.com", "first_name": "John", "last_name": "Smith", "password": "newpassword", "provider": "", "provider_id": "", "role": "user" }
- Response (200 OK):
- Cookies Set: New
accessToken
andrefreshToken
are issued as HTTP-only cookies, and the access token is included in the Authorization header. - Response Body:
{ "message": "User created successfully" }
- Cookies Set: New
- Route:
DELETE /api/v1/users/{id}
- Response (204 No Content)
- Route:
POST /api/v1/polls
- Request:
{ "userId": "user123", "title": "Sample Poll", "description": "This is a sample poll description", "expiresAt": "2024-12-31T23:59:59Z", "status": "Active" }
- Response (201 Created):
{ "id": "poll-uuid", "userId": "user123", "title": "Sample Poll", "description": "This is a sample poll description", "created_at": "2023-05-01T10:00:00Z", "updated_at": "2023-05-01T10:00:00Z", "expiresAt": "2024-12-31T23:59:59Z", "status": "Active" }
- Route:
GET /api/v1/polls/{id}
- Response (200 OK):
{ "id": "poll-uuid", "userId": "user123", "title": "Sample Poll", "description": "This is a sample poll description", "created_at": "2023-05-01T10:00:00Z", "updated_at": "2023-05-01T10:00:00Z", "expiresAt": "2024-12-31T23:59:59Z", "status": "Active" }
- Route:
GET /api/v1/polls
- Response (200 OK):
[ { "id": "poll-uuid-1", "userId": "user123", "title": "Sample Poll", "description": "This is a sample poll description", "created_at": "2023-05-01T10:00:00Z", "updated_at": "2023-05-01T10:00:00Z", "expiresAt": "2024-12-31T23:59:59Z", "status": "Active" }, { "id": "poll-uuid-2", "userId": "user123", "title": "Another Sample Poll", "description": "This is another sample poll description", "created_at": "2023-05-02T10:00:00Z", "updated_at": "2023-05-02T10:00:00Z", "expiresAt": "2024-12-31T23:59:59Z", "status": "Active" } ]
- Route:
PUT /api/v1/polls/{id}
- Request:
{ "userId": "user123", "title": "Updated Poll", "description": "This is an updated poll description", "expiresAt": "2024-12-31T23:59:59Z", "status": "Inactive" }
- Response (200 OK):
{ "id": "poll-uuid", "userId": "user123", "title": "Updated Poll", "description": "This is an updated poll description", "created_at": "2023-05-01T10:00:00Z", "updated_at": "2023-05-03T15:30:00Z", "expiresAt": "2024-12-31T23:59:59Z", "status": "Inactive" }
- Route:
DELETE /api/v1/polls/{id}
- Response (204 No Content)
- Route:
POST /api/v1/polls/{pollId}/options
- Request:
{ "options": [ { "name": "Option 1", "value": "Value 1" }, { "name": "Option 2", "value": "Value 2" } ] }
- Response (201 Created):
[ { "id": "option-uuid-1", "name": "Option 1", "value": "Value 1", "poll_id": "poll-uuid", "created_at": "2023-05-01T10:10:00Z", "updated_at": "2023-05-01T10:10:00Z" }, { "id": "option-uuid-2", "name": "Option 2", "value": "Value 2", "poll_id": "poll-uuid", "created_at": "2023-05-01T10:10:00Z", "updated_at": "2023-05-01T10:10:00Z" } ]
- Route:
GET /api/v1/polls/{pollId}/options/{optionId}
- Response (200 OK):
{ "id": "option-uuid", "name": "Option 1", "value": "Value 1", "poll_id": "poll-uuid", "created_at": "2023-05-01T10:10:00Z", "updated_at": "2023-05-01T10:10:00Z" }
- Route:
GET /api/v1/polls/{pollId}/options
- Response (200 OK):
[ { "id": "option-uuid-1", "name": "Option 1", "value": "Value 1", "poll_id": "poll-uuid", "created_at": "2023-05-01T10:10:00Z", "updated_at": "2023-05-01T10:10:00Z" }, { "id": "option-uuid-2", "name": "Option 2", "value": "Value 2", "poll_id": "poll-uuid", "created_at": "2023-05-01T10:10:00Z", "updated_at": "2023-05-01T10:10:00Z" } ]
- Route:
PUT /api/v1/polls/{pollId}/options/{optionId}
- Request:
{ "id": "option-uuid", "name": "Updated Option", "value": "Updated Value" }
- Response (200 OK):
{ "id": "option-uuid", "name": "Updated Option", "value": "Updated Value", "poll_id": "poll-uuid", "created_at": "2023-05-01T10:10:00Z", "updated_at": "2023-05-03T16:20:00Z" }
- Route:
DELETE /api/v1/polls/{pollId}/options/{optionId}
- Response (204 No Content)
- Route:
POST /api/v1/polls/{pollId}/votes
- Request:
{ "userId": "user123", "optionId": "option-uuid" }
- Response (201 Created):
{ "id": "vote-uuid", "pollId": "poll-uuid", "optionId": "option-uuid", "userId": "user123", "created_at": "2023-05-01T11:00:00Z" }
- Route:
GET /api/v1/polls/{pollId}/votes
- Response (200 OK):
[ { "id": "vote-uuid-1", "pollId": "poll-uuid", "optionId": "option-uuid-1", "userId": "user123", "created_at": "2023-05-01T11:00:00Z" }, { "id": "vote-uuid-2", "pollId": "poll-uuid", "optionId": "option-uuid-2", "userId": "user456", "created_at": "2023-05-01T11:30:00Z" } ]
- Route:
DELETE /api/v1/votes/{voteId}
- Response (204 No Content)
- Authentication: The API uses JWT tokens for authentication, with both access and refresh tokens.
- Cookie Security: Authentication tokens are stored as HTTP-only cookies with appropriate security settings.
- Database: The API uses PostgreSQL with foreign key constraints and cascading deletes.
- Transaction Support: Critical operations like user creation and token management use database transactions to ensure data consistency.
- Role-Based Access Control: Certain endpoints are restricted to admin users.
- OAuth Integration: Google OAuth is supported for authentication.
The API implements a consistent error response format to facilitate front-end form validation and error handling:
-
Format: All errors are returned in a standardized format:
{ "errors": { "field_name": "Error message specific to this field" } }
-
Field-Specific Errors: The error response includes field names as keys, allowing the front-end to easily map errors to specific form fields:
- For validation errors (e.g., email format, required fields), the field name will indicate which input needs correction
- For general errors, the status code field may be used with a general error message
-
HTTP Status Codes:
400 Bad Request
: Invalid input data401 Unauthorized
: Authentication required or failed403 Forbidden
: Permission denied404 Not Found
: Resource not found409 Conflict
: Resource already exists (e.g., email already taken)500 Internal Server Error
: Server-side errors
This error handling system makes it easy to implement client-side form validation and display appropriate error messages to users.
The application can be deployed using Docker. See the Docker documentation for more details.
docker compose up --build
The API will be available at http://localhost:8080.