This document outlines the project configuration and roadmap for the Rust-based GraphQL Auth Service for Dimensional Pocket. The service will provide REST endpoints, a GraphQL API, and user management functionalities.
- Project Name:
dp-auth-service - Indentation: Use 2 spaces for Rust code (not the standard 4 spaces)
- GraphQL Library:
async-graphql - Database:
sqlxwith SQLite on a file in the repo root'sdatadirectory (will be mounted in production) - REST endpoints:
/and/health, viaaxum - GraphQL Endpoint:
/graphql - Async Runtime:
tokio - Password Hashing:
argon2; 16-byte salt generated withrand::rngs::OsRng - Design Patterns:
- GraphQL queries and mutations only call Service objects and handle the response
- E.g.,
getServerTimestampquery callsServerService::get_server_timestamp - Service objects are static and do not require instantiation
- Service objects have their own unit tests with full coverage
- GraphQL Query and Mutation tests only test if they're calling the Service methods with the correct parameters
- Service objects are stored in
src/services - Each GraphQL query and mutation are implemented in independent files in
src/graphql/queriesandsrc/graphql/mutationsrespectively- Files are named directly after the query or mutation they implement, e.g.,
get_server_timestamp.rsfor thegetServerTimestampquery - The structs for the queries and mutations are suffixed with
QueryorMutation- E.g.,
GetServerTimestampQueryfor thegetServerTimestampquery
- E.g.,
- Files are named directly after the query or mutation they implement, e.g.,
- E.g.,
- SQL Queries are executed via SQL Query objects
- E.g.,
UserByIdQuery::run(user_id) - All SQL query objects have a
runmethod (arguments may vary) that executes the database query and returns the result - SQL Query objects are static and do not require instantiation
- SQL Query objects are stored in
src/queries
- E.g.,
- GraphQL queries and mutations only call Service objects and handle the response
- Initialize Rust project
- Configure
async-graphql,axum,sqlx, andtokio - Create REST endpoints:
-
/- Returns a simple "OK" message -
/health- Returns a simple "OK" message
-
- Implement
getServerTimestampGraphQL query and associated Service object:- Returns the current server timestamp (milliseconds since epoch)
- Test the REST endpoints,
getServerTimestampquery, and the Service object - Document the API endpoints and query
- Suggest industry standards for logging endpoints and queries in Rust/GraphQL
- Implement logging for existing REST endpoints and GraphQL queries (check Phase 1 for a list of endpoints and queries)
- For REST endpoints, log the request method, path, response status, and response time
- For GraphQL queries, log the operation name and response time (no parameters)
- Implement
PasswordServicefor user password management-
generatemethod to create a new password hash- Already includes the salt generation
-
verifymethod to check a password against a hash
-
- Test the
PasswordServicemethods - Document the
PasswordServicemethods via Rust doc comments
- Configure
sqlxto use SQLite database indata/development.db(filename to come from environment variable) - Create database schema with
sqlxmigrations- Create migration for
user_rolestable - Create migration for
userstable - Schema defined with proper foreign key constraints
- Migrations stored in
config/database/migrations
- Create migration for
- Run migrations to create the database schema
- Verify support for schema dump after running migrations, to be stored in
config/database/schema.sql - Implement SQL Query objects following project patterns
- Create comprehensive unit tests for all query objects
- Create integration tests demonstrating complete user creation flow
- Implement seeds system with default user roles
- Suggest industry standards for user roles and permissions in Rust/GraphQL
- Permissions are granular (e.g.,
can_create_user,can_delete_user, etc.) and can be assigned to roles - Update the database schema to store permissions for roles (details to be defined)
- Implement
UserRoleServicefor managing user roles and checking permissions-
get_role_by_idmethod to retrieve a role by ID -
get_role_by_namemethod to retrieve a role by name -
check_permissionmethod to check if a user has a specific permission
-
- Implement
UserServicefor user management-
create_usermethod to create a new user- Validates input (username and password) and checks for username already in use
- Uses
PasswordServiceto hash the password - Assigns default role to the user
-
get_user_by_idmethod to retrieve a user by ID -
get_user_by_namemethod to retrieve a user by name
-
- Implement
createUserGraphQL mutation- Calls
UserService::create_user - Returns the created user object
- Calls
- Unit tests for the
createUsermutation- Tests should verify that the mutation calls the
UserService::create_usermethod with the correct parameters
- Tests should verify that the mutation calls the
- Integration tests for the
createUsermutation- Call the endpoint to create a user
- Implement
SessionServicefor managing session tokens- We're using a custom encrypted token format, not JWT
- Why not JWT? Because we don't want to expose the payload structure to consumers
- Consumers will call a future
getCurrentSessionquery to retrieve the session payload with the token in the request (header or cookie)
- Define the payload structure for the session token - JSON-serializable
- Fields:
sub(subject - user id),iat(issued at timestamp),exp(expiration timestamp)
- Fields:
- Requires a secret key for signing tokens, stored in an environment variable (
DP_AUTH_SECRET_KEY)- Determine how the service will act if the secret key is not set
-
encode_tokenmethod to create a session token from a payload -
decode_tokenmethod to decode a session token and retrieve the payload
- We're using a custom encrypted token format, not JWT
- Test the
SessionServicemethods - Document the
SessionServicemethods via Rust doc comments
- Implement middleware for session token management, to be used in the GraphQL API only (all queries and mutations)
- Middleware should:
- Check for the session token in the request header or cookie, in that order (if both present, prefer the header)
- Do not fallback to cookie if the header token is present but invalid
- Decode the token using
SessionService::decode_token - If valid, attach the decoded session token payload to the request context
- If invalid or missing, don't attach the payload and allow the request to proceed without it (each resolver will handle the absence of the payload)
- Check for the session token in the request header or cookie, in that order (if both present, prefer the header)
- Decisions to make:
- What is the prefix for the auth header, considering it's a custom encrypted token? ("Bearer" or something else?)
- Do we need to know the cookie name? If so, use
DpAuthSessionas the cookie name and store this string in a constant
- Allow resolvers to have access to the session token payload via the request context
- Resolvers can then propagate the session token payload to the Service objects on a case-by-case basis
- Unit tests to ensure the middleware is attaching the session token payload when valid
- Integration tests will be implemented in a later phase, by resolvers that actually use the session token payload
- Accepts username and password as input
- Calls
UserService::get_user_by_nameto retrieve the user by username - Calls
PasswordService::verifyto check the password against the stored hash - If valid, calls
SessionService::encode_tokento create a session token - Returns the session token, or an error if the credentials are invalid
- Error message should be specific (e.g., "User is blank" or "User not found" or "Password is blank" or "Password does not match", etc)
- Errors should be logged using the existing logging system and must contain the given username (not the password) for debugging
- Errors should be logged at
infolevel, notwarnorerror-- those errors should not raise alarms in our logs, they're just infomational
- Verify existing CORS configuration for 'with_credentials' support
- Unit tests for the
create_sessionmethod - Document the
create_sessionmethod via Rust doc comments
- Implement
createSessionGraphQL mutation- Accepts username and password as input
- Calls
SessionService::create_session- On success, returns the session token and sets the cookie with the token in the response
- On failure, returns a user-friendly error message (not the internal error message), e.g., "Invalid credentials" for any username or password error, or "Internal server error" for unexpected errors
- Make a decision if errors should return 2XX or 4XX status codes, as it impacts the client-side error handling (decision: 2XX)
- Unit tests for the
createSessionmutation, ensuring it calls theSessionService::create_sessionmethod with the correct parameters - Integration tests for the
createSessionmutation, ensuring it returns a session token and sets the cookie in the response
- Returns the current session token payload, set by the session middleware
- Make a decision if the query should return 2XX or 401 status code if the session token is not present or invalid
- Unit tests for the
getCurrentSessionquery - Integration tests for the
getCurrentSessionquery
- Implement a 404 handler for the REST API
- Requests to non-existent endpoints should return a 404 status code with a plain "NOT FOUND" message
- Requests should be logged with the request method, path, querystring, and body size (if present); log level should be
info - Integration tests for the 404 handler
- Email support
- Cookie-less session management (using custom headers in response)
- Password change (when logged in)
- Password reset (requires email support)
- Username change
- User deletion/redaction
user_sessionstable to track user sessions and make tokens revocable