@mdguggenbichler/slugbase-core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/backend/dist/app-factory.d.ts +17 -0
- package/backend/dist/app-factory.d.ts.map +1 -0
- package/backend/dist/app-factory.js +106 -0
- package/backend/dist/app-factory.js.map +1 -0
- package/backend/dist/auth/authorization.d.ts +25 -0
- package/backend/dist/auth/authorization.d.ts.map +1 -0
- package/backend/dist/auth/authorization.js +100 -0
- package/backend/dist/auth/authorization.js.map +1 -0
- package/backend/dist/auth/jwt.d.ts +5 -0
- package/backend/dist/auth/jwt.d.ts.map +1 -0
- package/backend/dist/auth/jwt.js +34 -0
- package/backend/dist/auth/jwt.js.map +1 -0
- package/backend/dist/auth/oidc.d.ts +4 -0
- package/backend/dist/auth/oidc.d.ts.map +1 -0
- package/backend/dist/auth/oidc.js +201 -0
- package/backend/dist/auth/oidc.js.map +1 -0
- package/backend/dist/config/cloud-providers.d.ts +18 -0
- package/backend/dist/config/cloud-providers.d.ts.map +1 -0
- package/backend/dist/config/cloud-providers.js +60 -0
- package/backend/dist/config/cloud-providers.js.map +1 -0
- package/backend/dist/config/cookies.d.ts +17 -0
- package/backend/dist/config/cookies.d.ts.map +1 -0
- package/backend/dist/config/cookies.js +26 -0
- package/backend/dist/config/cookies.js.map +1 -0
- package/backend/dist/config/mode.d.ts +7 -0
- package/backend/dist/config/mode.d.ts.map +1 -0
- package/backend/dist/config/mode.js +7 -0
- package/backend/dist/config/mode.js.map +1 -0
- package/backend/dist/config/swagger.d.ts +2 -0
- package/backend/dist/config/swagger.d.ts.map +1 -0
- package/backend/dist/config/swagger.js +76 -0
- package/backend/dist/config/swagger.js.map +1 -0
- package/backend/dist/config.d.ts +29 -0
- package/backend/dist/config.d.ts.map +1 -0
- package/backend/dist/config.js +41 -0
- package/backend/dist/config.js.map +1 -0
- package/backend/dist/db/index.d.ts +14 -0
- package/backend/dist/db/index.d.ts.map +1 -0
- package/backend/dist/db/index.js +114 -0
- package/backend/dist/db/index.js.map +1 -0
- package/backend/dist/db/migrate-slug-nullable.d.ts +10 -0
- package/backend/dist/db/migrate-slug-nullable.d.ts.map +1 -0
- package/backend/dist/db/migrate-slug-nullable.js +87 -0
- package/backend/dist/db/migrate-slug-nullable.js.map +1 -0
- package/backend/dist/db/migrations/001_migrate_slug_nullable.d.ts +10 -0
- package/backend/dist/db/migrations/001_migrate_slug_nullable.d.ts.map +1 -0
- package/backend/dist/db/migrations/001_migrate_slug_nullable.js +103 -0
- package/backend/dist/db/migrations/001_migrate_slug_nullable.js.map +1 -0
- package/backend/dist/db/migrations/002_add_oidc_custom_endpoints.d.ts +11 -0
- package/backend/dist/db/migrations/002_add_oidc_custom_endpoints.d.ts.map +1 -0
- package/backend/dist/db/migrations/002_add_oidc_custom_endpoints.js +92 -0
- package/backend/dist/db/migrations/002_add_oidc_custom_endpoints.js.map +1 -0
- package/backend/dist/db/migrations/003_add_bookmark_features.d.ts +5 -0
- package/backend/dist/db/migrations/003_add_bookmark_features.d.ts.map +1 -0
- package/backend/dist/db/migrations/003_add_bookmark_features.js +98 -0
- package/backend/dist/db/migrations/003_add_bookmark_features.js.map +1 -0
- package/backend/dist/db/migrations/004_make_slug_globally_unique.d.ts +12 -0
- package/backend/dist/db/migrations/004_make_slug_globally_unique.d.ts.map +1 -0
- package/backend/dist/db/migrations/004_make_slug_globally_unique.js +152 -0
- package/backend/dist/db/migrations/004_make_slug_globally_unique.js.map +1 -0
- package/backend/dist/db/migrations/005_add_email_verification.d.ts +5 -0
- package/backend/dist/db/migrations/005_add_email_verification.d.ts.map +1 -0
- package/backend/dist/db/migrations/005_add_email_verification.js +102 -0
- package/backend/dist/db/migrations/005_add_email_verification.js.map +1 -0
- package/backend/dist/db/migrations/006_refresh_tokens.d.ts +9 -0
- package/backend/dist/db/migrations/006_refresh_tokens.d.ts.map +1 -0
- package/backend/dist/db/migrations/006_refresh_tokens.js +61 -0
- package/backend/dist/db/migrations/006_refresh_tokens.js.map +1 -0
- package/backend/dist/db/migrations/007_password_reset_token_hash.d.ts +10 -0
- package/backend/dist/db/migrations/007_password_reset_token_hash.d.ts.map +1 -0
- package/backend/dist/db/migrations/007_password_reset_token_hash.js +40 -0
- package/backend/dist/db/migrations/007_password_reset_token_hash.js.map +1 -0
- package/backend/dist/db/migrations/008_slug_preferences.d.ts +8 -0
- package/backend/dist/db/migrations/008_slug_preferences.d.ts.map +1 -0
- package/backend/dist/db/migrations/008_slug_preferences.js +24 -0
- package/backend/dist/db/migrations/008_slug_preferences.js.map +1 -0
- package/backend/dist/db/migrations/009_signup_email_verified.d.ts +5 -0
- package/backend/dist/db/migrations/009_signup_email_verified.d.ts.map +1 -0
- package/backend/dist/db/migrations/009_signup_email_verified.js +79 -0
- package/backend/dist/db/migrations/009_signup_email_verified.js.map +1 -0
- package/backend/dist/db/migrations/010_organizations.d.ts +5 -0
- package/backend/dist/db/migrations/010_organizations.d.ts.map +1 -0
- package/backend/dist/db/migrations/010_organizations.js +113 -0
- package/backend/dist/db/migrations/010_organizations.js.map +1 -0
- package/backend/dist/db/migrations/011_org_scoped_teams.d.ts +11 -0
- package/backend/dist/db/migrations/011_org_scoped_teams.d.ts.map +1 -0
- package/backend/dist/db/migrations/011_org_scoped_teams.js +59 -0
- package/backend/dist/db/migrations/011_org_scoped_teams.js.map +1 -0
- package/backend/dist/db/migrations/012_org_invitations_token_hash.d.ts +10 -0
- package/backend/dist/db/migrations/012_org_invitations_token_hash.d.ts.map +1 -0
- package/backend/dist/db/migrations/012_org_invitations_token_hash.js +40 -0
- package/backend/dist/db/migrations/012_org_invitations_token_hash.js.map +1 -0
- package/backend/dist/db/migrations/013_signup_verification_token_hash.d.ts +10 -0
- package/backend/dist/db/migrations/013_signup_verification_token_hash.d.ts.map +1 -0
- package/backend/dist/db/migrations/013_signup_verification_token_hash.js +39 -0
- package/backend/dist/db/migrations/013_signup_verification_token_hash.js.map +1 -0
- package/backend/dist/db/migrations/014_stats_indexes.d.ts +8 -0
- package/backend/dist/db/migrations/014_stats_indexes.d.ts.map +1 -0
- package/backend/dist/db/migrations/014_stats_indexes.js +19 -0
- package/backend/dist/db/migrations/014_stats_indexes.js.map +1 -0
- package/backend/dist/db/migrations/015_api_tokens.d.ts +9 -0
- package/backend/dist/db/migrations/015_api_tokens.d.ts.map +1 -0
- package/backend/dist/db/migrations/015_api_tokens.js +48 -0
- package/backend/dist/db/migrations/015_api_tokens.js.map +1 -0
- package/backend/dist/db/migrations/016_free_plan_grace_ends_at.d.ts +9 -0
- package/backend/dist/db/migrations/016_free_plan_grace_ends_at.d.ts.map +1 -0
- package/backend/dist/db/migrations/016_free_plan_grace_ends_at.js +50 -0
- package/backend/dist/db/migrations/016_free_plan_grace_ends_at.js.map +1 -0
- package/backend/dist/db/migrations/017_ai_suggestions.d.ts +11 -0
- package/backend/dist/db/migrations/017_ai_suggestions.d.ts.map +1 -0
- package/backend/dist/db/migrations/017_ai_suggestions.js +78 -0
- package/backend/dist/db/migrations/017_ai_suggestions.js.map +1 -0
- package/backend/dist/db/migrations/018_ai_cache_output_language.d.ts +10 -0
- package/backend/dist/db/migrations/018_ai_cache_output_language.d.ts.map +1 -0
- package/backend/dist/db/migrations/018_ai_cache_output_language.js +89 -0
- package/backend/dist/db/migrations/018_ai_cache_output_language.js.map +1 -0
- package/backend/dist/db/migrations/019_ai_suggestion_usage.d.ts +10 -0
- package/backend/dist/db/migrations/019_ai_suggestion_usage.d.ts.map +1 -0
- package/backend/dist/db/migrations/019_ai_suggestion_usage.js +47 -0
- package/backend/dist/db/migrations/019_ai_suggestion_usage.js.map +1 -0
- package/backend/dist/db/migrations/020_tenant_scope.d.ts +5 -0
- package/backend/dist/db/migrations/020_tenant_scope.d.ts.map +1 -0
- package/backend/dist/db/migrations/020_tenant_scope.js +105 -0
- package/backend/dist/db/migrations/020_tenant_scope.js.map +1 -0
- package/backend/dist/db/migrations/_legacy_cloud_010_organizations.d.ts +5 -0
- package/backend/dist/db/migrations/_legacy_cloud_010_organizations.d.ts.map +1 -0
- package/backend/dist/db/migrations/_legacy_cloud_010_organizations.js +113 -0
- package/backend/dist/db/migrations/_legacy_cloud_010_organizations.js.map +1 -0
- package/backend/dist/db/migrations/_legacy_cloud_011_org_scoped_teams.d.ts +11 -0
- package/backend/dist/db/migrations/_legacy_cloud_011_org_scoped_teams.d.ts.map +1 -0
- package/backend/dist/db/migrations/_legacy_cloud_011_org_scoped_teams.js +59 -0
- package/backend/dist/db/migrations/_legacy_cloud_011_org_scoped_teams.js.map +1 -0
- package/backend/dist/db/migrations/_legacy_cloud_012_org_invitations_token_hash.d.ts +10 -0
- package/backend/dist/db/migrations/_legacy_cloud_012_org_invitations_token_hash.d.ts.map +1 -0
- package/backend/dist/db/migrations/_legacy_cloud_012_org_invitations_token_hash.js +40 -0
- package/backend/dist/db/migrations/_legacy_cloud_012_org_invitations_token_hash.js.map +1 -0
- package/backend/dist/db/migrations/_legacy_cloud_016_free_plan_grace_ends_at.d.ts +9 -0
- package/backend/dist/db/migrations/_legacy_cloud_016_free_plan_grace_ends_at.d.ts.map +1 -0
- package/backend/dist/db/migrations/_legacy_cloud_016_free_plan_grace_ends_at.js +50 -0
- package/backend/dist/db/migrations/_legacy_cloud_016_free_plan_grace_ends_at.js.map +1 -0
- package/backend/dist/db/migrations/_legacy_cloud_017_ai_suggestions.d.ts +11 -0
- package/backend/dist/db/migrations/_legacy_cloud_017_ai_suggestions.d.ts.map +1 -0
- package/backend/dist/db/migrations/_legacy_cloud_017_ai_suggestions.js +78 -0
- package/backend/dist/db/migrations/_legacy_cloud_017_ai_suggestions.js.map +1 -0
- package/backend/dist/db/migrations/index.d.ts +22 -0
- package/backend/dist/db/migrations/index.d.ts.map +1 -0
- package/backend/dist/db/migrations/index.js +194 -0
- package/backend/dist/db/migrations/index.js.map +1 -0
- package/backend/dist/db/pool.d.ts +13 -0
- package/backend/dist/db/pool.d.ts.map +1 -0
- package/backend/dist/db/pool.js +41 -0
- package/backend/dist/db/pool.js.map +1 -0
- package/backend/dist/db/seed-data.d.ts +66 -0
- package/backend/dist/db/seed-data.d.ts.map +1 -0
- package/backend/dist/db/seed-data.js +394 -0
- package/backend/dist/db/seed-data.js.map +1 -0
- package/backend/dist/db/seed.d.ts +19 -0
- package/backend/dist/db/seed.d.ts.map +1 -0
- package/backend/dist/db/seed.js +406 -0
- package/backend/dist/db/seed.js.map +1 -0
- package/backend/dist/index.d.ts +3 -0
- package/backend/dist/index.d.ts.map +1 -0
- package/backend/dist/index.js +64 -0
- package/backend/dist/index.js.map +1 -0
- package/backend/dist/instrument.d.ts +2 -0
- package/backend/dist/instrument.d.ts.map +1 -0
- package/backend/dist/instrument.js +16 -0
- package/backend/dist/instrument.js.map +1 -0
- package/backend/dist/load-env.d.ts +2 -0
- package/backend/dist/load-env.d.ts.map +1 -0
- package/backend/dist/load-env.js +33 -0
- package/backend/dist/load-env.js.map +1 -0
- package/backend/dist/middleware/auth.d.ts +23 -0
- package/backend/dist/middleware/auth.d.ts.map +1 -0
- package/backend/dist/middleware/auth.js +69 -0
- package/backend/dist/middleware/auth.js.map +1 -0
- package/backend/dist/middleware/error-handler.d.ts +11 -0
- package/backend/dist/middleware/error-handler.d.ts.map +1 -0
- package/backend/dist/middleware/error-handler.js +40 -0
- package/backend/dist/middleware/error-handler.js.map +1 -0
- package/backend/dist/middleware/security.d.ts +26 -0
- package/backend/dist/middleware/security.d.ts.map +1 -0
- package/backend/dist/middleware/security.js +162 -0
- package/backend/dist/middleware/security.js.map +1 -0
- package/backend/dist/middleware/stats-auth.d.ts +7 -0
- package/backend/dist/middleware/stats-auth.d.ts.map +1 -0
- package/backend/dist/middleware/stats-auth.js +20 -0
- package/backend/dist/middleware/stats-auth.js.map +1 -0
- package/backend/dist/middleware/tenant.d.ts +3 -0
- package/backend/dist/middleware/tenant.d.ts.map +1 -0
- package/backend/dist/middleware/tenant.js +13 -0
- package/backend/dist/middleware/tenant.js.map +1 -0
- package/backend/dist/register-routes.d.ts +12 -0
- package/backend/dist/register-routes.d.ts.map +1 -0
- package/backend/dist/register-routes.js +59 -0
- package/backend/dist/register-routes.js.map +1 -0
- package/backend/dist/routes/admin/demo-reset.d.ts +8 -0
- package/backend/dist/routes/admin/demo-reset.d.ts.map +1 -0
- package/backend/dist/routes/admin/demo-reset.js +66 -0
- package/backend/dist/routes/admin/demo-reset.js.map +1 -0
- package/backend/dist/routes/admin/settings.d.ts +3 -0
- package/backend/dist/routes/admin/settings.d.ts.map +1 -0
- package/backend/dist/routes/admin/settings.js +452 -0
- package/backend/dist/routes/admin/settings.js.map +1 -0
- package/backend/dist/routes/admin/stats.d.ts +7 -0
- package/backend/dist/routes/admin/stats.d.ts.map +1 -0
- package/backend/dist/routes/admin/stats.js +66 -0
- package/backend/dist/routes/admin/stats.js.map +1 -0
- package/backend/dist/routes/admin/teams.d.ts +3 -0
- package/backend/dist/routes/admin/teams.d.ts.map +1 -0
- package/backend/dist/routes/admin/teams.js +509 -0
- package/backend/dist/routes/admin/teams.js.map +1 -0
- package/backend/dist/routes/admin/users.d.ts +3 -0
- package/backend/dist/routes/admin/users.d.ts.map +1 -0
- package/backend/dist/routes/admin/users.js +525 -0
- package/backend/dist/routes/admin/users.js.map +1 -0
- package/backend/dist/routes/auth.d.ts +3 -0
- package/backend/dist/routes/auth.d.ts.map +1 -0
- package/backend/dist/routes/auth.js +992 -0
- package/backend/dist/routes/auth.js.map +1 -0
- package/backend/dist/routes/billing.d.ts +8 -0
- package/backend/dist/routes/billing.d.ts.map +1 -0
- package/backend/dist/routes/billing.js +481 -0
- package/backend/dist/routes/billing.js.map +1 -0
- package/backend/dist/routes/bookmarks.d.ts +3 -0
- package/backend/dist/routes/bookmarks.d.ts.map +1 -0
- package/backend/dist/routes/bookmarks.js +1593 -0
- package/backend/dist/routes/bookmarks.js.map +1 -0
- package/backend/dist/routes/config.d.ts +7 -0
- package/backend/dist/routes/config.d.ts.map +1 -0
- package/backend/dist/routes/config.js +52 -0
- package/backend/dist/routes/config.js.map +1 -0
- package/backend/dist/routes/contact.d.ts +9 -0
- package/backend/dist/routes/contact.d.ts.map +1 -0
- package/backend/dist/routes/contact.js +99 -0
- package/backend/dist/routes/contact.js.map +1 -0
- package/backend/dist/routes/csrf.d.ts +3 -0
- package/backend/dist/routes/csrf.d.ts.map +1 -0
- package/backend/dist/routes/csrf.js +39 -0
- package/backend/dist/routes/csrf.js.map +1 -0
- package/backend/dist/routes/dashboard.d.ts +3 -0
- package/backend/dist/routes/dashboard.d.ts.map +1 -0
- package/backend/dist/routes/dashboard.js +212 -0
- package/backend/dist/routes/dashboard.js.map +1 -0
- package/backend/dist/routes/email-verification.d.ts +3 -0
- package/backend/dist/routes/email-verification.d.ts.map +1 -0
- package/backend/dist/routes/email-verification.js +124 -0
- package/backend/dist/routes/email-verification.js.map +1 -0
- package/backend/dist/routes/folders.d.ts +3 -0
- package/backend/dist/routes/folders.d.ts.map +1 -0
- package/backend/dist/routes/folders.js +524 -0
- package/backend/dist/routes/folders.js.map +1 -0
- package/backend/dist/routes/go-helpers.d.ts +18 -0
- package/backend/dist/routes/go-helpers.d.ts.map +1 -0
- package/backend/dist/routes/go-helpers.js +64 -0
- package/backend/dist/routes/go-helpers.js.map +1 -0
- package/backend/dist/routes/go.d.ts +23 -0
- package/backend/dist/routes/go.d.ts.map +1 -0
- package/backend/dist/routes/go.js +361 -0
- package/backend/dist/routes/go.js.map +1 -0
- package/backend/dist/routes/health.d.ts +6 -0
- package/backend/dist/routes/health.d.ts.map +1 -0
- package/backend/dist/routes/health.js +79 -0
- package/backend/dist/routes/health.js.map +1 -0
- package/backend/dist/routes/invitations.d.ts +3 -0
- package/backend/dist/routes/invitations.d.ts.map +1 -0
- package/backend/dist/routes/invitations.js +172 -0
- package/backend/dist/routes/invitations.js.map +1 -0
- package/backend/dist/routes/oidc-providers.d.ts +3 -0
- package/backend/dist/routes/oidc-providers.d.ts.map +1 -0
- package/backend/dist/routes/oidc-providers.js +495 -0
- package/backend/dist/routes/oidc-providers.js.map +1 -0
- package/backend/dist/routes/organizations.d.ts +3 -0
- package/backend/dist/routes/organizations.d.ts.map +1 -0
- package/backend/dist/routes/organizations.js +538 -0
- package/backend/dist/routes/organizations.js.map +1 -0
- package/backend/dist/routes/password-reset.d.ts +3 -0
- package/backend/dist/routes/password-reset.d.ts.map +1 -0
- package/backend/dist/routes/password-reset.js +212 -0
- package/backend/dist/routes/password-reset.js.map +1 -0
- package/backend/dist/routes/redirect.d.ts +3 -0
- package/backend/dist/routes/redirect.d.ts.map +1 -0
- package/backend/dist/routes/redirect.js +124 -0
- package/backend/dist/routes/redirect.js.map +1 -0
- package/backend/dist/routes/tags.d.ts +3 -0
- package/backend/dist/routes/tags.d.ts.map +1 -0
- package/backend/dist/routes/tags.js +302 -0
- package/backend/dist/routes/tags.js.map +1 -0
- package/backend/dist/routes/teams.d.ts +3 -0
- package/backend/dist/routes/teams.d.ts.map +1 -0
- package/backend/dist/routes/teams.js +60 -0
- package/backend/dist/routes/teams.js.map +1 -0
- package/backend/dist/routes/tokens.d.ts +3 -0
- package/backend/dist/routes/tokens.d.ts.map +1 -0
- package/backend/dist/routes/tokens.js +157 -0
- package/backend/dist/routes/tokens.js.map +1 -0
- package/backend/dist/routes/users.d.ts +3 -0
- package/backend/dist/routes/users.d.ts.map +1 -0
- package/backend/dist/routes/users.js +199 -0
- package/backend/dist/routes/users.js.map +1 -0
- package/backend/dist/services/ai-suggestions.d.ts +29 -0
- package/backend/dist/services/ai-suggestions.d.ts.map +1 -0
- package/backend/dist/services/ai-suggestions.js +163 -0
- package/backend/dist/services/ai-suggestions.js.map +1 -0
- package/backend/dist/services/api-tokens.d.ts +66 -0
- package/backend/dist/services/api-tokens.d.ts.map +1 -0
- package/backend/dist/services/api-tokens.js +129 -0
- package/backend/dist/services/api-tokens.js.map +1 -0
- package/backend/dist/services/fetch-page-metadata.d.ts +15 -0
- package/backend/dist/services/fetch-page-metadata.d.ts.map +1 -0
- package/backend/dist/services/fetch-page-metadata.js +205 -0
- package/backend/dist/services/fetch-page-metadata.js.map +1 -0
- package/backend/dist/services/stats.d.ts +73 -0
- package/backend/dist/services/stats.d.ts.map +1 -0
- package/backend/dist/services/stats.js +145 -0
- package/backend/dist/services/stats.js.map +1 -0
- package/backend/dist/types/oidc-provider.d.ts +17 -0
- package/backend/dist/types/oidc-provider.d.ts.map +1 -0
- package/backend/dist/types/oidc-provider.js +2 -0
- package/backend/dist/types/oidc-provider.js.map +1 -0
- package/backend/dist/types.d.ts +86 -0
- package/backend/dist/types.d.ts.map +1 -0
- package/backend/dist/types.js +2 -0
- package/backend/dist/types.js.map +1 -0
- package/backend/dist/utils/ai-feature.d.ts +23 -0
- package/backend/dist/utils/ai-feature.d.ts.map +1 -0
- package/backend/dist/utils/ai-feature.js +62 -0
- package/backend/dist/utils/ai-feature.js.map +1 -0
- package/backend/dist/utils/email.d.ts +40 -0
- package/backend/dist/utils/email.d.ts.map +1 -0
- package/backend/dist/utils/email.js +456 -0
- package/backend/dist/utils/email.js.map +1 -0
- package/backend/dist/utils/encryption.d.ts +18 -0
- package/backend/dist/utils/encryption.d.ts.map +1 -0
- package/backend/dist/utils/encryption.js +95 -0
- package/backend/dist/utils/encryption.js.map +1 -0
- package/backend/dist/utils/env-validation.d.ts +6 -0
- package/backend/dist/utils/env-validation.d.ts.map +1 -0
- package/backend/dist/utils/env-validation.js +51 -0
- package/backend/dist/utils/env-validation.js.map +1 -0
- package/backend/dist/utils/jwt.d.ts +20 -0
- package/backend/dist/utils/jwt.d.ts.map +1 -0
- package/backend/dist/utils/jwt.js +48 -0
- package/backend/dist/utils/jwt.js.map +1 -0
- package/backend/dist/utils/org-cleanup.d.ts +6 -0
- package/backend/dist/utils/org-cleanup.d.ts.map +1 -0
- package/backend/dist/utils/org-cleanup.js +37 -0
- package/backend/dist/utils/org-cleanup.js.map +1 -0
- package/backend/dist/utils/organizations.d.ts +12 -0
- package/backend/dist/utils/organizations.d.ts.map +1 -0
- package/backend/dist/utils/organizations.js +24 -0
- package/backend/dist/utils/organizations.js.map +1 -0
- package/backend/dist/utils/plan-errors.d.ts +18 -0
- package/backend/dist/utils/plan-errors.d.ts.map +1 -0
- package/backend/dist/utils/plan-errors.js +21 -0
- package/backend/dist/utils/plan-errors.js.map +1 -0
- package/backend/dist/utils/refresh-token.d.ts +31 -0
- package/backend/dist/utils/refresh-token.d.ts.map +1 -0
- package/backend/dist/utils/refresh-token.js +63 -0
- package/backend/dist/utils/refresh-token.js.map +1 -0
- package/backend/dist/utils/session-store.d.ts +46 -0
- package/backend/dist/utils/session-store.d.ts.map +1 -0
- package/backend/dist/utils/session-store.js +222 -0
- package/backend/dist/utils/session-store.js.map +1 -0
- package/backend/dist/utils/tenant.d.ts +5 -0
- package/backend/dist/utils/tenant.d.ts.map +1 -0
- package/backend/dist/utils/tenant.js +12 -0
- package/backend/dist/utils/tenant.js.map +1 -0
- package/backend/dist/utils/user-key.d.ts +24 -0
- package/backend/dist/utils/user-key.d.ts.map +1 -0
- package/backend/dist/utils/user-key.js +116 -0
- package/backend/dist/utils/user-key.js.map +1 -0
- package/backend/dist/utils/validation.d.ts +91 -0
- package/backend/dist/utils/validation.d.ts.map +1 -0
- package/backend/dist/utils/validation.js +337 -0
- package/backend/dist/utils/validation.js.map +1 -0
- package/backend/index.js +15 -0
- package/frontend/index.js +5 -0
- package/frontend/index.tsx +7 -0
- package/package.json +16 -0
- package/types/index.js +4 -0
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import passport from 'passport';
|
|
3
|
+
import bcrypt from 'bcryptjs';
|
|
4
|
+
import { query, queryOne, execute, isInitialized } from '../db/index.js';
|
|
5
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
6
|
+
import { generateToken } from '../utils/jwt.js';
|
|
7
|
+
import { requireAuth } from '../middleware/auth.js';
|
|
8
|
+
import { authRateLimiter, refreshRateLimiter, strictRateLimiter } from '../middleware/security.js';
|
|
9
|
+
import { validateEmail, normalizeEmail, validatePassword, validateLength, sanitizeString } from '../utils/validation.js';
|
|
10
|
+
import { generateUserKey } from '../utils/user-key.js';
|
|
11
|
+
import { getAuthCookieOptions, getClearAuthCookieOptions } from '../config/cookies.js';
|
|
12
|
+
import { sendSignupVerificationEmail } from '../utils/email.js';
|
|
13
|
+
import crypto from 'crypto';
|
|
14
|
+
import { getDefaultTenantId } from '../utils/tenant.js';
|
|
15
|
+
const router = Router();
|
|
16
|
+
const DB_TYPE = process.env.DB_TYPE || 'sqlite';
|
|
17
|
+
function hashToken(token) {
|
|
18
|
+
return crypto.createHash('sha256').update(token).digest('hex');
|
|
19
|
+
}
|
|
20
|
+
/** Convert ? placeholders to $1, $2 for PostgreSQL */
|
|
21
|
+
function toPg(sqlStr) {
|
|
22
|
+
let n = 0;
|
|
23
|
+
return sqlStr.replace(/\?/g, () => `$${++n}`);
|
|
24
|
+
}
|
|
25
|
+
function sql(sqlStr, params) {
|
|
26
|
+
return DB_TYPE === 'postgresql' ? [toPg(sqlStr), params] : [sqlStr, params];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Find signup verification token by submitted token (hash-first, then legacy plaintext).
|
|
30
|
+
* Returns row with id, user_id, expires_at, used; for status/resend also need email (join users).
|
|
31
|
+
* Migrates legacy rows to token_hash on use.
|
|
32
|
+
*/
|
|
33
|
+
async function findSignupTokenByToken(submittedToken) {
|
|
34
|
+
const tokenHash = hashToken(submittedToken);
|
|
35
|
+
const [qHash, pHash] = sql('SELECT id, user_id, expires_at, used FROM signup_verification_tokens WHERE token_hash = ?', [tokenHash]);
|
|
36
|
+
let row = await queryOne(qHash, pHash);
|
|
37
|
+
if (row)
|
|
38
|
+
return row;
|
|
39
|
+
// Legacy: token stored in plaintext (token_hash IS NULL)
|
|
40
|
+
const [qLegacy, pLegacy] = sql('SELECT id, user_id, expires_at, used FROM signup_verification_tokens WHERE token = ? AND token_hash IS NULL', [submittedToken]);
|
|
41
|
+
row = await queryOne(qLegacy, pLegacy);
|
|
42
|
+
if (!row)
|
|
43
|
+
return null;
|
|
44
|
+
const r = row;
|
|
45
|
+
// Migrate legacy row to token_hash
|
|
46
|
+
const [qUp, pUp] = sql('UPDATE signup_verification_tokens SET token_hash = ?, token = ? WHERE id = ?', [tokenHash, 'h:' + r.id, r.id]);
|
|
47
|
+
await execute(qUp, pUp);
|
|
48
|
+
return row;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Find signup verification token with user email (for status/resend endpoints).
|
|
52
|
+
*/
|
|
53
|
+
async function findSignupTokenWithEmailByToken(submittedToken) {
|
|
54
|
+
const tokenHash = hashToken(submittedToken);
|
|
55
|
+
const [qHash, pHash] = sql(`SELECT svt.id, svt.user_id, svt.expires_at, svt.used, u.email
|
|
56
|
+
FROM signup_verification_tokens svt
|
|
57
|
+
JOIN users u ON u.id = svt.user_id
|
|
58
|
+
WHERE svt.token_hash = ?`, [tokenHash]);
|
|
59
|
+
let row = await queryOne(qHash, pHash);
|
|
60
|
+
if (row)
|
|
61
|
+
return row;
|
|
62
|
+
// Legacy: token stored in plaintext (token_hash IS NULL)
|
|
63
|
+
const [qLegacy, pLegacy] = sql(`SELECT svt.id, svt.user_id, svt.expires_at, svt.used, u.email
|
|
64
|
+
FROM signup_verification_tokens svt
|
|
65
|
+
JOIN users u ON u.id = svt.user_id
|
|
66
|
+
WHERE svt.token = ? AND svt.token_hash IS NULL`, [submittedToken]);
|
|
67
|
+
row = await queryOne(qLegacy, pLegacy);
|
|
68
|
+
if (!row)
|
|
69
|
+
return null;
|
|
70
|
+
const r = row;
|
|
71
|
+
// Migrate legacy row to token_hash
|
|
72
|
+
const [qUp, pUp] = sql('UPDATE signup_verification_tokens SET token_hash = ?, token = ? WHERE id = ?', [tokenHash, 'h:' + r.id, r.id]);
|
|
73
|
+
await execute(qUp, pUp);
|
|
74
|
+
return row;
|
|
75
|
+
}
|
|
76
|
+
/** Set auth cookie for self-hosted JWT auth. */
|
|
77
|
+
function setAuthCookies(res, options) {
|
|
78
|
+
const accessMaxAgeMs = 7 * 24 * 60 * 60 * 1000;
|
|
79
|
+
res.cookie('token', options.accessToken, { ...getAuthCookieOptions(accessMaxAgeMs), maxAge: accessMaxAgeMs });
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* @swagger
|
|
83
|
+
* /api/auth/providers:
|
|
84
|
+
* get:
|
|
85
|
+
* summary: Get available OIDC providers
|
|
86
|
+
* description: Returns a list of all configured OIDC providers (public endpoint, no authentication required)
|
|
87
|
+
* tags: [Authentication]
|
|
88
|
+
* responses:
|
|
89
|
+
* 200:
|
|
90
|
+
* description: List of OIDC providers
|
|
91
|
+
* content:
|
|
92
|
+
* application/json:
|
|
93
|
+
* schema:
|
|
94
|
+
* type: array
|
|
95
|
+
* items:
|
|
96
|
+
* type: object
|
|
97
|
+
* properties:
|
|
98
|
+
* id:
|
|
99
|
+
* type: string
|
|
100
|
+
* example: "123e4567-e89b-12d3-a456-426614174000"
|
|
101
|
+
* provider_key:
|
|
102
|
+
* type: string
|
|
103
|
+
* example: "google"
|
|
104
|
+
* issuer_url:
|
|
105
|
+
* type: string
|
|
106
|
+
* example: "https://accounts.google.com"
|
|
107
|
+
* 500:
|
|
108
|
+
* description: Server error
|
|
109
|
+
*/
|
|
110
|
+
router.get('/providers', async (req, res) => {
|
|
111
|
+
try {
|
|
112
|
+
const baseUrl = process.env.BASE_URL || 'http://localhost:5000';
|
|
113
|
+
const providers = await query('SELECT id, provider_key, issuer_url FROM oidc_providers WHERE tenant_id = ?', [getDefaultTenantId()]);
|
|
114
|
+
const providersList = Array.isArray(providers) ? providers : (providers ? [providers] : []);
|
|
115
|
+
const providersWithCallback = providersList.map((p) => ({
|
|
116
|
+
...p,
|
|
117
|
+
callback_url: `${baseUrl}/api/auth/${p.provider_key}/callback`,
|
|
118
|
+
}));
|
|
119
|
+
res.json(providersWithCallback);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
res.status(500).json({ error: error.message });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
/**
|
|
126
|
+
* @swagger
|
|
127
|
+
* /api/auth/me:
|
|
128
|
+
* get:
|
|
129
|
+
* summary: Get current user information
|
|
130
|
+
* description: Returns the authenticated user's profile information
|
|
131
|
+
* tags: [Authentication]
|
|
132
|
+
* security:
|
|
133
|
+
* - cookieAuth: []
|
|
134
|
+
* - bearerAuth: []
|
|
135
|
+
* responses:
|
|
136
|
+
* 200:
|
|
137
|
+
* description: Current user information
|
|
138
|
+
* content:
|
|
139
|
+
* application/json:
|
|
140
|
+
* schema:
|
|
141
|
+
* type: object
|
|
142
|
+
* properties:
|
|
143
|
+
* id:
|
|
144
|
+
* type: string
|
|
145
|
+
* example: "123e4567-e89b-12d3-a456-426614174000"
|
|
146
|
+
* email:
|
|
147
|
+
* type: string
|
|
148
|
+
* example: "user@example.com"
|
|
149
|
+
* name:
|
|
150
|
+
* type: string
|
|
151
|
+
* example: "John Doe"
|
|
152
|
+
* user_key:
|
|
153
|
+
* type: string
|
|
154
|
+
* example: "abc12345"
|
|
155
|
+
* is_admin:
|
|
156
|
+
* type: boolean
|
|
157
|
+
* example: false
|
|
158
|
+
* language:
|
|
159
|
+
* type: string
|
|
160
|
+
* example: "en"
|
|
161
|
+
* theme:
|
|
162
|
+
* type: string
|
|
163
|
+
* example: "auto"
|
|
164
|
+
* 401:
|
|
165
|
+
* description: Unauthorized
|
|
166
|
+
*/
|
|
167
|
+
router.get('/me', requireAuth(), async (req, res) => {
|
|
168
|
+
const authReq = req;
|
|
169
|
+
const user = authReq.user;
|
|
170
|
+
const userRow = await queryOne('SELECT id, email, name, user_key, is_admin, language, theme, ai_suggestions_enabled FROM users WHERE id = ?', [user.id]);
|
|
171
|
+
const u = userRow;
|
|
172
|
+
const payload = {
|
|
173
|
+
id: user.id,
|
|
174
|
+
email: u?.email ?? user.email,
|
|
175
|
+
name: u?.name ?? user.name,
|
|
176
|
+
user_key: user.user_key,
|
|
177
|
+
is_admin: user.is_admin,
|
|
178
|
+
language: u?.language || user.language || 'en',
|
|
179
|
+
theme: u?.theme || user.theme || 'auto',
|
|
180
|
+
ai_suggestions_enabled: u?.ai_suggestions_enabled !== 0 && u?.ai_suggestions_enabled !== false,
|
|
181
|
+
};
|
|
182
|
+
res.json(payload);
|
|
183
|
+
});
|
|
184
|
+
/**
|
|
185
|
+
* @swagger
|
|
186
|
+
* /api/auth/login:
|
|
187
|
+
* post:
|
|
188
|
+
* summary: Login with email and password
|
|
189
|
+
* description: Authenticate a user with email and password. Returns user information and sets an httpOnly JWT cookie.
|
|
190
|
+
* tags: [Authentication]
|
|
191
|
+
* requestBody:
|
|
192
|
+
* required: true
|
|
193
|
+
* content:
|
|
194
|
+
* application/json:
|
|
195
|
+
* schema:
|
|
196
|
+
* type: object
|
|
197
|
+
* required:
|
|
198
|
+
* - email
|
|
199
|
+
* - password
|
|
200
|
+
* properties:
|
|
201
|
+
* email:
|
|
202
|
+
* type: string
|
|
203
|
+
* format: email
|
|
204
|
+
* example: "user@example.com"
|
|
205
|
+
* password:
|
|
206
|
+
* type: string
|
|
207
|
+
* format: password
|
|
208
|
+
* example: "securepassword123"
|
|
209
|
+
* responses:
|
|
210
|
+
* 200:
|
|
211
|
+
* description: Login successful
|
|
212
|
+
* content:
|
|
213
|
+
* application/json:
|
|
214
|
+
* schema:
|
|
215
|
+
* type: object
|
|
216
|
+
* properties:
|
|
217
|
+
* id:
|
|
218
|
+
* type: string
|
|
219
|
+
* example: "123e4567-e89b-12d3-a456-426614174000"
|
|
220
|
+
* email:
|
|
221
|
+
* type: string
|
|
222
|
+
* example: "user@example.com"
|
|
223
|
+
* name:
|
|
224
|
+
* type: string
|
|
225
|
+
* example: "John Doe"
|
|
226
|
+
* user_key:
|
|
227
|
+
* type: string
|
|
228
|
+
* example: "abc12345"
|
|
229
|
+
* is_admin:
|
|
230
|
+
* type: boolean
|
|
231
|
+
* example: false
|
|
232
|
+
* language:
|
|
233
|
+
* type: string
|
|
234
|
+
* example: "en"
|
|
235
|
+
* theme:
|
|
236
|
+
* type: string
|
|
237
|
+
* example: "auto"
|
|
238
|
+
* headers:
|
|
239
|
+
* Set-Cookie:
|
|
240
|
+
* description: JWT token in httpOnly cookie
|
|
241
|
+
* schema:
|
|
242
|
+
* type: string
|
|
243
|
+
* example: "token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...; HttpOnly; Secure; SameSite=Strict"
|
|
244
|
+
* 400:
|
|
245
|
+
* description: Missing email or password
|
|
246
|
+
* 401:
|
|
247
|
+
* description: Invalid credentials or account has no password set
|
|
248
|
+
*/
|
|
249
|
+
// Local authentication (email/password)
|
|
250
|
+
router.post('/login', authRateLimiter, async (req, res, next) => {
|
|
251
|
+
try {
|
|
252
|
+
const { email, password } = req.body;
|
|
253
|
+
if (!email || !password) {
|
|
254
|
+
return res.status(400).json({ error: 'Email and password are required' });
|
|
255
|
+
}
|
|
256
|
+
// Validate and normalize email
|
|
257
|
+
const emailValidation = validateEmail(email);
|
|
258
|
+
if (!emailValidation.valid) {
|
|
259
|
+
return res.status(400).json({ error: emailValidation.error });
|
|
260
|
+
}
|
|
261
|
+
const normalizedEmail = normalizeEmail(email);
|
|
262
|
+
// Find user by email (use normalized email)
|
|
263
|
+
const user = await queryOne('SELECT * FROM users WHERE email = ?', [normalizedEmail]);
|
|
264
|
+
if (!user) {
|
|
265
|
+
return res.status(401).json({ error: 'Invalid email or password' });
|
|
266
|
+
}
|
|
267
|
+
// Check if user has a password set
|
|
268
|
+
if (!user.password_hash) {
|
|
269
|
+
return res.status(401).json({ error: 'This account does not have a password set. Please use OIDC login.' });
|
|
270
|
+
}
|
|
271
|
+
// Verify password
|
|
272
|
+
const isValid = await bcrypt.compare(password, user.password_hash);
|
|
273
|
+
if (!isValid) {
|
|
274
|
+
return res.status(401).json({ error: 'Invalid email or password' });
|
|
275
|
+
}
|
|
276
|
+
// Require email verification for password users (CLOUD signup flow)
|
|
277
|
+
const emailVerified = user.email_verified;
|
|
278
|
+
if (emailVerified === false || emailVerified === 0) {
|
|
279
|
+
return res.status(403).json({ error: 'Please verify your email', code: 'EMAIL_NOT_VERIFIED' });
|
|
280
|
+
}
|
|
281
|
+
const userPayload = {
|
|
282
|
+
id: user.id,
|
|
283
|
+
email: user.email,
|
|
284
|
+
name: user.name,
|
|
285
|
+
user_key: user.user_key,
|
|
286
|
+
is_admin: user.is_admin,
|
|
287
|
+
};
|
|
288
|
+
const token = generateToken(userPayload);
|
|
289
|
+
setAuthCookies(res, { accessToken: token });
|
|
290
|
+
const payload = {
|
|
291
|
+
id: user.id,
|
|
292
|
+
email: user.email,
|
|
293
|
+
name: user.name,
|
|
294
|
+
user_key: user.user_key,
|
|
295
|
+
is_admin: user.is_admin,
|
|
296
|
+
language: user.language,
|
|
297
|
+
theme: user.theme,
|
|
298
|
+
};
|
|
299
|
+
res.json(payload);
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
res.status(500).json({ error: error.message });
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
/**
|
|
306
|
+
* @swagger
|
|
307
|
+
* /api/auth/logout:
|
|
308
|
+
* post:
|
|
309
|
+
* summary: Logout current user
|
|
310
|
+
* description: Clears the authentication cookie
|
|
311
|
+
* tags: [Authentication]
|
|
312
|
+
* responses:
|
|
313
|
+
* 200:
|
|
314
|
+
* description: Logout successful
|
|
315
|
+
* content:
|
|
316
|
+
* application/json:
|
|
317
|
+
* schema:
|
|
318
|
+
* type: object
|
|
319
|
+
* properties:
|
|
320
|
+
* message:
|
|
321
|
+
* type: string
|
|
322
|
+
* example: "Logged out"
|
|
323
|
+
*/
|
|
324
|
+
router.post('/logout', async (req, res) => {
|
|
325
|
+
const clearOpts = getClearAuthCookieOptions();
|
|
326
|
+
res.clearCookie('token', clearOpts);
|
|
327
|
+
res.json({ message: 'Logged out' });
|
|
328
|
+
});
|
|
329
|
+
/**
|
|
330
|
+
* POST /auth/register — CLOUD only. Create account with email verification.
|
|
331
|
+
* Returns 404 when not CLOUD so SELFHOSTED never exposes public registration.
|
|
332
|
+
*/
|
|
333
|
+
router.post('/register', authRateLimiter, async (req, res) => {
|
|
334
|
+
const registrationsEnabled = process.env.REGISTRATIONS_ENABLED !== 'false';
|
|
335
|
+
if (!registrationsEnabled) {
|
|
336
|
+
return res.status(403).json({ error: 'Registrations are disabled' });
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
const { email, name, password } = req.body;
|
|
340
|
+
if (!email || !name || !password) {
|
|
341
|
+
return res.status(400).json({ error: 'Email, name, and password are required' });
|
|
342
|
+
}
|
|
343
|
+
const emailValidation = validateEmail(email);
|
|
344
|
+
if (!emailValidation.valid) {
|
|
345
|
+
return res.status(400).json({ error: emailValidation.error });
|
|
346
|
+
}
|
|
347
|
+
const normalizedEmail = normalizeEmail(email);
|
|
348
|
+
const nameValidation = validateLength(name, 'Name', 1, 255);
|
|
349
|
+
if (!nameValidation.valid) {
|
|
350
|
+
return res.status(400).json({ error: nameValidation.error });
|
|
351
|
+
}
|
|
352
|
+
const sanitizedName = sanitizeString(name);
|
|
353
|
+
const passwordValidation = validatePassword(password);
|
|
354
|
+
if (!passwordValidation.valid) {
|
|
355
|
+
return res.status(400).json({ error: passwordValidation.error });
|
|
356
|
+
}
|
|
357
|
+
const existingUser = await queryOne('SELECT id FROM users WHERE email = ?', [normalizedEmail]);
|
|
358
|
+
if (existingUser) {
|
|
359
|
+
return res.status(400).json({ error: 'Email already registered' });
|
|
360
|
+
}
|
|
361
|
+
const isFirstUser = !(await isInitialized());
|
|
362
|
+
const passwordHash = await bcrypt.hash(password, 10);
|
|
363
|
+
const userId = uuidv4();
|
|
364
|
+
let userKey = await generateUserKey();
|
|
365
|
+
const DB_TYPE = process.env.DB_TYPE || 'sqlite';
|
|
366
|
+
let retries = 0;
|
|
367
|
+
const maxRetries = 3;
|
|
368
|
+
while (retries < maxRetries) {
|
|
369
|
+
try {
|
|
370
|
+
if (DB_TYPE === 'postgresql') {
|
|
371
|
+
await execute(`INSERT INTO users (id, email, name, user_key, password_hash, is_admin, email_verified)
|
|
372
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, [userId, normalizedEmail, sanitizedName, userKey, passwordHash, isFirstUser, false]);
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
await execute(`INSERT INTO users (id, email, name, user_key, password_hash, is_admin, email_verified)
|
|
376
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, [userId, normalizedEmail, sanitizedName, userKey, passwordHash, isFirstUser, 0]);
|
|
377
|
+
}
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
if (error.message && (error.message.includes('UNIQUE constraint') || error.message.includes('duplicate')) && error.message.includes('user_key')) {
|
|
382
|
+
retries++;
|
|
383
|
+
if (retries >= maxRetries) {
|
|
384
|
+
return res.status(500).json({ error: 'Failed to complete registration. Please try again.' });
|
|
385
|
+
}
|
|
386
|
+
userKey = await generateUserKey();
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const tokenId = uuidv4();
|
|
393
|
+
const token = crypto.randomBytes(32).toString('hex');
|
|
394
|
+
const tokenHash = hashToken(token);
|
|
395
|
+
const tokenPlaceholder = 'h:' + tokenId;
|
|
396
|
+
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
|
397
|
+
const expiresAtStr = expiresAt.toISOString();
|
|
398
|
+
if (DB_TYPE === 'postgresql') {
|
|
399
|
+
await execute(`INSERT INTO signup_verification_tokens (id, user_id, token, token_hash, expires_at, used)
|
|
400
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [tokenId, userId, tokenPlaceholder, tokenHash, expiresAtStr, false]);
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
await execute(`INSERT INTO signup_verification_tokens (id, user_id, token, token_hash, expires_at, used)
|
|
404
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [tokenId, userId, tokenPlaceholder, tokenHash, expiresAtStr, 0]);
|
|
405
|
+
}
|
|
406
|
+
const frontendUrl = (process.env.FRONTEND_URL || 'http://localhost:3000').replace(/\/$/, '');
|
|
407
|
+
const verificationUrl = `${frontendUrl}/verify-email?token=${encodeURIComponent(token)}`;
|
|
408
|
+
await sendSignupVerificationEmail(normalizedEmail, verificationUrl);
|
|
409
|
+
return res.status(201).json({ message: 'Check your email to verify your account' });
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
console.error('Register error:', error);
|
|
413
|
+
return res.status(500).json({ error: error.message || 'Registration failed' });
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
/**
|
|
417
|
+
* POST /auth/verify-signup — CLOUD only. Verify signup token and set email_verified.
|
|
418
|
+
*/
|
|
419
|
+
router.post('/verify-signup', authRateLimiter, async (req, res) => {
|
|
420
|
+
try {
|
|
421
|
+
const { token } = req.body;
|
|
422
|
+
if (!token || typeof token !== 'string') {
|
|
423
|
+
return res.status(400).json({ error: 'Token is required' });
|
|
424
|
+
}
|
|
425
|
+
const row = await findSignupTokenByToken(token.trim());
|
|
426
|
+
if (!row) {
|
|
427
|
+
return res.status(400).json({ error: 'Invalid or expired verification link' });
|
|
428
|
+
}
|
|
429
|
+
const r = row;
|
|
430
|
+
if (r.used === true || r.used === 1) {
|
|
431
|
+
return res.status(400).json({ error: 'This verification link has already been used' });
|
|
432
|
+
}
|
|
433
|
+
const expiresAt = new Date(r.expires_at);
|
|
434
|
+
if (expiresAt.getTime() < Date.now()) {
|
|
435
|
+
return res.status(400).json({ error: 'This verification link has expired' });
|
|
436
|
+
}
|
|
437
|
+
if (DB_TYPE === 'postgresql') {
|
|
438
|
+
await execute('UPDATE users SET email_verified = TRUE WHERE id = ?', [r.user_id]);
|
|
439
|
+
await execute('UPDATE signup_verification_tokens SET used = TRUE WHERE id = ?', [r.id]);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
await execute('UPDATE users SET email_verified = 1 WHERE id = ?', [r.user_id]);
|
|
443
|
+
await execute('UPDATE signup_verification_tokens SET used = 1 WHERE id = ?', [r.id]);
|
|
444
|
+
}
|
|
445
|
+
return res.json({ message: 'Email verified. You can log in.' });
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
console.error('Verify-signup error:', error);
|
|
449
|
+
return res.status(500).json({ error: error.message || 'Verification failed' });
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
/**
|
|
453
|
+
* GET /auth/signup-verification/status — CLOUD only. Get status of signup verification token.
|
|
454
|
+
*/
|
|
455
|
+
router.get('/signup-verification/status', authRateLimiter, async (req, res) => {
|
|
456
|
+
try {
|
|
457
|
+
const token = req.query.token?.trim();
|
|
458
|
+
if (!token) {
|
|
459
|
+
return res.status(400).json({ status: 'invalid' });
|
|
460
|
+
}
|
|
461
|
+
const row = await findSignupTokenWithEmailByToken(token);
|
|
462
|
+
if (!row) {
|
|
463
|
+
return res.json({ status: 'invalid' });
|
|
464
|
+
}
|
|
465
|
+
const r = row;
|
|
466
|
+
if (r.used === true || r.used === 1) {
|
|
467
|
+
return res.json({ status: 'used' });
|
|
468
|
+
}
|
|
469
|
+
const expiresAt = new Date(r.expires_at);
|
|
470
|
+
if (expiresAt.getTime() < Date.now()) {
|
|
471
|
+
return res.json({ status: 'expired', email: r.email });
|
|
472
|
+
}
|
|
473
|
+
return res.json({ status: 'valid', email: r.email });
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
console.error('Signup verification status error:', error);
|
|
477
|
+
return res.status(500).json({ status: 'invalid' });
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
/**
|
|
481
|
+
* POST /auth/resend-signup-verification — CLOUD only. Resend verification email, optionally with updated email.
|
|
482
|
+
*/
|
|
483
|
+
router.post('/resend-signup-verification', authRateLimiter, async (req, res) => {
|
|
484
|
+
try {
|
|
485
|
+
const { token, newEmail } = req.body;
|
|
486
|
+
if (!token || typeof token !== 'string') {
|
|
487
|
+
return res.status(400).json({ error: 'Token is required' });
|
|
488
|
+
}
|
|
489
|
+
const row = await findSignupTokenWithEmailByToken(token.trim());
|
|
490
|
+
if (!row) {
|
|
491
|
+
return res.status(400).json({ error: 'Invalid verification token' });
|
|
492
|
+
}
|
|
493
|
+
const r = row;
|
|
494
|
+
if (r.used === true || r.used === 1) {
|
|
495
|
+
return res.status(400).json({ error: 'This verification link has already been used' });
|
|
496
|
+
}
|
|
497
|
+
let targetEmail = r.email;
|
|
498
|
+
if (newEmail && typeof newEmail === 'string' && newEmail.trim()) {
|
|
499
|
+
const emailValidation = validateEmail(newEmail.trim());
|
|
500
|
+
if (!emailValidation.valid) {
|
|
501
|
+
return res.status(400).json({ error: emailValidation.error });
|
|
502
|
+
}
|
|
503
|
+
const normalizedNew = normalizeEmail(newEmail.trim());
|
|
504
|
+
if (normalizedNew !== targetEmail) {
|
|
505
|
+
const existing = await queryOne('SELECT id FROM users WHERE email = ?', [normalizedNew]);
|
|
506
|
+
if (existing) {
|
|
507
|
+
return res.status(400).json({ error: 'Email already registered' });
|
|
508
|
+
}
|
|
509
|
+
targetEmail = normalizedNew;
|
|
510
|
+
await execute('UPDATE users SET email = ? WHERE id = ?', [targetEmail, r.user_id]);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
await execute('DELETE FROM signup_verification_tokens WHERE user_id = ?', [r.user_id]);
|
|
514
|
+
const tokenId = uuidv4();
|
|
515
|
+
const newToken = crypto.randomBytes(32).toString('hex');
|
|
516
|
+
const newTokenHash = hashToken(newToken);
|
|
517
|
+
const newTokenPlaceholder = 'h:' + tokenId;
|
|
518
|
+
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
|
519
|
+
const expiresAtStr = expiresAt.toISOString();
|
|
520
|
+
if (DB_TYPE === 'postgresql') {
|
|
521
|
+
await execute(`INSERT INTO signup_verification_tokens (id, user_id, token, token_hash, expires_at, used)
|
|
522
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [tokenId, r.user_id, newTokenPlaceholder, newTokenHash, expiresAtStr, false]);
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
await execute(`INSERT INTO signup_verification_tokens (id, user_id, token, token_hash, expires_at, used)
|
|
526
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [tokenId, r.user_id, newTokenPlaceholder, newTokenHash, expiresAtStr, 0]);
|
|
527
|
+
}
|
|
528
|
+
const frontendUrl = (process.env.FRONTEND_URL || 'http://localhost:3000').replace(/\/$/, '');
|
|
529
|
+
const verificationUrl = `${frontendUrl}/verify-email?token=${encodeURIComponent(newToken)}`;
|
|
530
|
+
await sendSignupVerificationEmail(targetEmail, verificationUrl);
|
|
531
|
+
return res.json({ message: 'Verification email sent' });
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
console.error('Resend signup verification error:', error);
|
|
535
|
+
return res.status(500).json({ error: error.message || 'Failed to resend verification' });
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
/**
|
|
539
|
+
* POST /auth/request-signup-resend — CLOUD only. Request resend of verification email by email (no token).
|
|
540
|
+
*/
|
|
541
|
+
router.post('/request-signup-resend', authRateLimiter, async (req, res) => {
|
|
542
|
+
try {
|
|
543
|
+
const { email } = req.body;
|
|
544
|
+
if (!email || typeof email !== 'string') {
|
|
545
|
+
return res.status(400).json({ error: 'Email is required' });
|
|
546
|
+
}
|
|
547
|
+
const emailValidation = validateEmail(email.trim());
|
|
548
|
+
if (!emailValidation.valid) {
|
|
549
|
+
return res.status(400).json({ error: emailValidation.error });
|
|
550
|
+
}
|
|
551
|
+
const normalizedEmail = normalizeEmail(email.trim());
|
|
552
|
+
const user = await queryOne('SELECT id FROM users WHERE email = ? AND (email_verified = FALSE OR email_verified = 0)', [normalizedEmail]);
|
|
553
|
+
if (user) {
|
|
554
|
+
const u = user;
|
|
555
|
+
await execute('DELETE FROM signup_verification_tokens WHERE user_id = ?', [u.id]);
|
|
556
|
+
const tokenId = uuidv4();
|
|
557
|
+
const newToken = crypto.randomBytes(32).toString('hex');
|
|
558
|
+
const newTokenHash = hashToken(newToken);
|
|
559
|
+
const newTokenPlaceholder = 'h:' + tokenId;
|
|
560
|
+
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
|
561
|
+
const expiresAtStr = expiresAt.toISOString();
|
|
562
|
+
if (DB_TYPE === 'postgresql') {
|
|
563
|
+
await execute(`INSERT INTO signup_verification_tokens (id, user_id, token, token_hash, expires_at, used)
|
|
564
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [tokenId, u.id, newTokenPlaceholder, newTokenHash, expiresAtStr, false]);
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
await execute(`INSERT INTO signup_verification_tokens (id, user_id, token, token_hash, expires_at, used)
|
|
568
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [tokenId, u.id, newTokenPlaceholder, newTokenHash, expiresAtStr, 0]);
|
|
569
|
+
}
|
|
570
|
+
const frontendUrl = (process.env.FRONTEND_URL || 'http://localhost:3000').replace(/\/$/, '');
|
|
571
|
+
const verificationUrl = `${frontendUrl}/verify-email?token=${encodeURIComponent(newToken)}`;
|
|
572
|
+
await sendSignupVerificationEmail(normalizedEmail, verificationUrl);
|
|
573
|
+
}
|
|
574
|
+
return res.json({ message: 'If an unverified account exists with that email, a new verification link has been sent.' });
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
console.error('Request signup resend error:', error);
|
|
578
|
+
return res.status(500).json({ error: error.message || 'Failed to request resend' });
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
/**
|
|
582
|
+
* @swagger
|
|
583
|
+
* /api/auth/refresh:
|
|
584
|
+
* post:
|
|
585
|
+
* summary: Refresh access token (CLOUD mode)
|
|
586
|
+
* description: Exchange refresh token cookie for new access + refresh tokens. CLOUD only.
|
|
587
|
+
* tags: [Authentication]
|
|
588
|
+
* responses:
|
|
589
|
+
* 200:
|
|
590
|
+
* description: New tokens set via cookies; returns user payload
|
|
591
|
+
* 401:
|
|
592
|
+
* description: Invalid or missing refresh token
|
|
593
|
+
*/
|
|
594
|
+
router.post('/refresh', refreshRateLimiter, async (req, res) => {
|
|
595
|
+
return res.status(404).json({ error: 'Not found' });
|
|
596
|
+
});
|
|
597
|
+
/**
|
|
598
|
+
* @swagger
|
|
599
|
+
* /api/auth/{provider}:
|
|
600
|
+
* get:
|
|
601
|
+
* summary: Initiate OIDC login
|
|
602
|
+
* description: Redirects to the OIDC provider's authentication page. This is a redirect endpoint, not a JSON API.
|
|
603
|
+
* tags: [Authentication]
|
|
604
|
+
* parameters:
|
|
605
|
+
* - in: path
|
|
606
|
+
* name: provider
|
|
607
|
+
* required: true
|
|
608
|
+
* schema:
|
|
609
|
+
* type: string
|
|
610
|
+
* description: OIDC provider key (e.g., "google", "github")
|
|
611
|
+
* example: "google"
|
|
612
|
+
* responses:
|
|
613
|
+
* 302:
|
|
614
|
+
* description: Redirect to OIDC provider
|
|
615
|
+
* 404:
|
|
616
|
+
* description: Provider not found
|
|
617
|
+
*/
|
|
618
|
+
// OIDC login route
|
|
619
|
+
// Note: OIDC requires sessions for the OAuth flow, so we don't use session: false here
|
|
620
|
+
router.get('/:provider', async (req, res, next) => {
|
|
621
|
+
const { provider } = req.params;
|
|
622
|
+
const strategies = passport._strategies || {};
|
|
623
|
+
if (!strategies[provider]) {
|
|
624
|
+
return res.status(404).json({ error: 'Not found' });
|
|
625
|
+
}
|
|
626
|
+
passport.authenticate(provider)(req, res, next);
|
|
627
|
+
});
|
|
628
|
+
/**
|
|
629
|
+
* @swagger
|
|
630
|
+
* /api/auth/{provider}/callback:
|
|
631
|
+
* get:
|
|
632
|
+
* summary: OIDC callback endpoint
|
|
633
|
+
* description: Handles the OIDC provider callback after authentication. This is a redirect endpoint used by the OIDC flow.
|
|
634
|
+
* tags: [Authentication]
|
|
635
|
+
* parameters:
|
|
636
|
+
* - in: path
|
|
637
|
+
* name: provider
|
|
638
|
+
* required: true
|
|
639
|
+
* schema:
|
|
640
|
+
* type: string
|
|
641
|
+
* description: OIDC provider key
|
|
642
|
+
* example: "google"
|
|
643
|
+
* - in: query
|
|
644
|
+
* name: code
|
|
645
|
+
* schema:
|
|
646
|
+
* type: string
|
|
647
|
+
* description: Authorization code from OIDC provider
|
|
648
|
+
* - in: query
|
|
649
|
+
* name: state
|
|
650
|
+
* schema:
|
|
651
|
+
* type: string
|
|
652
|
+
* description: State parameter for CSRF protection
|
|
653
|
+
* responses:
|
|
654
|
+
* 302:
|
|
655
|
+
* description: Redirect to frontend (success) or login page (error)
|
|
656
|
+
*/
|
|
657
|
+
// OIDC callback route
|
|
658
|
+
// Note: OIDC requires sessions for the OAuth flow, but we convert to JWT after authentication
|
|
659
|
+
router.get('/:provider/callback', (req, res, next) => {
|
|
660
|
+
const { provider } = req.params;
|
|
661
|
+
const strategies = passport._strategies || {};
|
|
662
|
+
if (!strategies[provider]) {
|
|
663
|
+
return res.status(404).json({ error: 'Not found' });
|
|
664
|
+
}
|
|
665
|
+
passport.authenticate(provider, async (err, user, info) => {
|
|
666
|
+
// Handle "ID token not present" error - some providers don't return ID tokens
|
|
667
|
+
// and passport-openidconnect fails before it can use userInfo endpoint
|
|
668
|
+
if (err && err.message === 'ID token not present in token response') {
|
|
669
|
+
try {
|
|
670
|
+
// Get provider configuration from database.
|
|
671
|
+
let configuredIssuer;
|
|
672
|
+
let configuredUserinfoUrl;
|
|
673
|
+
const providerConfig = await queryOne('SELECT issuer_url, userinfo_url FROM oidc_providers WHERE provider_key = ? AND tenant_id = ?', [provider, getDefaultTenantId()]);
|
|
674
|
+
if (!providerConfig)
|
|
675
|
+
throw new Error('Provider configuration not found');
|
|
676
|
+
configuredIssuer = providerConfig.issuer_url;
|
|
677
|
+
configuredUserinfoUrl = providerConfig.userinfo_url || `${configuredIssuer}/userinfo`;
|
|
678
|
+
// Get the access token from the session (stored by passport during OAuth flow)
|
|
679
|
+
// passport-openidconnect stores it under a key like 'openidconnect:issuer'
|
|
680
|
+
// Use the configured issuer, not user input
|
|
681
|
+
const sessionKey = `openidconnect:${configuredIssuer}`;
|
|
682
|
+
const oauthState = req.session?.[sessionKey];
|
|
683
|
+
if (!oauthState) {
|
|
684
|
+
// Try to find any openidconnect key in session that matches the configured issuer
|
|
685
|
+
const allKeys = Object.keys(req.session || {});
|
|
686
|
+
const oidcKeys = allKeys.filter((k) => k.startsWith('openidconnect:'));
|
|
687
|
+
// Find a key that matches our configured issuer
|
|
688
|
+
const matchingKey = oidcKeys.find((k) => k === sessionKey);
|
|
689
|
+
if (matchingKey) {
|
|
690
|
+
const matchingState = req.session?.[matchingKey];
|
|
691
|
+
if (matchingState?.token_response?.access_token) {
|
|
692
|
+
const accessToken = matchingState.token_response.access_token;
|
|
693
|
+
// Use the configured userinfo URL (safe, from database)
|
|
694
|
+
const userInfoResponse = await fetch(configuredUserinfoUrl, {
|
|
695
|
+
headers: {
|
|
696
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
697
|
+
'Accept': 'application/json',
|
|
698
|
+
},
|
|
699
|
+
});
|
|
700
|
+
if (!userInfoResponse.ok) {
|
|
701
|
+
const errorText = await userInfoResponse.text();
|
|
702
|
+
throw new Error(`UserInfo request failed: ${userInfoResponse.status} ${userInfoResponse.statusText} - ${errorText}`);
|
|
703
|
+
}
|
|
704
|
+
const userInfo = await userInfoResponse.json();
|
|
705
|
+
// Get the verify function from the strategy
|
|
706
|
+
const strategy = passport._strategies[provider];
|
|
707
|
+
if (!strategy || !strategy._verify) {
|
|
708
|
+
throw new Error('Verify function not found for provider');
|
|
709
|
+
}
|
|
710
|
+
// Create a mock profile from userInfo
|
|
711
|
+
const profile = {
|
|
712
|
+
id: userInfo.sub || userInfo.id,
|
|
713
|
+
displayName: userInfo.name || userInfo.preferred_username,
|
|
714
|
+
name: userInfo.name,
|
|
715
|
+
emails: userInfo.email ? [{ value: userInfo.email }] : [],
|
|
716
|
+
email: userInfo.email,
|
|
717
|
+
};
|
|
718
|
+
// Call verify function with userInfo data
|
|
719
|
+
strategy._verify(configuredIssuer, profile, {}, // context
|
|
720
|
+
null, // idToken (not available in this flow)
|
|
721
|
+
accessToken, matchingState.token_response.refresh_token, {}, // params
|
|
722
|
+
async (verifyErr, verifiedUser) => {
|
|
723
|
+
if (verifyErr || !verifiedUser) {
|
|
724
|
+
console.error(`[OIDC] Verify function error:`, verifyErr);
|
|
725
|
+
return res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:3000'}/login?error=auth_failed`);
|
|
726
|
+
}
|
|
727
|
+
user = verifiedUser;
|
|
728
|
+
await handleSuccess();
|
|
729
|
+
});
|
|
730
|
+
return; // Exit early, handleSuccess already ran
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
throw new Error('No OAuth state found in session');
|
|
734
|
+
}
|
|
735
|
+
const accessToken = oauthState.token_response?.access_token;
|
|
736
|
+
if (!accessToken) {
|
|
737
|
+
throw new Error('No access token found in token response');
|
|
738
|
+
}
|
|
739
|
+
// Use the configured userinfo URL (safe, from database, not user input)
|
|
740
|
+
const userInfoResponse = await fetch(configuredUserinfoUrl, {
|
|
741
|
+
headers: {
|
|
742
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
743
|
+
'Accept': 'application/json',
|
|
744
|
+
},
|
|
745
|
+
});
|
|
746
|
+
if (!userInfoResponse.ok) {
|
|
747
|
+
const errorText = await userInfoResponse.text();
|
|
748
|
+
throw new Error(`UserInfo request failed: ${userInfoResponse.status} ${userInfoResponse.statusText} - ${errorText}`);
|
|
749
|
+
}
|
|
750
|
+
const userInfo = await userInfoResponse.json();
|
|
751
|
+
// Get the verify function from the strategy
|
|
752
|
+
const strategy = passport._strategies[provider];
|
|
753
|
+
if (!strategy || !strategy._verify) {
|
|
754
|
+
throw new Error('Verify function not found for provider');
|
|
755
|
+
}
|
|
756
|
+
// Create a mock profile from userInfo
|
|
757
|
+
const profile = {
|
|
758
|
+
id: userInfo.sub || userInfo.id,
|
|
759
|
+
displayName: userInfo.name || userInfo.preferred_username,
|
|
760
|
+
name: userInfo.name,
|
|
761
|
+
emails: userInfo.email ? [{ value: userInfo.email }] : [],
|
|
762
|
+
email: userInfo.email,
|
|
763
|
+
};
|
|
764
|
+
// Call verify function with userInfo data
|
|
765
|
+
// Updated signature: (iss, profile, context, idToken, accessToken, refreshToken, params, cb)
|
|
766
|
+
strategy._verify(configuredIssuer, profile, {}, // context
|
|
767
|
+
null, // idToken (not available in this flow)
|
|
768
|
+
accessToken, oauthState.token_response?.refresh_token, {}, // params
|
|
769
|
+
async (verifyErr, verifiedUser) => {
|
|
770
|
+
if (verifyErr || !verifiedUser) {
|
|
771
|
+
console.error(`[OIDC] Verify function error:`, verifyErr);
|
|
772
|
+
return res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:3000'}/login?error=auth_failed`);
|
|
773
|
+
}
|
|
774
|
+
user = verifiedUser;
|
|
775
|
+
await handleSuccess();
|
|
776
|
+
});
|
|
777
|
+
return; // Exit early, handleSuccess already ran
|
|
778
|
+
}
|
|
779
|
+
catch (manualFetchError) {
|
|
780
|
+
console.error(`[OIDC] Manual userInfo fetch failed:`, {
|
|
781
|
+
message: manualFetchError.message,
|
|
782
|
+
stack: manualFetchError.stack,
|
|
783
|
+
});
|
|
784
|
+
return res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:3000'}/login?error=auth_failed`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (err || !user) {
|
|
788
|
+
// Check for specific error types
|
|
789
|
+
let errorParam = 'auth_failed';
|
|
790
|
+
if (err) {
|
|
791
|
+
// Sanitize provider for logging to prevent log injection
|
|
792
|
+
const safeProvider = String(provider || 'unknown').replace(/[^\w-]/g, '');
|
|
793
|
+
console.error('[OIDC] Authentication error', {
|
|
794
|
+
provider: safeProvider,
|
|
795
|
+
message: err.message,
|
|
796
|
+
stack: err.stack,
|
|
797
|
+
name: err.name,
|
|
798
|
+
});
|
|
799
|
+
if (err.message === 'AUTO_CREATE_DISABLED') {
|
|
800
|
+
errorParam = 'auto_create_disabled';
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
else if (!user) {
|
|
804
|
+
// Sanitize provider for logging to prevent log injection
|
|
805
|
+
const safeProvider = String(provider || 'unknown').replace(/[^\w-]/g, '');
|
|
806
|
+
console.error('[OIDC] No user returned', {
|
|
807
|
+
provider: safeProvider,
|
|
808
|
+
info: info,
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
return res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:3000'}/login?error=${errorParam}`);
|
|
812
|
+
}
|
|
813
|
+
async function handleSuccess() {
|
|
814
|
+
const userPayload = { id: user.id, email: user.email, name: user.name, user_key: user.user_key, is_admin: user.is_admin };
|
|
815
|
+
const token = generateToken(userPayload);
|
|
816
|
+
setAuthCookies(res, { accessToken: token });
|
|
817
|
+
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
|
|
818
|
+
const redirectUrl = frontendUrl;
|
|
819
|
+
req.session?.destroy((sessionErr) => {
|
|
820
|
+
if (sessionErr)
|
|
821
|
+
console.error('Error destroying session:', sessionErr);
|
|
822
|
+
res.redirect(redirectUrl);
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
// If we got here normally (not from manual fetch), handle success
|
|
826
|
+
if (user) {
|
|
827
|
+
await handleSuccess();
|
|
828
|
+
}
|
|
829
|
+
})(req, res, next);
|
|
830
|
+
});
|
|
831
|
+
/**
|
|
832
|
+
* @swagger
|
|
833
|
+
* /api/auth/setup:
|
|
834
|
+
* post:
|
|
835
|
+
* summary: Initial system setup
|
|
836
|
+
* description: Creates the first admin user. Only accessible when the system is not yet initialized.
|
|
837
|
+
* tags: [Authentication]
|
|
838
|
+
* requestBody:
|
|
839
|
+
* required: true
|
|
840
|
+
* content:
|
|
841
|
+
* application/json:
|
|
842
|
+
* schema:
|
|
843
|
+
* type: object
|
|
844
|
+
* required:
|
|
845
|
+
* - email
|
|
846
|
+
* - name
|
|
847
|
+
* - password
|
|
848
|
+
* properties:
|
|
849
|
+
* email:
|
|
850
|
+
* type: string
|
|
851
|
+
* format: email
|
|
852
|
+
* example: "admin@example.com"
|
|
853
|
+
* name:
|
|
854
|
+
* type: string
|
|
855
|
+
* example: "Admin User"
|
|
856
|
+
* password:
|
|
857
|
+
* type: string
|
|
858
|
+
* format: password
|
|
859
|
+
* minLength: 8
|
|
860
|
+
* example: "securepassword123"
|
|
861
|
+
* responses:
|
|
862
|
+
* 200:
|
|
863
|
+
* description: Setup completed successfully
|
|
864
|
+
* content:
|
|
865
|
+
* application/json:
|
|
866
|
+
* schema:
|
|
867
|
+
* type: object
|
|
868
|
+
* properties:
|
|
869
|
+
* message:
|
|
870
|
+
* type: string
|
|
871
|
+
* example: "Setup completed successfully. You can now log in."
|
|
872
|
+
* 400:
|
|
873
|
+
* description: Invalid input or user already exists
|
|
874
|
+
* 403:
|
|
875
|
+
* description: System already initialized
|
|
876
|
+
*/
|
|
877
|
+
// Setup route - only accessible when system is not initialized. SELFHOSTED only.
|
|
878
|
+
router.post('/setup', strictRateLimiter, async (req, res) => {
|
|
879
|
+
try {
|
|
880
|
+
const initialized = await isInitialized();
|
|
881
|
+
if (initialized) {
|
|
882
|
+
return res.status(403).json({ error: 'System already initialized' });
|
|
883
|
+
}
|
|
884
|
+
const { email, name, password } = req.body;
|
|
885
|
+
if (!email || !name || !password) {
|
|
886
|
+
return res.status(400).json({ error: 'Email, name, and password are required' });
|
|
887
|
+
}
|
|
888
|
+
// Validate and normalize email
|
|
889
|
+
const emailValidation = validateEmail(email);
|
|
890
|
+
if (!emailValidation.valid) {
|
|
891
|
+
return res.status(400).json({ error: emailValidation.error });
|
|
892
|
+
}
|
|
893
|
+
const normalizedEmail = normalizeEmail(email);
|
|
894
|
+
// Validate name length
|
|
895
|
+
const nameValidation = validateLength(name, 'Name', 1, 255);
|
|
896
|
+
if (!nameValidation.valid) {
|
|
897
|
+
return res.status(400).json({ error: nameValidation.error });
|
|
898
|
+
}
|
|
899
|
+
const sanitizedName = sanitizeString(name);
|
|
900
|
+
// Validate password complexity
|
|
901
|
+
const passwordValidation = validatePassword(password);
|
|
902
|
+
if (!passwordValidation.valid) {
|
|
903
|
+
return res.status(400).json({ error: passwordValidation.error });
|
|
904
|
+
}
|
|
905
|
+
// Check if email already exists (use normalized email)
|
|
906
|
+
const existingUser = await queryOne('SELECT id FROM users WHERE email = ?', [normalizedEmail]);
|
|
907
|
+
if (existingUser) {
|
|
908
|
+
return res.status(400).json({ error: 'User with this email already exists' });
|
|
909
|
+
}
|
|
910
|
+
// Hash password
|
|
911
|
+
const passwordHash = await bcrypt.hash(password, 10);
|
|
912
|
+
// Create first admin user
|
|
913
|
+
const userId = uuidv4();
|
|
914
|
+
let userKey = await generateUserKey();
|
|
915
|
+
// Retry logic for user_key collisions (should be extremely rare)
|
|
916
|
+
let retries = 0;
|
|
917
|
+
const maxRetries = 3;
|
|
918
|
+
while (retries < maxRetries) {
|
|
919
|
+
try {
|
|
920
|
+
await execute(`INSERT INTO users (id, email, name, user_key, password_hash, is_admin)
|
|
921
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [userId, normalizedEmail, sanitizedName, userKey, passwordHash, true] // First user is always admin
|
|
922
|
+
);
|
|
923
|
+
break; // Success, exit retry loop
|
|
924
|
+
}
|
|
925
|
+
catch (error) {
|
|
926
|
+
// If user_key collision, generate new key and retry
|
|
927
|
+
if (error.message && (error.message.includes('UNIQUE constraint') || error.message.includes('duplicate'))
|
|
928
|
+
&& error.message.includes('user_key')) {
|
|
929
|
+
retries++;
|
|
930
|
+
if (retries >= maxRetries) {
|
|
931
|
+
return res.status(500).json({ error: 'Failed to complete setup. Please try again.' });
|
|
932
|
+
}
|
|
933
|
+
userKey = await generateUserKey();
|
|
934
|
+
continue; // Retry with new key
|
|
935
|
+
}
|
|
936
|
+
// For other errors (like email duplicate), throw to outer catch
|
|
937
|
+
throw error;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
// Automatically log in the user after successful setup
|
|
941
|
+
const userPayload = { id: userId, email: normalizedEmail, name: sanitizedName, user_key: userKey, is_admin: true };
|
|
942
|
+
setAuthCookies(res, { accessToken: generateToken(userPayload) });
|
|
943
|
+
// Return user data (same format as login endpoint)
|
|
944
|
+
res.json({
|
|
945
|
+
id: userId,
|
|
946
|
+
email: normalizedEmail,
|
|
947
|
+
name: sanitizedName,
|
|
948
|
+
user_key: userKey,
|
|
949
|
+
is_admin: true,
|
|
950
|
+
language: 'en', // Default language
|
|
951
|
+
theme: 'auto', // Default theme
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
catch (error) {
|
|
955
|
+
// Handle unique constraint violations
|
|
956
|
+
if (error.message && (error.message.includes('UNIQUE constraint') || error.message.includes('duplicate'))) {
|
|
957
|
+
return res.status(400).json({ error: 'User with this email already exists' });
|
|
958
|
+
}
|
|
959
|
+
res.status(500).json({ error: error.message });
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
/**
|
|
963
|
+
* @swagger
|
|
964
|
+
* /api/auth/setup/status:
|
|
965
|
+
* get:
|
|
966
|
+
* summary: Check system initialization status
|
|
967
|
+
* description: Returns whether the system has been initialized (has at least one user)
|
|
968
|
+
* tags: [Authentication]
|
|
969
|
+
* responses:
|
|
970
|
+
* 200:
|
|
971
|
+
* description: System initialization status
|
|
972
|
+
* content:
|
|
973
|
+
* application/json:
|
|
974
|
+
* schema:
|
|
975
|
+
* type: object
|
|
976
|
+
* properties:
|
|
977
|
+
* initialized:
|
|
978
|
+
* type: boolean
|
|
979
|
+
* example: false
|
|
980
|
+
* description: true if system has been initialized, false otherwise
|
|
981
|
+
*/
|
|
982
|
+
router.get('/setup/status', async (req, res) => {
|
|
983
|
+
try {
|
|
984
|
+
const initialized = await isInitialized();
|
|
985
|
+
res.json({ initialized });
|
|
986
|
+
}
|
|
987
|
+
catch (error) {
|
|
988
|
+
res.status(500).json({ error: error.message });
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
export default router;
|
|
992
|
+
//# sourceMappingURL=auth.js.map
|