From 5357528b650577c954b4f328ff4c4039d340d081 Mon Sep 17 00:00:00 2001 From: Sergey Chernyshev Date: Sun, 1 Feb 2026 21:37:55 -0500 Subject: [PATCH 1/2] Formatted specs --- specs/accounts.md | 52 ++++++++++++++++-------------- specs/admin.md | 38 ++++++++++++---------- specs/api.md | 42 ++++++++++++++----------- specs/architecture.md | 44 +++++++++++++++----------- specs/billing.md | 73 ++++++++++++++++++++++++------------------- specs/contributors.md | 46 ++++++++++++++++----------- specs/gamification.md | 49 ++++++++++++++++------------- specs/iam.md | 56 +++++++++++++++++++-------------- 8 files changed, 226 insertions(+), 174 deletions(-) diff --git a/specs/accounts.md b/specs/accounts.md index 74a2b71..148b1a3 100644 --- a/specs/accounts.md +++ b/specs/accounts.md @@ -1,39 +1,45 @@ # Team & Account Management ## Overview + StartupAPI uses a multi-tenant architecture where users belong to "Accounts". An Account acts as a container for data, subscriptions, and team members, effectively allowing a single user to participate in multiple organizations or workspaces. ## Key Components ### 1. Account Entity -* **Class**: `Account` (`classes/Account.php`) -* **Role**: The primary unit of tenancy and billing. -* **Attributes**: - * `id`: Unique identifier. - * `name`: Account name. - * `plan`: Current subscription plan. - * `active`: Status flag. + +- **Class**: `Account` (`classes/Account.php`) +- **Role**: The primary unit of tenancy and billing. +- **Attributes**: + - `id`: Unique identifier. + - `name`: Account name. + - `plan`: Current subscription plan. + - `active`: Status flag. ### 2. User-Account Relationship -* **Many-to-Many**: A user can belong to multiple accounts; an account can have multiple users. -* **Roles**: - * `Account::ROLE_USER` (0): Standard member. - * `Account::ROLE_ADMIN` (1): Account administrator (can manage billing and users). -* **Context**: Users switch between accounts. `User::getCurrentAccount()` retrieves the active context. + +- **Many-to-Many**: A user can belong to multiple accounts; an account can have multiple users. +- **Roles**: + - `Account::ROLE_USER` (0): Standard member. + - `Account::ROLE_ADMIN` (1): Account administrator (can manage billing and users). +- **Context**: Users switch between accounts. `User::getCurrentAccount()` retrieves the active context. ### 3. Invitations -* **Class**: `Invitation` (`classes/Invitation.php`) -* **Functionality**: Allows adding users to the system or specific accounts. -* **Flows**: - * **Admin Invite**: System admins invite users to the platform. - * **Account Invite**: Account admins invite members to their team. -* **Data**: Stores `code`, `issuer`, `recipient_email`, `target_account_id`. + +- **Class**: `Invitation` (`classes/Invitation.php`) +- **Functionality**: Allows adding users to the system or specific accounts. +- **Flows**: + - **Admin Invite**: System admins invite users to the platform. + - **Account Invite**: Account admins invite members to their team. +- **Data**: Stores `code`, `issuer`, `recipient_email`, `target_account_id`. ## Workflows -* **Creation**: Every new user gets a personal Account by default. -* **Switching**: Users can switch context via the UI (usually top navigation). -* **Membership**: Account admins can add/remove users via the Account Settings page. + +- **Creation**: Every new user gets a personal Account by default. +- **Switching**: Users can switch context via the UI (usually top navigation). +- **Membership**: Account admins can add/remove users via the Account Settings page. ## Security -* **Isolation**: Data queries should be scoped to the `current_account` to ensure tenant isolation. -* **Permissions**: Only Account Admins can modify billing settings or plan subscriptions. + +- **Isolation**: Data queries should be scoped to the `current_account` to ensure tenant isolation. +- **Permissions**: Only Account Admins can modify billing settings or plan subscriptions. diff --git a/specs/admin.md b/specs/admin.md index 1fa3217..ffd3026 100644 --- a/specs/admin.md +++ b/specs/admin.md @@ -1,32 +1,38 @@ # Administrative Control ## Overview + The Admin Panel provides a centralized interface for platform owners to manage users, accounts, subscriptions, and system settings. It is secured to allow access only to users with global administrative privileges. ## Key Components ### 1. Admin Menu System -* **Class**: `MenuElement` (Abstract) and subclasses in `admin/adminMenus.php`. -* **Structure**: Hierarchical menu defining the admin navigation. -* **Features**: - * Supports nested sub-menus. - * Handles active state highlighting. - * Can disable menu items with "Coming soon" tooltips. + +- **Class**: `MenuElement` (Abstract) and subclasses in `admin/adminMenus.php`. +- **Structure**: Hierarchical menu defining the admin navigation. +- **Features**: + - Supports nested sub-menus. + - Handles active state highlighting. + - Can disable menu items with "Coming soon" tooltips. ### 2. User & Account Administration -* **Users**: (`admin/users.php`) List, search, and edit user details. -* **Accounts**: (`admin/accounts.php`) Manage account statuses, view details, and intervene in billing issues. -* **Impersonation**: Admins can log in as any user to reproduce bugs or assist with configuration. + +- **Users**: (`admin/users.php`) List, search, and edit user details. +- **Accounts**: (`admin/accounts.php`) Manage account statuses, view details, and intervene in billing issues. +- **Impersonation**: Admins can log in as any user to reproduce bugs or assist with configuration. ### 3. Subscription Management -* **Plans**: (`admin/plans.php`) View and edit subscription plans. -* **Outstanding Payments**: (`admin/outstanding.php`) Monitor failed charges and overdue accounts. -* **Transaction Logs**: (`admin/transaction_log.php`) Audit trail of all financial transactions. + +- **Plans**: (`admin/plans.php`) View and edit subscription plans. +- **Outstanding Payments**: (`admin/outstanding.php`) Monitor failed charges and overdue accounts. +- **Transaction Logs**: (`admin/transaction_log.php`) Audit trail of all financial transactions. ### 4. System Settings & Modules -* **Settings**: (`admin/settings.php`) General platform configuration. -* **Modules**: (`admin/modules.php`) Enable/disable and configure system modules (Authentication, Payment, etc.). + +- **Settings**: (`admin/settings.php`) General platform configuration. +- **Modules**: (`admin/modules.php`) Enable/disable and configure system modules (Authentication, Payment, etc.). ## Security -* **Access Control**: All admin pages verify that the current user has global admin privileges (`Account::ROLE_ADMIN` context on the system account or specific flag). -* **Audit**: Critical actions are logged. + +- **Access Control**: All admin pages verify that the current user has global admin privileges (`Account::ROLE_ADMIN` context on the system account or specific flag). +- **Audit**: Critical actions are logged. diff --git a/specs/api.md b/specs/api.md index 275a39c..73daf38 100644 --- a/specs/api.md +++ b/specs/api.md @@ -1,34 +1,40 @@ # Developer Platform / API ## Overview + StartupAPI provides a RESTful API (v1) to allow external applications and client-side frontends to interact with the platform. It features a structured endpoint system, parameter validation, and authentication. ## Key Components ### 1. Endpoint Architecture -* **Base Class**: `Endpoint` (`classes/API/Endpoint.php`). -* **Registration**: Endpoints are registered to a namespace and HTTP method via `Endpoint::register()`. -* **Discovery**: `api.php` handles routing based on the URL structure (e.g., `/api/v1/user`). + +- **Base Class**: `Endpoint` (`classes/API/Endpoint.php`). +- **Registration**: Endpoints are registered to a namespace and HTTP method via `Endpoint::register()`. +- **Discovery**: `api.php` handles routing based on the URL structure (e.g., `/api/v1/user`). ### 2. Request Handling -* **Routing**: Logic in `Endpoint::getEndpoint()` resolves the URL slug to a specific handler. -* **Parameters**: - * **Definition**: Endpoints define expected parameters (`Parameter` class). - * **Validation**: Built-in type checking and required/optional validation. - * **Parsing**: `parseURLEncoded` helper for processing input. + +- **Routing**: Logic in `Endpoint::getEndpoint()` resolves the URL slug to a specific handler. +- **Parameters**: + - **Definition**: Endpoints define expected parameters (`Parameter` class). + - **Validation**: Built-in type checking and required/optional validation. + - **Parsing**: `parseURLEncoded` helper for processing input. ### 3. Authentication & Security -* **Base Class**: `AuthenticatedEndpoint`. -* **Mechanism**: automatically checks for a valid session or API token before processing the request. -* **Exceptions**: - * `UnauthenticatedException` (401) - * `UnauthorizedException` (403) - * `MethodNotAllowedException` + +- **Base Class**: `AuthenticatedEndpoint`. +- **Mechanism**: automatically checks for a valid session or API token before processing the request. +- **Exceptions**: + - `UnauthenticatedException` (401) + - `UnauthorizedException` (403) + - `MethodNotAllowedException` ### 4. Core Endpoints -* **Namespace**: `v1` (configurable). -* **User**: `v1/User/Get.php` - Retrieve current user details. -* **Accounts**: `v1/Accounts.php` - List and manage user accounts. + +- **Namespace**: `v1` (configurable). +- **User**: `v1/User/Get.php` - Retrieve current user details. +- **Accounts**: `v1/Accounts.php` - List and manage user accounts. ## Documentation -* **Swagger/OpenAPI**: The project includes tools (`tools/swagger_validate.py`) and UI (`swagger-ui/`) to generate and display interactive API documentation. + +- **Swagger/OpenAPI**: The project includes tools (`tools/swagger_validate.py`) and UI (`swagger-ui/`) to generate and display interactive API documentation. diff --git a/specs/architecture.md b/specs/architecture.md index fbfd874..1860a51 100644 --- a/specs/architecture.md +++ b/specs/architecture.md @@ -1,35 +1,41 @@ # System Architecture & Utilities ## Overview + StartupAPI is built as a modular PHP application, designed for flexibility and rapid development. It employs a "Pluggable" architecture for core features and relies on established libraries for templating and frontend presentation. ## Core Architecture ### 1. Initialization & Bootstrapping -* **Entry Point**: `global.php` initializes the environment, loads configuration (`users_config.php`), and starts the session. -* **Main Class**: `StartupAPI` (`classes/StartupAPI.php`) serves as the central static accessor for global state and helper methods. -* **Autoloading**: Uses standard `require_once` patterns and Composer/library autoloaders where applicable. + +- **Entry Point**: `global.php` initializes the environment, loads configuration (`users_config.php`), and starts the session. +- **Main Class**: `StartupAPI` (`classes/StartupAPI.php`) serves as the central static accessor for global state and helper methods. +- **Autoloading**: Uses standard `require_once` patterns and Composer/library autoloaders where applicable. ### 2. Module System -* **Base Class**: `StartupAPIModule`. -* **Concept**: Functionality like Authentication, Payments, and Emailing are encapsulated in modules. -* **Registry**: `UserConfig::$all_modules` holds the list of active modules. -* **Extensibility**: Developers can create new modules by extending the base class and registering them in the config. + +- **Base Class**: `StartupAPIModule`. +- **Concept**: Functionality like Authentication, Payments, and Emailing are encapsulated in modules. +- **Registry**: `UserConfig::$all_modules` holds the list of active modules. +- **Extensibility**: Developers can create new modules by extending the base class and registering them in the config. ### 3. Frontend & Templating -* **Engine**: **Twig** is the primary templating engine (`twig/`). -* **Themes**: Support for multiple themes (`themes/awesome`, `themes/classic`). -* **UI Framework**: Heavy reliance on **Bootstrap** (v2/v3) for responsive layout and components. -* **Assets**: `bootswatch` integration allows for easy visual customization. + +- **Engine**: **Twig** is the primary templating engine (`twig/`). +- **Themes**: Support for multiple themes (`themes/awesome`, `themes/classic`). +- **UI Framework**: Heavy reliance on **Bootstrap** (v2/v3) for responsive layout and components. +- **Assets**: `bootswatch` integration allows for easy visual customization. ### 4. Utilities -* **Database Migration**: `dbupgrade.php` manages schema versioning and updates, ensuring the database stays in sync with the code. -* **Cron**: `cron.php` handles scheduled background tasks, essential for subscription billing and maintenance. -* **Dependency Check**: `depcheck.php` verifies that the server environment meets all requirements. + +- **Database Migration**: `dbupgrade.php` manages schema versioning and updates, ensuring the database stays in sync with the code. +- **Cron**: `cron.php` handles scheduled background tasks, essential for subscription billing and maintenance. +- **Dependency Check**: `depcheck.php` verifies that the server environment meets all requirements. ## File Structure -* `classes/`: Core logic and business entities. -* `modules/`: Pluggable functional blocks. -* `admin/`: Administrative interface logic. -* `themes/` & `view/`: Presentation layer. -* `controller/`: Request handling logic (MVC pattern). + +- `classes/`: Core logic and business entities. +- `modules/`: Pluggable functional blocks. +- `admin/`: Administrative interface logic. +- `themes/` & `view/`: Presentation layer. +- `controller/`: Request handling logic (MVC pattern). diff --git a/specs/billing.md b/specs/billing.md index c1180bb..a925e7a 100644 --- a/specs/billing.md +++ b/specs/billing.md @@ -1,51 +1,58 @@ # Subscription & Billing Engine ## Overview + The Subscription & Billing engine provides a flexible system for monetizing the application. It supports multiple subscription plans, varying payment schedules, and abstract payment gateways. ## Key Components ### 1. Plans -* **Class**: `Plan` (`classes/Plan.php`) -* **Definition**: Represents a subscription tier (e.g., "Basic", "Pro"). -* **Attributes**: - * `slug`: Unique identifier. - * `name`: Display name. - * `capabilities`: Feature flags enabled for this plan. - * `downgrade_to_slug`: Fallback plan upon cancellation. - * `grace_period`: Days to wait for payment before downgrading. -* **Hooks**: `account_activate_hook` and `account_deactivate_hook` for custom logic during plan changes. + +- **Class**: `Plan` (`classes/Plan.php`) +- **Definition**: Represents a subscription tier (e.g., "Basic", "Pro"). +- **Attributes**: + - `slug`: Unique identifier. + - `name`: Display name. + - `capabilities`: Feature flags enabled for this plan. + - `downgrade_to_slug`: Fallback plan upon cancellation. + - `grace_period`: Days to wait for payment before downgrading. +- **Hooks**: `account_activate_hook` and `account_deactivate_hook` for custom logic during plan changes. ### 2. Payment Schedules -* **Class**: `PaymentSchedule` (`classes/PaymentSchedule.php`) -* **Definition**: Defines how often and how much a user is charged for a plan. -* **Attributes**: - * `charge_amount`: Cost per period. - * `charge_period`: Frequency of billing (in days). - * `is_default`: Default schedule for a plan. + +- **Class**: `PaymentSchedule` (`classes/PaymentSchedule.php`) +- **Definition**: Defines how often and how much a user is charged for a plan. +- **Attributes**: + - `charge_amount`: Cost per period. + - `charge_period`: Frequency of billing (in days). + - `is_default`: Default schedule for a plan. ### 3. Payment Engines -* **Class**: `PaymentEngine` (`classes/PaymentEngine.php`) -* **Role**: Abstract interface for payment providers. -* **Implementations**: - * **Stripe**: Credit card processing. - * **External**: Manual or off-platform payments. -* **Functionality**: Handles charge requests, recurrent billing setup, and webhook processing. + +- **Class**: `PaymentEngine` (`classes/PaymentEngine.php`) +- **Role**: Abstract interface for payment providers. +- **Implementations**: + - **Stripe**: Credit card processing. + - **External**: Manual or off-platform payments. +- **Functionality**: Handles charge requests, recurrent billing setup, and webhook processing. ### 4. Account Billing State -* **Management**: Handled within the `Account` class. -* **States**: - * Active plan. - * Next plan (scheduled change). - * Outstanding balance. -* **Lifecycle**: - * **Upgrades/Downgrades**: Handled with proration logic. - * **Cancellation**: Reverts to the "downgrade" plan (usually Free) after the current period or grace period. + +- **Management**: Handled within the `Account` class. +- **States**: + - Active plan. + - Next plan (scheduled change). + - Outstanding balance. +- **Lifecycle**: + - **Upgrades/Downgrades**: Handled with proration logic. + - **Cancellation**: Reverts to the "downgrade" plan (usually Free) after the current period or grace period. ## Configuration -* **Plan Definition**: Plans and schedules are defined in `users_config.php` passed to `Plan::init()`. -* **Gateways**: Credentials for providers like Stripe are configured in `UserConfig`. + +- **Plan Definition**: Plans and schedules are defined in `users_config.php` passed to `Plan::init()`. +- **Gateways**: Credentials for providers like Stripe are configured in `UserConfig`. ## User Interface -* **Plans Page** (`plans.php`): Displays available plans and allows users to subscribe or switch. -* **Billing History**: Users can view past transactions and receipts (managed by `Account` and `TransactionLogger`). + +- **Plans Page** (`plans.php`): Displays available plans and allows users to subscribe or switch. +- **Billing History**: Users can view past transactions and receipts (managed by `Account` and `TransactionLogger`). diff --git a/specs/contributors.md b/specs/contributors.md index eb19b72..e5a4edc 100644 --- a/specs/contributors.md +++ b/specs/contributors.md @@ -3,40 +3,48 @@ This document outlines the primary contributors for each major feature of the StartupAPI project, based on git commit history. ## Summary + **Sergey Chernyshev** is the primary architect and developer across all components of the system. **whale2 (whale)** and **Alex Druk (Alex)** have made targeted contributions to specific modules. ## Detailed Breakdown ### 1. User Identity & Access Management (IAM) -* **Sergey Chernyshev**: 179 commits -* *Note: Sole contributor to the core IAM logic.* + +- **Sergey Chernyshev**: 179 commits +- _Note: Sole contributor to the core IAM logic._ ### 2. Subscription & Billing -* **Sergey Chernyshev**: 35 commits -* **whale2 (whale)**: 12 commits -* *Note: Significant collaboration on the billing engine.* + +- **Sergey Chernyshev**: 35 commits +- **whale2 (whale)**: 12 commits +- _Note: Significant collaboration on the billing engine._ ### 3. Team & Account Management -* **Sergey Chernyshev**: 42 commits -* *Note: Sole contributor.* + +- **Sergey Chernyshev**: 42 commits +- _Note: Sole contributor._ ### 4. Gamification & Engagement -* **Sergey Chernyshev**: 16 commits -* **Alex Druk**: 1 commit -* *Note: Primarily Sergey, with minor input from Alex.* + +- **Sergey Chernyshev**: 16 commits +- **Alex Druk**: 1 commit +- _Note: Primarily Sergey, with minor input from Alex._ ### 5. Administrative Control (Admin Panel) -* **Sergey Chernyshev**: 227 commits -* **whale2**: 17 commits -* *Note: Heavy collaboration on the admin interface.* + +- **Sergey Chernyshev**: 227 commits +- **whale2**: 17 commits +- _Note: Heavy collaboration on the admin interface._ ### 6. Developer Platform / API -* **Sergey Chernyshev**: 23 commits -* *Note: Sole contributor.* + +- **Sergey Chernyshev**: 23 commits +- _Note: Sole contributor._ ### 7. System Architecture -* **Sergey Chernyshev**: 107 commits -* **whale2 (whale)**: 12 commits -* **Alex Druk (Alex)**: 3 commits -* *Note: Core system design was led by Sergey with contributions from others.* + +- **Sergey Chernyshev**: 107 commits +- **whale2 (whale)**: 12 commits +- **Alex Druk (Alex)**: 3 commits +- _Note: Core system design was led by Sergey with contributions from others._ diff --git a/specs/gamification.md b/specs/gamification.md index 925bd5b..05b9ef0 100644 --- a/specs/gamification.md +++ b/specs/gamification.md @@ -1,36 +1,41 @@ # Gamification & Engagement ## Overview + This module increases user retention and engagement through a badge-based achievement system and detailed campaign tracking for attribution. ## Key Components ### 1. Badge System -* **Class**: `Badge` (`classes/Badge.php`) -* **Concept**: Awards users for specific actions or milestones. -* **Structure**: - * `id` & `slug`: Unique identifiers. - * `set`: Grouping for badges. - * `title` & `description`: User-facing text. - * `hint`: Clue on how to unlock the badge. - * `calls_to_action`: Messages encouraging further progress. -* **Triggers**: `BadgeActivityTrigger` allows defining events that automatically award badges. -* **Levels**: Badges can have multiple levels, supporting progressive achievement. + +- **Class**: `Badge` (`classes/Badge.php`) +- **Concept**: Awards users for specific actions or milestones. +- **Structure**: + - `id` & `slug`: Unique identifiers. + - `set`: Grouping for badges. + - `title` & `description`: User-facing text. + - `hint`: Clue on how to unlock the badge. + - `calls_to_action`: Messages encouraging further progress. +- **Triggers**: `BadgeActivityTrigger` allows defining events that automatically award badges. +- **Levels**: Badges can have multiple levels, supporting progressive achievement. ### 2. Campaign Tracking -* **Class**: `CampaignTracker` (`classes/CampaignTracker.php`) -* **Purpose**: Tracks marketing effectiveness. -* **Functionality**: - * **UTM Parameters**: Captures `utm_source`, `utm_medium`, `utm_campaign`, etc., from the URL on landing. - * **Referrer**: Captures `HTTP_REFERER` on the first visit. - * **Persistence**: Stores data in cookies until registration, then associates it with the created User record. -* **Configuration**: `UserConfig::$campaign_variables` defines which URL parameters to track. + +- **Class**: `CampaignTracker` (`classes/CampaignTracker.php`) +- **Purpose**: Tracks marketing effectiveness. +- **Functionality**: + - **UTM Parameters**: Captures `utm_source`, `utm_medium`, `utm_campaign`, etc., from the URL on landing. + - **Referrer**: Captures `HTTP_REFERER` on the first visit. + - **Persistence**: Stores data in cookies until registration, then associates it with the created User record. +- **Configuration**: `UserConfig::$campaign_variables` defines which URL parameters to track. ### 3. Cohorts -* **Class**: `Cohort` (`classes/Cohort.php`) -* **Purpose**: Groups users based on signup date or other shared characteristics for analysis. -* **Usage**: Helps in analyzing retention rates and feature usage over time. + +- **Class**: `Cohort` (`classes/Cohort.php`) +- **Purpose**: Groups users based on signup date or other shared characteristics for analysis. +- **Usage**: Helps in analyzing retention rates and feature usage over time. ## User Experience -* **Notifications**: Users are notified when badges are unlocked. -* **Profile**: Badges are displayed on the user's public or private profile (`show_badge.php`). + +- **Notifications**: Users are notified when badges are unlocked. +- **Profile**: Badges are displayed on the user's public or private profile (`show_badge.php`). diff --git a/specs/iam.md b/specs/iam.md index f20c403..88dc710 100644 --- a/specs/iam.md +++ b/specs/iam.md @@ -1,43 +1,51 @@ # User Identity & Access Management (IAM) ## Overview + The IAM system in StartupAPI is designed to handle user authentication, session management, and profile administration. It supports a wide range of authentication methods through a modular architecture, including email/password, email verification (passwordless), and various OAuth providers. ## Key Components ### 1. User Entity -* **Class**: `User` (`classes/User.php`) -* **Responsibilities**: - * Represents a registered user. - * Manages login sessions via `CookieStorage`. - * Handles user profile data. - * Provides static methods for retrieval (`User::get()`) and access control (`User::require_login()`). + +- **Class**: `User` (`classes/User.php`) +- **Responsibilities**: + - Represents a registered user. + - Manages login sessions via `CookieStorage`. + - Handles user profile data. + - Provides static methods for retrieval (`User::get()`) and access control (`User::require_login()`). ### 2. Authentication Modules + Authentication is handled via subclasses of `AuthenticationModule` (extending `StartupAPIModule`). -* **Location**: `modules/` -* **Types**: - * **Username/Password**: Standard email and password login (`modules/usernamepass`). - * **Verified Email**: Login via a link sent to email (`modules/email`). - * **OAuth Providers**: Facebook, Google, Twitter, GitHub, LinkedIn, etc. - * **Service Auth**: Integration with platforms like Amazon and Etsy. + +- **Location**: `modules/` +- **Types**: + - **Username/Password**: Standard email and password login (`modules/usernamepass`). + - **Verified Email**: Login via a link sent to email (`modules/email`). + - **OAuth Providers**: Facebook, Google, Twitter, GitHub, LinkedIn, etc. + - **Service Auth**: Integration with platforms like Amazon and Etsy. ### 3. Session Management -* **Mechanism**: Encrypted cookies. -* **Storage**: `MrClay_CookieStorage`. -* **Security**: Supports `HttpOnly` and `Secure` flags. -* **Configuration**: `UserConfig::$SESSION_SECRET` used for encryption. + +- **Mechanism**: Encrypted cookies. +- **Storage**: `MrClay_CookieStorage`. +- **Security**: Supports `HttpOnly` and `Secure` flags. +- **Configuration**: `UserConfig::$SESSION_SECRET` used for encryption. ### 4. Registration & Login Flows -* **Registration**: Users can sign up via enabled modules. New users are automatically provisioned a personal account. -* **Login**: Unified login page presenting available authentication options. -* **Impersonation**: Administrators can impersonate other users for support purposes (`User::get(true)` allows impersonation). + +- **Registration**: Users can sign up via enabled modules. New users are automatically provisioned a personal account. +- **Login**: Unified login page presenting available authentication options. +- **Impersonation**: Administrators can impersonate other users for support purposes (`User::get(true)` allows impersonation). ## Configuration -* **Modules**: Enabled in `users_config.php`. -* **Namespace**: `UserConfig` class holds global IAM settings. + +- **Modules**: Enabled in `users_config.php`. +- **Namespace**: `UserConfig` class holds global IAM settings. ## Security Features -* **CSRF Protection**: `UserTools::preventCSRF()` (implied usage in forms). -* **Password Hashing**: Implemented within the `usernamepass` module (details in module code). -* **Access Control**: `User::require_login()` enforces authentication on protected pages. + +- **CSRF Protection**: `UserTools::preventCSRF()` (implied usage in forms). +- **Password Hashing**: Implemented within the `usernamepass` module (details in module code). +- **Access Control**: `User::require_login()` enforces authentication on protected pages. From 547b6a2248747737ef41a6da363cf6a0b9721db9 Mon Sep 17 00:00:00 2001 From: Sergey Chernyshev Date: Sun, 1 Feb 2026 21:45:53 -0500 Subject: [PATCH 2/2] Added basic Account DO --- src/AccountDO.ts | 96 +++++++++++++++++++++++++++++++++++++++ src/UserDO.ts | 36 +++++++++++++++ src/auth/index.ts | 39 ++++++++++++++++ src/index.ts | 29 ++++++++++-- test/accountdo.spec.ts | 56 +++++++++++++++++++++++ test/userdo.spec.ts | 23 ++++++++++ vitest.config.mts | 1 + worker-configuration.d.ts | 22 ++++++--- wrangler.jsonc | 12 +++++ 9 files changed, 305 insertions(+), 9 deletions(-) create mode 100644 src/AccountDO.ts create mode 100644 test/accountdo.spec.ts diff --git a/src/AccountDO.ts b/src/AccountDO.ts new file mode 100644 index 0000000..5971b44 --- /dev/null +++ b/src/AccountDO.ts @@ -0,0 +1,96 @@ +import type { StartupAPIEnv } from './StartupAPIEnv'; + +/** + * A Durable Object representing an Account (Tenant). + * This class handles account-specific data, settings, and memberships. + */ +export class AccountDO implements DurableObject { + state: DurableObjectState; + env: StartupAPIEnv; + sql: SqlStorage; + + constructor(state: DurableObjectState, env: StartupAPIEnv) { + this.state = state; + this.env = env; + this.sql = state.storage.sql; + + // Initialize database schema + this.sql.exec(` + CREATE TABLE IF NOT EXISTS account_info ( + key TEXT PRIMARY KEY, + value TEXT + ); + + CREATE TABLE IF NOT EXISTS members ( + user_id TEXT PRIMARY KEY, + role INTEGER, + joined_at INTEGER + ); + `); + } + + async fetch(request: Request): Promise { + const url = new URL(request.url); + const path = url.pathname; + const method = request.method; + + if (path === '/info' && method === 'GET') { + return this.getInfo(); + } else if (path === '/info' && method === 'POST') { + return this.updateInfo(request); + } else if (path === '/members' && method === 'GET') { + return this.getMembers(); + } else if (path === '/members' && method === 'POST') { + return this.addMember(request); + } else if (path.startsWith('/members/') && method === 'DELETE') { + const userId = path.replace('/members/', ''); + return this.removeMember(userId); + } + + return new Response('Not Found', { status: 404 }); + } + + async getInfo(): Promise { + const result = this.sql.exec('SELECT key, value FROM account_info'); + const info: Record = {}; + for (const row of result) { + // @ts-ignore + info[row.key] = JSON.parse(row.value as string); + } + return Response.json(info); + } + + async updateInfo(request: Request): Promise { + const data = (await request.json()) as Record; + + try { + this.state.storage.transactionSync(() => { + for (const [key, value] of Object.entries(data)) { + this.sql.exec('INSERT OR REPLACE INTO account_info (key, value) VALUES (?, ?)', key, JSON.stringify(value)); + } + }); + return Response.json({ success: true }); + } catch (e: any) { + return new Response(e.message, { status: 500 }); + } + } + + async getMembers(): Promise { + const result = this.sql.exec('SELECT user_id, role, joined_at FROM members'); + const members = Array.from(result); + return Response.json(members); + } + + async addMember(request: Request): Promise { + const { user_id, role } = (await request.json()) as { user_id: string; role: number }; + const now = Date.now(); + + this.sql.exec('INSERT OR REPLACE INTO members (user_id, role, joined_at) VALUES (?, ?, ?)', user_id, role, now); + return Response.json({ success: true }); + } + + async removeMember(userId: string): Promise { + this.sql.exec('DELETE FROM members WHERE user_id = ?', userId); + return Response.json({ success: true }); + } +} diff --git a/src/UserDO.ts b/src/UserDO.ts index b5b40bc..63948ed 100644 --- a/src/UserDO.ts +++ b/src/UserDO.ts @@ -54,6 +54,12 @@ export class UserDO implements DurableObject { value BLOB, mime_type TEXT ); + + CREATE TABLE IF NOT EXISTS memberships ( + account_id TEXT PRIMARY KEY, + role INTEGER, + is_current INTEGER + ); `); } @@ -81,6 +87,10 @@ export class UserDO implements DurableObject { return this.deleteSession(request); } else if (path === '/validate-session' && method === 'POST') { return this.validateSession(request); + } else if (path === '/memberships' && method === 'GET') { + return this.getMemberships(); + } else if (path === '/memberships' && method === 'POST') { + return this.addMembership(request); } else if (path.startsWith('/images/') && method === 'GET') { const key = path.replace('/images/', ''); return this.getImage(key); @@ -251,4 +261,30 @@ export class UserDO implements DurableObject { this.sql.exec('DELETE FROM sessions WHERE id = ?', sessionId); return Response.json({ success: true }); } + + async getMemberships(): Promise { + const result = this.sql.exec('SELECT account_id, role, is_current FROM memberships'); + const memberships = Array.from(result); + return Response.json(memberships); + } + + async addMembership(request: Request): Promise { + const { account_id, role, is_current } = (await request.json()) as { + account_id: string; + role: number; + is_current?: boolean; + }; + + if (is_current) { + this.sql.exec('UPDATE memberships SET is_current = 0'); + } + + this.sql.exec( + 'INSERT OR REPLACE INTO memberships (account_id, role, is_current) VALUES (?, ?, ?)', + account_id, + role, + is_current ? 1 : 0, + ); + return Response.json({ success: true }); + } } diff --git a/src/auth/index.ts b/src/auth/index.ts index 6ddbc43..7c31305 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -83,6 +83,45 @@ export async function handleAuth(request: Request, env: StartupAPIEnv, url: URL, }), }); + // Ensure user has at least one account + const membershipsRes = await stub.fetch('http://do/memberships'); + const memberships = (await membershipsRes.json()) as any[]; + + if (memberships.length === 0) { + // Create a personal account + const accountId = env.ACCOUNT.newUniqueId(); + const accountStub = env.ACCOUNT.get(accountId); + const accountIdStr = accountId.toString(); + + // Initialize account info + await accountStub.fetch('http://do/info', { + method: 'POST', + body: JSON.stringify({ + name: `${profile.name || profile.id}'s Account`, + personal: true, + }), + }); + + // Add user as ADMIN to the account + await accountStub.fetch('http://do/members', { + method: 'POST', + body: JSON.stringify({ + user_id: id.toString(), + role: 1, // ADMIN + }), + }); + + // Add membership to user + await stub.fetch('http://do/memberships', { + method: 'POST', + body: JSON.stringify({ + account_id: accountIdStr, + role: 1, // ADMIN + is_current: true, + }), + }); + } + // Create Session const sessionRes = await stub.fetch('http://do/sessions', { method: 'POST' }); const session = (await sessionRes.json()) as any; diff --git a/src/index.ts b/src/index.ts index 96dc775..5ba119a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,11 @@ import { handleAuth } from './auth/index'; import { injectPowerStrip } from './PowerStrip'; import { UserDO } from './UserDO'; +import { AccountDO } from './AccountDO'; const DEFAULT_USERS_PATH = '/users/'; -export { UserDO }; +export { UserDO, AccountDO }; import type { StartupAPIEnv } from './StartupAPIEnv'; @@ -100,11 +101,33 @@ async function handleMe(request: Request, env: StartupAPIEnv): Promise try { const id = env.USER.idFromString(doId); - const stub = env.USER.get(id); - return await stub.fetch('http://do/validate-session', { + const userStub = env.USER.get(id); + const validateRes = await userStub.fetch('http://do/validate-session', { method: 'POST', body: JSON.stringify({ sessionId }), }); + + if (!validateRes.ok) return validateRes; + + const data = (await validateRes.json()) as any; + + // Fetch memberships to find current account + const membershipsRes = await userStub.fetch('http://do/memberships'); + const memberships = (await membershipsRes.json()) as any[]; + const currentMembership = memberships.find((m) => m.is_current) || memberships[0]; + + if (currentMembership) { + const accountId = env.ACCOUNT.idFromString(currentMembership.account_id); + const accountStub = env.ACCOUNT.get(accountId); + const accountInfoRes = await accountStub.fetch('http://do/info'); + if (accountInfoRes.ok) { + data.account = await accountInfoRes.json(); + data.account.id = currentMembership.account_id; + data.account.role = currentMembership.role; + } + } + + return Response.json(data); } catch (e) { return new Response('Unauthorized', { status: 401 }); } diff --git a/test/accountdo.spec.ts b/test/accountdo.spec.ts new file mode 100644 index 0000000..dd1aff0 --- /dev/null +++ b/test/accountdo.spec.ts @@ -0,0 +1,56 @@ +import { env } from 'cloudflare:test'; +import { describe, it, expect } from 'vitest'; + +describe('AccountDO Durable Object', () => { + it('should store and retrieve account info', async () => { + const id = env.ACCOUNT.newUniqueId(); + const stub = env.ACCOUNT.get(id); + + // Update info + const infoData = { name: 'Test Account', plan: 'pro' }; + let res = await stub.fetch('http://do/info', { + method: 'POST', + body: JSON.stringify(infoData), + }); + expect(res.status).toBe(200); + await res.json(); // Drain body + + // Get info + res = await stub.fetch('http://do/info'); + const data = await res.json(); + expect(data).toEqual(infoData); + }); + + it('should manage members', async () => { + const id = env.ACCOUNT.newUniqueId(); + const stub = env.ACCOUNT.get(id); + + const userId = 'user-123'; + const role = 1; // ADMIN + + // Add member + let res = await stub.fetch('http://do/members', { + method: 'POST', + body: JSON.stringify({ user_id: userId, role }), + }); + expect(res.status).toBe(200); + + // Get members + res = await stub.fetch('http://do/members'); + const members: any[] = await res.json(); + expect(members).toHaveLength(1); + expect(members[0].user_id).toBe(userId); + expect(members[0].role).toBe(role); + + // Remove member + res = await stub.fetch(`http://do/members/${userId}`, { + method: 'DELETE', + }); + expect(res.status).toBe(200); + + // Verify member is removed + res = await stub.fetch('http://do/members'); + const membersAfter: any[] = await res.json(); + expect(membersAfter).toHaveLength(0); + }); +}); diff --git a/test/userdo.spec.ts b/test/userdo.spec.ts index b648f31..e158844 100644 --- a/test/userdo.spec.ts +++ b/test/userdo.spec.ts @@ -67,4 +67,27 @@ describe('UserDO Durable Object', () => { validData = await validRes.json(); expect(validData.valid).toBe(false); }); + + it('should manage memberships', async () => { + const id = env.USER.newUniqueId(); + const stub = env.USER.get(id); + + const accountId = 'account-456'; + const role = 1; + + // Add membership + let res = await stub.fetch('http://do/memberships', { + method: 'POST', + body: JSON.stringify({ account_id: accountId, role, is_current: true }), + }); + expect(res.status).toBe(200); + + // Get memberships + res = await stub.fetch('http://do/memberships'); + const memberships: any[] = await res.json(); + expect(memberships).toHaveLength(1); + expect(memberships[0].account_id).toBe(accountId); + expect(memberships[0].role).toBe(role); + expect(memberships[0].is_current).toBe(1); + }); }); diff --git a/vitest.config.mts b/vitest.config.mts index d9430c7..e475b73 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -4,6 +4,7 @@ export default defineWorkersConfig({ test: { poolOptions: { workers: { + isolatedStorage: false, wrangler: { configPath: './wrangler.jsonc' }, }, }, diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index e7155c2..f912031 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -1,22 +1,32 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: 168df5a55f798683edd1cf81f277768c) +// Generated by Wrangler by running `wrangler types` (hash: cd99501151d250db775e0e02b8713ec1) // Runtime types generated with workerd@1.20260120.0 2025-09-27 global_fetch_strictly_public declare namespace Cloudflare { interface GlobalProps { mainModule: typeof import("./src/index"); - durableNamespaces: "UserDO"; + durableNamespaces: "UserDO" | "AccountDO"; } interface PreviewEnv { ASSETS: Fetcher; - ORIGIN_URL: "https://example.com"; - USERS_PATH: "/preview-users/"; + ORIGIN_URL: string; + AUTH_ORIGIN: string; + GOOGLE_CLIENT_ID: string; + GOOGLE_CLIENT_SECRET: string; + TWITCH_CLIENT_ID: string; + TWITCH_CLIENT_SECRET: string; USER: DurableObjectNamespace; + ACCOUNT: DurableObjectNamespace; } interface Env { + ORIGIN_URL: string; + AUTH_ORIGIN: string; + GOOGLE_CLIENT_ID: string; + GOOGLE_CLIENT_SECRET: string; + TWITCH_CLIENT_ID: string; + TWITCH_CLIENT_SECRET: string; ASSETS: Fetcher; - ORIGIN_URL?: "https://example.com"; - USERS_PATH?: "/preview-users/"; USER: DurableObjectNamespace; + ACCOUNT: DurableObjectNamespace; } } interface Env extends Cloudflare.Env {} diff --git a/wrangler.jsonc b/wrangler.jsonc index 60b6aaa..395157b 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -22,6 +22,10 @@ "name": "USER", "class_name": "UserDO", }, + { + "name": "ACCOUNT", + "class_name": "AccountDO", + }, ], }, "migrations": [ @@ -29,6 +33,10 @@ "tag": "v1", "new_sqlite_classes": ["UserDO"], }, + { + "tag": "v2", + "new_sqlite_classes": ["AccountDO"], + }, ], "env": { "preview": { @@ -44,6 +52,10 @@ "name": "USER", "class_name": "UserDO", }, + { + "name": "ACCOUNT", + "class_name": "AccountDO", + }, ], }, },