Skip to content

Commit e784f1d

Browse files
authored
Merge pull request #6 from kavinda-100/develop
Develop
2 parents f4fa29e + 384f4c1 commit e784f1d

12 files changed

Lines changed: 385 additions & 109 deletions

.github/copilot-instructions.md

Lines changed: 0 additions & 105 deletions
This file was deleted.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
description: 'Use when a task spans both backend and frontend in Sprintly (route/DTO/response contract changes). Enforces strict cross-stack compatibility between Rust API and public UI.'
3+
name: 'Sprintly Cross-Stack Integration Rules'
4+
---
5+
6+
# Sprintly Cross-Stack Integration Rules
7+
8+
- Treat every rule in this file as a strict requirement, not a suggestion.
9+
- If backend DTOs/routes/response fields change, update frontend API usage in `public/` within the same task.
10+
- If frontend behavior requires contract changes, update controllers/routes/DTOs in the same task.
11+
- Keep response compatibility strict: frontend must consume `ApiResponse<T>` shape (`success`, `status_code`, `message`, `data`).
12+
- Keep error compatibility strict: backend errors must continue to come from `ApiError` and remain parseable by frontend flows.
13+
- Do not merge changes that leave backend and frontend out of sync.
14+
15+
# For Now We Only work with the backend/api. (DO NOT GENERATE FRONTEND CODE YET UNTIL I ASK YOU TO DO SO)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
description: 'Use when implementing or modifying Sprintly backend code in Rust/Axum/SQLx, including controllers, routes, auth, DTO validation, utils, and migrations. Enforces strict backend architecture and error/response patterns.'
3+
name: 'Sprintly Backend Strict Conventions'
4+
applyTo: 'src/**/*.rs, migrations/**/*.sql, API.http'
5+
---
6+
7+
# Sprintly Backend Strict Conventions
8+
9+
- Treat every rule in this file as a strict requirement, not a suggestion.
10+
- Use Rust with async/await for all I/O, especially DB and HTTP handlers.
11+
- Keep the architecture flow per resource as models -> controllers -> routes.
12+
- Prefer UUID primary keys for users, tasks, projects, workspaces, and lookup entities.
13+
- Use SQLx idioms (`query_as`, `FromRow`) and map DB errors to HTTP status codes (`404` not found, `500` internal error).
14+
- In Axum handlers, use `Router::new().route(...)` and shared state through `State`.
15+
- Use `chrono::Utc` or `chrono::NaiveDateTime` for timestamps.
16+
- Keep naming consistent: `snake_case` for functions and `PascalCase` for structs.
17+
- Keep imports clean and grouped as standard, external, then internal.
18+
- Prefer readable multiline SQL for complex queries.
19+
- For many-to-many task assignments, use join tables (for example `task_assignees`) with proper foreign keys.
20+
- Keep migrations in `migrations/`, add constraints deliberately (`UNIQUE`, foreign keys with `ON DELETE CASCADE` where appropriate), and use `uuid-ossp` for UUID generation when needed.
21+
22+
## Controller Error/Response Pattern (Strict)
23+
24+
- Follow the controller style used in `src/controllers/auth_controller.rs` for new and modified handlers.
25+
- Controller signatures must return `Result<Json<ApiResponse<T>>, ApiError>`.
26+
- Success responses must use `ApiResponse::new(...)` from `src/utils/response.rs`.
27+
- Failures must use `ApiError` variants from `src/utils/api_error.rs`; do not return ad-hoc JSON/string errors.
28+
- For DTO validation, call `payload.validate()` and convert errors using `format_validation_errors(...)` from `src/utils/mod.rs`, then return `ApiError::BadRequest(...)`.
29+
- Map database/crypto/JWT failures with `map_err(...)` into the correct `ApiError` variant.
30+
- Keep structured tracing logs (`tracing::info!`, `tracing::warn!`, `tracing::error!`) for key controller actions and failures.
31+
- Return API responses using `ApiResponse<T>` with consistent keys: `success`, `status_code`, `message`, `data`.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
description: 'Use when implementing or modifying Sprintly static UI in public HTML/CSS/JS files. Enforces strict public-folder conventions and backend response compatibility.'
3+
name: 'Sprintly Frontend Strict Conventions'
4+
applyTo: 'public/**/*.html, public/**/*.css, public/**/*.js'
5+
---
6+
7+
# Sprintly Frontend Strict Conventions
8+
9+
- Treat every rule in this file as a strict requirement, not a suggestion.
10+
- Keep frontend implementation simple and framework-free (plain HTML/CSS/JS) unless explicitly requested otherwise.
11+
- Preserve existing page structure and style conventions across `public/index.html`, `public/login.html`, `public/register.html`, `public/styles.css`, and `public/table.js`.
12+
- Keep static pages compatible with backend routes and auth flow.
13+
- When changing frontend API interactions, keep endpoint paths and HTTP methods aligned with backend routes.
14+
- Parse and handle API responses using the established response shape: `success`, `status_code`, `message`, `data`.
15+
- Do not introduce response contracts on the frontend that differ from `ApiResponse<T>` returned by backend controllers.

