Changed terminology from "API" to "Routes" to better reflect server-rendered HTML approach: - Renamed submission_api.md → submission_routes.md - Renamed dashboard_api.md → dashboard_routes.md - Renamed admin_api.md → admin_routes.md - Updated headers to clarify "Response Type: Server-rendered HTML (no JavaScript required)" - Updated references in plan.md and quickstart.md This clarifies that the application uses traditional web routes with form submissions and HTML responses, not REST API endpoints with JSON. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
13 KiB
Admin Routes Contract
Scope: Product and user management web routes (User Story P4) Authentication: Required (admin role only) Response Type: Server-rendered HTML (no JavaScript required)
GET /admin/products
Display list of all registered products.
Request
Authentication: Required (admin role)
Response
Success (200 OK):
Content-Type: text/html
<!DOCTYPE html>
<html>
<head><title>Product Management</title></head>
<body>
<h1>Product Management</h1>
<a href="/admin/products/new">+ Create New Product</a>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Status</th>
<th>Submission URL</th>
<th>Target Language</th>
<th>Feedback Count</th>
<th>Assigned Owners</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>001-acme-app</td>
<td>Acme Mobile App</td>
<td>Active</td>
<td><a href="/submit/acme-app">/submit/acme-app</a></td>
<td>English (en)</td>
<td>127</td>
<td>2 owners</td>
<td>
<a href="/admin/products/001-acme-app/edit">Edit</a> |
<a href="/admin/products/001-acme-app/archive">Archive</a>
</td>
</tr>
<!-- More rows... -->
</tbody>
</table>
</body>
</html>
Error (403 Forbidden): User is not an administrator
Content-Type: text/html
<!DOCTYPE html>
<html>
<body>
<h1>Access Denied</h1>
<p>Administrator privileges required.</p>
</body>
</html>
Functional Requirements Covered
- FR-045: List all products
- FR-054: Display product statistics
GET /admin/products/new
Display form to create a new product.
Request
Authentication: Required (admin role)
Response
Success (200 OK):
Content-Type: text/html
<!DOCTYPE html>
<html>
<body>
<h1>Create New Product</h1>
<form method="POST" action="/admin/products">
<label>Product ID (URL-safe):
<input type="text" name="id" pattern="[a-z0-9-]+" required placeholder="001-my-product">
</label>
<label>Name:
<input type="text" name="name" maxlength="100" required placeholder="My Product">
</label>
<label>Description:
<textarea name="description" maxlength="500" placeholder="Optional description"></textarea>
</label>
<label>Target Language for Translations:
<select name="target_language" required>
<option value="en">English</option>
<option value="de">German</option>
<option value="fr">French</option>
<option value="es">Spanish</option>
<option value="ja">Japanese</option>
<!-- More languages... -->
</select>
</label>
<label>Submission URL Slug:
<input type="text" name="submission_url_slug" pattern="[a-z0-9-]+" required placeholder="my-product">
</label>
<label>Assigned Product Owners:
<select name="assigned_owner_ids" multiple>
<option value="owner-001">Jane Smith (jane.smith@example.com)</option>
<option value="owner-002">John Doe (john.doe@example.com)</option>
<!-- More owners... -->
</select>
</label>
<button type="submit">Create Product</button>
</form>
</body>
</html>
Functional Requirements Covered
- FR-045: Form to register new products
- FR-047: Set preferred target language
- FR-048: Assign product owners
POST /admin/products
Create a new product.
Request
Authentication: Required (admin role)
Form Data:
id(string, required): Unique product identifier (URL-safe, lowercase, hyphens allowed)name(string, required): Display name (1-100 characters)description(string, optional): Description (max 500 characters)target_language(string, required): ISO 639-1 language codesubmission_url_slug(string, required): URL-safe slug (unique)assigned_owner_ids(string[], required): At least one product owner ID
Response
Success (302 Redirect):
HTTP/1.1 302 Found
Location: /admin/products
Error (400 Bad Request): Validation failure
Content-Type: text/html
<!DOCTYPE html>
<html>
<body>
<h1>Validation Error</h1>
<ul>
<li>Product ID must be unique</li>
<li>Product ID must be URL-safe (lowercase, hyphens only)</li>
<li>Submission URL slug must be unique</li>
<li>At least one product owner must be assigned</li>
<li>Target language must be valid ISO 639-1 code</li>
</ul>
</body>
</html>
Side Effects
-
File System:
- Creates
data/products/{product_id}/ - Writes
data/products/{product_id}/config.yaml - Creates
data/products/{product_id}/feedback/directory
- Creates
-
Config File (
config.yaml):
id: "001-acme-app"
name: "Acme Mobile App"
description: "Customer feedback for Acme's flagship mobile application"
target_language: "en"
submission_url_slug: "acme-app"
created_date: "2025-10-15"
status: "active"
assigned_owner_ids:
- "owner-001"
- "owner-002"
statistics:
total_feedback_count: 0
last_submission: null
Functional Requirements Covered
- FR-045: Register new products
- FR-046: Unique product identifier
- FR-047: Set target language
- FR-048: Assign product owners
- FR-049: Generate unique submission URL
GET /admin/products/{product_id}/edit
Display form to edit an existing product.
Request
Authentication: Required (admin role)
Path Parameters:
product_id(string, required): Product identifier
Response
Success (200 OK): Same form as create, pre-populated with existing values
Error (404 Not Found): Product does not exist
Functional Requirements Covered
- FR-051: Update product details
POST /admin/products/{product_id}
Update an existing product.
Request
Authentication: Required (admin role)
Path Parameters:
product_id(string, required): Product identifier
Form Data: Same as POST /admin/products (except id is immutable)
Response
Success (302 Redirect):
HTTP/1.1 302 Found
Location: /admin/products
Error (400 Bad Request): Validation failure Error (404 Not Found): Product does not exist
Side Effects
- Updates
data/products/{product_id}/config.yaml - Product
idcannot be changed (immutable) - Changing
target_languageaffects future feedback translations only (FR-048)
Functional Requirements Covered
- FR-048: Update product owner assignments
- FR-051: Update product details
POST /admin/products/{product_id}/archive
Archive a product (stop accepting new feedback).
Request
Authentication: Required (admin role)
Path Parameters:
product_id(string, required): Product identifier
Response
Success (302 Redirect):
HTTP/1.1 302 Found
Location: /admin/products
Error (404 Not Found): Product does not exist
Side Effects
- Updates
data/products/{product_id}/config.yaml: Setsstatus: "archived" - Submission form (GET /submit/{product_slug}) returns 404 for archived products (FR-053)
- Historical feedback preserved (FR-052)
Functional Requirements Covered
- FR-052: Archive products without deleting feedback
- FR-053: Prevent new submissions to archived products
POST /admin/products/{product_id}/unarchive
Reactivate an archived product.
Request
Authentication: Required (admin role)
Path Parameters:
product_id(string, required): Product identifier
Response
Success (302 Redirect):
HTTP/1.1 302 Found
Location: /admin/products
Side Effects
- Updates
data/products/{product_id}/config.yaml: Setsstatus: "active" - Submission form becomes available again
Functional Requirements Covered
- Allow reversing archive operation (not explicitly in FR but useful)
GET /admin/users
Display list of all users (product owners and admins).
Request
Authentication: Required (admin role)
Response
Success (200 OK):
Content-Type: text/html
<!DOCTYPE html>
<html>
<body>
<h1>User Management</h1>
<a href="/admin/users/new">+ Create New User</a>
<table>
<thead>
<tr>
<th>ID</th>
<th>Email</th>
<th>Name</th>
<th>Role</th>
<th>Assigned Products</th>
<th>Last Login</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>owner-001</td>
<td>jane.smith@example.com</td>
<td>Jane Smith</td>
<td>Product Owner</td>
<td>2 products</td>
<td>2025-10-15 09:23</td>
<td>
<a href="/admin/users/owner-001/edit">Edit</a> |
<a href="/admin/users/owner-001/delete">Delete</a>
</td>
</tr>
<!-- More rows... -->
</tbody>
</table>
</body>
</html>
Functional Requirements Covered
- User management interface (implied by FR-048: assigning owners)
GET /admin/users/new
Display form to create a new user.
Request
Authentication: Required (admin role)
Response
Success (200 OK):
Content-Type: text/html
<!DOCTYPE html>
<html>
<body>
<h1>Create New User</h1>
<form method="POST" action="/admin/users">
<label>Email:
<input type="email" name="email" required>
</label>
<label>Name:
<input type="text" name="name" maxlength="100" required>
</label>
<label>Password:
<input type="password" name="password" minlength="8" required>
</label>
<label>Role:
<select name="role" required>
<option value="product_owner">Product Owner</option>
<option value="admin">Administrator</option>
</select>
</label>
<button type="submit">Create User</button>
</form>
</body>
</html>
POST /admin/users
Create a new user.
Request
Authentication: Required (admin role)
Form Data:
email(string, required): Valid email address (unique)name(string, required): Display name (1-100 characters)password(string, required): Password (min 8 characters)role(string, required): "product_owner" or "admin"
Response
Success (302 Redirect):
HTTP/1.1 302 Found
Location: /admin/users
Error (400 Bad Request): Validation failure
Content-Type: text/html
<!DOCTYPE html>
<html>
<body>
<h1>Validation Error</h1>
<ul>
<li>Email must be unique</li>
<li>Password must be at least 8 characters</li>
<li>Invalid role specified</li>
</ul>
</body>
</html>
Side Effects
- Appends new user to
data/users.yaml - Password hashed with bcrypt (cost factor 12) before storage (FR-063)
- Generates unique user ID (e.g., "owner-001", "admin-002")
Functional Requirements Covered
- FR-063: Secure password storage (bcrypt)
- User creation for product owner assignment
GET /admin/users/{user_id}/edit
Display form to edit an existing user.
Request
Authentication: Required (admin role)
Path Parameters:
user_id(string, required): User identifier
Response
Success (200 OK): Same form as create, pre-populated (except password field empty)
Error (404 Not Found): User does not exist
POST /admin/users/{user_id}
Update an existing user.
Request
Authentication: Required (admin role)
Path Parameters:
user_id(string, required): User identifier
Form Data:
email(string, required): Valid email addressname(string, required): Display namepassword(string, optional): New password (if changing)role(string, required): "product_owner" or "admin"
Response
Success (302 Redirect):
HTTP/1.1 302 Found
Location: /admin/users
Side Effects
- Updates user entry in
data/users.yaml - If password provided, re-hash with bcrypt
- Email and role can be updated
POST /admin/users/{user_id}/delete
Delete a user.
Request
Authentication: Required (admin role)
Path Parameters:
user_id(string, required): User identifier
Response
Success (302 Redirect):
HTTP/1.1 302 Found
Location: /admin/users
Error (400 Bad Request): Cannot delete self
Content-Type: text/html
<!DOCTYPE html>
<html>
<body>
<h1>Cannot Delete</h1>
<p>You cannot delete your own account.</p>
</body>
</html>
Side Effects
- Removes user from
data/users.yaml - User automatically unassigned from all products
- Historical feedback metadata unchanged (no user tracking in feedback)
Access Control
All admin endpoints enforce:
- User must be authenticated (session cookie)
- User role must be "admin"
- Otherwise: 403 Forbidden response
Functional Requirements Covered
- FR-045: Admin can register products
- FR-048: Admin can assign product owners
- FR-051: Admin can update products
- FR-052: Admin can archive products