Compare commits

..
124 Commits
Author SHA1 Message Date
MythEclipse 97c2fce7be Refactor API endpoints for consistency and clarity
- Updated route paths for hackathon submissions, notifications, registrations, and teams to include more descriptive actions (e.g., "update", "create", "delete").
- Removed deprecated routes and adjusted corresponding test cases to reflect new endpoint structures.
- Enhanced test scripts to ensure compatibility with updated API routes and improved error handling for OTP resend functionality.
- Adjusted server startup script for better Windows compatibility and streamlined process management.
2025-10-29 14:27:07 +07:00
MythEclipse 98c46611fb refactor: Update deployment script for Rust binaries with improved comments and error handling 2025-10-28 21:58:49 +07:00
MythEclipse fbfe3dcd51 refactor: Remove unused imports and clean up status update logic in hackathon service and controller 2025-10-28 19:57:43 +07:00
MythEclipse 5cc5a3dfe1 feat: Implement hackathon status change functionality with audit logging
- Added HackathonStatusChangeRequestDto for status change requests.
- Implemented update_hackathon_status method in HackathonRepository to handle status updates.
- Enhanced HackathonService to validate and process status changes, including audit logging.
- Introduced HackathonAuditLogSchema to track changes and actions related to hackathons.
- Created HackathonAuditRepository for managing audit logs.
- Added validation functions for hackathon operations, including dates, organizers, and prizes.
- Implemented atomic service for creating hackathons with timelines and events, ensuring all-or-nothing behavior.
- Updated mod.rs to include new modules for audit logging and validation.
2025-10-28 19:55:38 +07:00
MythEclipse 95ae55c9df refactor: Simplify string conversion for hackathon and timeline attributes in seed test submission 2025-10-28 18:47:52 +07:00
MythEclipse f7ca67d720 feat: Add contact fields to hackathon and submission data; update registration service to handle string ID 2025-10-28 17:34:03 +07:00
MythEclipse 02421e9bc2 fix: Remove unused HeaderMap import from payment middleware 2025-10-28 14:59:26 +07:00
MythEclipse 5e2b0d3caf feat: Add session counting methods for mentors and users; enhance registration queries with related data 2025-10-28 14:57:20 +07:00
MythEclipse b9a51ce6cc feat: Enhance validation and permissions handling across controllers
- Added `ValidatedJson` extractor for automatic JSON validation in `events_controller.rs`, `testimonials_controller.rs`, `mentors_controller.rs`, `gacha_items_controller.rs`, and `hackathon_controller.rs`.
- Replaced manual permission checks with `require_permissions!` and `require_auth!` macros in relevant controllers to streamline permission handling.
- Introduced `sanitization` utilities in `sanitization.rs` for improved input sanitization.
- Added `permission_macros.rs` to encapsulate permission checking logic and reduce boilerplate.
- Updated dependencies in `Cargo.toml` to include `serde_json` and `validator`.
- Implemented error handling improvements in `notification_service.rs` for better response management.
2025-10-28 14:04:41 +07:00
MythEclipse d4a6c4c9ea Add new test suites for registrations and notifications; update existing tests for improved error handling and security checks
- Updated `run-tests.sh` to include new test suites for registrations and notifications.
- Modified `test-cms.sh` to skip SQL injection tests due to query timeout issues and adjusted expected status codes for XSS tests.
- Adjusted expected status codes in `test-auth.sh` for SQL injection and XSS tests; updated missing password test to return 422.
- Updated `test-roles-permissions.sh` to expect 409 for duplicate role creation.
- Changed expected status for duplicate user creation in `test-users.sh` to 409.
- Added comprehensive tests for notification endpoints in `test-notifications.sh`, including edge cases and pagination.
- Created `test-registrations.sh` to cover hackathon registration endpoints, including registration, approval, and check-in processes.
2025-10-28 10:27:19 +07:00
MythEclipse ece6499e2b feat: Implement hackathon registration module with controller, DTOs, repository, schema, and service
- Added registration_controller.rs to handle registration-related routes and logic.
- Created registration_dto.rs for data transfer objects related to registrations.
- Implemented registration_repository.rs for database interactions concerning registrations.
- Defined registration_schema.rs to represent the registration data structure.
- Developed registration_service.rs to encapsulate business logic for registrations.
- Established routes for registering, listing, updating, and checking in participants for hackathons.
- Added validation for registration requests and status updates.
- Included statistics retrieval for hackathon registrations.
2025-10-27 19:53:05 +07:00
MythEclipse 1caaa8404b a 2025-10-27 19:23:29 +07:00
MythEclipse cb6eef2054 feat: Add endpoint to retrieve the current user's team and corresponding service logic 2025-10-27 17:44:13 +07:00
MythEclipse e77797d7bb feat: Update hackathon event and submission routes to require admin permissions and adjust response descriptions 2025-10-27 17:26:53 +07:00
MythEclipse d5ccf4cf75 Add comprehensive security tests for authentication, roles, and user management
- Enhance `test-auth.sh` with SQL injection, XSS, and credential validation tests.
- Extend `test-roles-permissions.sh` to include unauthorized access and duplicate role creation tests.
- Improve `test-users.sh` with checks for invalid emails, duplicate users, and unauthorized actions.
- Introduce `test-security.sh` for thorough security assessments including CSRF, SQL injection, XSS, rate limiting, and session management.
- Add `.serena.gitignore` and `.serena/project.yml` for project configuration and file management.
2025-10-27 16:33:08 +07:00
MythEclipse 1a36698962 feat: Improve validation logic in HackathonService by adding a helper function and using parameterized queries 2025-10-24 18:14:09 +07:00
MythEclipse 15a2ee1950 feat: Enhance hackathon submission process with additional fields and validation checks 2025-10-24 18:06:47 +07:00
MythEclipse bef8482402 feat: Add response validation functions and examples for API tests 2025-10-24 15:42:06 +07:00
MythEclipse 768e583f81 a 2025-10-24 15:34:04 +07:00
MythEclipse 1ca6d8f47c Add comprehensive tests for CMS, Gacha, Hackathon, IAM, and User Management endpoints
- Implemented tests for Events and Testimonials endpoints in `test-cms.sh`
- Added common functions and variables for API testing in `test-common.sh`
- Created tests for Mentor endpoints in `test-mentors.sh`
- Developed tests for Gacha endpoints in `test-gacha.sh`
- Established tests for Hackathon endpoints in `test-hackathon.sh`
- Implemented tests for Authentication endpoints in `test-auth.sh`
- Added tests for Roles and Permissions endpoints in `test-roles-permissions.sh`
- Created tests for Teams endpoints in `test-teams.sh`
- Developed tests for User Management endpoints in `test-users.sh`
2025-10-24 10:38:21 +07:00
MythEclipse 3fcfb3709e refactor: Clean up unused imports and improve error handling in middleware and DTOs 2025-10-23 22:44:32 +07:00
MythEclipse 6915a97d79 feat: Enhance Hackathon Timeline Management and Admin Features
- Updated HackathonTimelineCreateRequestDto to accept optional title and name fields.
- Added custom validators for HackathonPhase and date checks in hackathon_dto.rs.
- Implemented admin-sensitive data management DTOs for handling user scores and personal info.
- Introduced new admin routes for managing users, roles, and permissions in IAM module.
- Added timeline enforcement middleware to restrict access based on hackathon phases.
- Created tests for timeline enforcement and admin permissions to ensure proper access control.
- Implemented payment middleware as a placeholder for future payment processing logic.
- Enhanced audit logging middleware for improved error handling and logging.
2025-10-13 10:57:53 +07:00
MythEclipse 6f596efadd feat: Implement audit logging and rate limiting middleware with SurrealDB integration
- Added audit logging middleware to track admin actions and save logs to SurrealDB.
- Introduced rate limiting middleware for public endpoints and authentication endpoints.
- Enhanced security headers middleware with nonce generation for CSP in development.
- Created utility functions for extracting real client IP addresses from headers.
- Updated Cargo.toml and Cargo.lock to include new dependencies.
- Added new schemas for audit logs and rate limiting in the entities module.
- Refactored permissions middleware to support new permission checks.
2025-10-12 01:54:34 +07:00
MythEclipse b6b5f48055 Implement rate limiting middleware for authentication endpoints, adding security headers middleware, and comprehensive error handling. Enhance validation tests for various DTOs and ensure proper functionality of gacha credits and rolls. Add unit tests for rate limiting and security headers middleware to validate behavior under different conditions. 2025-10-11 23:23:51 +07:00
Your Name 789b20c278 tests(hackathon): align tests with schema changes and fix clippy warnings 2025-10-11 15:33:52 +07:00
MythEclipse 884b679ba8 fix: Update string formatting in hackathon service and repository tests for consistency 2025-10-11 15:28:27 +07:00
MythEclipse 6ef624c169 feat: Enhance hackathon submission and participant management
- Updated HackathonSubmissionsSchema to use Option types for team_id, project_name, description, technologies, submission_status, and submitted_at.
- Modified seed_hackathons and seed_test_submission scripts to accommodate new optional fields.
- Added routes for participant registration and listing in hackathon_controller.
- Implemented register_participant and list_participants functions in hackathon_controller.
- Introduced HackathonParticipantSchema and corresponding DTOs for participant management.
- Enhanced HackathonRepository with CRUD operations for hackathon participants.
- Updated HackathonService to include methods for participant registration and listing.
- Refactored TeamsService to allow admin-level updates and invitations, bypassing leader-only restrictions.
- Added validation for member emails in TeamsCreateRequestDto and TeamInviteRequestDto.
2025-10-11 15:06:12 +07:00
MythEclipse c10443f881 Add minimal test for basic hackathon operations and enhance test utilities
- Introduced a new test module for hackathon-related functionality.
- Implemented a basic test for creating a hackathon using a mock repository.
- Enhanced the test utilities in `lib.rs` for better request handling and response extraction.
- Added a `ServiceClient` struct to facilitate HTTP requests in tests.
- Created a `RequestBuilder` to streamline building and sending requests with headers and JSON bodies.
2025-10-11 10:58:51 +07:00
MythEclipse 466ba3391a feat: Add seed_test_data script to populate initial test data for events, testimonials, hackathons, and mentors 2025-10-08 23:03:00 +07:00
MythEclipse 8c2527d61e Add access level descriptions to API responses across multiple controllers
- Updated event controller to specify public and admin access levels in response descriptions.
- Modified testimonials controller to indicate public access for list and detail responses.
- Enhanced mentors controller with admin access labels for various responses.
- Adjusted gacha claims and items controllers to reflect admin access in response descriptions.
- Updated hackathon controller to clarify public and admin access levels in response messages.
- Revised authentication controller to specify public access for login and registration responses.
- Enhanced admin teams controller with admin access labels for team-related responses.
- Updated users controller to clarify admin and user access levels in response descriptions.
2025-10-07 17:45:47 +07:00
MythEclipse 018124f7b3 feat: Implement permissions checks for hackathon and testimonial routes, enhancing security with header validation 2025-10-07 13:08:23 +07:00
MythEclipse 7749f6fdec feat: Add public routes for hackathons and enhance permissions checks to support both names and IDs 2025-10-05 20:23:34 +07:00
MythEclipse b27e4a4404 refactor: Enhance permission checks to accept both name and ID for Administrator role 2025-10-05 19:39:15 +07:00
MythEclipse 3dde67a4ac Enhance test coverage for IAM module by validating all required fields in response DTOs
- Added assertions to check for non-empty fields in TokenDto, UsersSchema, PermissionsSchema, and RolesDetailItemDto.
- Ensured that all necessary fields are validated in team-related DTOs, including TeamsCreateResponseDto and TeamsDetailResponseDto.
- Improved checks for user details in UsersDetailItemDto and UsersListItemDto to ensure data integrity.
- Verified that sensitive fields are present in admin team responses and member details.
- Updated tests to ensure that created and updated timestamps are present and valid across various entities.
2025-10-05 18:16:34 +07:00
MythEclipse 49abcf28c3 Refactor tests to enhance response validation and error handling
- Updated gacha claims service tests to parse and verify error JSON content for various scenarios.
- Improved gacha credits controller tests by adding checks for success messages in JSON responses.
- Enhanced gacha items service tests to validate response data structure and content.
- Modified IAM auth controller and service tests to ensure token data integrity and presence of required fields.
- Refined permissions controller and service tests to assert response data correctness.
- Improved roles controller and service tests to validate response data and ensure non-empty fields.
- Enhanced teams controller tests to verify sensitive data exposure and required fields in responses.
- Updated users controller and service tests to ensure response data integrity and presence of required fields.
2025-10-05 17:05:06 +07:00
MythEclipse 9cecc8fa05 Refactor tests to improve response parsing and validation
- Introduced a new common module with response helper functions to streamline response parsing across tests.
- Updated role controller tests to assert success messages in responses for create, update, and delete operations.
- Enhanced role service tests to validate response structures and ensure proper error handling.
- Modified team controller and service tests to check for success messages and validate response data formats.
- Improved user controller and service tests to include success message assertions and error handling for invalid inputs.
- Ensured all tests utilize the new response parsing functions for consistency and maintainability.
2025-10-05 16:00:46 +07:00
MythEclipse a32f54873b feat: Add previous winners field to hackathon schema and update related tests 2025-09-27 22:30:42 +07:00
MythEclipse af41a98111 feat: Add previous winners field to hackathon DTOs and schema 2025-09-27 21:05:34 +07:00
MythEclipse 4c435708c0 feat: Add endpoint to retrieve hackathon submissions and implement seeding for test submissions 2025-09-27 19:24:45 +07:00
MythEclipse ac462ea771 Refactor IAM tests: Remove unused repository tests, streamline service tests, and enhance validation checks for team and user services 2025-09-27 17:58:15 +07:00
MythEclipse 3e52223730 refactor: Update route parameters in hackathon controller for consistency 2025-09-27 13:40:02 +07:00
MythEclipse 01e479350b test: Add not found scenarios for hackathon, event, timeline, and submission repository operations 2025-09-27 13:16:13 +07:00
MythEclipse b0cf9817d7 feat: Implement submission retrieval and timeline validation in hackathon service 2025-09-27 13:06:55 +07:00
MythEclipse e530eba60d Add comprehensive tests for hackathon service functionality
- Implemented tests for creating, retrieving, updating, and deleting hackathons.
- Added validation tests for hackathon creation and updates.
- Included tests for hackathon events and timelines, ensuring proper handling of edge cases.
- Created tests for hackathon submissions, including validation and submission status updates.
- Organized tests into a dedicated module for better structure and maintainability.
2025-09-27 13:01:08 +07:00
MythEclipse ef1d63e893 refactor: Update OTP handling in auth repository tests and clean up unused variables in team and user repository tests 2025-09-26 23:30:13 +07:00
MythEclipse 5859af5294 Refactor environment module: Rename enviroment to environment and consolidate environment configuration management
- Updated all references from `enviroment` to `environment` across the codebase.
- Removed the old `enviroment` module and replaced it with a new `environment` module that includes centralized configuration management.
- Enhanced OTP generation to include secure hashing and expiration handling.
- Improved CSRF token generation and validation with better error handling.
- Cleaned up logging statements in various modules for clarity and consistency.
- Updated response formatting to include versioning from Cargo.toml.
- Removed unused mock test module from utils.
2025-09-26 23:15:33 +07:00
MythEclipse c12da948aa refactor: Clean up Argon2 password hashing implementation and remove unused configuration constants 2025-09-26 22:24:23 +07:00
MythEclipse 3cde18360c refactor: Simplify gateway service initialization and enhance seeding process for gacha rolls 2025-09-26 17:50:31 +07:00
MythEclipse 14b22328de Refactor and enhance SurrealDB integration and resource management
- Updated `lib.rs` to selectively expose specific entities and services for better clarity.
- Improved SurrealDB client initialization with detailed logging in `surrealdb/mod.rs`.
- Enhanced resource definitions in `resource.rs` with additional utility methods for better resource management.
- Refactored user data retrieval logic in `auth_middleware/mod.rs` for improved readability and efficiency.
- Cleaned up middleware exports in `lib.rs` for clearer API surface.
- Added detailed comments and documentation throughout the SurrealDB module for better maintainability.
- Updated tests to ensure compatibility with new changes and improved structure.
- Introduced new permissions module structure in `imphnen-utils` for future enhancements.
2025-09-26 17:35:30 +07:00
MythEclipse 96debc210a Refactor permissions and user DTOs to use imphnen_entities module
- Updated permissions_repository.rs to import PermissionsItemDto from imphnen_entities.
- Modified permissions_schema.rs to import PermissionsItemDto and PermissionsQueryDto from imphnen_entities.
- Changed roles_dto.rs to import PermissionsItemDto and PermissionsQueryDto from imphnen_entities.
- Updated users_dto.rs to import ExperienceDto, EducationDto, UsersDetailQueryDto, RolesDetailQueryDto, and RolesDetailItemDto from imphnen_entities, and removed redundant struct definitions.
- Refactored users_repository.rs to import UsersDetailQueryDto from imphnen_entities.
- Updated users_schema.rs to import UsersDetailQueryDto, ExperienceDto, and EducationDto from imphnen_entities.
- Modified users_service.rs to use UsersDetailQueryDto from imphnen_entities and added UserLookupService implementation.
- Updated Cargo.toml in imphnen-libs to include async-trait as a workspace dependency.
- Refactored axum module to remove redundant imports and streamline code.
- Updated lib.rs to include AppState struct with user_lookup_service and auth_repository fields.
- Refactored surrealdb module to define SurrealWsClient and SurrealMemClient types.
- Removed imphnen-iam dependency from middleware's Cargo.toml.
- Updated auth_middleware to use UsersDetailQueryDto from imphnen_entities and refactored user retrieval logic.
- Refactored permissions_middleware to use PermissionsEnum from imphnen_entities and updated user retrieval logic.
- Updated mock_test.rs to create AppState with user_lookup_service and auth_repository.
- Added permissions.rs and users.rs to imphnen_entities with necessary DTOs and enums.
- Created services.rs in imphnen-libs to define UserLookupService and AuthRepositoryTrait traits.
2025-09-26 15:05:45 +07:00
MythEclipse b80662188f feat: Update admin teams endpoint to return 403 Forbidden for unauthorized access 2025-09-26 13:04:48 +07:00
MythEclipse 7b9ce6cda1 feat: Add seeding process for teams and ensure permissions are seeded even when other seeds are skipped 2025-09-26 02:23:55 +07:00
MythEclipse 1bb2d6a419 feat: Add Administrator permission to seeding process and ensure permissions are seeded even when other seeds are skipped 2025-09-26 01:35:06 +07:00
MythEclipse 14690a378f feat: Allow access for users with Administrator permission in permissions guard 2025-09-26 01:30:00 +07:00
MythEclipse 17cc4ccdba refactor: Update permissions handling to filter out None values and ensure safe access 2025-09-26 01:08:18 +07:00
MythEclipse 1b7368a4b7 feat: Add Administrator permission and update permissions handling for roles and users 2025-09-26 00:45:57 +07:00
MythEclipse ff2e14d5f9 feat: Introduce Administrator permission and update permissions checks for access control 2025-09-25 23:47:29 +07:00
MythEclipse 012aff60ef refactor: Update admin team routes to remove versioning and avoid conflicts 2025-09-25 23:28:37 +07:00
MythEclipse f3d88e2ac9 Add comprehensive tests for Gacha Credits, Auth, and User services
- Implement tests for GachaCreditsService including creation, retrieval, updating, and deletion of gacha credits.
- Add tests for AuthController covering user login, registration, email verification, and error handling for invalid credentials.
- Enhance AuthService tests to include login, registration, email verification, OTP resend, password reset, and token refresh functionalities.
- Create tests for UsersService to validate user creation, retrieval, updating, deletion, and fetching by email.
- Ensure all tests utilize a consistent setup and teardown process for database interactions.
2025-09-25 22:53:26 +07:00
MythEclipse 0e48b6755f refactor: Remove unused execute_safe_count_query import from teams_repository 2025-09-25 22:07:38 +07:00
MythEclipse 7056f8c8a6 Add comprehensive tests for IAM permissions, roles, teams, and users
- Implemented unit tests for PermissionsController and PermissionsService, covering create, read, update, and delete operations.
- Added tests for RolesController and RolesService, including handling of duplicates and retrieval by ID.
- Developed tests for TeamsController, including creation, retrieval, updating, deletion, and search functionality.
- Created tests for UsersController, ensuring user creation and validation of attributes.
- Each test includes setup, execution, and cleanup to maintain database integrity.
2025-09-25 22:06:21 +07:00
MythEclipse 4ecdcd12d2 feat: Add administrator permissions to PermissionsEnum and update related team management functionality 2025-09-25 20:57:31 +07:00
MythEclipse dd4a73e360 feat: Add comprehensive admin team management functionality with new permissions and endpoints 2025-09-25 20:28:08 +07:00
MythEclipse 49e0a89ad1 feat: Enhance team management and permissions structure with detailed DTOs and service methods 2025-09-25 20:18:16 +07:00
MythEclipse 82b85abe8f a 2025-09-25 17:29:17 +07:00
MythEclipse df11aa5eb8 a 2025-09-25 17:26:39 +07:00
MythEclipse ac44867ec1 feat: Add member team management functionality with detailed DTOs and service methods 2025-09-22 17:38:54 +07:00
MythEclipse 7205afe43b feat: Implement admin team management endpoints with permissions and DTOs 2025-09-22 17:25:02 +07:00
MythEclipse b1c678c72b feat: Add public team listing and detail endpoints with DTOs for public access 2025-09-22 17:07:14 +07:00
MythEclipse fa3687c6be feat: Add team-related DTOs and update routes for team management 2025-09-22 16:55:03 +07:00
MythEclipse ba46a19a5a refactor: Replace surrealdb_helpers with integrated query builder methods for improved clarity and functionality 2025-09-22 16:38:07 +07:00
MythEclipse 8cdbca24ba feat: Enhance query builder with new condition methods and safe query execution 2025-09-22 16:09:22 +07:00
MythEclipse 8f981572cd feat: Introduce surrealdb_helpers for query building and execution utilities 2025-09-22 15:06:37 +07:00
MythEclipse 39b75f410f Add unit tests for TeamsRepository and TeamsService
- Implement tests for team creation, retrieval, updating, and deletion in TeamsRepository.
- Add tests for team member management including adding and removing members.
- Create tests for team invitations and searching teams.
- Ensure proper cleanup of test data after each test case.
- Validate unauthorized operations for team management.
2025-09-22 14:59:38 +07:00
MythEclipse 6da1b2a7e7 refactor: Replace make_thing with make_thing_from_enum for consistency and clarity across multiple modules 2025-09-22 09:10:20 +07:00
MythEclipse acccae9f7c fix: Correct typo in FAILED_TESTS_SUMMARY variable and enhance HTTP status code handling in API tests 2025-09-22 08:21:21 +07:00
MythEclipse e7c17784cc feat: Simplify token generation by removing unnecessary permissions parameter from access and refresh token functions 2025-09-22 07:52:00 +07:00
Maulana Sodiqin 042e0b4eff Merge pull request #45 from IMPHNEN/feat/auth-google
Feat/auth google
2025-08-17 18:26:24 +07:00
MythEclipse 0b51a56dee feat: Enhance CORS middleware to dynamically include development port and allow OPTIONS method 2025-08-17 16:06:21 +07:00
MythEclipse cb98440b65 feat: Refactor update_user_avatar method to be static and enhance avatar deletion logic 2025-08-17 15:22:15 +07:00
MythEclipse 3c19d262f3 feat: Increase CSRF token maximum age to 30 minutes and enhance logging for token generation 2025-08-17 15:08:58 +07:00
MythEclipse bdffe85544 Update dependencies and refactor OAuth service
- Updated various dependencies in Cargo.toml to their latest versions for improved performance and security.
- Refactored the Google OAuth service to streamline the creation of the OAuth client and improve code readability.
- Changed the way WebSocket messages are sent in clear_db_test.rs to use `into()` for better type handling.
- Enhanced error handling and logging in the Google OAuth callback method.
2025-08-16 19:10:27 +07:00
MythEclipse 9ed619e3fd feat: Optimize password verification by using blocking tasks for improved performance 2025-08-16 13:47:29 +07:00
MythEclipse 00fd7e1907 feat: Refactor password hashing and JWT encoding to use static keys for improved performance and security 2025-08-16 13:43:18 +07:00
MythEclipse b70126734e feat: Refactor password reset flow to use async processing and improve token validation 2025-08-16 13:38:35 +07:00
MythEclipse 6af9461079 a 2025-08-16 13:22:39 +07:00
MythEclipse 57b3662951 feat: Refactor auth service and middleware for improved user data handling and caching 2025-08-16 13:18:55 +07:00
MythEclipse c0c08f2821 Enhance Users Service and DTOs
- Added Default trait to UsersDetailItemDto for easier instantiation.
- Refactored UsersServiceTrait to use Pin<Box<dyn Future<...>> for async functions, improving compatibility with async/await.
- Updated all service methods to return futures instead of using async_trait.
- Improved error handling and logging in upload_file method, ensuring proper handling of multipart data.
- Masked sensitive information in Env struct's Debug implementation for better security in logs.
- Cleaned up Cargo.toml by removing unnecessary async-trait workspace dependency.
2025-08-16 12:48:24 +07:00
MythEclipse 8402415649 feat: Enhance user permissions and update event handling with improved response structures 2025-08-15 00:32:15 +07:00
MythEclipse 01e20baa45 feat: Add axum-extra dependency and refactor auth middleware for improved token handling 2025-08-14 23:16:41 +07:00
MythEclipse 10dc869eaf feat: Enhance permissions and roles management with improved indexing and state handling 2025-08-14 23:11:44 +07:00
MythEclipse 199d0c885e feat: Add axum-extra dependency and implement typed headers
feat: Define unique index on users table for email and fix minor syntax error