src/controllers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod auth_controller;
2+
pub mod project_controller;
23
pub mod root_controller;
34
pub mod workspace_controller;
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
use axum::{
2+
Json,
3+
extract::{Path, State},
4+
http::StatusCode,
5+
};
6+
use uuid::Uuid;
7+
use validator::Validate;
8+
9+
use crate::{
10+
config::AppState,
11+
dtos::project_dto::{CreateProjectPayload, ProjectResponse, UpdateProjectPayload},
12+
middleware::auth::AuthUser,
13+
models::Project,
14+
utils::{api_error::ApiError, format_validation_errors, response::ApiResponse},
15+
};
16+
17+
/**
18+
* Controller function to create a new project within a workspace. Validates the input payload, checks user authentication, and interacts with the database to persist the new project. Returns a structured API response with the created project details or appropriate error messages.
19+
* Path: POST /api/v1/projects
20+
*/
21+
pub async fn create_project(
22+
State(state): State<AppState>,
23+
AuthUser(user): AuthUser,
24+
Json(payload): Json<CreateProjectPayload>,
25+
) -> Result<Json<ApiResponse<ProjectResponse>>, ApiError> {
26+
// logging the project creation attempt with the email (but not the password)
27+
tracing::info!(
28+
"Attempting to create project with name: {} for workspace: {} by user: {}",
29+
payload.name,
30+
payload.workspace_id,
31+
user.email
32+
);
33+
34+
// Validate the input payload
35+
payload.validate().map_err(|e| {
36+
let error_messages = format_validation_errors(&e);
37+
tracing::error!("Validation errors: {}", error_messages);
38+
ApiError::BadRequest(error_messages)
39+
})?;
40+
41+
// create the project in the database
42+
let project = sqlx::query_as::<_, Project>(
43+
"INSERT INTO projects (workspace_id, name, description)
44+
VALUES ($1, $2, $3)
45+
RETURNING *",
46+
)
47+
.bind(payload.workspace_id)
48+
.bind(&payload.name)
49+
.bind(&payload.description)
50+
.fetch_one(&state.db)
51+
.await
52+
.map_err(|_| ApiError::InternalServerError("Failed to create project".into()))?;
53+
54+
// return the created project in the response
55+
Ok(Json(ApiResponse::new(
56+
true,
57+
StatusCode::CREATED,
58+
"Project created successfully",
59+
Some(ProjectResponse {
60+
id: project.id,
61+
workspace_id: project.workspace_id,
62+
name: project.name,
63+
description: project.description,
64+
created_at: project.created_at,
65+
updated_at: project.updated_at,
66+
}),
67+
)))
68+
}
69+
70+
/**
71+
* Controller function to update an existing project. Validates the input payload, checks user authentication, verifies project existence, and updates the project details in the database. Returns a structured API response with the updated project details or appropriate error messages.
72+
* Path: PUT /api/v1/projects/{id}
73+
*/
74+
pub async fn update_project(
75+
State(state): State<AppState>,
76+
AuthUser(user): AuthUser,
77+
Path(project_id): Path<Uuid>,
78+
Json(payload): Json<UpdateProjectPayload>,
79+
) -> Result<Json<ApiResponse<ProjectResponse>>, ApiError> {
80+
// logging the project update attempt with the email (but not the password)
81+
tracing::info!(
82+
"Attempting to update project with id: {} by user: {}",
83+
project_id,
84+
user.email
85+
);
86+
87+
// Validate the input payload
88+
payload.validate().map_err(|e| {
89+
let error_messages = format_validation_errors(&e);
90+
tracing::error!("Validation errors: {}", error_messages);
91+
ApiError::BadRequest(error_messages)
92+
})?;
93+
94+
// Check if the project exists
95+
let existing_project = sqlx::query_as::<_, Project>("SELECT * FROM projects WHERE id = $1")
96+
.bind(project_id)
97+
.fetch_optional(&state.db)
98+
.await
99+
.map_err(|_| ApiError::InternalServerError("Failed to query project".into()))?;
100+
101+
// If the project does not exist, return a 404 error
102+
if existing_project.is_none() {
103+
tracing::warn!("Project with id: {} not found", project_id);
104+
return Err(ApiError::NotFound("Project not found".into()));
105+
}
106+
107+
// Update the project in the database
108+
let updated_project = sqlx::query_as::<_, Project>(
109+
"UPDATE projects SET name = COALESCE($1, name), description = COALESCE($2, description), updated_at = NOW()
110+
WHERE id = $3
111+
RETURNING *",
112+
)
113+
.bind(&payload.name)
114+
.bind(&payload.description)
115+
.bind(project_id)
116+
.fetch_one(&state.db)
117+
.await
118+
.map_err(|_| ApiError::InternalServerError("Failed to update project".into()))?;
119+
120+
// return the updated project in the response
121+
Ok(Json(ApiResponse::new(
122+
true,
123+
StatusCode::OK,
124+
"Project updated successfully",
125+
Some(ProjectResponse {
126+
id: updated_project.id,
127+
workspace_id: updated_project.workspace_id,
128+
name: updated_project.name,
129+
description: updated_project.description,
130+
created_at: updated_project.created_at,
131+
updated_at: updated_project.updated_at,
132+
}),
133+
)))
134+
}
135+
136+
/**
137+
* Controller function to delete an existing project. Checks user authentication, verifies project existence, and deletes the project from the database. Returns a structured API response indicating success or appropriate error messages.
138+
* Path: DELETE /api/v1/projects/{id}
139+
*/
140+
pub async fn delete_project(
141+
State(state): State<AppState>,
142+
AuthUser(user): AuthUser,
143+
Path(project_id): Path<Uuid>,
144+
) -> Result<Json<ApiResponse<()>>, ApiError> {
145+
// logging the project deletion attempt with the email (but not the password)
146+
tracing::info!(
147+
"Attempting to delete project with id: {} by user: {}",
148+
project_id,
149+
user.email
150+
);
151+
152+
// Check if the project exists
153+
let existing_project = sqlx::query_as::<_, Project>("SELECT * FROM projects WHERE id = $1")
154+
.bind(project_id)
155+
.fetch_optional(&state.db)
156+
.await
157+
.map_err(|_| ApiError::InternalServerError("Failed to query project".into()))?;
158+
159+
// If the project does not exist, return a 404 error
160+
if existing_project.is_none() {
161+
tracing::warn!("Project with id: {} not found", project_id);
162+
return Err(ApiError::NotFound("Project not found".into()));
163+
}
164+
165+
// Delete the project from the database
166+
sqlx::query("DELETE FROM projects WHERE id = $1")
167+
.bind(project_id)
168+
.execute(&state.db)
169+
.await
170+
.map_err(|_| ApiError::InternalServerError("Failed to delete project".into()))?;
171+
172+
// return a success response with no data
173+
Ok(Json(ApiResponse::new(
174+
true,
175+
StatusCode::OK,
176+
"Project deleted successfully",
177+
None,
178+
)))
179+
}

0 commit comments

Comments
 (0)