refactor: Update permissions_guard to use claims from JWT and improve user retrieval

refactor: Modify user-related service methods to accept user details directly

fix: Update token generation functions to include user ID and permissions

test: Update Google OAuth flow tests to reflect changes in token generation
2025-08-14 22:46:42 +07:00
MythEclipse 89ef48a57e feat: Update permissions for user and gacha claim access 2025-08-14 21:38:47 +07:00
MythEclipse da1826ebba feat: Add file upload with deduplication to MinIO service 2025-08-14 17:03:23 +07:00
MythEclipse 874986f1d6 feat: Implement file upload functionality with MinIO integration
- Added new dependencies for HMAC, hex, and urlencoding in Cargo.toml.
- Introduced FileUploadSchema for handling multipart file uploads.
- Enhanced UsersService with upload_file method to handle file uploads to MinIO.
- Implemented MinioService for managing MinIO interactions, including file uploads and presigned URL generation.
- Updated environment configuration to include MinIO region and secure settings.
- Added validation for file types and sizes during upload.
- Improved error handling and logging for file upload processes.
- Created utility functions for base64 decoding and content type extraction.
2025-08-14 16:53:14 +07:00
MythEclipse bbff555a06 feat(docs): add upload_file endpoint to OpenAPI documentation 2025-08-14 14:16:07 +07:00
Asep Haryana Saputra d3eb687a58 Merge branch 'feat/auth-google' of https://github.com/IMPHNEN/imphnen-backend-service into feat/auth-google 2025-08-14 06:55:26 +00:00
Asep Haryana Saputra c5516d4006 a 2025-08-14 06:54:56 +00:00
MythEclipse 5f51dd281d feat: update dependencies and add MinIO integration
- Updated `async-channel` to version 2.5.0 and added new dependencies in `Cargo.lock`.
- Introduced `minio` crate for file upload functionality.
- Added `career_status` field to user-related DTOs and schemas.
- Implemented file upload endpoint in `users_controller.rs` with multipart support.
- Created `MinioService` for handling file uploads to MinIO.
- Updated user seeding and test cases to accommodate new `career_status` field.
- Refactored permissions guard to return user details.
2025-08-14 13:16:39 +07:00
MythEclipse f6232832af feat(users): Enhance user schema and DTOs with additional fields for website, social links, location, skills, experience, and education 2025-08-13 20:40:02 +07:00
MythEclipse 277cf0770d feat(auth): Add GOOGLE_REDIRECT_URL to .env.example for Google OAuth callback 2025-08-13 01:29:41 +07:00
MythEclipse 650728a385 feat(auth): Update Google OAuth service to support custom redirect URIs 2025-08-12 23:21:34 +07:00
MythEclipse 3cc3e2a7d8 feat(auth): Update Google OAuth flow to support custom redirect URIs 2025-08-12 22:06:26 +07:00
MythEclipse 39fc2f86ad feat(users): Include avatar field in user creation from DTO 2025-08-12 19:50:52 +07:00
Asep Haryana Saputra 7bd0ef2274 a 2025-08-12 12:41:16 +00:00
MythEclipse b40a430c49 feat(auth): Enhance Google OAuth flow with async email extraction and caching 2025-08-12 19:25:10 +07:00
MythEclipse 44b1e09551 fix(users): Change meta parameter extraction from Json to Query in get_user_list function 2025-08-12 18:53:11 +07:00
MythEclipse 1b9251b2dd Refactor mentor and user schemas to centralize personal data management
- Updated `seed_mentor_user.rs` to include additional fields in the `app_users` creation and removed redundant fields from `app_mentors`.
- Modified `seed_users.rs` to initialize new user fields including legal name, domicile, and professional links.
- Adjusted `mentors_dto.rs` to remove personal data fields from `MentorDetailWithUserDto` and `MentorDetailResponseDto`, now sourced from `UsersSchema`.
- Updated `mentors_repository.rs` to search for user data through `user_id` instead of mentor fields.
- Refined `mentors_schema.rs` to remove personal data fields, now managed in `UsersSchema`.
- Enhanced `mentors_service.rs` to populate mentor responses with user data from `UsersSchema`.
- Removed identity document URL and related fields from various DTOs and schemas across the codebase.
- Updated tests to reflect the removal of identity document URL and ensure compatibility with the new schema structure.
2025-08-12 18:43:30 +07:00
MythEclipse 34b6cdabfb feat(users): Add optional fields to user schemas and update user avatar functionality 2025-08-12 17:31:12 +07:00
MythEclipse a25e896fa2 feat(auth): Enhance Google OAuth integration with PKCE support and CSRF validation improvements 2025-08-12 16:11:15 +07:00
Asep Haryana Saputra adf3ef8e28 a 2025-08-12 08:14:05 +00:00
MythEclipse ad46687f07 refactor(tests): Remove unused imports in CSRF token tests 2025-08-12 01:51:10 +07:00
MythEclipse 0ba04abee9 feat(auth): Enhance Google OAuth service with CSRF protection and validation logic
feat(auth): Add error handling for authentication and validation errors
feat(auth): Implement default role assignment for new users in Google OAuth flow
feat(utils): Introduce CSRF token generation and validation utilities
fix(dependencies): Update Cargo.toml to include base64 and sha2 dependencies
2025-08-12 00:32:59 +07:00
MythEclipse 1e25a5b496 feat(auth): Update Google OAuth integration to enhance response structure and improve callback handling 2025-08-11 23:47:15 +07:00
MythEclipse d7178c792d a 2025-08-11 23:06:45 +07:00
MythEclipse d2754de9ef feat(auth): Refactor Google OAuth service and controller to utilize environment variables and enhance response structure 2025-08-11 22:54:10 +07:00
MythEclipse cda950ed7e feat(auth): Implement Google OAuth 2.0 integration with user creation and JWT generation
- Added Google OAuth controller and service to handle authentication via Google.
- Introduced DTOs for Google user and token responses.
- Updated AuthService and UsersService traits to support new Google OAuth functionality.
- Implemented logic to create a new user if they do not exist in the system after Google authentication.
- Enhanced existing user retrieval and JWT generation upon successful login.
- Added tests for Google OAuth flow, including login redirection and callback handling for both new and existing users.
- Updated environment configuration to include Google OAuth credentials.
2025-08-11 22:31:51 +07:00
MythEclipse c6a95c1231 fix: update default surrealdb password to 'root' for consistency 2025-07-31 20:34:36 +07:00
MythEclipse 86116b8157 feat: add logging for SurrealDB queries across multiple repositories 2025-07-31 16:51:08 +07:00
MythEclipse ac06224ed1 feat: enhance logging and dependency management with dotenvy and tracing-subscriber 2025-07-31 15:42:15 +07:00
MythEclipse 83e765f358 feat: add logger module with initialization function using tracing and dotenvy 2025-07-31 15:15:55 +07:00
MythEclipse 558e0ce568 refactor: simplify environment variable loading with helper function and remove unused mail configuration 2025-07-31 15:12:01 +07:00
MythEclipse 148cf3a5cd refactor: streamline environment variable loading and improve logging 2025-07-31 15:07:18 +07:00
Maulana Sodiqin ee9d23e01b Merge pull request #44 from IMPHNEN/feat/mentor
Add comprehensive tests for mentor repository and authentication
2025-07-21 22:18:14 +07:00
274 changed files with 43356 additions and 4318 deletions
+32 -7
View File
@@ -1,4 +1,30 @@
# RUST_ENV : Mode environment aplikasi (misal: development, production)
# RUST_LOG : Level logging aplikasi Rust (misal: debug, info, warn, error)
# PORT : Port yang digunakan backend service
# SURREALDB_URL : URL koneksi ke SurrealDB (gunakan ws:// untuk WebSocket)
# SURREALDB_USERNAME : Username untuk SurrealDB
# SURREALDB_PASSWORD : Password untuk SurrealDB
# SURREALDB_NAMESPACE : Namespace SurrealDB yang digunakan
# SURREALDB_DBNAME : Nama database SurrealDB
# ACCESS_TOKEN_SECRET : Secret key untuk JWT access token (ganti dengan nilai rahasia)
# REFRESH_TOKEN_SECRET : Secret key untuk JWT refresh token (ganti dengan nilai rahasia)
# SMTP_EMAIL : Email yang digunakan untuk mengirim email (SMTP)
# SMTP_PASSWORD : Password email SMTP
# SMTP_NAME : Nama pengirim email (misal: nama aplikasi)
# SMTP_HOST : Host SMTP server (misal: smtp.gmail.com)
# REDISDB_URL : URL/host Redis database
# FE_URL : URL frontend aplikasi (misal: http://localhost)
# MINIO_ENDPOINT : Endpoint MinIO object storage
# MINIO_BUCKET_NAME : Nama bucket default MinIO
# MINIO_ACCESS_KEY : Access key MinIO atau MINIO_ROOT_USER
# MINIO_SECRET_KEY : Secret key MinIO atau MINIO_ROOT_PASSWORD
# MINIO_ROOT_USER : Username root MinIO (default: minioadmin)
# MINIO_ROOT_PASSWORD : Password root MinIO (default: minioadmin)
# GOOGLE_CLIENT_ID : Client ID OAuth Google (untuk login Google)
# GOOGLE_CLIENT_SECRET : Client Secret OAuth Google
RUST_ENV=development
RUST_LOG=debug
PORT=4099
SURREALDB_URL=ws://localhost:8000/rpc
SURREALDB_USERNAME=root
@@ -15,10 +41,9 @@ REDISDB_URL=localhost
FE_URL=http://localhost
MINIO_ENDPOINT=http://localhost:9000
MINIO_BUCKET_NAME=default_bucket
MINIO_ACCESS_KEY=your-minio-access-key
MINIO_SECRET_KEY=your-minio-secret-key
MAIL_USER=your-email@example.com
MAIL_PASSWORD=your-smtp-password
MAIL_HOST=smtp.gmail.com
MAIL_PORT=465
MAIL_SECURE=true
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
MINIO_SECURE=false
GOOGLE_CLIENT_ID="your_google_client_id"
GOOGLE_CLIENT_SECRET="your_google_client_secret"
+1
View File
@@ -13,3 +13,4 @@
.env.development
.env.staging
.env.production
**/**.log
+1
View File
@@ -0,0 +1 @@
/cache
+71
View File
@@ -0,0 +1,71 @@
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
# * For C, use cpp
# * For JavaScript, use typescript
# Special requirements:
# * csharp: Requires the presence of a .sln file in the project folder.
language: rust
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed) on 2025-04-07
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
project_name: "imphnen-backend-service"
Generated
+575 -219
View File
File diff suppressed because it is too large Load Diff
+51 -37
View File
@@ -2,61 +2,74 @@
resolver = "2"
members = [
"tests",
"imphnen-iam",
"imphnen-cms",
"imphnen-libs",
"imphnen-utils",
"imphnen-gacha",
"imphnen-gateway",
"imphnen-backend",
"imphnen-entities",
"imphnen-dimentorin",
"imphnen-middleware",
"imphnen-entities", # Most basic - core data structures
"imphnen-libs", # Depends on entities
"imphnen-utils", # Depends on libs and entities
"imphnen-middleware",# Utility for permissions
"imphnen-iam", # Core auth service, depends on libs, utils, entities
"imphnen-cms", # Content management, depends on core services
"imphnen-gacha", # Game mechanics, depends on core services
"imphnen-dimentorin",# Learning platform, depends on core services
"imphnen-hackathon", # Hackathon service, depends on core services
"imphnen-gateway", # API gateway, depends on all services
"imphnen-backend", # Main application, depends on all services
]
[workspace.dependencies]
async-trait = "0.1.83"
oauth2 = "5.0.0"
reqwest = { version = "0.12.23", features = ["json"] }
serde_json = "1.0.142"
axum = { version = "0.8.4", features = ["multipart"] }
log = "0.4.25"
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.138"
tokio = { version = "1.45.0", features = ["full"] }
log = "0.4.27"
serde = { version = "1.0.219", features = ["derive"] }
tokio = { version = "1.47.1", features = ["full"] }
argon2 = { version = "0.5.3", features = ["password-hash"] }
jsonwebtoken = "9.3.1"
chrono = "0.4.41"
utoipa = { version = "5.3.1", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "9.0.0", features = ["axum"] }
lettre = { version = "0.11.16", features = ["tokio1-native-tls"] }
utoipa = { version = "5.4.0", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
lettre = { version = "0.11.18", features = ["tokio1-native-tls"] }
surrealdb = { version = "2.3.7", features = ["kv-mem", "kv-fdb"] }
thiserror = "2.0.12"
anyhow = "1.0.98"
rand = { version = "0.9.1", features = ["std", "alloc"] }
thiserror = "2.0.14"
anyhow = "1.0.99"
rand = { version = "0.9.2", features = ["std", "alloc"] }
rand_distr = "0.5.1"
tower-http = { version = "0.6.4", features = ["cors", "trace"] }
http-body-util = "0.1.1"
tower-http = { version = "0.6.6", features = ["cors", "trace"] }
http-body-util = "0.1.3"
validator = { version = "0.20.0", features = ["derive"] }
lazy_static = "1.4.0"
lazy_static = "1.5.0"
regex = "1.11.1"
axum-test = "17.2.0"
fancy-regex = "0.15.0"
axum-test = "17.3.0"
axum-extra = { version = "0.10.1", features = ["typed-header"] }
fancy-regex = "0.16.1"
futures = "0.3.31"
tower = "0.5.2"
env_logger = "0.11.8"
dotenvy = "0.15.7"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
once_cell = "1.21.3"
uuid = { version = "1.8.0", features = ["v4", "fast-rng", "serde"] }
strum = { version = "0.27.1", features = ["derive"] }
strum_macros = "0.27.1"
tracing = "0.1.41"
uuid = { version = "1.18.0", features = ["v4", "fast-rng", "serde"] }
strum = { version = "0.27.2", features = ["derive"] }
strum_macros = "0.27.2"
base64 = "0.22.1"
sha2 = "0.10.9"
hmac = "0.12"
hex = "0.4"
urlencoding = "2.1"
hyper = "1.6.0"
hyper-util = "0.1.0"
async-trait = "0.1.75"
tokio-tungstenite = "0.23"
hyper-util = "0.1.16"
minio = "0.3.0"
tokio-test = "0.4.4"
mockall = "0.13.1"
once_cell = "1.21.3"
dotenvy = "0.15.7"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tokio-tungstenite = "0.27"
url = "2.5"
futures-util = "0.3"
http = "1.3"
tests = { path = "./tests" }
imphnen-iam = { path = "./imphnen-iam" }
imphnen-cms = { path = "./imphnen-cms" }
@@ -67,6 +80,7 @@ imphnen-gateway = { path = "./imphnen-gateway" }
imphnen-backend = { path = "./imphnen-backend" }
imphnen-entities = { path = "./imphnen-entities" }
imphnen-dimentorin = { path = "./imphnen-dimentorin" }
imphnen-hackathon = { path = "./imphnen-hackathon" }
imphnen-middleware = { path = "./imphnen-middleware" }
[profile.release]
+49
View File
@@ -0,0 +1,49 @@
#!/bin/bash
# Script deploy semua binary hasil build Rust (untuk Linux)
set -e
REMOTE_USER="asephs"
REMOTE_HOST="70.153.9.42"
REMOTE_PATH="/home/asephs/imphnen-backend-service"
# Build release binary
echo "🔧 Building Rust project..."
cargo build --release
# Filter hanya file executable tanpa ekstensi .exe, .rlib, atau .d
BINARIES=$(find target/release -maxdepth 1 -type f ! -name "*.exe" ! -name "*.rlib" ! -name "*.d")
if [ -z "$BINARIES" ]; then
echo "❌ Tidak ada binary Linux (.exe/.rlib/.d diabaikan)"
exit 1
fi
# Upload semua binary yang valid ke server
echo "🚀 Mengirim binary ke server..."
rsync -avz --compress-level=9 --progress \
$BINARIES \
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/target/release/
# Jalankan ulang service utama di VPS
echo "♻️ Restart service utama di server..."
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
set -e
cd /home/asephs/imphnen-backend-service
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
if [ -f ~/.bashrc ]; then
source ~/.bashrc
fi
# Pastikan semua binary bisa dieksekusi
chmod +x target/release/*
# Restart service utama (misalnya api.d)
if pm2 list | grep -q api; then
pm2 restart api --update-env
else
pm2 start target/release/api.d --name api
fi
EOF
echo "✅ Deploy semua binary Linux selesai ke $REMOTE_HOST:$REMOTE_PATH"
+16
View File
@@ -14,3 +14,19 @@ services:
command: start --log trace --user root --pass root
ports:
- "8000:8000"
minio:
image: minio/minio:latest
container_name: minio
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
command: server /data --console-address ":9001"
volumes:
- minio_data:/data
volumes:
minio_data:
+50
View File
@@ -3,6 +3,54 @@ name = "imphnen-backend"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "api"
path = "src/main.rs"
[[bin]]
name = "clear_db_test"
path = "src/bin/clear_db_test.rs"
[[bin]]
name = "seeder"
path = "src/bin/seeder.rs"
[[bin]]
name = "seed_events"
path = "src/bin/seed_events.rs"
[[bin]]
name = "seed_gacha_rolls"
path = "src/bin/seed_gacha_rolls.rs"
[[bin]]
name = "seed_mentor_user"
path = "src/bin/seed_mentor_user.rs"
[[bin]]
name = "seed_permissions"
path = "src/bin/seed_permissions.rs"
[[bin]]
name = "seed_roles"
path = "src/bin/seed_roles.rs"
[[bin]]
name = "seed_roles_permissions"
path = "src/bin/seed_roles_permissions.rs"
[[bin]]
name = "seed_teams"
path = "src/bin/seed_teams.rs"
[[bin]]
name = "seed_hackathons"
path = "src/bin/seed_hackathons.rs"
[[bin]]
name = "seed_test_data"
path = "src/bin/seed_test_data.rs"
[dependencies]
imphnen-libs.workspace = true
imphnen-utils.workspace = true
@@ -10,7 +58,9 @@ imphnen-gateway.workspace = true
imphnen-entities.workspace = true
imphnen-iam.workspace = true
imphnen-cms.workspace = true
imphnen-gacha.workspace = true
imphnen-dimentorin.workspace = true
imphnen-hackathon.workspace = true
axum.workspace = true
serde.workspace = true
serde_json.workspace = true
+6 -6
View File
@@ -30,7 +30,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let url = Url::parse(SURREALDB_URL_WS)?; // Menggunakan SURREALDB_URL_WS statis
let (ws_stream, _) = connect_async(url).await?;
let (ws_stream, _) = connect_async(url.as_str()).await?;
let (mut write, mut read) = ws_stream.split();
// Authenticate (signin)
@@ -43,7 +43,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
"id": 1
}).to_string();
println!("DEBUG: Sending signin query: {}", signin_query);
write.send(Message::Text(signin_query)).await?;
write.send(Message::Text(signin_query.into())).await?;
let signin_response = read.next().await.ok_or("Failed to read signin response")?;
let signin_response_msg = signin_response?;
@@ -60,7 +60,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
"id": 2
}).to_string();
println!("DEBUG: Sending use query: {}", use_query);
write.send(Message::Text(use_query)).await?;
write.send(Message::Text(use_query.into())).await?;
let use_response = read.next().await.ok_or("Failed to read use response")?;
let use_response_msg = use_response?;
@@ -82,7 +82,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}).to_string();
println!("DEBUG: Attempting REMOVE TABLE {}: {}", table, query_json);
write.send(Message::Text(query_json)).await?;
write.send(Message::Text(query_json.into())).await?;
let response_result = read.next().await.ok_or("Stream ended unexpectedly")?;
@@ -100,7 +100,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}).to_string();
println!("DEBUG: Attempting DELETE {}: {}", table, delete_all_json);
write.send(Message::Text(delete_all_json)).await?;
write.send(Message::Text(delete_all_json.into())).await?;
let delete_response_result = read.next().await.ok_or("Stream ended unexpectedly during DELETE type::")?;
match delete_response_result {
@@ -137,7 +137,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
"params": [select_query],
"id": i + 1000
}).to_string();
write.send(Message::Text(select_json)).await?;
write.send(Message::Text(select_json.into())).await?;
let select_response_result = read.next().await.ok_or("Stream ended unexpectedly during SELECT check")?;
match select_response_result {
Ok(select_msg) => {
+19
View File
@@ -0,0 +1,19 @@
use imphnen_libs::jsonwebtoken::encode_access_token;
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: mk_token <email_or_sub>");
std::process::exit(1);
}
let sub = args[1].clone();
// Use sub as both sub and user_id
match encode_access_token(sub.clone(), sub.clone()) {
Ok(token) => println!("{}", token),
Err(e) => {
eprintln!("Failed to generate token: {:?}", e);
std::process::exit(2);
}
}
}
+5 -6
View File
@@ -1,21 +1,20 @@
use imphnen_cms::v1::landing::events::events_schema::EventsSchema;
use imphnen_libs::enviroment::load_env;
use imphnen_utils::{get_iso_date, Env};
use imphnen_utils::{get_iso_date};
use std::error::Error;
use surrealdb::engine::any;
use surrealdb::{opt::auth::Root, sql::Thing, Uuid}; // Added Uuid
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
load_env();
let env = Env::new();
let env = &imphnen_libs::environment::ENV;
let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root {
username: &env.surrealdb_username,
password: &env.surrealdb_password,
})
.await?;
db.use_ns(env.surrealdb_namespace)
.use_db(env.surrealdb_dbname)
db.use_ns(env.surrealdb_namespace.clone())
.use_db(env.surrealdb_dbname.clone())
.await?;
let events = vec![
+8 -12
View File
@@ -1,14 +1,11 @@
use imphnen_libs::enviroment::load_env;
use imphnen_utils::{get_iso_date, Env};
use imphnen_utils::{get_iso_date};
use std::error::Error;
use surrealdb::opt::auth::Root;
use surrealdb::sql::Thing;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
load_env();
let env = Env::new();
let env = &imphnen_libs::environment::ENV;
use surrealdb::engine::any;
let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root {
@@ -16,18 +13,17 @@ async fn main() -> Result<(), Box<dyn Error>> {
password: &env.surrealdb_password,
})
.await?;
db.use_ns(env.surrealdb_namespace)
.use_db(env.surrealdb_dbname)
db.use_ns(env.surrealdb_namespace.clone())
.use_db(env.surrealdb_dbname.clone())
.await?;
db.query("DELETE type::thing('app_gacha_items', $id)")
.bind(("id", "gacha_item_test_id"))
.bind(("id", "1"))
.await?;
db.query("DELETE type::thing('app_gacha_rolls', $id)")
.bind(("id", "gacha_roll_test_id"))
.bind(("id", "test-gacha-roll-001"))
.await?;
let gacha_item_id = "gacha_item_test_id";
let gacha_item_id = "1";
db.query("CREATE type::thing('app_gacha_items', $id) SET name = $name, image_url = $image_url, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at")
.bind(("id", gacha_item_id))
.bind(("name", "Test Gacha Item"))
@@ -38,7 +34,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
.await?;
println!("Gacha Item seeded successfully!");
let gacha_roll_id = "gacha_roll_test_id";
let gacha_roll_id = "test-gacha-roll-001";
db.query("CREATE type::thing('app_gacha_rolls', $id) SET item = $item, quantity = $quantity, weight = $weight, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at")
.bind(("id", gacha_roll_id))
.bind(("item", Thing::from(("app_gacha_items", gacha_item_id))))
+368
View File
@@ -0,0 +1,368 @@
use chrono::{DateTime, Utc};
use imphnen_hackathon::v1::hackathon::hackathon_schema::{
HackathonSchema, HackathonEventsSchema, HackathonTimelineSchema, HackathonSubmissionsSchema,
HackathonStatus, HackathonEventType, HackathonPhase, SubmissionStatus, Prize
};
use imphnen_utils::get_iso_date;
use std::error::Error;
use surrealdb::{opt::auth::Root, sql::Thing};
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::environment::ENV;
use surrealdb::engine::any;
let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root {
username: &env.surrealdb_username,
password: &env.surrealdb_password,
})
.await?;
db.use_ns(env.surrealdb_namespace.clone())
.use_db(env.surrealdb_dbname.clone())
.await?;
// Sample hackathon data
let hackathons = vec![
(
"hackathon-001",
"AI Innovation Challenge 2025",
"Build the next generation of AI-powered applications that solve real-world problems.",
"2025-10-15T09:00:00Z",
"2025-10-17T18:00:00Z",
"2025-10-01T23:59:59Z",
Some(100),
HackathonStatus::RegistrationOpen,
Some("Artificial Intelligence & Machine Learning".to_string()),
Some("1. All code must be original\n2. Teams can have 2-5 members\n3. Projects must use AI/ML technologies".to_string()),
Some(vec![
Prize { position: 1, title: "Grand Prize".to_string(), description: Some("Winner gets full scholarship".to_string()), value: Some("$10,000".to_string()) },
Prize { position: 2, title: "Second Place".to_string(), description: Some("Runner-up prize".to_string()), value: Some("$5,000".to_string()) },
Prize { position: 3, title: "Third Place".to_string(), description: Some("Third place prize".to_string()), value: Some("$2,500".to_string()) },
]),
vec!["c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2".to_string()], // admin user
),
(
"hackathon-002",
"Green Tech Hackathon",
"Develop sustainable technology solutions for environmental challenges.",
"2025-11-20T10:00:00Z",
"2025-11-22T17:00:00Z",
"2025-11-05T23:59:59Z",
Some(75),
HackathonStatus::Draft,
Some("Sustainability & Green Technology".to_string()),
Some("Focus on renewable energy, waste reduction, and environmental monitoring.".to_string()),
Some(vec![
Prize { position: 1, title: "Eco Champion".to_string(), description: Some("Best environmental impact".to_string()), value: Some("$7,500".to_string()) },
Prize { position: 2, title: "Innovation Award".to_string(), description: Some("Most innovative solution".to_string()), value: Some("$3,500".to_string()) },
]),
vec!["c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2".to_string()],
),
];
// Sample hackathon events
let hackathon_events = vec![
(
"hackathon-001",
"event-001",
"Opening Ceremony",
Some("Welcome and kickoff event for the AI Innovation Challenge".to_string()),
HackathonEventType::Ceremony,
"2025-10-15T09:00:00Z",
"2025-10-15T10:00:00Z",
Some("Main Auditorium".to_string()),
None,
Some(150),
true,
),
(
"hackathon-001",
"event-002",
"AI Workshop: Getting Started",
Some("Introduction to AI frameworks and tools".to_string()),
HackathonEventType::Workshop,
"2025-10-15T14:00:00Z",
"2025-10-15T16:00:00Z",
None,
Some("https://zoom.us/meeting/ai-workshop".to_string()),
Some(80),
false,
),
(
"hackathon-001",
"event-003",
"Judging Session",
Some("Final project presentations and judging".to_string()),
HackathonEventType::Judging,
"2025-10-17T14:00:00Z",
"2025-10-17T17:00:00Z",
Some("Innovation Lab".to_string()),
None,
Some(100),
true,
),
];
// Sample hackathon timeline
let hackathon_timeline = vec![
(
"hackathon-001",
HackathonPhase::Registration,
"Registration Phase",
Some("Register your team and submit initial project ideas".to_string()),
"2025-10-01T00:00:00Z",
"2025-10-10T23:59:59Z",
true,
1,
),
(
"hackathon-001",
HackathonPhase::Ideation,
"Ideation & Planning",
Some("Brainstorm and plan your AI solution".to_string()),
"2025-10-11T00:00:00Z",
"2025-10-14T23:59:59Z",
false,
2,
),
(
"hackathon-001",
HackathonPhase::Development,
"Development Sprint",
Some("Build your AI-powered application".to_string()),
"2025-10-15T00:00:00Z",
"2025-10-16T23:59:59Z",
false,
3,
),
(
"hackathon-001",
HackathonPhase::Submission,
"Project Submission",
Some("Submit your final project and demo video".to_string()),
"2025-10-17T00:00:00Z",
"2025-10-17T12:00:00Z",
false,
4,
),
(
"hackathon-001",
HackathonPhase::Judging,
"Judging & Awards",
Some("Presentations and prize ceremony".to_string()),
"2025-10-17T13:00:00Z",
"2025-10-17T18:00:00Z",
false,
5,
),
];
// Sample hackathon submissions
let hackathon_submissions = vec![
(
"hackathon-001",
"team-dev-001",
"AI-Powered Health Monitor",
"A machine learning application that predicts health risks using wearable device data.",
Some("https://github.com/team-dev/ai-health-monitor".to_string()),
Some("https://demo.ai-health-monitor.com".to_string()),
None,
vec!["Python".to_string(), "TensorFlow".to_string(), "React".to_string()],
SubmissionStatus::Submitted,
"2025-10-17T11:30:00Z",
),
(
"hackathon-001",
"team-design-001",
"Smart City Traffic Optimizer",
"AI system that optimizes traffic flow using computer vision and predictive analytics.",
Some("https://github.com/team-design/smart-traffic".to_string()),
Some("https://demo.smart-traffic.com".to_string()),
Some("https://slides.smart-traffic.com/presentation".to_string()),
vec!["JavaScript".to_string(), "Node.js".to_string(), "OpenCV".to_string()],
SubmissionStatus::UnderReview,
"2025-10-17T10:45:00Z",
),
];
// Seed hackathons
for (
id,
name,
description,
start_date,
end_date,
registration_deadline,
max_participants,
status,
theme,
rules,
prizes,
organizers,
) in hackathons {
db.query("DELETE type::thing('app_hackathons', $id)")
.bind(("id", id))
.await?;
let hackathon = HackathonSchema {
id: Thing::from(("app_hackathons", id)),
name: name.into(),
description: description.into(),
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
registration_deadline: DateTime::parse_from_rfc3339(registration_deadline)?.with_timezone(&Utc),
max_participants,
status,
theme,
rules,
prizes,
previous_winners: None,
organizers,
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
db.create::<Option<HackathonSchema>>(("app_hackathons", id))
.content(hackathon)
.await?;
println!("✅ Inserted hackathon: {name}");
}
// Seed hackathon events
for (
hackathon_id,
event_id,
title,
description,
event_type,
start_time,
end_time,
location,
virtual_link,
max_attendees,
is_mandatory,
) in hackathon_events {
db.query("DELETE type::thing('app_hackathon_events', $id)")
.bind(("id", event_id))
.await?;
let event = HackathonEventsSchema {
id: Thing::from(("app_hackathon_events", event_id)),
hackathon_id: Thing::from(("app_hackathons", hackathon_id)),
title: title.into(),
description,
event_type,
start_time: DateTime::parse_from_rfc3339(start_time)?.with_timezone(&Utc),
end_time: DateTime::parse_from_rfc3339(end_time)?.with_timezone(&Utc),
location,
virtual_link,
max_attendees,
is_mandatory,
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
db.create::<Option<HackathonEventsSchema>>(("app_hackathon_events", event_id))
.content(event)
.await?;
println!("✅ Inserted hackathon event: {title}");
}
// Seed hackathon timeline
for (
hackathon_id,
phase,
title,
description,
start_date,
end_date,
is_active,
order,
) in hackathon_timeline {
let timeline_id = format!("timeline-{}-{}", hackathon_id, order);
db.query("DELETE type::thing('app_hackathon_timeline', $id)")
.bind(("id", timeline_id.clone()))
.await?;
let timeline = HackathonTimelineSchema {
id: Thing::from(("app_hackathon_timeline", timeline_id.as_str())),
hackathon_id: Thing::from(("app_hackathons", hackathon_id)),
phase,
title: title.into(),
description,
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
is_active,
order,
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
db.create::<Option<HackathonTimelineSchema>>(("app_hackathon_timeline", timeline_id))
.content(timeline)
.await?;
println!("✅ Inserted hackathon timeline: {title}");
}
// Seed hackathon submissions
for (
hackathon_id,
team_id,
project_name,
description,
repository_url,
demo_url,
slides_url,
technologies,
submission_status,
submitted_at,
) in hackathon_submissions {
let submission_id = format!("submission-{}-{}", hackathon_id, team_id);
db.query("DELETE type::thing('app_hackathon_submissions', $id)")
.bind(("id", submission_id.clone()))
.await?;
let submission = HackathonSubmissionsSchema {
id: Thing::from(("app_hackathon_submissions", submission_id.as_str())),
hackathon_id: Thing::from(("app_hackathons", hackathon_id)),
judge_feedback: None,
team_id: Some(Thing::from(("app_teams", team_id))),
project_name: Some(project_name.into()),
description: Some(description.into()),
repository_url,
upload_file_url: None,
demo_url,
slides_url,
technologies: Some(technologies),
contact_instagram: None,
contact_twitter: None,
contact_linkedin: None,
contact_facebook: None,
contact_youtube: None,
contact_tiktok: None,
contact_other: None,
submission_status: Some(submission_status),
submitted_at: Some(DateTime::parse_from_rfc3339(submitted_at)?.with_timezone(&Utc)),
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
db.create::<Option<HackathonSubmissionsSchema>>(("app_hackathon_submissions", submission_id))
.content(submission)
.await?;
println!("✅ Inserted hackathon submission: {project_name}");
}
println!("✅ All Hackathons seeded");
Ok(())
}
File diff suppressed because one or more lines are too long
+6 -7
View File
@@ -1,6 +1,5 @@
use imphnen_iam::PermissionsEnum;
use imphnen_libs::enviroment::load_env;
use imphnen_utils::{get_iso_date, Env};
use imphnen_utils::{get_iso_date};
use serde_json::json;
use std::error::Error;
use surrealdb::engine::any;
@@ -8,17 +7,15 @@ use surrealdb::opt::auth::Root;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
load_env();
let env = Env::new();
let env = &imphnen_libs::environment::ENV;
let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root {
username: &env.surrealdb_username,
password: &env.surrealdb_password,
})
.await?;
db.use_ns(env.surrealdb_namespace)
.use_db(env.surrealdb_dbname)
db.use_ns(env.surrealdb_namespace.clone())
.use_db(env.surrealdb_dbname.clone())
.await?;
for permission in [
@@ -54,8 +51,10 @@ async fn main() -> Result<(), Box<dyn Error>> {
PermissionsEnum::ReadOwnMentorProfile,
PermissionsEnum::UpdateOwnMentorProfile,
PermissionsEnum::ReadOwnMentorStatus,
PermissionsEnum::UpdateMentors,
PermissionsEnum::VerifyMentors,
PermissionsEnum::DeleteMentors,
PermissionsEnum::Administrator,
] {
db.query("CREATE type::thing('app_permissions', $id) CONTENT $data")
.bind(("id", permission.id()))
+7 -9
View File
@@ -1,21 +1,19 @@
use imphnen_libs::enviroment::load_env;
use imphnen_utils::{get_iso_date, Env};
use imphnen_utils::{get_iso_date};
use serde_json::json;
use std::error::Error;
use surrealdb::engine::any;
use surrealdb::opt::auth::Root;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
load_env();
let env = Env::new();
let env = &imphnen_libs::environment::ENV;
let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root {
username: &env.surrealdb_username,
password: &env.surrealdb_password,
})
.await?;
db.use_ns(env.surrealdb_namespace)
.use_db(env.surrealdb_dbname)
db.use_ns(env.surrealdb_namespace.clone())
.use_db(env.surrealdb_dbname.clone())
.await?;
let roles = vec![
@@ -28,7 +26,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
(
"5713cb37-dc02-4e87-8048-d7a41d352059",
"User",
None,
Some("2025-02-28T14:53:58.576688+00"),
Some("2025-02-28T14:53:58.576688+00"),
),
(
@@ -46,13 +44,13 @@ async fn main() -> Result<(), Box<dyn Error>> {
(
"f6b03f25-e416-4893-ac88-caaa690afb07",
"Admin",
None,
Some("2025-02-22T15:38:39.868306+00"),
Some("2025-02-22T15:38:39.868306+00"),
),
(
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
"Mentor",
None,
Some("2025-07-06T10:00:00.000000+00"),
Some("2025-07-06T10:00:00.000000+00"),
),
];
@@ -1,64 +1,41 @@
use imphnen_iam::{get_iso_date, make_thing, Env, PermissionsEnum};
use imphnen_libs::enviroment::load_env;
use imphnen_iam::{get_iso_date, make_thing, PermissionsEnum};
use std::error::Error;
use surrealdb::engine::any;
use surrealdb::opt::auth::Root;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
load_env();
let env = Env::new();
let env = &imphnen_libs::environment::ENV;
let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root {
username: &env.surrealdb_username,
password: &env.surrealdb_password,
})
.await?;
db.use_ns(env.surrealdb_namespace)
.use_db(env.surrealdb_dbname)
db.use_ns(env.surrealdb_namespace.clone())
.use_db(env.surrealdb_dbname.clone())
.await?;
db.query("DEFINE INDEX user_email_index ON TABLE users COLUMNS email UNIQUE;")
.await?;
db.query("DEFINE INDEX role_name_idx ON TABLE roles COLUMNS name UNIQUE;")
.await?;
println!("✅ Index 'user_email_index' defined on table 'users' for column 'email'.");
let roles_permissions = vec![
(
"f6b03f25-e416-4893-ac88-caaa690afb07",
vec![
PermissionsEnum::ReadListUsers,
PermissionsEnum::ReadDetailUsers,
PermissionsEnum::CreateUsers,
PermissionsEnum::DeleteUsers,
PermissionsEnum::UpdateUsers,
PermissionsEnum::ActivateUsers,
PermissionsEnum::ReadListRoles,
PermissionsEnum::ReadDetailRoles,
PermissionsEnum::CreateRoles,
PermissionsEnum::DeleteRoles,
PermissionsEnum::UpdateRoles,
PermissionsEnum::ReadListPermissions,
PermissionsEnum::ReadDetailPermissions,
PermissionsEnum::CreatePermissions,
PermissionsEnum::DeletePermissions,
PermissionsEnum::UpdatePermissions,
PermissionsEnum::CreateGachaClaims,
PermissionsEnum::ReadDetailGachaClaims,
PermissionsEnum::ReadListGachaItems,
PermissionsEnum::ReadDetailGachaItems,
PermissionsEnum::CreateGachaItems,
PermissionsEnum::DeleteGachaItems,
PermissionsEnum::UpdateGachaItems,
PermissionsEnum::ReadDetailGachaRolls,
PermissionsEnum::CreateGachaRolls,
PermissionsEnum::ExecuteGachaRolls,
PermissionsEnum::ReadListMentors,
PermissionsEnum::ReadDetailMentors,
PermissionsEnum::RegisterMentors,
PermissionsEnum::UpdateMentors,
PermissionsEnum::VerifyMentors,
PermissionsEnum::DeleteMentors,
// Only Administrator permission - grants access to everything
PermissionsEnum::Administrator,
],
),
(
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
vec![
PermissionsEnum::ReadListUsers, // Added ReadListUsers permission
PermissionsEnum::ReadOwnMentorProfile,
PermissionsEnum::UpdateOwnMentorProfile,
PermissionsEnum::ReadOwnMentorStatus,
@@ -76,6 +53,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
vec![
PermissionsEnum::ReadListGachaItems,
PermissionsEnum::ReadDetailGachaItems,
PermissionsEnum::ReadListUsers,
PermissionsEnum::ReadDetailUsers,
PermissionsEnum::CreateGachaClaims,
PermissionsEnum::ReadDetailGachaClaims,
PermissionsEnum::ReadDetailGachaRolls,
@@ -91,13 +70,14 @@ async fn main() -> Result<(), Box<dyn Error>> {
(
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
vec![
// Staff should be able to list roles and permissions in tests
PermissionsEnum::ReadListRoles,
PermissionsEnum::ReadListPermissions,
PermissionsEnum::ReadListUsers,
PermissionsEnum::ReadListMentors,
PermissionsEnum::ReadDetailUsers,
PermissionsEnum::ActivateUsers,
PermissionsEnum::ReadListRoles,
PermissionsEnum::ReadDetailRoles,
PermissionsEnum::ReadListPermissions,
PermissionsEnum::ReadDetailPermissions,
PermissionsEnum::ReadListGachaItems,
PermissionsEnum::ReadDetailGachaItems,
@@ -106,6 +86,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
PermissionsEnum::ReadDetailGachaRolls,
PermissionsEnum::CreateGachaRolls,
PermissionsEnum::ExecuteGachaRolls,
PermissionsEnum::ManageAllTeams,
],
),
(
@@ -118,7 +99,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
for (role_id, permissions) in roles_permissions {
let permission_refs: Vec<_> = permissions
.iter()
.map(|perm| make_thing("app_permissions", perm.id()))
.map(|perm| make_thing("app_permissions", &perm.id()))
.collect();
db.query("UPDATE type::thing('app_roles', $role_id) SET permissions = $permissions, updated_at = $updated_at WHERE is_deleted = false")
+85
View File
@@ -0,0 +1,85 @@
use imphnen_iam::v1::teams::TeamsSchema;
use imphnen_utils::get_iso_date;
use std::error::Error;
use surrealdb::{opt::auth::Root, sql::Thing};
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::environment::ENV;
use surrealdb::engine::any;
let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root {
username: &env.surrealdb_username,
password: &env.surrealdb_password,
})
.await?;
db.use_ns(env.surrealdb_namespace.clone())
.use_db(env.surrealdb_dbname.clone())
.await?;
let teams = vec![
(
"team-dev-001",
"Development Team",
Some("Core development team for the platform".to_string()),
"c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2", // admin user
true,
Some(10),
Some(vec!["Rust".to_string(), "Backend".to_string()]),
Some("Remote".to_string()),
),
(
"team-design-001",
"Design Team",
Some("UI/UX design team".to_string()),
"c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2", // admin user
true,
Some(5),
Some(vec!["Figma".to_string(), "Design".to_string()]),
Some("Remote".to_string()),
),
(
"team-qa-001",
"Quality Assurance Team",
Some("Testing and quality assurance team".to_string()),
"c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2", // admin user
false,
Some(8),
Some(vec!["Testing".to_string(), "Automation".to_string()]),
Some("Remote".to_string()),
),
];
for (id, name, description, leader_id, is_open, max_members, skills_required, location) in teams {
db.query("DELETE type::thing('app_teams', $id)")
.bind(("id", id))
.await?;
let team = TeamsSchema {
id: Thing::from(("app_teams", id)),
name: name.into(),
description,
leader_id: Thing::from(("app_users", leader_id)),
is_open,
max_members,
skills_required,
location,
avatar: None,
website_url: None,
github_url: None,
is_active: true,
is_deleted: false,
created_at: get_iso_date(),
updated_at: get_iso_date(),
};
db.create::<Option<TeamsSchema>>(("app_teams", id))
.content(team)
.await?;
println!("✅ Inserted team: {name}");
}
println!("✅ All Teams seeded");
Ok(())
}
+175
View File
@@ -0,0 +1,175 @@
use imphnen_cms::v1::landing::events::events_schema::EventsSchema;
use imphnen_cms::v1::landing::testimonials::testimonials_schema::TestimonialsSchema;
use imphnen_dimentorin::v1::mentors::mentors_schema::MentorSchema;
use imphnen_dimentorin::v1::mentors::mentors_dto::MentoringRate;
use imphnen_hackathon::v1::hackathon::hackathon_schema::{
HackathonSchema, HackathonEventsSchema, HackathonTimelineSchema,
HackathonStatus, HackathonEventType, HackathonPhase
};
use imphnen_utils::get_iso_date;
use std::error::Error;
use surrealdb::{opt::auth::Root, sql::Thing};
use chrono::Utc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::environment::ENV;
use surrealdb::engine::any;
let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root {
username: &env.surrealdb_username,
password: &env.surrealdb_password,
})
.await?;
db.use_ns(env.surrealdb_namespace.clone())
.use_db(env.surrealdb_dbname.clone())
.await?;
// Seed Events - handle existing data
let event = EventsSchema {
id: Thing::from(("app_events", "1")),
name: "Test Event".to_string(),
description: "Test event description".to_string(),
detail_link: "https://example.com/event".to_string(),
price: 50.0,
is_online: true,
start_date: get_iso_date(),
end_date: get_iso_date(),
location: None,
is_deleted: false,
created_at: get_iso_date(),
updated_at: get_iso_date(),
};
match db.create::<Option<EventsSchema>>(("app_events", "1"))
.content(event)
.await {
Ok(_) => println!("✅ Inserted test event"),
Err(_) => println!("⚠️ Test event already exists, skipping"),
};
// Seed Testimonials - handle existing data
let testimonial = TestimonialsSchema {
id: Thing::from(("app_testimonials", "1")),
user: Thing::from(("app_users", "c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")),
role: "Student".to_string(),
content: "This is a great platform!".to_string(),
is_deleted: false,
created_at: get_iso_date(),
updated_at: get_iso_date(),
};
match db.create::<Option<TestimonialsSchema>>(("app_testimonials", "1"))
.content(testimonial)
.await {
Ok(_) => println!("✅ Inserted test testimonial"),
Err(_) => println!("⚠️ Test testimonial already exists, skipping"),
};
// Seed Hackathon - handle existing data
let hackathon = HackathonSchema {
id: Thing::from(("app_hackathons", "1")),
name: "Test Hackathon".to_string(),
description: "Test hackathon description".to_string(),
start_date: Utc::now() + chrono::Duration::days(30),
end_date: Utc::now() + chrono::Duration::days(37),
registration_deadline: Utc::now() + chrono::Duration::days(25),
max_participants: Some(100),
status: HackathonStatus::Draft,
theme: Some("Technology".to_string()),
rules: Some("Follow the rules".to_string()),
prizes: Some(vec![]),
previous_winners: Some(vec![]),
organizers: vec!["c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2".to_string()],
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
match db.create::<Option<HackathonSchema>>(("app_hackathons", "1"))
.content(hackathon)
.await {
Ok(_) => println!("✅ Inserted test hackathon"),
Err(_) => println!("⚠️ Test hackathon already exists, skipping"),
};
// Seed Hackathon Event
let hackathon_event = HackathonEventsSchema {
id: Thing::from(("app_hackathon_events", "test-event-001")),
hackathon_id: Thing::from(("app_hackathons", "1")),
title: "Test Event".to_string(),
description: Some("Test hackathon event description".to_string()),
event_type: HackathonEventType::Workshop,
start_time: Utc::now() + chrono::Duration::days(30),
end_time: Utc::now() + chrono::Duration::days(30) + chrono::Duration::hours(6),
location: Some("Online".to_string()),
virtual_link: None,
max_attendees: Some(50),
is_mandatory: false,
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
// Try to create hackathon event, skip if already exists
match db.create::<Option<HackathonEventsSchema>>(("app_hackathon_events", "test-event-001"))
.content(hackathon_event)
.await {
Ok(_) => println!("✅ Inserted test hackathon event"),
Err(_) => println!("⚠️ Test hackathon event already exists, skipping"),
};
// Seed Hackathon Timeline
let hackathon_timeline = HackathonTimelineSchema {
id: Thing::from(("app_hackathon_timeline", "test-timeline-001")),
hackathon_id: Thing::from(("app_hackathons", "1")),
phase: HackathonPhase::Registration,
title: "Test Timeline".to_string(),
description: Some("Test hackathon timeline description".to_string()),
start_date: Utc::now(),
end_date: Utc::now() + chrono::Duration::days(7),
is_active: true,
order: 1,
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
// Try to create hackathon timeline, skip if already exists
match db.create::<Option<HackathonTimelineSchema>>(("app_hackathon_timeline", "test-timeline-001"))
.content(hackathon_timeline)
.await {
Ok(_) => println!("✅ Inserted test hackathon timeline"),
Err(_) => println!("⚠️ Test hackathon timeline already exists, skipping"),
};
// Seed Mentor - handle existing data
let mentor = MentorSchema {
id: Thing::from(("app_mentors", "e6f78d23-83bf-5c2b-bcd4-001345678901")),
user_id: Some(Thing::from(("app_users", "e6f78d23-83bf-5c2b-bcd4-001345678901"))),
industries: vec!["Technology".to_string(), "Education".to_string()],
expertise: vec!["Software Development".to_string()],
languages: vec!["English".to_string(), "Indonesian".to_string()],
current_company: "Tech Corp".to_string(),
current_role: "Senior Engineer".to_string(),
years_of_experience: 5,
topics_of_interest: vec!["Rust".to_string(), "Web Development".to_string()],
preferred_mentee_level: vec!["Beginner".to_string()],
preferred_mentoring_formats: vec!["1:1".to_string(), "Group".to_string()],
availability_commitment: "Weekly".to_string(),
mentoring_rate: MentoringRate {
amount: 100,
currency: "IDR".to_string(),
per_duration: "hour".to_string(),
},
status: "active".to_string(),
is_deleted: false,
created_at: get_iso_date(),
updated_at: get_iso_date(),
};
match db.create::<Option<MentorSchema>>(("app_mentors", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
.content(mentor)
.await {
Ok(_) => println!("✅ Inserted test mentor"),
Err(_) => println!("⚠️ Test mentor already exists, skipping"),
};
println!("✅ All test data seeded successfully");
Ok(())
}
@@ -0,0 +1,433 @@
use chrono::{DateTime, Utc};
use imphnen_hackathon::v1::hackathon::hackathon_schema::{
HackathonSchema, HackathonTimelineSchema, HackathonSubmissionsSchema,
HackathonStatus, HackathonPhase, SubmissionStatus, Prize,
};
use imphnen_iam::{UsersSchema, v1::teams::TeamsSchema};
use imphnen_utils::{get_iso_date, hash_password};
use std::error::Error;
use surrealdb::{opt::auth::Root, sql::Thing};
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::environment::ENV;
use surrealdb::engine::any;
let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root {
username: &env.surrealdb_username,
password: &env.surrealdb_password,
})
.await?;
db.use_ns(env.surrealdb_namespace.clone())
.use_db(env.surrealdb_dbname.clone())
.await?;
// Test users for submission testing
let test_users = vec![
(
"test-user-001",
"testuser1@example.com",
"Test User 1",
"5713cb37-dc02-4e87-8048-d7a41d352059", // User role
),
(
"test-user-002",
"testuser2@example.com",
"Test User 2",
"5713cb37-dc02-4e87-8048-d7a41d352059",
),
(
"test-user-003",
"testuser3@example.com",
"Test User 3",
"5713cb37-dc02-4e87-8048-d7a41d352059",
),
];
// Test teams for submission testing
let test_teams = vec![
(
"test-team-001",
"Test Team Alpha",
Some("Team for testing hackathon submissions".to_string()),
"test-user-001", // team leader
true,
Some(5),
Some(vec!["JavaScript".to_string(), "React".to_string()]),
Some("Remote".to_string()),
),
(
"test-team-002",
"Test Team Beta",
Some("Another team for testing submissions".to_string()),
"test-user-002",
true,
Some(4),
Some(vec!["Python".to_string(), "Django".to_string()]),
Some("Remote".to_string()),
),
];
// Test hackathon for submission testing
let test_hackathons = vec![
(
"test-hackathon-001",
"Test Hackathon 2025",
"Hackathon for testing submission functionality.",
"2025-12-01T09:00:00Z",
"2025-12-03T18:00:00Z",
"2025-11-25T23:59:59Z",
Some(50),
HackathonStatus::RegistrationOpen,
Some("Testing & Development".to_string()),
Some("1. Test all submission features\n2. Teams can have 2-5 members\n3. Submit by deadline".to_string()),
Some(vec![
Prize { position: 1, title: "Test Winner".to_string(), description: Some("Best test submission".to_string()), value: Some("$1,000".to_string()) },
Prize { position: 2, title: "Test Runner-up".to_string(), description: Some("Second best submission".to_string()), value: Some("$500".to_string()) },
]),
vec!["c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2".to_string()], // admin user
),
];
// Test hackathon timeline
let test_timeline = vec![
(
"test-hackathon-001",
HackathonPhase::Registration,
"Registration Phase",
Some("Register your team for the test hackathon".to_string()),
"2025-11-20T00:00:00Z",
"2025-11-25T23:59:59Z",
true,
1,
),
(
"test-hackathon-001",
HackathonPhase::Development,
"Development Phase",
Some("Build your test project".to_string()),
"2025-12-01T00:00:00Z",
"2025-12-02T23:59:59Z",
false,
2,
),
(
"test-hackathon-001",
HackathonPhase::Submission,
"Submission Phase",
Some("Submit your test project".to_string()),
"2025-12-03T00:00:00Z",
"2025-12-03T12:00:00Z",
false,
3,
),
];
// Test submissions
let test_submissions = vec![
(
"test-hackathon-001",
"test-team-001",
"Test Project Alpha",
"A comprehensive test project demonstrating all features.",
Some("https://github.com/test-team-alpha/test-project".to_string()),
Some("https://demo.test-project-alpha.com".to_string()),
Some("https://slides.test-project-alpha.com".to_string()),
vec!["JavaScript".to_string(), "React".to_string(), "Node.js".to_string()],
SubmissionStatus::Draft,
"2025-12-02T10:00:00Z",
),
(
"test-hackathon-001",
"test-team-002",
"Test Project Beta",
"Another test project with different technologies.",
Some("https://github.com/test-team-beta/test-project".to_string()),
Some("https://demo.test-project-beta.com".to_string()),
None,
vec!["Python".to_string(), "Django".to_string(), "PostgreSQL".to_string()],
SubmissionStatus::Submitted,
"2025-12-03T09:30:00Z",
),
];
// Seed test users
for (id, email, fullname, role_id) in test_users {
db.query("DELETE type::thing('app_users', $id)")
.bind(("id", id))
.await?;
let user = UsersSchema {
id: Thing::from(("app_users", id)),
fullname: fullname.into(),
legal_name: Some(format!("{} Legal Name", fullname)),
email: email.into(),
password: hash_password("password").unwrap(),
avatar: Some("https://example.com/avatar.jpg".into()),
phone_number: "081234567890".into(),
phone_for_verification: Some("081234567890".into()),
is_active: true,
is_deleted: false,
mentor_id: None,
gender: Some("male".into()),
birthdate: Some("1995-01-01".into()),
domicile: Some("Jakarta, Indonesia".into()),
bio: Some(format!("{} is a test user for hackathon submissions.", fullname)),
last_education: Some("S1 Computer Science".into()),
linkedin_url: Some("https://linkedin.com/in/testuser".into()),
github_url: Some("https://github.com/testuser".into()),
cv_url: Some("https://example.com/cv.pdf".into()),
portfolio_url: Some("https://example.com/portfolio".into()),
website_url: Some("https://example.com/website".into()),
twitter_url: Some("https://twitter.com/testuser".into()),
location: Some("Jakarta, Indonesia".into()),
skills: Some(vec!["JavaScript".to_string(), "Python".to_string()]),
experience: None,
education: None,
career_status: Some("Developer".into()),
role: Thing::from(("app_roles", role_id)),
created_at: get_iso_date(),
updated_at: get_iso_date(),
};
db.create::<Option<UsersSchema>>(("app_users", id))
.content(user)
.await?;
println!("✅ Inserted test user: {fullname} ({email})");
}
// Seed test teams
for (id, name, description, leader_id, is_open, max_members, skills_required, location) in test_teams {
db.query("DELETE type::thing('app_teams', $id)")
.bind(("id", id))
.await?;
let team = TeamsSchema {
id: Thing::from(("app_teams", id)),
name: name.into(),
description,
leader_id: Thing::from(("app_users", leader_id)),
is_open,
max_members,
skills_required,
location,
avatar: None,
website_url: None,
github_url: None,
is_active: true,
is_deleted: false,
created_at: get_iso_date(),
updated_at: get_iso_date(),
};
db.create::<Option<TeamsSchema>>(("app_teams", id))
.content(team)
.await?;
println!("✅ Inserted test team: {name}");
}
// Seed test hackathons
for (
id,
name,
description,
start_date,
end_date,
registration_deadline,
max_participants,
status,
theme,
rules,
prizes,
organizers,
) in test_hackathons {
db.query("DELETE type::thing('app_hackathons', $id)")
.bind(("id", id))
.await?;
let hackathon = HackathonSchema {
id: Thing::from(("app_hackathons", id)),
name: name.into(),
description: description.into(),
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
registration_deadline: DateTime::parse_from_rfc3339(registration_deadline)?.with_timezone(&Utc),
max_participants,
status: status.clone(),
theme: theme.clone(),
rules: rules.clone(),
prizes: prizes.clone(),
previous_winners: None,
organizers: organizers.clone(),
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
db.create::<Option<HackathonSchema>>(("app_hackathons", id))
.content(hackathon)
.await?;
println!("✅ Inserted test hackathon: {name}");
// Also create an alias canonical id 'test-hackathon' so tests referencing
// /v1/hackathons/test-hackathon/... can find a hackathon record.
if id != "test-hackathon" && id.starts_with("test-hackathon") {
let alias_id = "test-hackathon";
db.query("DELETE type::thing('app_hackathons', $id)")
.bind(("id", alias_id))
.await?;
let alias_hackathon = HackathonSchema {
id: Thing::from(("app_hackathons", alias_id)),
name: name.into(),
description: description.into(),
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
registration_deadline: DateTime::parse_from_rfc3339(registration_deadline)?.with_timezone(&Utc),
max_participants,
status: status.clone(),
theme: theme.clone(),
rules: rules.clone(),
prizes: prizes.clone(),
previous_winners: None,
organizers: organizers.clone(),
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
db.create::<Option<HackathonSchema>>(("app_hackathons", alias_id))
.content(alias_hackathon)
.await?;
println!("✅ Inserted test hackathon alias: {alias_id}");
}
}
// Seed test hackathon timeline
for (
hackathon_id,
phase,
title,
description,
start_date,
end_date,
is_active,
order,
) in test_timeline {
let timeline_id = format!("test-timeline-{}-{}", hackathon_id, order);
db.query("DELETE type::thing('app_hackathon_timeline', $id)")
.bind(("id", timeline_id.clone()))
.await?;
let timeline = HackathonTimelineSchema {
id: Thing::from(("app_hackathon_timeline", timeline_id.as_str())),
hackathon_id: Thing::from(("app_hackathons", hackathon_id)),
phase: phase.clone(),
title: title.into(),
description: description.clone(),
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
is_active,
order,
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
db.create::<Option<HackathonTimelineSchema>>(("app_hackathon_timeline", timeline_id))
.content(timeline)
.await?;
println!("✅ Inserted test hackathon timeline: {title}");
// Also create alias timeline entries for the canonical test id 'test-hackathon'
if hackathon_id != "test-hackathon" && hackathon_id.starts_with("test-hackathon") {
let alias_hackathon_id = "test-hackathon";
let alias_timeline_id = format!("test-timeline-{}-{}", alias_hackathon_id, order);
db.query("DELETE type::thing('app_hackathon_timeline', $id)")
.bind(("id", alias_timeline_id.clone()))
.await?;
let alias_timeline = HackathonTimelineSchema {
id: Thing::from(("app_hackathon_timeline", alias_timeline_id.as_str())),
hackathon_id: Thing::from(("app_hackathons", alias_hackathon_id)),
phase: phase.clone(),
title: title.into(),
description: description.clone(),
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
is_active,
order,
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
db.create::<Option<HackathonTimelineSchema>>( ("app_hackathon_timeline", alias_timeline_id.clone()) )
.content(alias_timeline)
.await?;
println!("✅ Inserted test hackathon timeline alias: {alias_timeline_id}");
}
}
// Seed test hackathon submissions
for (
hackathon_id,
team_id,
project_name,
description,
repository_url,
demo_url,
slides_url,
technologies,
submission_status,
submitted_at,
) in test_submissions {
let submission_id = format!("test-submission-{}-{}", hackathon_id, team_id);
db.query("DELETE type::thing('app_hackathon_submissions', $id)")
.bind(("id", submission_id.clone()))
.await?;
let submission = HackathonSubmissionsSchema {
id: Thing::from(("app_hackathon_submissions", submission_id.as_str())),
hackathon_id: Thing::from(("app_hackathons", hackathon_id)),
team_id: Some(Thing::from(("app_teams", team_id))),
project_name: Some(project_name.into()),
description: Some(description.into()),
repository_url,
upload_file_url: None,
demo_url,
slides_url,
technologies: Some(technologies),
contact_instagram: None,
contact_twitter: None,
contact_linkedin: None,
contact_facebook: None,
contact_youtube: None,
contact_tiktok: None,
contact_other: None,
submission_status: Some(submission_status),
judge_feedback: None,
submitted_at: Some(DateTime::parse_from_rfc3339(submitted_at)?.with_timezone(&Utc)),
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
db.create::<Option<HackathonSubmissionsSchema>>(("app_hackathon_submissions", submission_id))
.content(submission)
.await?;
println!("✅ Inserted test hackathon submission: {project_name}");
}
println!("✅ All test submission data seeded");
Ok(())
}
+42 -9
View File
@@ -1,13 +1,11 @@
use imphnen_iam::UsersSchema;
use imphnen_libs::enviroment::load_env;
use imphnen_utils::{get_iso_date, hash_password, Env};
use imphnen_utils::{get_iso_date, hash_password};
use std::error::Error;
use surrealdb::{opt::auth::Root, sql::Thing};
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
load_env();
let env = Env::new();
let env = &imphnen_libs::environment::ENV;
use surrealdb::engine::any;
let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root {
@@ -15,8 +13,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
password: &env.surrealdb_password,
})
.await?;
db.use_ns(env.surrealdb_namespace)
.use_db(env.surrealdb_dbname)
db.use_ns(env.surrealdb_namespace.clone())
.use_db(env.surrealdb_dbname.clone())
.await?;
let users = vec![
@@ -38,6 +36,24 @@ async fn main() -> Result<(), Box<dyn Error>> {
"User",
"5713cb37-dc02-4e87-8048-d7a41d352059",
),
(
"testuser1-id",
"testuser1@example.com",
"Test User 1",
"5713cb37-dc02-4e87-8048-d7a41d352059",
),
(
"testuser2-id",
"testuser2@example.com",
"Test User 2",
"5713cb37-dc02-4e87-8048-d7a41d352059",
),
(
"testuser3-id",
"testuser3@example.com",
"Test User 3",
"5713cb37-dc02-4e87-8048-d7a41d352059",
),
];
for (id, email, fullname, role_id) in users {
@@ -48,15 +64,32 @@ async fn main() -> Result<(), Box<dyn Error>> {
let user = UsersSchema {
id: Thing::from(("app_users", id)),
fullname: fullname.into(),
legal_name: Some(format!("{} Legal Name", fullname)),
email: email.into(),
password: hash_password("password").unwrap(),
avatar: None,
avatar: Some("https://example.com/avatar.jpg".into()),
phone_number: "081234567890".into(),
phone_for_verification: Some("081234567890".into()),
is_active: true,
is_deleted: false,
mentor_id: None,
gender: None,
birthdate: None,
gender: Some("male".into()),
birthdate: Some("1990-05-15".into()),
domicile: Some("Jakarta, Indonesia".into()),
// identity_document_url: None, // Sudah tidak dipakai, bisa dihapus dari schema jika tidak diperlukan
bio: Some(format!("{} adalah user dengan data pribadi lengkap untuk testing.", fullname)),
last_education: Some("S1 Teknik Informatika".into()),
linkedin_url: Some("https://linkedin.com/in/user".into()),
github_url: Some("https://github.com/user".into()),
cv_url: Some("https://example.com/cv.pdf".into()),
portfolio_url: Some("https://example.com/portfolio".into()),
website_url: Some("https://example.com/website".into()),
twitter_url: Some("https://twitter.com/user".into()),
location: Some("Jakarta, Indonesia".into()),
skills: Some(vec!["JavaScript".into(), "React".into(), "Node.js".into()]),
experience: None,
education: None,
career_status: Some("Senior Developer".into()),
role: Thing::from(("app_roles", role_id)),
created_at: get_iso_date(),
updated_at: get_iso_date(),
+2
View File
@@ -20,8 +20,10 @@ fn main() -> Result<(), Box<dyn Error>> {
run_seed("seed_roles_permissions")?;
run_seed("seed_users")?;
run_seed("seed_events")?;
run_seed("seed_hackathons")?;
run_seed("seed_gacha_rolls")?;
run_seed("seed_mentor_user")?;
run_seed("seed_test_data")?;
println!("\n✅ All seeding completed successfully.");
Ok(())
}
+8 -1
View File
@@ -1,2 +1,9 @@
pub mod v1;
pub use v1::*;
pub use v1::landing;
pub use v1::landing::events;
pub use v1::landing::testimonials;
pub use v1::landing::events::events_public_routes;
pub use v1::landing::events::events_protected_routes;
pub use v1::landing::testimonials::testimonials_public_routes;
pub use v1::landing::testimonials::testimonials_protected_routes;
@@ -7,11 +7,12 @@ use super::{
};
use axum::extract::{Path, Query};
use axum::response::IntoResponse;
use axum::{Extension, Json};
use axum::{Extension, http::HeaderMap};
use imphnen_libs::{
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
ResponseSuccessDto,
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
ResponseSuccessDto, ValidatedJson,
};
use imphnen_iam::{PermissionsEnum, require_permissions};
#[utoipa::path(
get,
@@ -26,7 +27,7 @@ use imphnen_libs::{
("filter_by" = Option<String>, Query, description = "Field to filter by"),
),
responses(
(status = 200, description = "Get event list", body = ResponseListSuccessDto<Vec<EventsListItemDto>>)
(status = 200, description = "[PUBLIC] Get event list", body = ResponseListSuccessDto<Vec<EventsListItemDto>>)
),
tag = "Events"
)]
@@ -44,7 +45,7 @@ pub async fn get_event_list(
("id" = String, Path, description = "Event ID")
),
responses(
(status = 200, description = "Get event by ID", body = ResponseSuccessDto<EventsDetailItemDto>)
(status = 200, description = "[PUBLIC] Get event by ID", body = ResponseSuccessDto<EventsDetailItemDto>)
),
tag = "Events"
)]
@@ -63,15 +64,18 @@ pub async fn get_event_by_id(
path = "/v1/cms/landing/events/create",
request_body = EventsCreateRequestDto,
responses(
(status = 201, description = "Create new event", body = MessageResponseDto)
(status = 201, description = "[ADMIN] Create new event", body = MessageResponseDto)
),
tag = "Events"
)]
pub async fn post_create_event(
Extension(state): Extension<AppState>,
Json(payload): Json<EventsCreateRequestDto>,
headers: HeaderMap,
Extension(state): Extension<AppState>,
ValidatedJson(payload): ValidatedJson<EventsCreateRequestDto>,
) -> impl IntoResponse {
EventsService::create_event(&state, payload).await
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
EventsService::create_event(&state, payload).await
})
}
#[utoipa::path(
@@ -85,16 +89,19 @@ pub async fn post_create_event(
),
request_body = EventsUpdateRequestDto,
responses(
(status = 200, description = "Update event", body = MessageResponseDto)
(status = 200, description = "[ADMIN] Update event", body = MessageResponseDto)
),
tag = "Events"
)]
pub async fn patch_update_event(
Extension(state): Extension<AppState>,
Path(id): Path<String>,
Json(payload): Json<EventsUpdateRequestDto>,
headers: HeaderMap,
Extension(state): Extension<AppState>,
Path(id): Path<String>,
ValidatedJson(payload): ValidatedJson<EventsUpdateRequestDto>,
) -> impl IntoResponse {
EventsService::update_event(&state, id, payload).await
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
EventsService::update_event(&state, id, payload).await
})
}
#[utoipa::path(
@@ -107,13 +114,16 @@ pub async fn patch_update_event(
("id" = String, Path, description = "Event ID")
),
responses(
(status = 200, description = "Soft delete event", body = MessageResponseDto)
(status = 200, description = "[ADMIN] Soft delete event", body = MessageResponseDto)
),
tag = "Events"
)]
pub async fn delete_event(
Extension(state): Extension<AppState>,
Path(id): Path<String>,
headers: HeaderMap,
Extension(state): Extension<AppState>,
Path(id): Path<String>,
) -> impl IntoResponse {
EventsService::delete_event(&state, id).await
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
EventsService::delete_event(&state, id).await
})
}
+46 -10
View File
@@ -1,34 +1,70 @@
use lazy_static::lazy_static;
lazy_static! {
pub static ref VALID_URL_REGEX: regex::Regex =
regex::Regex::new(r"^https?://").unwrap();
}
use chrono::{DateTime, Utc};
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use surrealdb::sql::Thing;
use utoipa::ToSchema;
use validator::Validate;
use validator::{Validate, ValidationError};
// Custom URL validator that ensures valid HTTP/HTTPS URLs
pub fn validate_url(url: &str) -> Result<(), ValidationError> {
lazy_static! {
static ref VALID_URL_REGEX: Regex = Regex::new(r"^https?://[^\s$.?#].[^\s]*$").unwrap();
}
if VALID_URL_REGEX.is_match(url) {
Ok(())
} else {
Err(ValidationError::new("invalid_url"))
}
}
// Custom validator for future dates
pub fn validate_future_date(end_date: &DateTime<Utc>) -> Result<(), ValidationError> {
let now = Utc::now();
if end_date > &now {
Ok(())
} else {
Err(ValidationError::new("future_date"))
}
}
// Custom validator for event date ranges (for combined validation)
pub fn validate_date_range(start_date: &DateTime<Utc>, end_date: &DateTime<Utc>) -> Result<(), ValidationError> {
if start_date <= end_date {
Ok(())
} else {
Err(ValidationError::new("date_range"))
}
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct EventsCreateRequestDto {
#[validate(length(min = 1, message = "Name is required"))]
#[validate(length(min = 1, max = 100, message = "Name must be between 1 and 100 characters"))]
pub name: String,
#[validate(length(min = 1, message = "Description is required"))]
#[validate(length(min = 1, max = 1000, message = "Description must be between 1 and 1000 characters"))]
pub description: String,
#[validate(url(message = "Detail link must be a valid URL"))]
#[validate(custom(
function = "validate_url",
message = "Detail link must be a valid HTTP/HTTPS URL"
))]
pub detail_link: String,
#[validate(range(min = 0.0, message = "Price cannot be negative"))]
#[validate(range(min = 0.0, max = 1_000_000.0, message = "Price must be between 0 and 1,000,000"))]
pub price: f64,
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
#[validate(custom(
function = "validate_future_date",
message = "End date must be in the future"
))]
pub end_date: DateTime<Utc>,
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
pub start_date: DateTime<Utc>,
#[validate(length(max = 200, message = "Location name cannot exceed 200 characters"))]
pub location: Option<String>,
pub is_online: bool,
}
@@ -1,9 +1,10 @@
use super::{events_dto::EventsQueryDto, events_schema::EventsSchema};
use anyhow::{Result, bail};
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date};
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date, make_thing};
use std::time::Instant;
use tracing::instrument;
use tracing::info;
pub struct EventsRepository<'a> {
state: &'a AppState,
@@ -25,6 +26,7 @@ impl<'a> EventsRepository<'a> {
.with_pagination(meta.page, Some(10))
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
.build();
info!(query = %query, "Executing SurrealDB query");
let res: Vec<EventsQueryDto> =
self.state.surrealdb_ws.query(query).await?.take(0)?;
let elapsed = now.elapsed();
@@ -44,10 +46,20 @@ impl<'a> EventsRepository<'a> {
pub async fn query_event_by_id(&self, id: String) -> Result<EventsQueryDto> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
// Attempt to parse the ID. If it's a full Thing (e.g., "events:some_id"), extract the ID part.
// Otherwise, assume it's already the raw ID.
let parsed_id = if id.contains(":") {
let thing = make_thing(ResourceEnum::Events.to_string().as_str(), &id);
get_id(&thing)?.1.to_string()
} else {
id.clone()
};
let builder = DetailQueryBuilder::new(ResourceEnum::Events.to_string())
.with_id(&id)
.with_id(&parsed_id)
.with_select_fields(vec!["*"]);
let sql = builder.build();
info!(query = %sql, "Executing SurrealDB query");
let result: Option<EventsQueryDto> =
builder.apply_bindings(db.query(sql)).await?.take(0)?;
let elapsed = now.elapsed();
@@ -72,6 +84,8 @@ impl<'a> EventsRepository<'a> {
pub async fn query_create_event(&self, data: EventsSchema) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let query_str = format!("CREATE {} CONTENT ...", ResourceEnum::Events);
info!(query = %query_str, "Executing SurrealDB query");
let record: Option<EventsSchema> = db
.create(ResourceEnum::Events.to_string())
.content(data)
@@ -106,6 +120,8 @@ impl<'a> EventsRepository<'a> {
};
let record_key = get_id(&merged.id)?;
let query_str = format!("UPDATE {:?} MERGE ...", record_key);
info!(query = %query_str, "Executing SurrealDB query");
let record: Option<EventsSchema> = db.update(record_key).merge(merged).await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
@@ -130,6 +146,8 @@ impl<'a> EventsRepository<'a> {
}
let record_key = get_id(&event.id)?;
let query_str = format!("UPDATE {:?} MERGE {{ is_deleted: true }}", record_key);
info!(query = %query_str, "Executing SurrealDB query");
let record: Option<EventsSchema> = db
.update(record_key)
.merge(serde_json::json!({ "is_deleted": true }))
+19 -25
View File
@@ -9,36 +9,30 @@ pub mod events_repository;
pub mod events_schema;
pub mod events_service;
pub use events_controller::*;
pub use events_dto::*;
pub use events_repository::*;
pub use events_schema::*;
pub use events_service::*;
// Export only the necessary public items
pub use events_dto::{
EventsCreateRequestDto,
EventsUpdateRequestDto,
EventsListItemDto,
EventsDetailItemDto,
};
pub use events_controller::{
get_event_list,
get_event_by_id,
post_create_event,
patch_update_event,
delete_event,
};
pub fn events_public_routes() -> Router {
Router::new()
.route(
"/cms/landing/events",
get(events_controller::get_event_list),
)
.route(
"/cms/landing/events/detail/{id}",
get(events_controller::get_event_by_id),
)
.route("/cms/landing/events", get(get_event_list))
.route("/cms/landing/events/detail/{id}", get(get_event_by_id))
}
pub fn events_protected_routes() -> Router {
Router::new()
.route(
"/cms/landing/events/create",
post(events_controller::post_create_event),
)
.route(
"/cms/landing/events/update/{id}",
patch(events_controller::patch_update_event),
)
.route(
"/cms/landing/events/delete/{id}",
delete(events_controller::delete_event),
)
.route("/cms/landing/events/create", post(post_create_event))
.route("/cms/landing/events/update/{id}", patch(patch_update_event))
.route("/cms/landing/events/delete/{id}", delete(delete_event))
}
+4 -2
View File
@@ -1,5 +1,7 @@
pub mod events;
pub mod testimonials;
pub use events::*;
pub use testimonials::*;
pub use events::events_public_routes;
pub use events::events_protected_routes;
pub use testimonials::testimonials_public_routes;
pub use testimonials::testimonials_protected_routes;
+19 -25
View File
@@ -9,36 +9,30 @@ pub mod testimonials_repository;
pub mod testimonials_schema;
pub mod testimonials_service;
pub use testimonials_controller::*;
pub use testimonials_dto::*;
pub use testimonials_repository::*;
pub use testimonials_schema::*;
pub use testimonials_service::*;
// Export only the necessary public items
pub use testimonials_dto::{
TestimonialsCreateRequestDto,
TestimonialsUpdateRequestDto,
TestimonialsListItemDto,
TestimonialsDetailItemDto,
};
pub use testimonials_controller::{
get_testimonial_list,
get_testimonial_by_id,
post_create_testimonial,
patch_update_testimonial,
delete_testimonial,
};
pub fn testimonials_public_routes() -> Router {
Router::new()
.route(
"/cms/landing/testimonials",
get(testimonials_controller::get_testimonial_list),
)
.route(
"/cms/landing/testimonials/detail/{id}",
get(testimonials_controller::get_testimonial_by_id),
)
.route("/cms/landing/testimonials", get(get_testimonial_list))
.route("/cms/landing/testimonials/detail/{id}", get(get_testimonial_by_id))
}
pub fn testimonials_protected_routes() -> Router {
Router::new()
.route(
"/cms/landing/testimonials/create",
post(testimonials_controller::post_create_testimonial),
)
.route(
"/cms/landing/testimonials/update/{id}",
patch(testimonials_controller::patch_update_testimonial),
)
.route(
"/cms/landing/testimonials/delete/{id}",
delete(testimonials_controller::delete_testimonial),
)
.route("/cms/landing/testimonials/create", post(post_create_testimonial))
.route("/cms/landing/testimonials/update/{id}", patch(patch_update_testimonial))
.route("/cms/landing/testimonials/delete/{id}", delete(delete_testimonial))
}
@@ -7,11 +7,11 @@ use super::{
};
use axum::extract::{Path, Query};
use axum::response::IntoResponse;
use axum::{Extension, Json};
use imphnen_iam::UsersDetailQueryDto;
use axum::{Extension, http::HeaderMap};
use imphnen_iam::{UsersDetailQueryDto, require_auth};
use imphnen_libs::{
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
ResponseSuccessDto,
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
ResponseSuccessDto, ValidatedJson,
};
#[utoipa::path(
@@ -27,7 +27,7 @@ use imphnen_libs::{
("filter_by" = Option<String>, Query, description = "Field to filter by"),
),
responses(
(status = 200, description = "Get testimonial list", body = ResponseListSuccessDto<Vec<TestimonialsListItemDto>>)
(status = 200, description = "[PUBLIC] Get testimonial list", body = ResponseListSuccessDto<Vec<TestimonialsListItemDto>>)
),
tag = "Testimonials"
)]
@@ -45,7 +45,7 @@ pub async fn get_testimonial_list(
("id" = String, Path, description = "Testimonial ID")
),
responses(
(status = 200, description = "Get testimonial by ID", body = ResponseSuccessDto<TestimonialsDetailItemDto>)
(status = 200, description = "[PUBLIC] Get testimonial by ID", body = ResponseSuccessDto<TestimonialsDetailItemDto>)
),
tag = "Testimonials"
)]
@@ -64,16 +64,19 @@ pub async fn get_testimonial_by_id(
path = "/v1/cms/landing/testimonials/create",
request_body = TestimonialsCreateRequestDto,
responses(
(status = 201, description = "Create new testimonial", body = MessageResponseDto)
(status = 201, description = "[USER] Create new testimonial", body = MessageResponseDto)
),
tag = "Testimonials"
)]
pub async fn post_create_testimonial(
Extension(state): Extension<AppState>,
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
Json(payload): Json<TestimonialsCreateRequestDto>,
headers: HeaderMap,
Extension(state): Extension<AppState>,
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
ValidatedJson(payload): ValidatedJson<TestimonialsCreateRequestDto>,
) -> impl IntoResponse {
TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await
require_auth!(headers, state, {
TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await
})
}
#[utoipa::path(
@@ -87,18 +90,20 @@ pub async fn post_create_testimonial(
),
request_body = TestimonialsUpdateRequestDto,
responses(
(status = 200, description = "Update testimonial", body = MessageResponseDto)
(status = 200, description = "[USER] Update testimonial", body = MessageResponseDto)
),
tag = "Testimonials"
)]
pub async fn patch_update_testimonial(
Path(id): Path<String>,
Extension(state): Extension<AppState>,
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
Json(payload): Json<TestimonialsUpdateRequestDto>,
headers: HeaderMap,
Path(id): Path<String>,
Extension(state): Extension<AppState>,
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
ValidatedJson(payload): ValidatedJson<TestimonialsUpdateRequestDto>,
) -> impl IntoResponse {
TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user)
.await
require_auth!(headers, state, {
TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user).await
})
}
#[utoipa::path(
@@ -111,14 +116,17 @@ pub async fn patch_update_testimonial(
("id" = String, Path, description = "Testimonial ID")
),
responses(
(status = 200, description = "Soft delete testimonial", body = MessageResponseDto)
(status = 200, description = "[USER] Soft delete testimonial", body = MessageResponseDto)
),
tag = "Testimonials"
)]
pub async fn delete_testimonial(
Extension(state): Extension<AppState>,
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
Path(id): Path<String>,
headers: HeaderMap,
Extension(state): Extension<AppState>,
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
Path(id): Path<String>,
) -> impl IntoResponse {
TestimonialsService::delete_testimonial(&state, id, &authenticated_user).await
require_auth!(headers, state, {
TestimonialsService::delete_testimonial(&state, id, &authenticated_user).await
})
}
@@ -1,31 +1,53 @@
use imphnen_iam::users::UsersSchema;
use imphnen_iam::v1::users::UsersSchema;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use surrealdb::sql::Thing;
use utoipa::ToSchema;
use validator::Validate;
use validator::{Validate, ValidationError};
// Custom validator for content length and format
pub fn validate_testimonial_content(content: &str) -> Result<(), ValidationError> {
lazy_static! {
static ref CONTENT_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9\s.,!?'-]+$").unwrap();
}
if CONTENT_REGEX.is_match(content) && content.len() <= 1000 {
Ok(())
} else {
Err(ValidationError::new("invalid_content"))
}
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct TestimonialsCreateRequestDto {
#[validate(length(min = 1, message = "Role is required"))]
#[validate(length(min = 1, max = 100, message = "Role must be between 1 and 100 characters"))]
pub role: String,
#[validate(length(
min = 1,
max = 500,
message = "Content must be between 1 and 500 characters"
max = 1000,
message = "Content must be between 1 and 1000 characters"
))]
#[validate(custom(
function = "validate_testimonial_content",
message = "Content contains invalid characters or is too long"
))]
pub content: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct TestimonialsUpdateRequestDto {
#[validate(length(min = 1, message = "Role is required"))]
#[validate(length(min = 1, max = 100, message = "Role must be between 1 and 100 characters"))]
pub role: String,
#[validate(length(
min = 1,
max = 500,
message = "Content must be between 1 and 500 characters"
max = 1000,
message = "Content must be between 1 and 1000 characters"
))]
#[validate(custom(
function = "validate_testimonial_content",
message = "Content contains invalid characters or is too long"
))]
pub content: String,
}
@@ -7,6 +7,7 @@ use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date};
use serde_json;
use std::time::Instant;
use tracing::instrument;
use tracing::info;
pub struct TestimonialsRepository<'a> {
state: &'a AppState,
@@ -28,6 +29,7 @@ impl<'a> TestimonialsRepository<'a> {
.with_pagination(meta.page, Some(10))
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
.build();
info!(query = %query, "Executing SurrealDB query");
let res: Vec<TestimonialsQueryDto> =
self.state.surrealdb_ws.query(query).await?.take(0)?;
let elapsed = now.elapsed();
@@ -50,11 +52,18 @@ impl<'a> TestimonialsRepository<'a> {
) -> Result<TestimonialsQueryDto> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
// Extract raw id if id is a thing string
let raw_id = if id.contains(':') {
id.split(':').last().unwrap().trim_matches(|c| c == '⟨' || c == '⟩').to_string()
} else {
id
};
let builder = DetailQueryBuilder::new(ResourceEnum::Testimonials.to_string())
.with_id(&id)
.with_id(&raw_id)
.with_condition("is_deleted = false")
.with_select_fields(vec!["*", "user.* as user"]);
let sql = builder.build();
info!(query = %sql, "Executing SurrealDB query");
let result: Option<TestimonialsQueryDto> =
builder.apply_bindings(db.query(sql)).await?.take(0)?;
let elapsed = now.elapsed();
@@ -79,9 +88,14 @@ impl<'a> TestimonialsRepository<'a> {
pub async fn query_create_testimonial(
&self,
data: TestimonialsSchema,
) -> Result<String> {
) -> Result<TestimonialsSchema> { // Change return type from String to TestimonialsSchema
let now = Instant::now();
let db = &self.state.surrealdb_ws;
info!(
resource = %ResourceEnum::Testimonials.to_string(),
payload = ?data,
"Executing SurrealDB create"
);
let record: Option<TestimonialsSchema> = db
.create(ResourceEnum::Testimonials.to_string())
.content(data)
@@ -94,7 +108,7 @@ impl<'a> TestimonialsRepository<'a> {
}
match record {
Some(_) => Ok("Success create testimonial".into()),
Some(created_testimonial) => Ok(created_testimonial), // Return the created testimonial
None => bail!("Failed to create testimonial"),
}
}
@@ -120,6 +134,11 @@ impl<'a> TestimonialsRepository<'a> {
};
let record_key = get_id(&merged.id)?;
info!(
record_key = ?record_key,
payload = ?merged,
"Executing SurrealDB update"
);
let record: Option<TestimonialsSchema> =
db.update(record_key).merge(merged).await?;
let elapsed = now.elapsed();
@@ -145,6 +164,10 @@ impl<'a> TestimonialsRepository<'a> {
}
let record_key = get_id(&testimonial.id)?;
info!(
record_key = ?record_key,
"Executing SurrealDB soft delete"
);
let record: Option<TestimonialsSchema> = db
.update(record_key)
.merge(serde_json::json!({ "is_deleted": true }))
@@ -5,80 +5,87 @@ use surrealdb::Uuid;
use surrealdb::sql::Thing;
use super::testimonials_dto::{
TestimonialsCreateRequestDto, TestimonialsQueryDto, TestimonialsUpdateRequestDto,
TestimonialsCreateRequestDto, TestimonialsQueryDto, TestimonialsUpdateRequestDto,
};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TestimonialsSchema {
pub id: Thing,
pub user: Thing,
pub role: String,
pub content: String,
pub is_deleted: bool,
pub created_at: String,
pub updated_at: String,
pub id: Thing,
pub user: Thing,
pub role: String,
pub content: String,
pub is_deleted: bool,
pub created_at: String,
pub updated_at: String,
}
impl Default for TestimonialsSchema {
fn default() -> Self {
Self {
id: make_thing(
&ResourceEnum::Testimonials.to_string(),
&Uuid::new_v4().to_string(),
),
user: make_thing(
&ResourceEnum::Users.to_string(),
&Uuid::new_v4().to_string(),
),
role: String::new(),
content: String::new(),
is_deleted: false,
created_at: get_iso_date(),
updated_at: get_iso_date(),
}
}
fn default() -> Self {
Self {
id: make_thing(
&ResourceEnum::Testimonials.to_string(),
&Uuid::new_v4().to_string(),
),
user: make_thing(
&ResourceEnum::Users.to_string(),
&Uuid::new_v4().to_string(),
),
role: String::new(),
content: String::new(),
is_deleted: false,
created_at: get_iso_date(),
updated_at: get_iso_date(),
}
}
}
impl TestimonialsSchema {
pub fn from(dto: TestimonialsQueryDto) -> Self {
Self {
id: dto.id,
user: dto.user.id,
role: dto.role,
content: dto.content,
is_deleted: dto.is_deleted,
created_at: dto.created_at,
updated_at: dto.updated_at,
}
}
pub fn from(dto: TestimonialsQueryDto) -> Self {
Self {
id: dto.id,
user: dto.user.id,
role: dto.role,
content: dto.content,
is_deleted: dto.is_deleted,
created_at: dto.created_at,
updated_at: dto.updated_at,
}
}
pub fn create(payload: TestimonialsCreateRequestDto, user_id: &Thing) -> Self {
Self {
id: make_thing(
&ResourceEnum::Testimonials.to_string(),
&Uuid::new_v4().to_string(),
),
user: user_id.clone(),
role: payload.role,
content: payload.content,
is_deleted: false,
created_at: get_iso_date(),
updated_at: get_iso_date(),
}
}
pub fn create(payload: TestimonialsCreateRequestDto, user_id: &Thing) -> Self {
Self {
id: make_thing(
&ResourceEnum::Testimonials.to_string(),
&Uuid::new_v4().to_string(),
),
user: user_id.clone(),
role: payload.role,
content: payload.content,
is_deleted: false,
created_at: get_iso_date(),
updated_at: get_iso_date(),
}
}
pub fn update(
payload: TestimonialsUpdateRequestDto,
id: String,
user_id: &Thing,
) -> Self {
Self {
id: make_thing(&ResourceEnum::Testimonials.to_string(), &id),
role: payload.role,
content: payload.content,
updated_at: get_iso_date(),
user: user_id.clone(),
..Default::default()
}
}
pub fn update(
payload: TestimonialsUpdateRequestDto,
id: String,
user_id: &Thing,
) -> Self {
// Normalize id: accept either raw id (uuid) or Thing-formatted id like "table:⟨id⟩"
let raw_id = if id.contains(':') {
id.split(':').last().unwrap().trim_matches(|c| c == '⟨' || c == '⟩').to_string()
} else {
id
};
Self {
id: make_thing(&ResourceEnum::Testimonials.to_string(), &raw_id),
role: payload.role,
content: payload.content,
updated_at: get_iso_date(),
user: user_id.clone(),
..Default::default()
}
}
}
@@ -11,7 +11,7 @@ use imphnen_libs::{
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
};
use imphnen_utils::{
common_response, success_list_response, success_response, validate_request,
common_response, success_list_response, success_response, success_created_response, validate_request,
};
pub struct TestimonialsService;
@@ -46,8 +46,8 @@ impl TestimonialsService {
Ok(testimonial) if !testimonial.is_deleted => {
success_response(ResponseSuccessDto {
data: TestimonialsDetailItemDto {
id: testimonial.id.id.to_raw(),
user_id: testimonial.user.id.id.to_raw(),
id: testimonial.id.to_raw(),
user_id: testimonial.user.id.to_raw(),
user_fullname: testimonial.user.fullname,
role: testimonial.role,
content: testimonial.content,
@@ -72,7 +72,19 @@ impl TestimonialsService {
let repo = TestimonialsRepository::new(state);
let schema = TestimonialsSchema::create(payload, &authenticated_user.id);
match repo.query_create_testimonial(schema).await {
Ok(msg) => common_response(StatusCode::CREATED, &msg),
Ok(created_testimonial) => {
success_created_response(ResponseSuccessDto {
data: TestimonialsDetailItemDto {
id: created_testimonial.id.to_raw(),
user_id: created_testimonial.user.id.to_raw(),
user_fullname: authenticated_user.fullname.clone(),
role: created_testimonial.role,
content: created_testimonial.content,
created_at: created_testimonial.created_at,
updated_at: created_testimonial.updated_at,
},
})
}
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
}
}
+6 -1
View File
@@ -1,3 +1,8 @@
pub mod landing;
pub use landing::*;
pub use landing::events;
pub use landing::testimonials;
pub use landing::events::events_public_routes;
pub use landing::events::events_protected_routes;
pub use landing::testimonials::testimonials_public_routes;
pub use landing::testimonials::testimonials_protected_routes;
+2
View File
@@ -8,7 +8,9 @@ imphnen-libs.workspace = true
imphnen-utils.workspace = true
imphnen-entities.workspace = true
imphnen-iam.workspace = true
imphnen-middleware.workspace = true
axum.workspace = true
async-trait.workspace = true
serde.workspace = true
serde_json.workspace = true
utoipa.workspace = true
+11 -1
View File
@@ -1,2 +1,12 @@
pub mod v1;
pub use v1::*;
// Explicitly export only what's needed from v1
pub use v1::dimentorin_router;
pub use v1::mentors::mentors_router;
pub use v1::sessions::{
sessions_router, BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListItemDto,
SessionListResponseDto, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
AvailabilitySlotDto,
};
@@ -4,30 +4,30 @@ use super::{
};
use crate::v1::mentors::mentors_dto::MentorRegisterResponseDto;
use ::axum::{
extract::{Extension, Json, Path, Query},
extract::{Extension, Path, Query},
http::HeaderMap,
response::{IntoResponse, Response},
response::Response,
};
use imphnen_entities::*;
use imphnen_iam::{PermissionsEnum, permissions_guard};
use imphnen_entities::MetaRequestDto;
use imphnen_libs::{AppState, ValidatedJson};
use imphnen_iam::{PermissionsEnum, require_permissions};
use imphnen_utils::extract_email;
use serde_json::json;
#[utoipa::path(
post,
path = "/v1/mentors/register",
path = "/v1/mentors/create",
request_body = MentorUserRegisterRequestDto,
responses(
(status = 200, description = "Mentor registered successfully", body = MentorRegisterResponseDto),
(status = 400, description = "Bad request - validation error"),
(status = 409, description = "Conflict - user already has mentor profile"),
(status = 500, description = "Internal server error")
(status = 200, description = "[PUBLIC] Mentor registered successfully", body = MentorRegisterResponseDto),
(status = 400, description = "[PUBLIC] Bad request - validation error"),
(status = 409, description = "[PUBLIC] Conflict - user already has mentor profile"),
(status = 500, description = "[PUBLIC] Internal server error")
),
tag = "Mentors"
)]
pub async fn post_register_mentor(
Extension(app_state): Extension<AppState>,
Json(dto): Json<MentorUserRegisterRequestDto>,
ValidatedJson(dto): ValidatedJson<MentorUserRegisterRequestDto>,
) -> Response {
MentorsService::register_mentor(&app_state, dto).await
}
@@ -43,8 +43,8 @@ pub async fn post_register_mentor(
("order" = Option<String>, Query, description = "Sort order (ASC/DESC)"),
),
responses(
(status = 200, description = "Get list of mentors", body = Vec<MentorListResponseDto>),
(status = 500, description = "Internal server error")
(status = 200, description = "[ADMIN] Get list of mentors", body = Vec<MentorListResponseDto>),
(status = 500, description = "[ADMIN] Internal server error")
),
tag = "Mentors",
security(
@@ -56,16 +56,9 @@ pub async fn get_mentor_list(
Extension(app_state): Extension<AppState>,
Query(meta): Query<MetaRequestDto>,
) -> Response {
match permissions_guard(
&headers,
app_state.clone(),
vec![PermissionsEnum::ReadListMentors],
)
.await
{
Ok(_) => MentorsService::get_mentor_list(&app_state, meta).await,
Err(response) => response,
}
require_permissions!(headers, app_state, [PermissionsEnum::ReadListMentors], {
MentorsService::get_mentor_list(&app_state, meta).await
})
}
#[utoipa::path(
@@ -75,9 +68,9 @@ pub async fn get_mentor_list(
("id" = String, Path, description = "Mentor ID")
),
responses(
(status = 200, description = "Get mentor by ID", body = MentorDetailResponseDto),
(status = 404, description = "Mentor not found"),
(status = 500, description = "Internal server error")
(status = 200, description = "[ADMIN] Get mentor by ID", body = MentorDetailResponseDto),
(status = 404, description = "[ADMIN] Mentor not found"),
(status = 500, description = "[ADMIN] Internal server error")
),
tag = "Mentors",
security(
@@ -89,16 +82,9 @@ pub async fn get_mentor_by_id(
Extension(app_state): Extension<AppState>,
Path(id): Path<String>,
) -> Response {
match permissions_guard(
&headers,
app_state.clone(),
vec![PermissionsEnum::ReadDetailMentors],
)
.await
{
Ok(_) => MentorsService::get_mentor_by_id(&app_state, &id).await,
Err(response) => response,
}
require_permissions!(headers, app_state, [PermissionsEnum::ReadDetailMentors], {
MentorsService::get_mentor_by_id(&app_state, &id).await
})
}
#[utoipa::path(
@@ -109,10 +95,10 @@ pub async fn get_mentor_by_id(
),
request_body = MentorUpdateRequestDto,
responses(
(status = 200, description = "Mentor updated successfully", body = MentorDetailResponseDto),
(status = 400, description = "Bad request - validation error"),
(status = 404, description = "Mentor not found"),
(status = 500, description = "Internal server error")
(status = 200, description = "[ADMIN] Mentor updated successfully", body = MentorDetailResponseDto),
(status = 400, description = "[ADMIN] Bad request - validation error"),
(status = 404, description = "[ADMIN] Mentor not found"),
(status = 500, description = "[ADMIN] Internal server error")
),
tag = "Mentors - Admin",
security(
@@ -123,18 +109,11 @@ pub async fn put_update_mentor(
headers: HeaderMap,
Extension(app_state): Extension<AppState>,
Path(id): Path<String>,
Json(dto): Json<MentorUpdateRequestDto>,
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
) -> Response {
match permissions_guard(
&headers,
app_state.clone(),
vec![PermissionsEnum::UpdateMentors],
)
.await
{
Ok(_) => MentorsService::update_mentor(&app_state, &id, dto).await,
Err(response) => response,
}
require_permissions!(headers, app_state, [PermissionsEnum::UpdateMentors], {
MentorsService::update_mentor(&app_state, &id, dto).await
})
}
#[utoipa::path(
@@ -144,9 +123,9 @@ pub async fn put_update_mentor(
("id" = String, Path, description = "Mentor ID")
),
responses(
(status = 200, description = "Mentor deleted successfully"),
(status = 404, description = "Mentor not found"),
(status = 500, description = "Internal server error")
(status = 200, description = "[ADMIN] Mentor deleted successfully"),
(status = 404, description = "[ADMIN] Mentor not found"),
(status = 500, description = "[ADMIN] Internal server error")
),
tag = "Mentors - Admin",
security(
@@ -158,16 +137,9 @@ pub async fn delete_mentor(
Extension(app_state): Extension<AppState>,
Path(id): Path<String>,
) -> Response {
match permissions_guard(
&headers,
app_state.clone(),
vec![PermissionsEnum::DeleteMentors],
)
.await
{
Ok(_) => MentorsService::delete_mentor(&app_state, &id).await,
Err(response) => response,
}
require_permissions!(headers, app_state, [PermissionsEnum::DeleteMentors], {
MentorsService::delete_mentor(&app_state, &id).await
})
}
#[utoipa::path(
@@ -178,10 +150,10 @@ pub async fn delete_mentor(
),
request_body = MentorVerifyRequestDto,
responses(
(status = 200, description = "Mentor verified successfully", body = MentorDetailResponseDto),
(status = 400, description = "Bad request - validation error"),
(status = 404, description = "Mentor not found"),
(status = 500, description = "Internal server error")
(status = 200, description = "[ADMIN] Mentor verified successfully", body = MentorDetailResponseDto),
(status = 400, description = "[ADMIN] Bad request - validation error"),
(status = 404, description = "[ADMIN] Mentor not found"),
(status = 500, description = "[ADMIN] Internal server error")
),
tag = "Mentors - Admin",
security(
@@ -192,28 +164,21 @@ pub async fn put_verify_mentor(
headers: HeaderMap,
Extension(app_state): Extension<AppState>,
Path(id): Path<String>,
Json(dto): Json<MentorVerifyRequestDto>,
ValidatedJson(dto): ValidatedJson<MentorVerifyRequestDto>,
) -> Response {
match permissions_guard(
&headers,
app_state.clone(),
vec![PermissionsEnum::VerifyMentors],
)
.await
{
Ok(_) => MentorsService::verify_mentor(&app_state, &id, dto).await,
Err(response) => response,
}
require_permissions!(headers, app_state, [PermissionsEnum::VerifyMentors], {
MentorsService::verify_mentor(&app_state, &id, dto).await
})
}
#[utoipa::path(
get,
path = "/v1/mentors/me",
responses(
(status = 200, description = "Current user's mentor profile", body = MentorDetailResponseDto),
(status = 401, description = "Unauthorized - invalid token"),
(status = 403, description = "Mentor profile not found for current user"),
(status = 500, description = "Internal server error")
(status = 200, description = "[MENTOR] Current user's mentor profile", body = MentorDetailResponseDto),
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
(status = 403, description = "[MENTOR] Mentor profile not found for current user"),
(status = 500, description = "[MENTOR] Internal server error")
),
tag = "Mentors",
security(
@@ -221,46 +186,33 @@ pub async fn put_verify_mentor(
)
)]
pub async fn get_mentor_me(
Extension(app_state): Extension<AppState>,
headers: HeaderMap,
Extension(app_state): Extension<AppState>,
) -> Response {
match permissions_guard(
&headers,
app_state.clone(),
vec![PermissionsEnum::ReadOwnMentorProfile],
)
.await
{
Ok(_) => {
let email = match extract_email(&headers) {
Some(email) => email,
None => {
return (
axum::http::StatusCode::UNAUTHORIZED,
Json(json!({
"error": "Unauthorized",
"message": "Token tidak valid"
})),
)
.into_response();
}
};
MentorsService::get_mentor_me(&app_state, &email).await
}
Err(response) => response,
}
require_permissions!(headers.clone(), app_state, [PermissionsEnum::ReadOwnMentorProfile], {
let email = match extract_email(&headers) {
Some(email) => email,
None => {
return imphnen_utils::common_response(
axum::http::StatusCode::UNAUTHORIZED,
"Token tidak valid",
);
}
};
MentorsService::get_mentor_me(&app_state, &email).await
})
}
#[utoipa::path(
put,
path = "/v1/mentors/update/me",
path = "/v1/mentors/me/update",
request_body = MentorUpdateRequestDto,
responses(
(status = 200, description = "Mentor profile updated successfully", body = MentorDetailResponseDto),
(status = 400, description = "Bad request - validation error"),
(status = 401, description = "Unauthorized - invalid token"),
(status = 404, description = "Mentor profile not found"),
(status = 500, description = "Internal server error")
(status = 200, description = "[MENTOR] Mentor profile updated successfully", body = MentorDetailResponseDto),
(status = 400, description = "[MENTOR] Bad request - validation error"),
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
(status = 404, description = "[MENTOR] Mentor profile not found"),
(status = 500, description = "[MENTOR] Internal server error")
),
tag = "Mentors",
security(
@@ -268,38 +220,29 @@ pub async fn get_mentor_me(
)
)]
pub async fn put_update_mentor_me(
Extension(app_state): Extension<AppState>,
headers: HeaderMap,
Json(dto): Json<MentorUpdateRequestDto>,
Extension(app_state): Extension<AppState>,
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
) -> Response {
match permissions_guard(
&headers,
app_state.clone(),
vec![PermissionsEnum::UpdateOwnMentorProfile],
)
.await
{
Ok(_) => {
let email = match extract_email(&headers) {
Some(email) => email,
None => {
return imphnen_utils::common_response(
axum::http::StatusCode::UNAUTHORIZED,
"Token tidak valid",
);
}
};
MentorsService::update_mentor_me(&app_state, &email, dto).await
}
Err(response) => response,
}
require_permissions!(headers.clone(), app_state, [PermissionsEnum::UpdateOwnMentorProfile], {
let email = match extract_email(&headers) {
Some(email) => email,
None => {
return imphnen_utils::common_response(
axum::http::StatusCode::UNAUTHORIZED,
"Token tidak valid",
);
}
};
MentorsService::update_mentor_me(&app_state, &email, dto).await
})
}
#[utoipa::path(
put,
path = "/v1/mentors/update",
request_body = MentorUpdateRequestDto,
responses(
(status = 400, description = "Bad request - Mentor ID is required for update"),
(status = 400, description = "[PUBLIC] Bad request - Mentor ID is required for update"),
),
tag = "Mentors - Admin"
)]
@@ -312,12 +255,12 @@ pub async fn put_update_mentor_no_id() -> Response {
#[utoipa::path(
get,
path = "/v1/mentors/status",
path = "/v1/mentors/me/status",
responses(
(status = 200, description = "Mentor application status", body = String),
(status = 401, description = "Unauthorized - invalid token"),
(status = 403, description = "No mentor application found for current user"),
(status = 500, description = "Internal server error")
(status = 200, description = "[MENTOR] Mentor application status", body = String),
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
(status = 403, description = "[MENTOR] No mentor application found for current user"),
(status = 500, description = "[MENTOR] Internal server error")
),
tag = "Mentors",
security(
@@ -325,32 +268,19 @@ pub async fn put_update_mentor_no_id() -> Response {
)
)]
pub async fn get_mentor_status(
Extension(app_state): Extension<AppState>,
headers: HeaderMap,
Extension(app_state): Extension<AppState>,
) -> Response {
match permissions_guard(
&headers,
app_state.clone(),
vec![PermissionsEnum::ReadOwnMentorStatus],
)
.await
{
Ok(_) => {
let email = match extract_email(&headers) {
Some(email) => email,
None => {
return (
axum::http::StatusCode::UNAUTHORIZED,
Json(json!({
"error": "Unauthorized",
"message": "Token tidak valid"
})),
)
.into_response();
}
};
MentorsService::get_mentor_status(&app_state, &email).await
}
Err(response) => response,
}
require_permissions!(headers.clone(), app_state, [PermissionsEnum::ReadOwnMentorStatus], {
let email = match extract_email(&headers) {
Some(email) => email,
None => {
return imphnen_utils::common_response(
axum::http::StatusCode::UNAUTHORIZED,
"Token tidak valid",
);
}
};
MentorsService::get_mentor_status(&app_state, &email).await
})
}
@@ -18,15 +18,9 @@ pub struct MentorListResponseDto {
pub struct MentorDetailWithUserDto {
pub id: Thing,
pub user_id: Thing,
pub fullname: Option<String>,
pub email: Option<String>,
pub legal_name: String,
pub identity_document_url: String,
pub phone_for_verification: String,
pub bio: String,
pub linkedin_url: Option<String>,
pub github_url: Option<String>,
pub cv_url: Option<String>,
// Personal data is now in UsersSchema, access via user_id
// Removed: fullname, email, legal_name, identity_document_url,
// phone_for_verification, bio, linkedin_url, github_url, cv_url
pub industries: Vec<String>,
pub expertise: Vec<String>,
pub languages: Vec<String>,
@@ -47,15 +41,20 @@ pub struct MentorDetailWithUserDto {
pub struct MentorDetailResponseDto {
pub id: String,
pub user_id: String,
// Personal data fields from UsersSchema
pub fullname: Option<String>,
pub email: Option<String>,
pub legal_name: String,
pub identity_document_url: String,
pub phone_for_verification: String,
pub bio: String,
pub legal_name: Option<String>,
pub gender: Option<String>,
pub domicile: Option<String>,
pub phone_for_verification: Option<String>,
pub bio: Option<String>,
pub last_education: Option<String>,
pub linkedin_url: Option<String>,
pub github_url: Option<String>,
pub cv_url: Option<String>,
pub portfolio_url: Option<String>,
// Professional data from MentorSchema
pub industries: Vec<String>,
pub expertise: Vec<String>,
pub languages: Vec<String>,
@@ -94,9 +93,6 @@ pub struct MentorUpdateRequestDto {
pub gender: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub domicile: Option<String>,
#[validate(url(message = "Invalid identity document URL"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub identity_document_url: Option<String>,
#[validate(length(
min = 10,
max = 15,
@@ -118,6 +114,7 @@ pub struct MentorUpdateRequestDto {
#[validate(url(message = "Invalid CV URL"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub cv_url: Option<String>,
#[validate(url(message = "Invalid portfolio URL"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub portfolio_url: Option<String>,
#[validate(length(min = 1, message = "At least 1 industry required"))]
@@ -170,7 +167,7 @@ pub struct MentorUserRegisterRequestDto {
message = "Password must have at least 8 characters"
))]
#[validate(custom(
function = "imphnen_iam::auth_dto::validate_password_complexity",
function = "imphnen_iam::v1::auth::auth_dto::validate_password_complexity",
message = "Password must include uppercase, lowercase, number, and special character"
))]
pub password: String,
@@ -279,18 +276,7 @@ pub struct MentoringRate {
pub struct MentorInsertDto {
pub id: Thing,
pub user_id: Option<Thing>,
pub email: Option<String>,
pub legal_name: String,
pub gender: Option<String>,
pub domicile: Option<String>,
pub identity_document_url: String,
pub phone_for_verification: String,
pub bio: String,
pub last_education: Option<String>,
pub linkedin_url: Option<String>,
pub github_url: Option<String>,
pub cv_url: Option<String>,
pub portfolio_url: Option<String>,
// Personal data removed - now stored in UsersSchema
pub industries: Vec<String>,
pub expertise: Vec<String>,
pub languages: Vec<String>,
@@ -313,18 +299,7 @@ impl From<MentorSchema> for MentorInsertDto {
MentorInsertDto {
id: schema.id,
user_id: schema.user_id,
email: schema.email,
legal_name: schema.legal_name,
gender: schema.gender,
domicile: schema.domicile,
identity_document_url: schema.identity_document_url,
phone_for_verification: schema.phone_for_verification,
bio: schema.bio,
last_education: schema.last_education,
linkedin_url: schema.linkedin_url,
github_url: schema.github_url,
cv_url: schema.cv_url,
portfolio_url: schema.portfolio_url,
// Personal data removed from MentorSchema
industries: schema.industries,
expertise: schema.expertise,
languages: schema.languages,
@@ -354,19 +329,10 @@ pub struct MentorVerifyRequestDto {
pub struct MentorDetailQueryDto {
pub id: Thing,
pub user_id: Thing,
pub fullname: Option<String>,
pub email: Option<String>,
pub legal_name: String,
pub gender: Option<String>,
pub domicile: Option<String>,
pub identity_document_url: String,
pub phone_for_verification: String,
pub bio: String,
pub last_education: Option<String>,
pub linkedin_url: Option<String>,
pub github_url: Option<String>,
pub cv_url: Option<String>,
pub portfolio_url: Option<String>,
// Personal data has been moved to UsersSchema
// Use user_id to get: fullname, email, legal_name, gender, domicile,
// identity_document_url, phone_for_verification, bio, last_education,
// linkedin_url, github_url, cv_url, portfolio_url
pub industries: Vec<String>,
pub expertise: Vec<String>,
pub languages: Vec<String>,
@@ -388,8 +354,8 @@ impl From<MentorDetailQueryDto> for MentorListResponseDto {
fn from(dto: MentorDetailQueryDto) -> Self {
Self {
id: extract_id(&dto.id),
fullname: dto.fullname,
email: dto.email,
fullname: None, // now in user table, must be populated from service layer
email: None, // now in user table, must be populated from service layer
status: dto.status,
created_at: dto.created_at,
updated_at: dto.updated_at,
@@ -402,15 +368,20 @@ impl From<MentorDetailQueryDto> for MentorDetailResponseDto {
Self {
id: extract_id(&dto.id),
user_id: extract_id(&dto.user_id),
fullname: dto.fullname,
email: dto.email,
legal_name: dto.legal_name,
identity_document_url: dto.identity_document_url,
phone_for_verification: dto.phone_for_verification,
bio: dto.bio,
linkedin_url: dto.linkedin_url,
github_url: dto.github_url,
cv_url: dto.cv_url,
// Personal data fields are populated in service layer from UsersSchema
fullname: None, // populated from user table in service layer
email: None, // populated from user table in service layer
legal_name: None, // populated from user table in service layer
gender: None, // populated from user table in service layer
domicile: None, // populated from user table in service layer
phone_for_verification: None, // populated from user table in service layer
bio: None, // populated from user table in service layer
last_education: None, // populated from user table in service layer
linkedin_url: None, // populated from user table in service layer
github_url: None, // populated from user table in service layer
cv_url: None, // populated from user table in service layer
portfolio_url: None, // populated from user table in service layer
// Professional data from mentor
industries: dto.industries,
expertise: dto.expertise,
languages: dto.languages,
@@ -433,7 +404,7 @@ impl From<MentorSchema> for MentorRegisterResponseDto {
Self {
id: schema.id.to_string(),
user_id: schema.user_id.map(|id| extract_id(&id)).unwrap_or_default(),
email: schema.email,
email: None, // schema.email - now in user table
status: schema.status,
created_at: schema.created_at,
updated_at: schema.updated_at,
@@ -446,19 +417,7 @@ impl From<MentorDetailWithUserDto> for MentorDetailQueryDto {
MentorDetailQueryDto {
id: dto.id,
user_id: dto.user_id,
fullname: dto.fullname,
email: dto.email,
legal_name: dto.legal_name,
gender: None, // Frontend form implies these are optional, not present in original MentorDetailWithUserDto
domicile: None, // Frontend form implies these are optional, not present in original MentorDetailWithUserDto
identity_document_url: dto.identity_document_url,
phone_for_verification: dto.phone_for_verification,
bio: dto.bio,
last_education: None, // Frontend form implies these are optional, not present in original MentorDetailWithUserDto
linkedin_url: dto.linkedin_url,
github_url: dto.github_url,
cv_url: dto.cv_url,
portfolio_url: None, // Frontend form implies these are optional, not present in original MentorDetailWithUserDto
// Personal data removed from MentorDetailWithUserDto - now in UsersSchema
industries: dto.industries,
expertise: dto.expertise,
languages: dto.languages,
@@ -2,12 +2,14 @@ use anyhow::{Result, bail};
use imphnen_iam::{get_id, make_thing};
use surrealdb::sql::Thing;
use crate::v1::mentors::{MentorDetailWithUserDto, MentorInsertDto, MentorSchema};
use crate::v1::mentors::mentors_dto::MentorDetailWithUserDto;
use crate::v1::mentors::{MentorInsertDto, MentorSchema};
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
use imphnen_utils::{DetailQueryBuilder, QueryListBuilder, get_iso_date};
use serde_json::{Map, Value};
use std::time::Instant;
use tracing::instrument;
use tracing::info;
pub struct MentorsRepository<'a> {
pub state: &'a AppState,
@@ -27,19 +29,11 @@ impl<'a> MentorsRepository<'a> {
let db = &self.state.surrealdb_ws;
let mentors_table = ResourceEnum::Mentors.to_string();
let builder = QueryListBuilder::new(db, &mentors_table, &meta)
.search_field("legal_name")
.search_field("user_id.legal_name") // Search in user data instead
.select_fields(vec![
"id",
"user_id",
"user_id.fullname as fullname",
"email",
"legal_name",
"identity_document_url",
"phone_for_verification",
"bio",
"linkedin_url",
"github_url",
"cv_url",
// Personal data comes from user relation, not mentor table
"industries",
"expertise",
"languages",
@@ -79,19 +73,11 @@ impl<'a> MentorsRepository<'a> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let mut builder = DetailQueryBuilder::new(ResourceEnum::Mentors.to_string())
.with_where("email", Some(email.clone()))
.with_where("user_id.email", Some(email.clone())) // Search in user table
.with_select_fields(vec![
"id",
"user_id",
"user_id.fullname as fullname",
"email",
"legal_name",
"identity_document_url",
"phone_for_verification",
"bio",
"linkedin_url",
"github_url",
"cv_url",
// Personal data comes from user relation, not mentor table
"industries",
"expertise",
"languages",
@@ -114,6 +100,7 @@ impl<'a> MentorsRepository<'a> {
}
let sql = builder.build();
info!(query = %sql, "Executing SurrealDB query in query_mentor_by_email");
let mentor_opt: Option<MentorDetailWithUserDto> =
builder.apply_bindings(db.query(sql)).await?.take(0)?;
let elapsed = now.elapsed();
@@ -136,20 +123,22 @@ impl<'a> MentorsRepository<'a> {
) -> Result<MentorDetailWithUserDto> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let mut builder = DetailQueryBuilder::new(ResourceEnum::Mentors.to_string())
.with_id(get_id(id)?.1)
// Validate ID format first
let mentor_id = match get_id(id) {
Ok((_, id_str)) => id_str,
Err(_) => bail!("Invalid mentor ID format"),
};
let mentors_table = ResourceEnum::Mentors.to_string();
// Build query with proper ID binding
let mut builder = DetailQueryBuilder::new(mentors_table.clone())
.with_id(mentor_id) // Use the extracted ID string
.with_select_fields(vec![
"id",
"user_id",
"user_id.fullname as fullname",
"email",
"legal_name",
"identity_document_url",
"phone_for_verification",
"bio",
"linkedin_url",
"github_url",
"cv_url",
// Personal data comes from user relation, not mentor table
"industries",
"expertise",
"languages",
@@ -172,20 +161,24 @@ impl<'a> MentorsRepository<'a> {
}
let sql = builder.build();
let mentor_opt: Option<MentorDetailWithUserDto> =
builder.apply_bindings(db.query(sql)).await?.take(0)?;
info!(query = %sql, "Executing SurrealDB query in query_mentor_by_id");
let mentor_opt: Option<MentorDetailWithUserDto> = builder
.apply_bindings(db.query(sql))
.await?
.take(0)?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_mentor_by_id' took: {elapsed:.2?}");
}
let Some(mentor) = mentor_opt else {
bail!("Mentor not found in database");
bail!("Mentor not found");
};
if mentor.is_deleted && !include_deleted {
bail!("Mentor has been deleted");
}
Ok(mentor)
}
@@ -194,8 +187,10 @@ impl<'a> MentorsRepository<'a> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let dto: MentorInsertDto = data.into();
let resource = ResourceEnum::Mentors.to_string();
info!(query = %resource, "Executing SurrealDB create in query_create_mentor");
let record: Option<MentorSchema> = db
.create(ResourceEnum::Mentors.to_string())
.create(resource)
.content(dto.clone())
.await?;
let elapsed = now.elapsed();
@@ -237,6 +232,7 @@ impl<'a> MentorsRepository<'a> {
merged_data_json.insert("updated_at".to_string(), Value::String(get_iso_date()));
info!(query = ?record_key, "Executing SurrealDB update in query_update_mentor");
let record: Option<MentorSchema> =
db.update(record_key).merge(merged_data_json).await?;
let elapsed = now.elapsed();
@@ -282,6 +278,7 @@ impl<'a> MentorsRepository<'a> {
patch.insert("is_deleted".to_string(), Value::Bool(true));
patch.insert("updated_at".to_string(), Value::String(get_iso_date()));
info!(query = ?record_key, "Executing SurrealDB soft delete in query_delete_mentor");
let record: Option<MentorSchema> = db.update(record_key).merge(patch).await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
@@ -1,5 +1,5 @@
use super::{
IdentityAndVerification, MentorDetailQueryDto, MentorUpdateRequestDto,
MentorDetailQueryDto, MentorUpdateRequestDto,
MentoringLogistics, MentoringRate, ProfessionalProfile,
};
use imphnen_libs::ResourceEnum;
@@ -12,18 +12,9 @@ pub struct MentorSchema {
pub id: Thing,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<Thing>,
pub email: Option<String>,
pub legal_name: String,
pub gender: Option<String>,
pub domicile: Option<String>,
pub identity_document_url: String,
pub phone_for_verification: String,
pub bio: String,
pub last_education: Option<String>,
pub linkedin_url: Option<String>,
pub github_url: Option<String>,
pub cv_url: Option<String>,
pub portfolio_url: Option<String>,
// Personal data has been moved to UsersSchema - use user_id to reference
// phone_for_verification, bio, last_education, linkedin_url, github_url,
// cv_url, portfolio_url
pub industries: Vec<String>,
pub expertise: Vec<String>,
pub languages: Vec<String>,
@@ -52,18 +43,6 @@ impl Default for MentorSchema {
ResourceEnum::Users.to_string().as_str(),
&Uuid::new_v4().to_string(),
)),
email: None,
legal_name: String::new(),
gender: None,
domicile: None,
identity_document_url: String::new(),
phone_for_verification: String::new(),
bio: String::new(),
last_education: None,
linkedin_url: None,
github_url: None,
cv_url: None,
portfolio_url: None,
industries: Vec::new(),
expertise: Vec::new(),
languages: Vec::new(),
@@ -89,11 +68,9 @@ impl Default for MentorSchema {
impl MentorSchema {
pub fn create(
identity_and_verification: IdentityAndVerification,
professional_profile: ProfessionalProfile,
mentoring_logistics: MentoringLogistics,
user_id_raw: String,
email_str: String,
) -> Self {
Self {
id: make_thing(
@@ -101,18 +78,7 @@ impl MentorSchema {
&Uuid::new_v4().to_string(),
),
user_id: Some(make_thing(&ResourceEnum::Users.to_string(), &user_id_raw)),
email: Some(email_str),
legal_name: identity_and_verification.legal_name,
gender: identity_and_verification.gender,
domicile: identity_and_verification.domicile,
identity_document_url: identity_and_verification.identity_document_url,
phone_for_verification: identity_and_verification.phone_for_verification,
bio: professional_profile.bio,
last_education: professional_profile.last_education,
linkedin_url: professional_profile.linkedin_url,
github_url: professional_profile.github_url,
cv_url: professional_profile.cv_url,
portfolio_url: professional_profile.portfolio_url,
// Personal data now stored in UsersSchema, not here
industries: professional_profile.industries,
expertise: professional_profile.expertise,
languages: professional_profile.languages,
@@ -139,18 +105,7 @@ impl MentorSchema {
Self {
id: dto.id,
user_id: Some(dto.user_id),
email: dto.email,
legal_name: dto.legal_name,
gender: dto.gender,
domicile: dto.domicile,
identity_document_url: dto.identity_document_url,
phone_for_verification: dto.phone_for_verification,
bio: dto.bio,
last_education: dto.last_education,
linkedin_url: dto.linkedin_url,
github_url: dto.github_url,
cv_url: dto.cv_url,
portfolio_url: dto.portfolio_url,
// Personal data now comes from UsersSchema via user_id
industries: dto.industries,
expertise: dto.expertise,
languages: dto.languages,
@@ -170,40 +125,10 @@ impl MentorSchema {
}
pub fn update(mut self, dto: MentorUpdateRequestDto) -> Self {
// Update fields only if they are Some(value), otherwise preserve current value
if let Some(val) = dto.legal_name {
self.legal_name = val;
}
if let Some(val) = dto.gender {
self.gender = Some(val);
}
if let Some(val) = dto.domicile {
self.domicile = Some(val);
}
if let Some(val) = dto.identity_document_url {
self.identity_document_url = val;
}
if let Some(val) = dto.phone_for_verification {
self.phone_for_verification = val;
}
if let Some(val) = dto.bio {
self.bio = val;
}
if let Some(val) = dto.last_education {
self.last_education = Some(val);
}
if let Some(val) = dto.linkedin_url {
self.linkedin_url = Some(val);
}
if let Some(val) = dto.github_url {
self.github_url = Some(val);
}
if let Some(val) = dto.cv_url {
self.cv_url = Some(val);
}
if let Some(val) = dto.portfolio_url {
self.portfolio_url = Some(val);
}
// phone_for_verification, bio, last_education, linkedin_url, github_url,
// cv_url, portfolio_url) are now updated in UsersSchema, not here
// Only update professional fields that are still in MentorSchema
if let Some(val) = dto.industries {
self.industries = val;
}
@@ -6,8 +6,9 @@ use crate::v1::mentors::{
use axum::http::StatusCode;
use axum::response::Response;
use imphnen_entities::{
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
};
use imphnen_libs::AppState;
use imphnen_iam::{
AuthRepository, RolesEnum, RolesRepository, UsersRepository, UsersSchema,
};
@@ -33,9 +34,9 @@ impl MentorsService {
let user_repo = UsersRepository::new(state);
let mentor_repo = MentorsRepository::new(state);
let role_repo = RolesRepository::new(state);
let auth_repo = AuthRepository::new(state);
let auth_repo = AuthRepository::new(state.surrealdb_mem.clone());
let user_email = dto.email.clone();
let user_email = &dto.email;
let mut _user_to_update: Option<UsersSchema> = None;
let existing_user_result =
@@ -56,10 +57,22 @@ impl MentorsService {
);
}
let mut user_schema = UsersSchema::from(user_detail_query_dto.clone());
let mut user_schema = UsersSchema::from(user_detail_query_dto);
user_schema.fullname = dto.fullname.clone();
user_schema.phone_number = dto.phone_number.clone();
// Update personal data from identity_and_verification
user_schema.legal_name = Some(dto.identity_and_verification.legal_name.clone());
user_schema.gender = dto.identity_and_verification.gender.clone();
user_schema.domicile = dto.identity_and_verification.domicile.clone();
user_schema.phone_for_verification = Some(dto.identity_and_verification.phone_for_verification.clone());
// Update personal data from professional_profile
user_schema.bio = Some(dto.professional_profile.bio.clone());
user_schema.last_education = dto.professional_profile.last_education.clone();
user_schema.linkedin_url = dto.professional_profile.linkedin_url.clone();
user_schema.github_url = dto.professional_profile.github_url.clone();
user_schema.cv_url = dto.professional_profile.cv_url.clone();
user_schema.portfolio_url = dto.professional_profile.portfolio_url.clone();
user_schema.updated_at = imphnen_utils::get_iso_date();
let hashed_password = match imphnen_utils::hash_password(&dto.password) {
@@ -87,7 +100,7 @@ impl MentorsService {
}
};
user_schema.role =
imphnen_utils::make_thing(&ResourceEnum::Roles.to_string(), &mentor_role.id);
imphnen_utils::make_thing_from_enum(ResourceEnum::Roles, &mentor_role.id);
user_schema.is_active = false;
if let Err(_err) = user_repo.query_update_user(user_schema.clone()).await {
@@ -127,18 +140,30 @@ impl MentorsService {
};
let new_user_schema = UsersSchema {
id: imphnen_utils::make_thing(
&ResourceEnum::Users.to_string(),
id: imphnen_utils::make_thing_from_enum(
ResourceEnum::Users,
&Uuid::new_v4().to_string(),
),
email: dto.email.clone(),
fullname: dto.fullname.clone(),
email: dto.email,
fullname: dto.fullname,
password: hashed_password,
phone_number: dto.phone_number.clone(),
phone_number: dto.phone_number,
// Store personal data from identity_and_verification in user
legal_name: Some(dto.identity_and_verification.legal_name.clone()),
gender: dto.identity_and_verification.gender.clone(),
domicile: dto.identity_and_verification.domicile.clone(),
phone_for_verification: Some(dto.identity_and_verification.phone_for_verification.clone()),
// Store personal data from professional_profile in user
bio: Some(dto.professional_profile.bio.clone()),
last_education: dto.professional_profile.last_education.clone(),
linkedin_url: dto.professional_profile.linkedin_url.clone(),
github_url: dto.professional_profile.github_url.clone(),
cv_url: dto.professional_profile.cv_url.clone(),
portfolio_url: dto.professional_profile.portfolio_url.clone(),
created_at: imphnen_utils::get_iso_date(),
updated_at: imphnen_utils::get_iso_date(),
role: imphnen_utils::make_thing(
&ResourceEnum::Roles.to_string(),
role: imphnen_utils::make_thing_from_enum(
ResourceEnum::Roles,
&mentor_role.id,
),
is_active: false,
@@ -162,11 +187,11 @@ impl MentorsService {
let otp = imphnen_utils::generate_otp::OtpManager::generate_otp();
match auth_repo
.query_store_otp(final_user_email.clone(), otp)
.query_store_otp(final_user_email.clone(), otp.clone())
.await
{
Ok(_) => {
let message = format!("your otp code is {otp}");
let message = format!("your otp code is {}", otp.code);
if let Err(_err) =
imphnen_utils::send_email(&final_user_email, "OTP Verification", &message)
{
@@ -187,11 +212,9 @@ impl MentorsService {
}
let mentor_schema = MentorSchema::create(
dto.identity_and_verification,
dto.professional_profile,
dto.mentoring_logistics,
user_id.to_raw(),
final_user_email.clone(),
);
match mentor_repo.query_create_mentor(mentor_schema.clone()).await {
@@ -233,16 +256,27 @@ impl MentorsService {
pub async fn get_mentor_list(state: &AppState, meta: MetaRequestDto) -> Response {
let repo = MentorsRepository::new(state);
let user_repo = UsersRepository::new(state);
match repo.query_mentor_list(meta).await {
Ok(result) => {
let data: Vec<MentorListResponseDto> = result
.data
.into_iter()
.map(MentorDetailQueryDto::from)
.map(MentorListResponseDto::from)
.collect();
let mut mentor_list_data: Vec<MentorListResponseDto> = Vec::new();
for mentor_with_user in result.data {
let mentor_dto = MentorDetailQueryDto::from(mentor_with_user);
let mut list_item = MentorListResponseDto::from(mentor_dto.clone());
// Get user data to populate personal fields
if let Ok(user) = user_repo.query_user_by_id(&mentor_dto.user_id).await {
list_item.fullname = Some(user.fullname);
list_item.email = Some(user.email);
}
mentor_list_data.push(list_item);
}
success_list_response(ResponseListSuccessDto {
data,
data: mentor_list_data,
meta: result.meta,
})
}
@@ -251,12 +285,56 @@ impl MentorsService {
}
pub async fn get_mentor_by_id(state: &AppState, id: &str) -> Response {
let repo = MentorsRepository::new(state);
let mentor_repo = MentorsRepository::new(state);
let user_repo = UsersRepository::new(state);
let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id));
match repo.query_mentor_by_id(&thing_id, false).await {
match mentor_repo.query_mentor_by_id(&thing_id, false).await {
Ok(mentor) => {
let dto = MentorDetailResponseDto::from(MentorDetailQueryDto::from(mentor));
success_response(ResponseSuccessDto { data: dto })
// Get user data separately
let user_result = user_repo.query_user_by_id(&mentor.user_id).await;
match user_result {
Ok(user) => {
// Combine mentor and user data
let dto = MentorDetailResponseDto {
id: mentor.id.to_raw(),
user_id: mentor.user_id.to_raw(),
// Personal data from user
fullname: Some(user.fullname),
email: Some(user.email),
legal_name: user.legal_name,
gender: user.gender,
domicile: user.domicile,
phone_for_verification: user.phone_for_verification,
bio: user.bio,
last_education: user.last_education,
linkedin_url: user.linkedin_url,
github_url: user.github_url,
cv_url: user.cv_url,
portfolio_url: user.portfolio_url,
// Professional data from mentor
industries: mentor.industries,
expertise: mentor.expertise,
languages: mentor.languages,
current_company: mentor.current_company,
current_role: mentor.current_role,
years_of_experience: mentor.years_of_experience,
topics_of_interest: mentor.topics_of_interest,
preferred_mentee_level: mentor.preferred_mentee_level,
preferred_mentoring_formats: mentor.preferred_mentoring_formats,
availability_commitment: mentor.availability_commitment,
mentoring_rate: mentor.mentoring_rate,
status: mentor.status,
created_at: mentor.created_at,
updated_at: mentor.updated_at,
};
success_response(ResponseSuccessDto { data: dto })
}
Err(_e) => {
error!("Failed to get user data for mentor {}: {}", id, _e);
common_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to get mentor user data")
}
}
}
Err(_e) => common_response(StatusCode::NOT_FOUND, &_e.to_string()),
}
+40 -9
View File
@@ -9,22 +9,53 @@ pub mod mentors_repository;
pub mod mentors_schema;
pub mod mentors_service;
pub use mentors_controller::*;
pub use mentors_dto::*;
pub use mentors_repository::*;
pub use mentors_schema::*;
pub use mentors_service::*;
// Explicitly export only public controller functions and key types
pub use mentors_controller::{
post_register_mentor,
get_mentor_list,
get_mentor_by_id,
put_update_mentor,
delete_mentor,
put_verify_mentor,
get_mentor_me,
put_update_mentor_me,
put_update_mentor_no_id,
get_mentor_status,
};
// Export key DTO types used across the API
pub use mentors_dto::{
MentorListResponseDto,
MentorDetailResponseDto,
MentorRegisterResponseDto,
MentorUpdateRequestDto,
MentorUserRegisterRequestDto,
MentorVerifyRequestDto,
MentorDetailQueryDto,
ProfessionalProfile,
MentoringLogistics,
MentoringRate,
IdentityAndVerification,
MentorInsertDto,
};
// Export service and repository for internal use
pub use mentors_service::MentorsService;
pub use mentors_repository::MentorsRepository;
// Export schema types for database interactions
pub use mentors_schema::MentorSchema;
pub fn mentors_router() -> Router {
Router::new()
.route("/", get(get_mentor_list))
.route("/register", post(post_register_mentor))
.route("/create", post(post_register_mentor))
.route("/me", get(get_mentor_me))
.route("/update/me", put(put_update_mentor_me))
.route("/status", get(get_mentor_status))
.route("/me/update", put(put_update_mentor_me))
.route("/me/status", get(get_mentor_status))
.route("/detail/{id}", get(get_mentor_by_id))
.route("/update/{id}", put(put_update_mentor))
.route("/update", put(put_update_mentor_no_id))
.route("/delete/{id}", delete(delete_mentor))
.route("/verify/{id}", put(put(put_verify_mentor)))
.route("/verify/{id}", put(put_verify_mentor))
}
+20 -1
View File
@@ -1,7 +1,26 @@
use axum::Router;
pub mod mentors;
pub mod sessions;
/// Creates the main Dimentorin router with all version 1 endpoints
/// Routes:
/// - /mentors -> mentors::mentors_router()
/// - /sessions -> sessions::sessions_router()
/// - /users/me/sessions -> sessions::get_my_sessions()
pub fn dimentorin_router() -> Router {
Router::new().nest("/mentors", mentors::mentors_router())
Router::new()
.nest("/mentors", mentors::mentors_router())
.merge(sessions::sessions_router())
}
// Explicitly re-export key items for easier consumption
pub use mentors::mentors_router;
pub use mentors::MentorsService;
pub use mentors::MentorsRepository;
pub use mentors::MentorSchema;
pub use sessions::sessions_router;
pub use sessions::SessionsService;
pub use sessions::SessionsRepository;
pub use sessions::SessionSchema;
+11
View File
@@ -0,0 +1,11 @@
pub mod sessions_controller;
pub mod sessions_dto;
pub mod sessions_repository;
pub mod sessions_schema;
pub mod sessions_service;
pub use sessions_controller::*;
pub use sessions_dto::*;
pub use sessions_repository::*;
pub use sessions_schema::*;
pub use sessions_service::*;
@@ -0,0 +1,297 @@
use super::{
BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListResponseDto,
SessionsService, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
};
use axum::{
extract::{Extension, Path, Query},
http::HeaderMap,
response::Response,
routing::{get, post, put},
Json, Router,
};
use imphnen_libs::AppState;
use imphnen_utils::extract_email;
use serde::Deserialize;
use utoipa::OpenApi;
#[derive(OpenApi)]
#[openapi(
paths(
post_book_session,
get_mentor_sessions,
get_mentor_availability,
put_update_session_status,
post_submit_feedback,
get_my_sessions,
),
components(schemas(
BookSessionRequestDto,
BookSessionResponseDto,
SessionListResponseDto,
super::SessionListItemDto,
MentorAvailabilityDto,
super::AvailabilitySlotDto,
UpdateSessionStatusRequestDto,
UpdateSessionStatusResponseDto,
SessionFeedbackRequestDto,
SessionFeedbackResponseDto,
)),
tags(
(name = "sessions", description = "Mentoring Sessions Management API")
)
)]
pub struct SessionsApiDoc;
// ============================================
// Book Session
// ============================================
#[utoipa::path(
post,
path = "/v1/mentors/{id}/sessions/create",
tag = "sessions",
summary = "Book a mentoring session",
description = "Book a mentoring session with a specific mentor. Requires authentication.",
security(("Bearer" = [])),
params(
("id" = String, Path, description = "Mentor ID"),
),
request_body = BookSessionRequestDto,
responses(
(status = 201, description = "Session booked successfully", body = BookSessionResponseDto),
(status = 400, description = "Invalid request"),
(status = 401, description = "Unauthorized"),
(status = 404, description = "Mentor not found"),
)
)]
pub async fn post_book_session(
headers: HeaderMap,
Extension(state): Extension<AppState>,
Path(mentor_id): Path<String>,
Json(dto): Json<BookSessionRequestDto>,
) -> Response {
let user_email = match extract_email(&headers) {
Some(email) => email,
None => {
return imphnen_utils::common_response(
axum::http::StatusCode::UNAUTHORIZED,
"Token tidak valid",
);
}
};
SessionsService::book_session(&state, mentor_id, user_email, dto).await
}
// ============================================
// Get Mentor's Sessions
// ============================================
#[derive(Deserialize)]
pub struct SessionStatusFilter {
status: Option<String>,
}
#[utoipa::path(
get,
path = "/v1/mentors/{id}/sessions",
tag = "sessions",
summary = "List mentor's sessions",
description = "Get all sessions for a specific mentor. Only accessible by the mentor themselves or admin.",
security(("Bearer" = [])),
params(
("id" = String, Path, description = "Mentor ID"),
("status" = Option<String>, Query, description = "Filter by status (pending, confirmed, completed, cancelled, no_show)"),
),
responses(
(status = 200, description = "Sessions retrieved successfully", body = SessionListResponseDto),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden"),
(status = 404, description = "Mentor not found"),
)
)]
pub async fn get_mentor_sessions(
headers: HeaderMap,
Extension(state): Extension<AppState>,
Path(mentor_id): Path<String>,
Query(filter): Query<SessionStatusFilter>,
) -> Response {
let user_email = match extract_email(&headers) {
Some(email) => email,
None => {
return imphnen_utils::common_response(
axum::http::StatusCode::UNAUTHORIZED,
"Token tidak valid",
);
}
};
SessionsService::get_mentor_sessions(&state, mentor_id, user_email, filter.status).await
}
// ============================================
// Get Mentor Availability
// ============================================
#[utoipa::path(
get,
path = "/v1/mentors/{id}/availability",
tag = "sessions",
summary = "Get mentor availability",
description = "Get available time slots for booking with a mentor. Public endpoint.",
params(
("id" = String, Path, description = "Mentor ID"),
),
responses(
(status = 200, description = "Availability retrieved successfully", body = MentorAvailabilityDto),
(status = 404, description = "Mentor not found"),
)
)]
pub async fn get_mentor_availability(
Extension(state): Extension<AppState>,
Path(mentor_id): Path<String>,
) -> Response {
SessionsService::get_mentor_availability(&state, mentor_id).await
}
// ============================================
// Update Session Status
// ============================================
#[utoipa::path(
put,
path = "/v1/sessions/update/{id}/status",
tag = "sessions",
summary = "Update session status",
description = "Update the status of a session (confirm, complete, cancel). Only accessible by the mentor.",
security(("Bearer" = [])),
params(
("id" = String, Path, description = "Session ID"),
),
request_body = UpdateSessionStatusRequestDto,
responses(
(status = 200, description = "Status updated successfully", body = UpdateSessionStatusResponseDto),
(status = 400, description = "Invalid request"),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden"),
(status = 404, description = "Session not found"),
)
)]
pub async fn put_update_session_status(
headers: HeaderMap,
Extension(state): Extension<AppState>,
Path(session_id): Path<String>,
Json(dto): Json<UpdateSessionStatusRequestDto>,
) -> Response {
let user_email = match extract_email(&headers) {
Some(email) => email,
None => {
return imphnen_utils::common_response(
axum::http::StatusCode::UNAUTHORIZED,
"Token tidak valid",
);
}
};
SessionsService::update_session_status(&state, session_id, user_email, dto).await
}
// ============================================
// Submit Feedback
// ============================================
#[utoipa::path(
post,
path = "/v1/sessions/{id}/feedback/create",
tag = "sessions",
summary = "Submit session feedback",
description = "Submit feedback and rating for a completed session. Only accessible by the mentee.",
security(("Bearer" = [])),
params(
("id" = String, Path, description = "Session ID"),
),
request_body = SessionFeedbackRequestDto,
responses(
(status = 200, description = "Feedback submitted successfully", body = SessionFeedbackResponseDto),
(status = 400, description = "Invalid request or session not completed"),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden"),
(status = 404, description = "Session not found"),
)
)]
pub async fn post_submit_feedback(
headers: HeaderMap,
Extension(state): Extension<AppState>,
Path(session_id): Path<String>,
Json(dto): Json<SessionFeedbackRequestDto>,
) -> Response {
let user_email = match extract_email(&headers) {
Some(email) => email,
None => {
return imphnen_utils::common_response(
axum::http::StatusCode::UNAUTHORIZED,
"Token tidak valid",
);
}
};
SessionsService::submit_feedback(&state, session_id, user_email, dto).await
}
// ============================================
// Get User's Sessions
// ============================================
#[utoipa::path(
get,
path = "/v1/users/me/sessions",
tag = "sessions",
summary = "Get my sessions",
description = "Get all sessions for the authenticated user (as mentee). Requires authentication.",
security(("Bearer" = [])),
params(
("status" = Option<String>, Query, description = "Filter by status (pending, confirmed, completed, cancelled, no_show)"),
),
responses(
(status = 200, description = "Sessions retrieved successfully", body = SessionListResponseDto),
(status = 401, description = "Unauthorized"),
)
)]
pub async fn get_my_sessions(
headers: HeaderMap,
Extension(state): Extension<AppState>,
Query(filter): Query<SessionStatusFilter>,
) -> Response {
let user_email = match extract_email(&headers) {
Some(email) => email,
None => {
return imphnen_utils::common_response(
axum::http::StatusCode::UNAUTHORIZED,
"Token tidak valid",
);
}
};
SessionsService::get_user_sessions(&state, user_email, filter.status).await
}
// ============================================
// Router
// ============================================
pub fn sessions_router() -> Router {
Router::new()
// Book session (under mentors path)
.route("/mentors/{id}/sessions/create", post(post_book_session))
// Get mentor's sessions
.route("/mentors/{id}/sessions", get(get_mentor_sessions))
// Get mentor availability (public - no auth)
.route("/mentors/{id}/availability", get(get_mentor_availability))
// Update session status
.route("/sessions/update/{id}/status", put(put_update_session_status))
// Submit feedback
.route("/sessions/{id}/feedback/create", post(post_submit_feedback))
// Get my sessions
.route("/users/me/sessions", get(get_my_sessions))
}
@@ -0,0 +1,197 @@
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use validator::Validate;
// ============================================
// Book Session (POST /v1/mentors/{id}/sessions/book)
// ============================================
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct BookSessionRequestDto {
#[validate(length(min = 3, max = 200, message = "Topic must be 3-200 characters"))]
pub topic: String,
#[validate(length(max = 1000, message = "Description must be max 1000 characters"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[validate(length(min = 1, message = "Scheduled time is required"))]
pub scheduled_at: String, // ISO 8601 datetime
#[validate(range(min = 15, max = 240, message = "Duration must be 15-240 minutes"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_minutes: Option<i32>,
#[validate(length(max = 50, message = "Session type must be max 50 characters"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub session_type: Option<String>, // "video_call", "phone_call", "chat"
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct BookSessionResponseDto {
pub id: String,
pub mentor_id: String,
pub mentee_id: String,
pub topic: String,
pub description: Option<String>,
pub scheduled_at: String,
pub duration_minutes: i32,
pub session_type: String,
pub status: String,
pub created_at: String,
}
// ============================================
// List Sessions (GET /v1/mentors/{id}/sessions & /v1/users/me/sessions)
// ============================================
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct SessionListItemDto {
pub id: String,
pub mentor_id: String,
pub mentee_id: String,
pub mentee_fullname: Option<String>,
pub mentee_email: Option<String>,
pub topic: String,
pub scheduled_at: String,
pub duration_minutes: i32,
pub session_type: String,
pub status: String,
pub rating: Option<i32>,
pub created_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct SessionListResponseDto {
pub sessions: Vec<SessionListItemDto>,
pub total: usize,
}
// ============================================
// Session Detail
// ============================================
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct SessionDetailDto {
pub id: String,
pub mentor_id: String,
pub mentor_fullname: Option<String>,
pub mentee_id: String,
pub mentee_fullname: Option<String>,
pub topic: String,
pub description: Option<String>,
pub scheduled_at: String,
pub duration_minutes: i32,
pub meeting_link: Option<String>,
pub session_type: String,
pub status: String,
pub feedback: Option<String>,
pub rating: Option<i32>,
pub feedback_submitted_at: Option<String>,
pub created_at: String,
pub updated_at: String,
}
// ============================================
// Mentor Availability (GET /v1/mentors/{id}/availability)
// ============================================
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct AvailabilitySlotDto {
pub date: String, // YYYY-MM-DD
pub time: String, // HH:MM
pub available: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct MentorAvailabilityDto {
pub mentor_id: String,
pub availability_commitment: String,
pub preferred_formats: Vec<String>,
pub slots: Vec<AvailabilitySlotDto>,
pub booked_dates: Vec<String>, // Dates with existing sessions
}
// ============================================
// Update Session Status (PUT /v1/sessions/{id}/status)
// ============================================
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct UpdateSessionStatusRequestDto {
#[validate(length(min = 1, max = 50, message = "Status must be 1-50 characters"))]
pub status: String, // "confirmed", "completed", "cancelled", "no_show"
#[validate(url(message = "Meeting link must be a valid URL"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub meeting_link: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct UpdateSessionStatusResponseDto {
pub id: String,
pub status: String,
pub meeting_link: Option<String>,
pub updated_at: String,
}
// ============================================
// Submit Feedback (POST /v1/sessions/{id}/feedback)
// ============================================
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct SessionFeedbackRequestDto {
#[validate(length(min = 10, max = 2000, message = "Feedback must be 10-2000 characters"))]
pub feedback: String,
#[validate(range(min = 1, max = 5, message = "Rating must be 1-5"))]
pub rating: i32,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct SessionFeedbackResponseDto {
pub id: String,
pub feedback: String,
pub rating: i32,
pub submitted_at: String,
}
// ============================================
// Query DTOs (internal use)
// ============================================
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SessionDetailQueryDto {
pub id: String,
pub mentor_id: String,
pub mentee_id: String,
pub topic: String,
pub description: Option<String>,
pub scheduled_at: String,
pub duration_minutes: i32,
pub meeting_link: Option<String>,
pub session_type: String,
pub status: String,
pub feedback: Option<String>,
pub rating: Option<i32>,
pub feedback_submitted_at: Option<String>,
pub created_at: String,
pub updated_at: String,
pub mentor_fullname: Option<String>,
pub mentee_fullname: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SessionListQueryDto {
pub id: String,
pub mentor_id: String,
pub mentee_id: String,
pub topic: String,
pub scheduled_at: String,
pub duration_minutes: i32,
pub session_type: String,
pub status: String,
pub rating: Option<i32>,
pub created_at: String,
pub mentee_fullname: Option<String>,
pub mentee_email: Option<String>,
}
@@ -0,0 +1,365 @@
use super::{SessionDetailQueryDto, SessionListQueryDto, SessionSchema};
use imphnen_libs::AppState;
use imphnen_utils::get_id;
use serde::Deserialize;
use surrealdb::sql::Thing;
pub struct SessionsRepository<'a> {
pub state: &'a AppState,
}
impl<'a> SessionsRepository<'a> {
pub fn new(state: &'a AppState) -> Self {
Self { state }
}
// ============================================
// Create Session
// ============================================
pub async fn create_session(&self, schema: SessionSchema) -> Result<SessionSchema, String> {
let db = &self.state.surrealdb_ws;
let created: Option<SessionSchema> = db
.create("sessions")
.content(schema)
.await
.map_err(|e| format!("Failed to create session: {}", e))?;
created.ok_or_else(|| "Session creation returned None".to_string())
}
// ============================================
// Get Session by ID
// ============================================
pub async fn query_session_by_id(&self, id: &Thing) -> Result<Option<SessionSchema>, String> {
let db = &self.state.surrealdb_ws;
let record_key = get_id(id).map_err(|e| e.to_string())?;
let session: Option<SessionSchema> = db
.select(record_key)
.await
.map_err(|e| format!("Failed to fetch session: {}", e))?;
Ok(session)
}
// ============================================
// Get Session Detail with User Info
// ============================================
pub async fn query_session_detail(&self, id: &Thing) -> Result<Option<SessionDetailQueryDto>, String> {
let db = &self.state.surrealdb_ws;
let query = r#"
SELECT
id,
mentor_id,
mentee_id,
topic,
description,
scheduled_at,
duration_minutes,
meeting_link,
session_type,
status,
feedback,
rating,
feedback_submitted_at,
created_at,
updated_at,
(SELECT fullname FROM $parent.mentor_id.user_id)[0].fullname AS mentor_fullname,
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname
FROM type::thing($table, $id)
"#;
let mut result = db
.query(query)
.bind(("table", "sessions"))
.bind(("id", id.id.to_string()))
.await
.map_err(|e| format!("Failed to query session detail: {}", e))?;
let session: Option<SessionDetailQueryDto> = result
.take(0)
.map_err(|e| format!("Failed to parse session detail: {}", e))?;
Ok(session)
}
// ============================================
// List Mentor's Sessions
// ============================================
pub async fn query_mentor_sessions(
&self,
mentor_id: &Thing,
status_filter: Option<String>,
) -> Result<Vec<SessionListQueryDto>, String> {
let query = if let Some(_status) = status_filter.as_ref() {
r#"
SELECT
id,
mentor_id,
mentee_id,
topic,
scheduled_at,
duration_minutes,
session_type,
status,
rating,
created_at,
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
FROM sessions
WHERE mentor_id = $mentor_id AND status = $status
ORDER BY scheduled_at DESC
"#
} else {
r#"
SELECT
id,
mentor_id,
mentee_id,
topic,
scheduled_at,
duration_minutes,
session_type,
status,
rating,
created_at,
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
FROM sessions
WHERE mentor_id = $mentor_id
ORDER BY scheduled_at DESC
"#
};
let db = &self.state.surrealdb_ws;
let mentor_id_clone = mentor_id.clone();
let mut result = if let Some(status_val) = status_filter {
db.query(query)
.bind(("mentor_id", mentor_id_clone))
.bind(("status", status_val))
.await
} else {
db.query(query)
.bind(("mentor_id", mentor_id_clone))
.await
}
.map_err(|e| format!("Failed to query mentor sessions: {}", e))?;
let sessions: Vec<SessionListQueryDto> = result
.take(0)
.map_err(|e| format!("Failed to parse mentor sessions: {}", e))?;
Ok(sessions)
}
// ============================================
// List User's Sessions (as mentee)
// ============================================
pub async fn query_user_sessions(
&self,
user_id: &Thing,
status_filter: Option<String>,
) -> Result<Vec<SessionListQueryDto>, String> {
let query = if let Some(_status) = status_filter.as_ref() {
r#"
SELECT
id,
mentor_id,
mentee_id,
topic,
scheduled_at,
duration_minutes,
session_type,
status,
rating,
created_at,
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
FROM sessions
WHERE mentee_id = $user_id AND status = $status
ORDER BY scheduled_at DESC
"#
} else {
r#"
SELECT
id,
mentor_id,
mentee_id,
topic,
scheduled_at,
duration_minutes,
session_type,
status,
rating,
created_at,
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
FROM sessions
WHERE mentee_id = $user_id
ORDER BY scheduled_at DESC
"#
};
let db = &self.state.surrealdb_ws;
let user_id_clone = user_id.clone();
let mut result = if let Some(status_val) = status_filter {
db.query(query)
.bind(("user_id", user_id_clone))
.bind(("status", status_val))
.await
} else {
db.query(query)
.bind(("user_id", user_id_clone))
.await
}
.map_err(|e| format!("Failed to query user sessions: {}", e))?;
let sessions: Vec<SessionListQueryDto> = result
.take(0)
.map_err(|e| format!("Failed to parse user sessions: {}", e))?;
Ok(sessions)
}
// ============================================
// Get Booked Dates for Mentor
// ============================================
pub async fn query_booked_dates(&self, mentor_id: &Thing) -> Result<Vec<String>, String> {
let query = r#"
SELECT scheduled_at FROM sessions
WHERE mentor_id = $mentor_id
AND status IN ['pending', 'confirmed']
ORDER BY scheduled_at ASC
"#;
let db = &self.state.surrealdb_ws;
let mentor_id_clone = mentor_id.clone();
let mut result = db
.query(query)
.bind(("mentor_id", mentor_id_clone))
.await
.map_err(|e| format!("Failed to query booked dates: {}", e))?;
#[derive(Deserialize)]
struct DateOnly {
scheduled_at: String,
}
let dates: Vec<DateOnly> = result
.take(0)
.map_err(|e| format!("Failed to parse booked dates: {}", e))?;
Ok(dates.into_iter().map(|d| d.scheduled_at).collect())
}
// ============================================
// Update Session
// ============================================
pub async fn update_session(&self, id: &Thing, schema: SessionSchema) -> Result<SessionSchema, String> {
let db = &self.state.surrealdb_ws;
let record_key = get_id(id).map_err(|e| e.to_string())?;
let updated: Option<SessionSchema> = db
.update(record_key)
.content(schema)
.await
.map_err(|e| format!("Failed to update session: {}", e))?;
updated.ok_or_else(|| "Session update returned None".to_string())
}
// ============================================
// Count Mentor Sessions
// ============================================
pub async fn count_mentor_sessions(
&self,
mentor_id: &Thing,
status_filter: Option<String>,
) -> Result<usize, String> {
let query = if status_filter.is_some() {
"SELECT count() FROM sessions WHERE mentor_id = $mentor_id AND status = $status GROUP ALL"
} else {
"SELECT count() FROM sessions WHERE mentor_id = $mentor_id GROUP ALL"
};
let db = &self.state.surrealdb_ws;
let mentor_id_clone = mentor_id.clone();
let mut result = if let Some(status_val) = status_filter {
db.query(query)
.bind(("mentor_id", mentor_id_clone))
.bind(("status", status_val))
.await
} else {
db.query(query)
.bind(("mentor_id", mentor_id_clone))
.await
}
.map_err(|e| format!("Failed to count mentor sessions: {}", e))?;
#[derive(serde::Deserialize)]
struct CountResult {
count: usize,
}
let count_result: Option<CountResult> = result
.take(0)
.map_err(|e| format!("Failed to parse count: {}", e))?;
Ok(count_result.map(|r| r.count).unwrap_or(0))
}
// ============================================
// Count User Sessions
// ============================================
pub async fn count_user_sessions(
&self,
user_id: &Thing,
status_filter: Option<String>,
) -> Result<usize, String> {
let query = if status_filter.is_some() {
"SELECT count() FROM sessions WHERE mentee_id = $user_id AND status = $status GROUP ALL"
} else {
"SELECT count() FROM sessions WHERE mentee_id = $user_id GROUP ALL"
};
let db = &self.state.surrealdb_ws;
let user_id_clone = user_id.clone();
let mut result = if let Some(status_val) = status_filter {
db.query(query)
.bind(("user_id", user_id_clone))
.bind(("status", status_val))
.await
} else {
db.query(query)
.bind(("user_id", user_id_clone))
.await
}
.map_err(|e| format!("Failed to count user sessions: {}", e))?;
#[derive(serde::Deserialize)]
struct CountResult {
count: usize,
}
let count_result: Option<CountResult> = result
.take(0)
.map_err(|e| format!("Failed to parse count: {}", e))?;
Ok(count_result.map(|r| r.count).unwrap_or(0))
}
// ============================================
// Delete Session (soft delete)
// ============================================
// Delete Session (soft delete)
// ============================================
pub async fn delete_session(&self, id: &Thing) -> Result<(), String> {
let db = &self.state.surrealdb_ws;
let record_key = get_id(id).map_err(|e| e.to_string())?;
let _: Option<SessionSchema> = db
.delete(record_key)
.await
.map_err(|e| format!("Failed to delete session: {}", e))?;
Ok(())
}
}
@@ -0,0 +1,89 @@
use super::{BookSessionRequestDto, SessionFeedbackRequestDto, UpdateSessionStatusRequestDto};
use imphnen_libs::ResourceEnum;
use imphnen_utils::{get_iso_date, make_thing};
use serde::{Deserialize, Serialize};
use surrealdb::{sql::Thing, Uuid};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SessionSchema {
pub id: Thing,
pub mentor_id: Thing,
pub mentee_id: Thing,
pub topic: String,
pub description: Option<String>,
pub scheduled_at: String, // ISO 8601 datetime
pub duration_minutes: i32,
pub meeting_link: Option<String>,
pub session_type: String, // "video_call", "phone_call", "chat"
pub status: String, // "pending", "confirmed", "completed", "cancelled", "no_show"
pub feedback: Option<String>,
pub rating: Option<i32>, // 1-5
pub feedback_submitted_at: Option<String>,
pub created_at: String,
pub updated_at: String,
}
impl Default for SessionSchema {
fn default() -> Self {
Self {
id: make_thing(
ResourceEnum::Sessions.to_string().as_str(),
&Uuid::new_v4().to_string(),
),
mentor_id: make_thing(
ResourceEnum::Mentors.to_string().as_str(),
&Uuid::new_v4().to_string(),
),
mentee_id: make_thing(
ResourceEnum::Users.to_string().as_str(),
&Uuid::new_v4().to_string(),
),
topic: String::new(),
description: None,
scheduled_at: get_iso_date(),
duration_minutes: 60,
meeting_link: None,
session_type: "video_call".to_string(),
status: "pending".to_string(),
feedback: None,
rating: None,
feedback_submitted_at: None,
created_at: get_iso_date(),
updated_at: get_iso_date(),
}
}
}
impl SessionSchema {
pub fn from_book_request(
mentor_id: Thing,
mentee_id: Thing,
request: BookSessionRequestDto,
) -> Self {
Self {
mentor_id,
mentee_id,
topic: request.topic,
description: request.description,
scheduled_at: request.scheduled_at,
duration_minutes: request.duration_minutes.unwrap_or(60),
session_type: request.session_type.unwrap_or_else(|| "video_call".to_string()),
..Default::default()
}
}
pub fn update_status(&mut self, request: UpdateSessionStatusRequestDto) {
self.status = request.status;
if let Some(link) = request.meeting_link {
self.meeting_link = Some(link);
}
self.updated_at = get_iso_date();
}
pub fn add_feedback(&mut self, request: SessionFeedbackRequestDto) {
self.feedback = Some(request.feedback);
self.rating = Some(request.rating);
self.feedback_submitted_at = Some(get_iso_date());
self.updated_at = get_iso_date();
}
}

Some files were not shown because too many files have changed in this diff Show More