@aurostack/stacks 0.1.0
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/LICENSE +21 -0
- package/README.md +209 -0
- package/cli/src/extract.mjs +165 -0
- package/cli/src/fsx.mjs +197 -0
- package/cli/src/generate.mjs +235 -0
- package/cli/src/hooks.mjs +55 -0
- package/cli/src/manifest.mjs +163 -0
- package/cli/src/strip.mjs +208 -0
- package/cli/src/tokens.mjs +102 -0
- package/cli/stack.mjs +672 -0
- package/package.json +37 -0
- package/templates/nest-api/files/.bin/db.ts +123 -0
- package/templates/nest-api/files/.bin/dc.ts +74 -0
- package/templates/nest-api/files/.bin/helpers.ts +49 -0
- package/templates/nest-api/files/.bin/secrets.ts +68 -0
- package/templates/nest-api/files/.bin/start.ts +47 -0
- package/templates/nest-api/files/.dockerignore +7 -0
- package/templates/nest-api/files/.env.example +91 -0
- package/templates/nest-api/files/.env.test.example +75 -0
- package/templates/nest-api/files/.forgejo/workflows/build-and-push.yml +41 -0
- package/templates/nest-api/files/.forgejo/workflows/test-e2e.yml +83 -0
- package/templates/nest-api/files/.forgejo/workflows/test.yml +53 -0
- package/templates/nest-api/files/.npmrc.example +1 -0
- package/templates/nest-api/files/.nvmrc +1 -0
- package/templates/nest-api/files/.prettierrc +8 -0
- package/templates/nest-api/files/.swcrc +15 -0
- package/templates/nest-api/files/.watchmanconfig +3 -0
- package/templates/nest-api/files/.yarnrc.yml +1 -0
- package/templates/nest-api/files/Dockerfile +40 -0
- package/templates/nest-api/files/__mocks__/activation-twofa.js +7 -0
- package/templates/nest-api/files/__mocks__/activation.js +7 -0
- package/templates/nest-api/files/__mocks__/forgot-password.js +7 -0
- package/templates/nest-api/files/__mocks__/main-layout.js +7 -0
- package/templates/nest-api/files/__mocks__/password-changed.js +7 -0
- package/templates/nest-api/files/__mocks__/reset-password.js +7 -0
- package/templates/nest-api/files/__mocks__/set-password.js +7 -0
- package/templates/nest-api/files/__mocks__/welcome.js +7 -0
- package/templates/nest-api/files/_gitignore +62 -0
- package/templates/nest-api/files/compose.test.yml +33 -0
- package/templates/nest-api/files/compose.yml +39 -0
- package/templates/nest-api/files/emails/activation-twofa.tsx +58 -0
- package/templates/nest-api/files/emails/activation.tsx +36 -0
- package/templates/nest-api/files/emails/forgot-password.tsx +29 -0
- package/templates/nest-api/files/emails/main-layout.tsx +134 -0
- package/templates/nest-api/files/emails/password-changed.tsx +24 -0
- package/templates/nest-api/files/emails/reset-password.tsx +24 -0
- package/templates/nest-api/files/emails/set-password.tsx +25 -0
- package/templates/nest-api/files/emails/welcome.tsx +39 -0
- package/templates/nest-api/files/eslint.config.mjs +28 -0
- package/templates/nest-api/files/global.d.ts +5 -0
- package/templates/nest-api/files/nest-cli.json +8 -0
- package/templates/nest-api/files/package.json +150 -0
- package/templates/nest-api/files/prisma/schema/auth.prisma +75 -0
- package/templates/nest-api/files/prisma/schema/base.prisma +17 -0
- package/templates/nest-api/files/prisma/schema/enum.prisma +17 -0
- package/templates/nest-api/files/prisma/schema/notification.prisma +14 -0
- package/templates/nest-api/files/prisma.config.ts +13 -0
- package/templates/nest-api/files/public/.gitkeep +0 -0
- package/templates/nest-api/files/seeders/index.ts +27 -0
- package/templates/nest-api/files/seeders/util.ts +88 -0
- package/templates/nest-api/files/src/__tests__/app.controller.spec.ts +29 -0
- package/templates/nest-api/files/src/app.controller.ts +40 -0
- package/templates/nest-api/files/src/app.module.ts +268 -0
- package/templates/nest-api/files/src/app.resolver.ts +32 -0
- package/templates/nest-api/files/src/app.setup.ts +178 -0
- package/templates/nest-api/files/src/common/__tests__/auth.service.spec.ts +31 -0
- package/templates/nest-api/files/src/common/__tests__/cache.service.spec.ts +214 -0
- package/templates/nest-api/files/src/common/__tests__/config.service.spec.ts +56 -0
- package/templates/nest-api/files/src/common/__tests__/generator.service.spec.ts +114 -0
- package/templates/nest-api/files/src/common/__tests__/health.controller.spec.ts +72 -0
- package/templates/nest-api/files/src/common/__tests__/helpers.spec.ts +124 -0
- package/templates/nest-api/files/src/common/__tests__/mail.service.spec.ts +75 -0
- package/templates/nest-api/files/src/common/__tests__/prisma.indicator.spec.ts +39 -0
- package/templates/nest-api/files/src/common/__tests__/prisma.service.spec.ts +15 -0
- package/templates/nest-api/files/src/common/__tests__/records.spec.ts +87 -0
- package/templates/nest-api/files/src/common/__tests__/schemas.spec.ts +142 -0
- package/templates/nest-api/files/src/common/__tests__/throttler-storage.service.spec.ts +80 -0
- package/templates/nest-api/files/src/common/common.module.ts +72 -0
- package/templates/nest-api/files/src/common/constants/index.ts +2 -0
- package/templates/nest-api/files/src/common/constants/options.ts +52 -0
- package/templates/nest-api/files/src/common/constants/pino.ts +24 -0
- package/templates/nest-api/files/src/common/controllers/health.controller.ts +54 -0
- package/templates/nest-api/files/src/common/controllers/index.ts +2 -0
- package/templates/nest-api/files/src/common/controllers/metrics.controller.ts +21 -0
- package/templates/nest-api/files/src/common/decorators/index.ts +2 -0
- package/templates/nest-api/files/src/common/decorators/misc.decorator.ts +49 -0
- package/templates/nest-api/files/src/common/decorators/multi-part.decorator.ts +6 -0
- package/templates/nest-api/files/src/common/dto/filters.dto.ts +25 -0
- package/templates/nest-api/files/src/common/dto/index.ts +1 -0
- package/templates/nest-api/files/src/common/entity/index.ts +2 -0
- package/templates/nest-api/files/src/common/entity/ok.entity.ts +11 -0
- package/templates/nest-api/files/src/common/entity/pagination.entity.ts +24 -0
- package/templates/nest-api/files/src/common/exceptions/index.ts +1 -0
- package/templates/nest-api/files/src/common/exceptions/prisma.exception.ts +57 -0
- package/templates/nest-api/files/src/common/guards/feature-flag.guard.ts +51 -0
- package/templates/nest-api/files/src/common/guards/index.ts +1 -0
- package/templates/nest-api/files/src/common/interceptors/index.ts +1 -0
- package/templates/nest-api/files/src/common/interceptors/metrics.interceptor.ts +37 -0
- package/templates/nest-api/files/src/common/misc/helpers.ts +26 -0
- package/templates/nest-api/files/src/common/misc/index.ts +3 -0
- package/templates/nest-api/files/src/common/misc/origin-builder.ts +56 -0
- package/templates/nest-api/files/src/common/misc/prisma.indicator.ts +24 -0
- package/templates/nest-api/files/src/common/modules/index.ts +3 -0
- package/templates/nest-api/files/src/common/modules/logger.module.ts +104 -0
- package/templates/nest-api/files/src/common/modules/metrics.module.ts +28 -0
- package/templates/nest-api/files/src/common/modules/queue.module.ts +23 -0
- package/templates/nest-api/files/src/common/processors/index.ts +1 -0
- package/templates/nest-api/files/src/common/processors/mail/index.ts +47 -0
- package/templates/nest-api/files/src/common/processors/mail/process.ts +225 -0
- package/templates/nest-api/files/src/common/schemas/fields.ts +76 -0
- package/templates/nest-api/files/src/common/schemas/index.ts +1 -0
- package/templates/nest-api/files/src/common/services/auth-rate-limit.storage.ts +79 -0
- package/templates/nest-api/files/src/common/services/auth.service.ts +16 -0
- package/templates/nest-api/files/src/common/services/cache.service.ts +118 -0
- package/templates/nest-api/files/src/common/services/config.service.ts +388 -0
- package/templates/nest-api/files/src/common/services/feature-flag.service.ts +117 -0
- package/templates/nest-api/files/src/common/services/generator.service.ts +54 -0
- package/templates/nest-api/files/src/common/services/index.ts +10 -0
- package/templates/nest-api/files/src/common/services/logger.service.ts +14 -0
- package/templates/nest-api/files/src/common/services/mail.service.ts +44 -0
- package/templates/nest-api/files/src/common/services/prisma.service.ts +106 -0
- package/templates/nest-api/files/src/common/services/throttler-storage.service.ts +112 -0
- package/templates/nest-api/files/src/common/types/email.ts +55 -0
- package/templates/nest-api/files/src/common/types/index.ts +3 -0
- package/templates/nest-api/files/src/common/types/misc.ts +20 -0
- package/templates/nest-api/files/src/common/types/prisma.ts +18 -0
- package/templates/nest-api/files/src/common/utils/enum.ts +39 -0
- package/templates/nest-api/files/src/common/utils/helpers.ts +139 -0
- package/templates/nest-api/files/src/common/utils/index.ts +5 -0
- package/templates/nest-api/files/src/common/utils/ip.ts +22 -0
- package/templates/nest-api/files/src/common/utils/records.ts +66 -0
- package/templates/nest-api/files/src/common/utils/redis.keys.ts +31 -0
- package/templates/nest-api/files/src/lib/__tests__/access.spec.ts +33 -0
- package/templates/nest-api/files/src/lib/access.ts +114 -0
- package/templates/nest-api/files/src/lib/auth.ts +76 -0
- package/templates/nest-api/files/src/lib/rate-limit.ts +20 -0
- package/templates/nest-api/files/src/main.ts +28 -0
- package/templates/nest-api/files/src/media/__tests__/media.service.spec.ts +60 -0
- package/templates/nest-api/files/src/media/__tests__/s3.service.spec.ts +159 -0
- package/templates/nest-api/files/src/media/entity/file.entity.ts +7 -0
- package/templates/nest-api/files/src/media/entity/index.ts +1 -0
- package/templates/nest-api/files/src/media/media.module.ts +20 -0
- package/templates/nest-api/files/src/media/processors/index.ts +44 -0
- package/templates/nest-api/files/src/media/services/index.ts +2 -0
- package/templates/nest-api/files/src/media/services/media.service.ts +29 -0
- package/templates/nest-api/files/src/media/services/s3.service.ts +168 -0
- package/templates/nest-api/files/src/notifications/controllers/index.ts +1 -0
- package/templates/nest-api/files/src/notifications/controllers/notifications.controller.ts +72 -0
- package/templates/nest-api/files/src/notifications/dto/index.ts +1 -0
- package/templates/nest-api/files/src/notifications/dto/notification.dto.ts +11 -0
- package/templates/nest-api/files/src/notifications/entity/index.ts +1 -0
- package/templates/nest-api/files/src/notifications/entity/notification.entity.ts +37 -0
- package/templates/nest-api/files/src/notifications/notifications.module.ts +11 -0
- package/templates/nest-api/files/src/notifications/services/index.ts +1 -0
- package/templates/nest-api/files/src/notifications/services/notifications.service.ts +98 -0
- package/templates/nest-api/files/src/realtime/__tests__/events.gateway.spec.ts +88 -0
- package/templates/nest-api/files/src/realtime/__tests__/rooms.spec.ts +29 -0
- package/templates/nest-api/files/src/realtime/__tests__/ws-permissions.guard.spec.ts +54 -0
- package/templates/nest-api/files/src/realtime/adapters/redis-io.adapter.ts +57 -0
- package/templates/nest-api/files/src/realtime/constants/index.ts +68 -0
- package/templates/nest-api/files/src/realtime/decorators/index.ts +1 -0
- package/templates/nest-api/files/src/realtime/decorators/ws-permissions.decorator.ts +14 -0
- package/templates/nest-api/files/src/realtime/gateways/events.gateway.ts +187 -0
- package/templates/nest-api/files/src/realtime/guards/index.ts +2 -0
- package/templates/nest-api/files/src/realtime/guards/ws-auth.guard.ts +20 -0
- package/templates/nest-api/files/src/realtime/guards/ws-permissions.guard.ts +35 -0
- package/templates/nest-api/files/src/realtime/misc/session-from-socket.ts +54 -0
- package/templates/nest-api/files/src/realtime/realtime.module.ts +26 -0
- package/templates/nest-api/files/src/realtime/services/presence.service.ts +56 -0
- package/templates/nest-api/files/src/realtime/services/realtime.service.ts +72 -0
- package/templates/nest-api/files/src/users/__tests__/current-user.controller.spec.ts +77 -0
- package/templates/nest-api/files/src/users/__tests__/current-user.service.spec.ts +159 -0
- package/templates/nest-api/files/src/users/__tests__/user.entity.spec.ts +175 -0
- package/templates/nest-api/files/src/users/__tests__/users.controller.spec.ts +89 -0
- package/templates/nest-api/files/src/users/__tests__/users.service.spec.ts +398 -0
- package/templates/nest-api/files/src/users/controllers/current-user.controller.ts +82 -0
- package/templates/nest-api/files/src/users/controllers/index.ts +2 -0
- package/templates/nest-api/files/src/users/controllers/users.controller.ts +40 -0
- package/templates/nest-api/files/src/users/decorators/index.ts +1 -0
- package/templates/nest-api/files/src/users/decorators/permissions.decorator.ts +24 -0
- package/templates/nest-api/files/src/users/dto/avatar.dto.ts +13 -0
- package/templates/nest-api/files/src/users/dto/filters.dto.ts +8 -0
- package/templates/nest-api/files/src/users/dto/index.ts +2 -0
- package/templates/nest-api/files/src/users/entity/index.ts +1 -0
- package/templates/nest-api/files/src/users/entity/user.entity.ts +53 -0
- package/templates/nest-api/files/src/users/services/current-user.service.ts +46 -0
- package/templates/nest-api/files/src/users/services/index.ts +2 -0
- package/templates/nest-api/files/src/users/services/users.service.ts +88 -0
- package/templates/nest-api/files/src/users/users.module.ts +10 -0
- package/templates/nest-api/files/test/app.spec.ts +102 -0
- package/templates/nest-api/files/test/auth.spec.ts +108 -0
- package/templates/nest-api/files/test/current-user.spec.ts +154 -0
- package/templates/nest-api/files/test/factory/app.ts +172 -0
- package/templates/nest-api/files/test/factory/constants.ts +22 -0
- package/templates/nest-api/files/test/factory/dto.ts +27 -0
- package/templates/nest-api/files/test/factory/entity.ts +70 -0
- package/templates/nest-api/files/test/factory/gen.ts +75 -0
- package/templates/nest-api/files/test/factory/util.ts +21 -0
- package/templates/nest-api/files/test/notifications.spec.ts +176 -0
- package/templates/nest-api/files/test/rate-limit.spec.ts +180 -0
- package/templates/nest-api/files/test/setup.ts +1 -0
- package/templates/nest-api/files/test/users.spec.ts +437 -0
- package/templates/nest-api/files/tsconfig.build.json +13 -0
- package/templates/nest-api/files/tsconfig.json +35 -0
- package/templates/nest-api/files/vitest.config.ts +53 -0
- package/templates/nest-api/template.json +452 -0
- package/templates/node-worker/files/.bin/db.ts +123 -0
- package/templates/node-worker/files/.bin/helpers.ts +49 -0
- package/templates/node-worker/files/.bin/secrets.ts +68 -0
- package/templates/node-worker/files/.dockerignore +5 -0
- package/templates/node-worker/files/.env.example +21 -0
- package/templates/node-worker/files/.forgejo/workflows/build-and-push.yml +41 -0
- package/templates/node-worker/files/.nvmrc +1 -0
- package/templates/node-worker/files/.prettierignore +4 -0
- package/templates/node-worker/files/.prettierrc +8 -0
- package/templates/node-worker/files/.yarnrc.yml +5 -0
- package/templates/node-worker/files/Dockerfile +63 -0
- package/templates/node-worker/files/Dockerfile.dev +59 -0
- package/templates/node-worker/files/_gitignore +48 -0
- package/templates/node-worker/files/ecosystem.config.cjs +10 -0
- package/templates/node-worker/files/eslint.config.js +37 -0
- package/templates/node-worker/files/package.json +67 -0
- package/templates/node-worker/files/prisma/_gitignore +1 -0
- package/templates/node-worker/files/prisma/schema/base.prisma +36 -0
- package/templates/node-worker/files/prisma.config.ts +13 -0
- package/templates/node-worker/files/src/config.ts +55 -0
- package/templates/node-worker/files/src/db.ts +118 -0
- package/templates/node-worker/files/src/index.ts +7 -0
- package/templates/node-worker/files/src/logger.ts +45 -0
- package/templates/node-worker/files/src/types.ts +23 -0
- package/templates/node-worker/files/src/utils/browser.ts +16 -0
- package/templates/node-worker/files/src/utils/date.ts +68 -0
- package/templates/node-worker/files/src/utils/helpers.ts +17 -0
- package/templates/node-worker/files/src/utils/number.ts +47 -0
- package/templates/node-worker/files/src/utils/worker.ts +29 -0
- package/templates/node-worker/files/src/workers/constants.ts +6 -0
- package/templates/node-worker/files/src/workers/example/index.ts +27 -0
- package/templates/node-worker/files/src/workers/index.ts +1 -0
- package/templates/node-worker/files/tsconfig.json +30 -0
- package/templates/node-worker/template.json +135 -0
- package/templates/py-worker/files/.dockerignore +176 -0
- package/templates/py-worker/files/.env.example +20 -0
- package/templates/py-worker/files/.forgejo/workflows/build-amd-push.yml +39 -0
- package/templates/py-worker/files/Dockerfile +10 -0
- package/templates/py-worker/files/README.md +3 -0
- package/templates/py-worker/files/_gitignore +176 -0
- package/templates/py-worker/files/config.py +89 -0
- package/templates/py-worker/files/models/decorators.py +42 -0
- package/templates/py-worker/files/models/enums.py +32 -0
- package/templates/py-worker/files/models/mixins.py +61 -0
- package/templates/py-worker/files/requirements.txt +18 -0
- package/templates/py-worker/files/utils/db.py +67 -0
- package/templates/py-worker/files/utils/logger.py +97 -0
- package/templates/py-worker/files/utils/misc.py +45 -0
- package/templates/py-worker/files/utils/worker.py +80 -0
- package/templates/py-worker/files/worker.py +41 -0
- package/templates/py-worker/files/workers/hello/process.py +22 -0
- package/templates/py-worker/template.json +87 -0
- package/templates/react-app/README.md +78 -0
- package/templates/react-app/derive.sh +128 -0
- package/templates/react-app/files/.bin/openapi.js +32 -0
- package/templates/react-app/files/.bin/secrets.js +47 -0
- package/templates/react-app/files/.dockerignore +16 -0
- package/templates/react-app/files/.editorconfig +12 -0
- package/templates/react-app/files/.env.example +34 -0
- package/templates/react-app/files/.forgejo/workflows/build-and-push.yml +55 -0
- package/templates/react-app/files/.forgejo/workflows/ci.yml +39 -0
- package/templates/react-app/files/.husky/pre-commit +1 -0
- package/templates/react-app/files/.nvmrc +1 -0
- package/templates/react-app/files/.prettierignore +6 -0
- package/templates/react-app/files/.prettierrc +9 -0
- package/templates/react-app/files/.yarnrc.yml +2 -0
- package/templates/react-app/files/Dockerfile +52 -0
- package/templates/react-app/files/_gitignore +33 -0
- package/templates/react-app/files/build.sh +53 -0
- package/templates/react-app/files/docker/nginx.conf +134 -0
- package/templates/react-app/files/eslint.config.mjs +75 -0
- package/templates/react-app/files/index.html +62 -0
- package/templates/react-app/files/openapi-ts.config.ts +22 -0
- package/templates/react-app/files/package.json +80 -0
- package/templates/react-app/files/src/app/nav.tsx +32 -0
- package/templates/react-app/files/src/app/providers.tsx +39 -0
- package/templates/react-app/files/src/app/router.tsx +142 -0
- package/templates/react-app/files/src/app/store.ts +20 -0
- package/templates/react-app/files/src/features/auth/components/auth-card.tsx +23 -0
- package/templates/react-app/files/src/features/auth/components/checkbox-field.tsx +32 -0
- package/templates/react-app/files/src/features/auth/components/google-button.tsx +41 -0
- package/templates/react-app/files/src/features/auth/components/or-divider.tsx +9 -0
- package/templates/react-app/files/src/features/auth/components/password-field.tsx +70 -0
- package/templates/react-app/files/src/features/auth/components/submit-button.tsx +16 -0
- package/templates/react-app/files/src/features/auth/lib/auth-actions.ts +68 -0
- package/templates/react-app/files/src/features/auth/lib/password-strength.ts +20 -0
- package/templates/react-app/files/src/features/auth/lib/redirect.ts +6 -0
- package/templates/react-app/files/src/features/auth/lib/schemas.ts +37 -0
- package/templates/react-app/files/src/features/seo/routes.ts +65 -0
- package/templates/react-app/files/src/lib/env.ts +28 -0
- package/templates/react-app/files/src/lib/paths.ts +58 -0
- package/templates/react-app/files/src/main.tsx +19 -0
- package/templates/react-app/files/src/routes/admin/home.tsx +24 -0
- package/templates/react-app/files/src/routes/auth/forgot-password.tsx +93 -0
- package/templates/react-app/files/src/routes/auth/login.tsx +87 -0
- package/templates/react-app/files/src/routes/auth/reset-password.tsx +83 -0
- package/templates/react-app/files/src/routes/auth/signup.tsx +117 -0
- package/templates/react-app/files/src/routes/auth/verified.tsx +31 -0
- package/templates/react-app/files/src/routes/auth/verify-email.tsx +97 -0
- package/templates/react-app/files/src/routes/home.tsx +17 -0
- package/templates/react-app/files/src/routes/legal/privacy.tsx +26 -0
- package/templates/react-app/files/src/routes/legal/terms.tsx +28 -0
- package/templates/react-app/files/src/routes/marketing/home.tsx +18 -0
- package/templates/react-app/files/src/routes/settings.tsx +10 -0
- package/templates/react-app/files/src/shared/api/base-api.ts +24 -0
- package/templates/react-app/files/src/shared/api/base-query.ts +29 -0
- package/templates/react-app/files/src/shared/api/config.ts +21 -0
- package/templates/react-app/files/src/shared/api/index.ts +15 -0
- package/templates/react-app/files/src/shared/api/realtime-events.ts +34 -0
- package/templates/react-app/files/src/shared/api/socket.ts +41 -0
- package/templates/react-app/files/src/shared/api/store.ts +39 -0
- package/templates/react-app/files/src/shared/api/upload-query.ts +115 -0
- package/templates/react-app/files/src/shared/auth/client.ts +27 -0
- package/templates/react-app/files/src/shared/auth/config.ts +28 -0
- package/templates/react-app/files/src/shared/auth/guards.tsx +75 -0
- package/templates/react-app/files/src/shared/auth/hooks.ts +13 -0
- package/templates/react-app/files/src/shared/auth/index.ts +20 -0
- package/templates/react-app/files/src/shared/auth/logout.ts +23 -0
- package/templates/react-app/files/src/shared/auth/provider.tsx +52 -0
- package/templates/react-app/files/src/shared/auth/redirect.ts +65 -0
- package/templates/react-app/files/src/shared/auth/slice.ts +54 -0
- package/templates/react-app/files/src/shared/hooks/index.ts +6 -0
- package/templates/react-app/files/src/shared/layouts/app-shell/app-shell.tsx +225 -0
- package/templates/react-app/files/src/shared/layouts/app-shell/user-menu.tsx +74 -0
- package/templates/react-app/files/src/shared/layouts/auth-layout.tsx +137 -0
- package/templates/react-app/files/src/shared/layouts/error-boundary.tsx +38 -0
- package/templates/react-app/files/src/shared/layouts/index.ts +23 -0
- package/templates/react-app/files/src/shared/layouts/marketing/marketing-layout.tsx +29 -0
- package/templates/react-app/files/src/shared/layouts/marketing/section-heading.tsx +38 -0
- package/templates/react-app/files/src/shared/layouts/marketing/section.tsx +30 -0
- package/templates/react-app/files/src/shared/layouts/states/app-shell-skeleton.tsx +47 -0
- package/templates/react-app/files/src/shared/layouts/states/empty.tsx +22 -0
- package/templates/react-app/files/src/shared/layouts/states/forbidden.tsx +14 -0
- package/templates/react-app/files/src/shared/layouts/states/loading.tsx +11 -0
- package/templates/react-app/files/src/shared/layouts/states/not-found.tsx +21 -0
- package/templates/react-app/files/src/shared/layouts/states/page-skeleton.tsx +39 -0
- package/templates/react-app/files/src/shared/layouts/states/route-fallback.tsx +26 -0
- package/templates/react-app/files/src/shared/layouts/states/state-screen.tsx +22 -0
- package/templates/react-app/files/src/shared/types/README.md +28 -0
- package/templates/react-app/files/src/shared/types/api/index.ts +9 -0
- package/templates/react-app/files/src/shared/types/api/zod.gen.ts +5 -0
- package/templates/react-app/files/src/shared/types/index.ts +4 -0
- package/templates/react-app/files/src/shared/ui/components/brand/logo.tsx +51 -0
- package/templates/react-app/files/src/shared/ui/components/brand/social-icons.tsx +37 -0
- package/templates/react-app/files/src/shared/ui/components/form/form.tsx +120 -0
- package/templates/react-app/files/src/shared/ui/components/ui/alert.tsx +25 -0
- package/templates/react-app/files/src/shared/ui/components/ui/button.tsx +43 -0
- package/templates/react-app/files/src/shared/ui/components/ui/calendar.tsx +78 -0
- package/templates/react-app/files/src/shared/ui/components/ui/card.tsx +44 -0
- package/templates/react-app/files/src/shared/ui/components/ui/checkbox.tsx +16 -0
- package/templates/react-app/files/src/shared/ui/components/ui/date-picker.tsx +76 -0
- package/templates/react-app/files/src/shared/ui/components/ui/input.tsx +22 -0
- package/templates/react-app/files/src/shared/ui/components/ui/label.tsx +19 -0
- package/templates/react-app/files/src/shared/ui/components/ui/modal.tsx +94 -0
- package/templates/react-app/files/src/shared/ui/components/ui/popover.tsx +31 -0
- package/templates/react-app/files/src/shared/ui/components/ui/progress.tsx +33 -0
- package/templates/react-app/files/src/shared/ui/components/ui/search-input.tsx +69 -0
- package/templates/react-app/files/src/shared/ui/components/ui/select.tsx +80 -0
- package/templates/react-app/files/src/shared/ui/components/ui/skeleton.tsx +12 -0
- package/templates/react-app/files/src/shared/ui/components/ui/sonner.tsx +23 -0
- package/templates/react-app/files/src/shared/ui/components/ui/spinner.tsx +8 -0
- package/templates/react-app/files/src/shared/ui/components/ui/textarea.tsx +22 -0
- package/templates/react-app/files/src/shared/ui/index.ts +45 -0
- package/templates/react-app/files/src/shared/ui/lib/utils.ts +7 -0
- package/templates/react-app/files/src/shared/ui/lib/zod-setup.ts +28 -0
- package/templates/react-app/files/src/shared/ui/meta/brand.ts +18 -0
- package/templates/react-app/files/src/shared/ui/meta/meta-context.ts +30 -0
- package/templates/react-app/files/src/shared/ui/meta/meta-provider.tsx +24 -0
- package/templates/react-app/files/src/shared/ui/meta/page-meta.tsx +69 -0
- package/templates/react-app/files/src/shared/ui/meta/use-meta-config.ts +11 -0
- package/templates/react-app/files/src/shared/ui/styles/globals.css +285 -0
- package/templates/react-app/files/src/shared/ui/styles/theme.css +48 -0
- package/templates/react-app/files/src/shared/ui/theme/init-script.ts +23 -0
- package/templates/react-app/files/src/shared/ui/theme/theme-colors.ts +14 -0
- package/templates/react-app/files/src/shared/ui/theme/theme-context.ts +60 -0
- package/templates/react-app/files/src/shared/ui/theme/theme-provider.tsx +101 -0
- package/templates/react-app/files/src/shared/ui/theme/theme-toggle.tsx +40 -0
- package/templates/react-app/files/src/shared/ui/theme/use-theme.ts +11 -0
- package/templates/react-app/files/src/vite-env.d.ts +13 -0
- package/templates/react-app/files/tsconfig.json +26 -0
- package/templates/react-app/files/tsconfig.node.json +25 -0
- package/templates/react-app/files/vite.config.ts +344 -0
- package/templates/react-app/overrides/src/routes/auth/forgot-password.tsx +93 -0
- package/templates/react-app/overrides/src/routes/auth/login.tsx +87 -0
- package/templates/react-app/overrides/src/routes/auth/signup.tsx +117 -0
- package/templates/react-app/overrides/src/routes/auth/verified.tsx +31 -0
- package/templates/react-app/overrides/src/routes/auth/verify-email.tsx +97 -0
- package/templates/react-app/overrides/src/shared/auth/config.ts +28 -0
- package/templates/react-app/overrides/src/shared/auth/guards.tsx +75 -0
- package/templates/react-app/overrides/src/shared/auth/index.ts +20 -0
- package/templates/react-app/overrides/src/shared/auth/redirect.ts +65 -0
- package/templates/react-app/overrides/src/shared/hooks/index.ts +6 -0
- package/templates/react-app/overrides/src/shared/types/README.md +28 -0
- package/templates/react-app/template.json +222 -0
- package/templates/react-monorepo/files/.dockerignore +19 -0
- package/templates/react-monorepo/files/.editorconfig +12 -0
- package/templates/react-monorepo/files/.forgejo/workflows/build-and-push.yml +98 -0
- package/templates/react-monorepo/files/.forgejo/workflows/ci.yml +35 -0
- package/templates/react-monorepo/files/.husky/pre-commit +1 -0
- package/templates/react-monorepo/files/.nvmrc +1 -0
- package/templates/react-monorepo/files/.prettierignore +6 -0
- package/templates/react-monorepo/files/.prettierrc +9 -0
- package/templates/react-monorepo/files/.yarnrc.yml +2 -0
- package/templates/react-monorepo/files/Dockerfile +90 -0
- package/templates/react-monorepo/files/_gitignore +35 -0
- package/templates/react-monorepo/files/apps/admin/.bin/secrets.js +57 -0
- package/templates/react-monorepo/files/apps/admin/.env.example +16 -0
- package/templates/react-monorepo/files/apps/admin/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/apps/admin/index.html +57 -0
- package/templates/react-monorepo/files/apps/admin/package.json +33 -0
- package/templates/react-monorepo/files/apps/admin/src/App.tsx +3 -0
- package/templates/react-monorepo/files/apps/admin/src/app/nav.tsx +14 -0
- package/templates/react-monorepo/files/apps/admin/src/app/providers.tsx +42 -0
- package/templates/react-monorepo/files/apps/admin/src/app/router.tsx +50 -0
- package/templates/react-monorepo/files/apps/admin/src/app/store.ts +15 -0
- package/templates/react-monorepo/files/apps/admin/src/lib/env.ts +12 -0
- package/templates/react-monorepo/files/apps/admin/src/main.tsx +14 -0
- package/templates/react-monorepo/files/apps/admin/src/routes/home.tsx +17 -0
- package/templates/react-monorepo/files/apps/admin/src/routes/settings.tsx +10 -0
- package/templates/react-monorepo/files/apps/admin/src/vite-env.d.ts +14 -0
- package/templates/react-monorepo/files/apps/admin/tsconfig.json +10 -0
- package/templates/react-monorepo/files/apps/admin/vite.config.ts +13 -0
- package/templates/react-monorepo/files/apps/auth/.bin/secrets.js +57 -0
- package/templates/react-monorepo/files/apps/auth/.env.example +16 -0
- package/templates/react-monorepo/files/apps/auth/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/apps/auth/index.html +57 -0
- package/templates/react-monorepo/files/apps/auth/package.json +32 -0
- package/templates/react-monorepo/files/apps/auth/src/App.tsx +3 -0
- package/templates/react-monorepo/files/apps/auth/src/app/providers.tsx +42 -0
- package/templates/react-monorepo/files/apps/auth/src/app/router.tsx +19 -0
- package/templates/react-monorepo/files/apps/auth/src/app/store.ts +15 -0
- package/templates/react-monorepo/files/apps/auth/src/components/auth-card.tsx +23 -0
- package/templates/react-monorepo/files/apps/auth/src/components/checkbox-field.tsx +32 -0
- package/templates/react-monorepo/files/apps/auth/src/components/google-button.tsx +41 -0
- package/templates/react-monorepo/files/apps/auth/src/components/or-divider.tsx +9 -0
- package/templates/react-monorepo/files/apps/auth/src/components/password-field.tsx +70 -0
- package/templates/react-monorepo/files/apps/auth/src/components/submit-button.tsx +16 -0
- package/templates/react-monorepo/files/apps/auth/src/lib/auth-actions.ts +68 -0
- package/templates/react-monorepo/files/apps/auth/src/lib/env.ts +8 -0
- package/templates/react-monorepo/files/apps/auth/src/lib/password-strength.ts +20 -0
- package/templates/react-monorepo/files/apps/auth/src/lib/redirect.ts +6 -0
- package/templates/react-monorepo/files/apps/auth/src/lib/schemas.ts +37 -0
- package/templates/react-monorepo/files/apps/auth/src/main.tsx +14 -0
- package/templates/react-monorepo/files/apps/auth/src/routes/forgot-password.tsx +91 -0
- package/templates/react-monorepo/files/apps/auth/src/routes/login.tsx +82 -0
- package/templates/react-monorepo/files/apps/auth/src/routes/reset-password.tsx +83 -0
- package/templates/react-monorepo/files/apps/auth/src/routes/signup.tsx +117 -0
- package/templates/react-monorepo/files/apps/auth/src/routes/verified.tsx +30 -0
- package/templates/react-monorepo/files/apps/auth/src/routes/verify-email.tsx +96 -0
- package/templates/react-monorepo/files/apps/auth/src/vite-env.d.ts +14 -0
- package/templates/react-monorepo/files/apps/auth/tsconfig.json +10 -0
- package/templates/react-monorepo/files/apps/auth/vite.config.ts +13 -0
- package/templates/react-monorepo/files/apps/client/.bin/secrets.js +57 -0
- package/templates/react-monorepo/files/apps/client/.env.example +16 -0
- package/templates/react-monorepo/files/apps/client/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/apps/client/index.html +57 -0
- package/templates/react-monorepo/files/apps/client/package.json +35 -0
- package/templates/react-monorepo/files/apps/client/src/App.tsx +3 -0
- package/templates/react-monorepo/files/apps/client/src/app/nav.tsx +14 -0
- package/templates/react-monorepo/files/apps/client/src/app/providers.tsx +42 -0
- package/templates/react-monorepo/files/apps/client/src/app/router.tsx +50 -0
- package/templates/react-monorepo/files/apps/client/src/app/store.ts +15 -0
- package/templates/react-monorepo/files/apps/client/src/lib/env.ts +12 -0
- package/templates/react-monorepo/files/apps/client/src/main.tsx +19 -0
- package/templates/react-monorepo/files/apps/client/src/routes/home.tsx +17 -0
- package/templates/react-monorepo/files/apps/client/src/routes/settings.tsx +10 -0
- package/templates/react-monorepo/files/apps/client/src/vite-env.d.ts +15 -0
- package/templates/react-monorepo/files/apps/client/tsconfig.json +10 -0
- package/templates/react-monorepo/files/apps/client/vite.config.ts +120 -0
- package/templates/react-monorepo/files/apps/landing/.bin/secrets.js +57 -0
- package/templates/react-monorepo/files/apps/landing/.env.example +20 -0
- package/templates/react-monorepo/files/apps/landing/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/apps/landing/index.html +51 -0
- package/templates/react-monorepo/files/apps/landing/package.json +33 -0
- package/templates/react-monorepo/files/apps/landing/src/App.tsx +3 -0
- package/templates/react-monorepo/files/apps/landing/src/app/providers.tsx +42 -0
- package/templates/react-monorepo/files/apps/landing/src/app/router.tsx +21 -0
- package/templates/react-monorepo/files/apps/landing/src/app/store.ts +18 -0
- package/templates/react-monorepo/files/apps/landing/src/features/seo/routes.ts +65 -0
- package/templates/react-monorepo/files/apps/landing/src/lib/env.ts +10 -0
- package/templates/react-monorepo/files/apps/landing/src/main.tsx +14 -0
- package/templates/react-monorepo/files/apps/landing/src/routes/home.tsx +18 -0
- package/templates/react-monorepo/files/apps/landing/src/vite-env.d.ts +15 -0
- package/templates/react-monorepo/files/apps/landing/tsconfig.json +10 -0
- package/templates/react-monorepo/files/apps/landing/vite.config.ts +169 -0
- package/templates/react-monorepo/files/build.sh +61 -0
- package/templates/react-monorepo/files/docker/nginx.conf +136 -0
- package/templates/react-monorepo/files/package.json +37 -0
- package/templates/react-monorepo/files/packages/.gitkeep +0 -0
- package/templates/react-monorepo/files/packages/api/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/packages/api/package.json +30 -0
- package/templates/react-monorepo/files/packages/api/src/base-api.ts +24 -0
- package/templates/react-monorepo/files/packages/api/src/base-query.ts +29 -0
- package/templates/react-monorepo/files/packages/api/src/config.ts +21 -0
- package/templates/react-monorepo/files/packages/api/src/index.ts +15 -0
- package/templates/react-monorepo/files/packages/api/src/realtime-events.ts +34 -0
- package/templates/react-monorepo/files/packages/api/src/socket.ts +41 -0
- package/templates/react-monorepo/files/packages/api/src/store.ts +39 -0
- package/templates/react-monorepo/files/packages/api/src/upload-query.ts +115 -0
- package/templates/react-monorepo/files/packages/api/tsconfig.json +4 -0
- package/templates/react-monorepo/files/packages/auth/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/packages/auth/package.json +33 -0
- package/templates/react-monorepo/files/packages/auth/src/client.ts +27 -0
- package/templates/react-monorepo/files/packages/auth/src/config.ts +22 -0
- package/templates/react-monorepo/files/packages/auth/src/guards.tsx +61 -0
- package/templates/react-monorepo/files/packages/auth/src/hooks.ts +13 -0
- package/templates/react-monorepo/files/packages/auth/src/index.ts +20 -0
- package/templates/react-monorepo/files/packages/auth/src/logout.ts +23 -0
- package/templates/react-monorepo/files/packages/auth/src/provider.tsx +52 -0
- package/templates/react-monorepo/files/packages/auth/src/redirect.ts +37 -0
- package/templates/react-monorepo/files/packages/auth/src/slice.ts +54 -0
- package/templates/react-monorepo/files/packages/auth/tsconfig.json +4 -0
- package/templates/react-monorepo/files/packages/config/README.md +47 -0
- package/templates/react-monorepo/files/packages/config/eslint/base.mjs +83 -0
- package/templates/react-monorepo/files/packages/config/package.json +37 -0
- package/templates/react-monorepo/files/packages/config/tailwind/theme.css +48 -0
- package/templates/react-monorepo/files/packages/config/tsconfig/base.json +22 -0
- package/templates/react-monorepo/files/packages/config/tsconfig/node.json +9 -0
- package/templates/react-monorepo/files/packages/config/tsconfig/react.json +9 -0
- package/templates/react-monorepo/files/packages/config/vite/index.d.ts +18 -0
- package/templates/react-monorepo/files/packages/config/vite/index.mjs +129 -0
- package/templates/react-monorepo/files/packages/hooks/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/packages/hooks/package.json +16 -0
- package/templates/react-monorepo/files/packages/hooks/src/index.ts +3 -0
- package/templates/react-monorepo/files/packages/hooks/tsconfig.json +4 -0
- package/templates/react-monorepo/files/packages/layouts/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/packages/layouts/package.json +30 -0
- package/templates/react-monorepo/files/packages/layouts/src/app-shell/app-shell.tsx +225 -0
- package/templates/react-monorepo/files/packages/layouts/src/app-shell/user-menu.tsx +74 -0
- package/templates/react-monorepo/files/packages/layouts/src/auth-layout.tsx +137 -0
- package/templates/react-monorepo/files/packages/layouts/src/error-boundary.tsx +38 -0
- package/templates/react-monorepo/files/packages/layouts/src/index.ts +23 -0
- package/templates/react-monorepo/files/packages/layouts/src/marketing/marketing-layout.tsx +29 -0
- package/templates/react-monorepo/files/packages/layouts/src/marketing/section-heading.tsx +38 -0
- package/templates/react-monorepo/files/packages/layouts/src/marketing/section.tsx +30 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/app-shell-skeleton.tsx +47 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/empty.tsx +22 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/forbidden.tsx +14 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/loading.tsx +11 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/not-found.tsx +21 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/page-skeleton.tsx +39 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/route-fallback.tsx +26 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/state-screen.tsx +22 -0
- package/templates/react-monorepo/files/packages/layouts/tsconfig.json +4 -0
- package/templates/react-monorepo/files/packages/types/.bin/openapi.js +32 -0
- package/templates/react-monorepo/files/packages/types/.bin/secrets.js +57 -0
- package/templates/react-monorepo/files/packages/types/README.md +28 -0
- package/templates/react-monorepo/files/packages/types/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/packages/types/openapi-ts.config.ts +22 -0
- package/templates/react-monorepo/files/packages/types/package.json +29 -0
- package/templates/react-monorepo/files/packages/types/src/index.ts +4 -0
- package/templates/react-monorepo/files/packages/types/src/types/api/index.ts +9 -0
- package/templates/react-monorepo/files/packages/types/src/types/api/zod.gen.ts +5 -0
- package/templates/react-monorepo/files/packages/types/tsconfig.json +7 -0
- package/templates/react-monorepo/files/packages/ui/components.json +21 -0
- package/templates/react-monorepo/files/packages/ui/eslint.config.mjs +13 -0
- package/templates/react-monorepo/files/packages/ui/package.json +44 -0
- package/templates/react-monorepo/files/packages/ui/src/components/brand/logo.tsx +51 -0
- package/templates/react-monorepo/files/packages/ui/src/components/brand/social-icons.tsx +37 -0
- package/templates/react-monorepo/files/packages/ui/src/components/form/form.tsx +120 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/alert.tsx +25 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/button.tsx +43 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/calendar.tsx +78 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/card.tsx +44 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/checkbox.tsx +16 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/date-picker.tsx +76 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/input.tsx +22 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/label.tsx +19 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/modal.tsx +94 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/popover.tsx +31 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/progress.tsx +33 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/search-input.tsx +69 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/select.tsx +80 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/skeleton.tsx +12 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/sonner.tsx +23 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/spinner.tsx +8 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/textarea.tsx +22 -0
- package/templates/react-monorepo/files/packages/ui/src/index.ts +45 -0
- package/templates/react-monorepo/files/packages/ui/src/lib/utils.ts +7 -0
- package/templates/react-monorepo/files/packages/ui/src/lib/zod-setup.ts +28 -0
- package/templates/react-monorepo/files/packages/ui/src/meta/brand.ts +18 -0
- package/templates/react-monorepo/files/packages/ui/src/meta/meta-context.ts +30 -0
- package/templates/react-monorepo/files/packages/ui/src/meta/meta-provider.tsx +24 -0
- package/templates/react-monorepo/files/packages/ui/src/meta/page-meta.tsx +69 -0
- package/templates/react-monorepo/files/packages/ui/src/meta/use-meta-config.ts +11 -0
- package/templates/react-monorepo/files/packages/ui/src/styles/globals.css +285 -0
- package/templates/react-monorepo/files/packages/ui/src/theme/init-script.ts +23 -0
- package/templates/react-monorepo/files/packages/ui/src/theme/theme-colors.ts +14 -0
- package/templates/react-monorepo/files/packages/ui/src/theme/theme-context.ts +60 -0
- package/templates/react-monorepo/files/packages/ui/src/theme/theme-provider.tsx +101 -0
- package/templates/react-monorepo/files/packages/ui/src/theme/theme-toggle.tsx +40 -0
- package/templates/react-monorepo/files/packages/ui/src/theme/use-theme.ts +11 -0
- package/templates/react-monorepo/files/packages/ui/tsconfig.json +9 -0
- package/templates/react-monorepo/files/turbo.json +24 -0
- package/templates/react-monorepo/template.json +230 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Socket.io path — kept distinct from the REST surface so a reverse proxy can
|
|
3
|
+
* route WebSocket upgrades independently. The frontend must connect with the
|
|
4
|
+
* same `path`.
|
|
5
|
+
*/
|
|
6
|
+
export const SOCKET_PATH = '/realtime';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Server → client event names. Keeping them centralized means the gateway, the
|
|
10
|
+
* publisher service and the frontend share a single source of truth instead of
|
|
11
|
+
* trading raw strings.
|
|
12
|
+
*/
|
|
13
|
+
export const ServerEvent = {
|
|
14
|
+
/** Presence roster changed (a user came online / went offline). */
|
|
15
|
+
PRESENCE_UPDATE: 'presence:update',
|
|
16
|
+
/** Someone started/stopped typing in a room. */
|
|
17
|
+
TYPING: 'activity:typing',
|
|
18
|
+
/** Someone is viewing a resource. */
|
|
19
|
+
VIEWING: 'activity:viewing',
|
|
20
|
+
/** A user-directed notification. */
|
|
21
|
+
NOTIFICATION: 'notification',
|
|
22
|
+
/** Something in a channel changed; payload is app-defined. */
|
|
23
|
+
CHANNEL_UPDATE: 'channel:update'
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Client → server event names.
|
|
28
|
+
*/
|
|
29
|
+
export const ClientEvent = {
|
|
30
|
+
JOIN_ROOM: 'room:join',
|
|
31
|
+
LEAVE_ROOM: 'room:leave',
|
|
32
|
+
TYPING: 'activity:typing',
|
|
33
|
+
VIEWING: 'activity:viewing'
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
/** Every socket of a single user joins this room, so we can target the user. */
|
|
37
|
+
export const userRoom = (userId: string) => `user:${userId}`;
|
|
38
|
+
|
|
39
|
+
/** Firehose room — clients here see every broadcast on the default channel. */
|
|
40
|
+
export const FEED_ROOM = 'feed';
|
|
41
|
+
|
|
42
|
+
/** Room scoped to one resource; comments, typing and viewing land here. */
|
|
43
|
+
export const channelRoom = (channelId: string) => `channel:${channelId}`;
|
|
44
|
+
|
|
45
|
+
/** cuid-shaped ids, as generated by Prisma's `@default(cuid())`. */
|
|
46
|
+
const CHANNEL_ROOM_PATTERN = /^channel:[A-Za-z0-9_-]{1,64}$/;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Rooms a client may join via the generic `room:join` message.
|
|
50
|
+
*
|
|
51
|
+
* This is an allowlist on purpose. Rooms are plain strings, so an open
|
|
52
|
+
* `socket.join(body.room)` would let any authenticated user join `user:<id>`
|
|
53
|
+
* and receive another user's private notifications (see
|
|
54
|
+
* `RealtimeService.notifyUser`). That is reachable only through this door, so
|
|
55
|
+
* widen the allowlist deliberately — never by passing the client's string
|
|
56
|
+
* straight to `socket.join`.
|
|
57
|
+
*/
|
|
58
|
+
export const isJoinableRoom = (room: string): boolean =>
|
|
59
|
+
room === FEED_ROOM || CHANNEL_ROOM_PATTERN.test(room);
|
|
60
|
+
|
|
61
|
+
/** Redis key holding the set of currently-connected user ids (presence). */
|
|
62
|
+
export const PRESENCE_KEY = 'realtime:presence:online';
|
|
63
|
+
/**
|
|
64
|
+
* Per-user connection counter. A user may have several tabs/devices; they only
|
|
65
|
+
* count as "offline" once every socket has disconnected.
|
|
66
|
+
*/
|
|
67
|
+
export const presenceCountKey = (userId: string) =>
|
|
68
|
+
`realtime:presence:count:${userId}`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ws-permissions.decorator';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SetMetadata } from '@nestjs/common';
|
|
2
|
+
import { Permission } from 'lib/access';
|
|
3
|
+
|
|
4
|
+
export const WS_PERMISSIONS_KEY = 'ws:permissions';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Require a permission on a `@SubscribeMessage` handler, mirroring what
|
|
8
|
+
* `@UserHasPermission` does for HTTP routes. Enforced by `WsPermissionsGuard`.
|
|
9
|
+
*
|
|
10
|
+
* Websocket handlers never pass through the global HTTP `AuthGuard`, so without
|
|
11
|
+
* this they are gated on identity alone.
|
|
12
|
+
*/
|
|
13
|
+
export const WsPermissions = (permission: Permission) =>
|
|
14
|
+
SetMetadata(WS_PERMISSIONS_KEY, permission);
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { UseGuards } from '@nestjs/common';
|
|
2
|
+
import {
|
|
3
|
+
ConnectedSocket,
|
|
4
|
+
MessageBody,
|
|
5
|
+
OnGatewayConnection,
|
|
6
|
+
OnGatewayDisconnect,
|
|
7
|
+
OnGatewayInit,
|
|
8
|
+
SubscribeMessage,
|
|
9
|
+
WebSocketGateway,
|
|
10
|
+
WebSocketServer,
|
|
11
|
+
WsException
|
|
12
|
+
} from '@nestjs/websockets';
|
|
13
|
+
import { Server, Socket } from 'socket.io';
|
|
14
|
+
import { CustomAuthService, LoggerService } from 'common/services';
|
|
15
|
+
import { PresenceService } from '../services/presence.service';
|
|
16
|
+
import { RealtimeService } from '../services/realtime.service';
|
|
17
|
+
import { WsAuthGuard, WsPermissionsGuard } from '../guards';
|
|
18
|
+
import { getSessionUserFromSocket } from '../misc/session-from-socket';
|
|
19
|
+
import {
|
|
20
|
+
ClientEvent,
|
|
21
|
+
ServerEvent,
|
|
22
|
+
isJoinableRoom,
|
|
23
|
+
userRoom
|
|
24
|
+
} from '../constants';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Central Socket.io gateway.
|
|
28
|
+
*
|
|
29
|
+
* Responsibilities (PRD §2.2 / §3.1):
|
|
30
|
+
* - Authenticate every socket against the better-auth session on connect.
|
|
31
|
+
* - Maintain online-user presence across replicas (via {@link PresenceService}).
|
|
32
|
+
* - Let clients subscribe to live market rooms for price tickers.
|
|
33
|
+
* - Relay lightweight activity signals (typing / viewing) within a room.
|
|
34
|
+
*
|
|
35
|
+
* The `cors` / `path` for the server are configured on the Redis adapter
|
|
36
|
+
* (`RedisIoAdapter`), so the decorator stays option-free.
|
|
37
|
+
*/
|
|
38
|
+
@WebSocketGateway()
|
|
39
|
+
// Applied at class level so a newly added handler is guarded by default rather
|
|
40
|
+
// than by remembering to decorate it. `handleConnection` is not a message
|
|
41
|
+
// handler and is unaffected — it does its own session check.
|
|
42
|
+
/**
|
|
43
|
+
* Guards are applied at the **class** level, so every `@SubscribeMessage`
|
|
44
|
+
* handler is covered by default — Nest concatenates class- then method-level
|
|
45
|
+
* guard metadata. `WsAuthGuard` establishes identity; `WsPermissionsGuard`
|
|
46
|
+
* enforces `@WsPermissions({ <resource>: ['<action>'] })` from
|
|
47
|
+
* `realtime/decorators`, which is the only authorization path websockets get
|
|
48
|
+
* (they never traverse the HTTP `AuthGuard`).
|
|
49
|
+
*/
|
|
50
|
+
@UseGuards(WsAuthGuard, WsPermissionsGuard)
|
|
51
|
+
export class EventsGateway
|
|
52
|
+
implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
|
|
53
|
+
{
|
|
54
|
+
@WebSocketServer() private server!: Server;
|
|
55
|
+
|
|
56
|
+
constructor(
|
|
57
|
+
private readonly auth: CustomAuthService,
|
|
58
|
+
private readonly presence: PresenceService,
|
|
59
|
+
private readonly realtime: RealtimeService,
|
|
60
|
+
private readonly logger: LoggerService
|
|
61
|
+
) {}
|
|
62
|
+
|
|
63
|
+
afterInit(server: Server) {
|
|
64
|
+
// Hand the live server to the publisher so other modules can emit.
|
|
65
|
+
this.realtime.bindServer(server);
|
|
66
|
+
this.logger.debug('Realtime gateway initialized.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async handleConnection(socket: Socket) {
|
|
70
|
+
const user = await getSessionUserFromSocket(socket, this.auth);
|
|
71
|
+
if (!user) {
|
|
72
|
+
// No valid session — refuse the connection.
|
|
73
|
+
socket.emit('error', { message: 'Unauthorized' });
|
|
74
|
+
socket.disconnect(true);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
socket.data.user = user;
|
|
79
|
+
// Every socket of a user joins their personal room for direct messaging.
|
|
80
|
+
await socket.join(userRoom(user.id));
|
|
81
|
+
|
|
82
|
+
const cameOnline = await this.presence.connect(user.id);
|
|
83
|
+
if (cameOnline) {
|
|
84
|
+
this.server.emit(ServerEvent.PRESENCE_UPDATE, {
|
|
85
|
+
userId: user.id,
|
|
86
|
+
status: 'online'
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.logger.debug(
|
|
91
|
+
{ userId: user.id, socketId: socket.id },
|
|
92
|
+
'Socket connected.'
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async handleDisconnect(socket: Socket) {
|
|
97
|
+
const user = socket.data.user;
|
|
98
|
+
if (!user) return;
|
|
99
|
+
|
|
100
|
+
const wentOffline = await this.presence.disconnect(user.id);
|
|
101
|
+
if (wentOffline) {
|
|
102
|
+
this.server.emit(ServerEvent.PRESENCE_UPDATE, {
|
|
103
|
+
userId: user.id,
|
|
104
|
+
status: 'offline'
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.logger.debug(
|
|
109
|
+
{ userId: user.id, socketId: socket.id },
|
|
110
|
+
'Socket disconnected.'
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// --- Presence ----------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
@SubscribeMessage('presence:list')
|
|
117
|
+
async listOnline() {
|
|
118
|
+
const userIds = await this.presence.online();
|
|
119
|
+
return { event: ServerEvent.PRESENCE_UPDATE, data: { online: userIds } };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// --- Rooms --------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
@SubscribeMessage(ClientEvent.JOIN_ROOM)
|
|
125
|
+
async joinRoom(
|
|
126
|
+
@ConnectedSocket() socket: Socket,
|
|
127
|
+
@MessageBody() body: { room: string }
|
|
128
|
+
) {
|
|
129
|
+
// Only rooms on the allowlist are joinable this way. `user:<id>` is not —
|
|
130
|
+
// it would hand the caller another user's private notifications.
|
|
131
|
+
if (!isJoinableRoom(body.room)) {
|
|
132
|
+
throw new WsException('Forbidden');
|
|
133
|
+
}
|
|
134
|
+
await socket.join(body.room);
|
|
135
|
+
return { ok: true, room: body.room };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@SubscribeMessage(ClientEvent.LEAVE_ROOM)
|
|
139
|
+
async leaveRoom(
|
|
140
|
+
@ConnectedSocket() socket: Socket,
|
|
141
|
+
@MessageBody() body: { room: string }
|
|
142
|
+
) {
|
|
143
|
+
await socket.leave(body.room);
|
|
144
|
+
return { ok: true };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --- Activity signals (who is typing / viewing) ------------------------
|
|
148
|
+
|
|
149
|
+
@SubscribeMessage(ClientEvent.TYPING)
|
|
150
|
+
typing(
|
|
151
|
+
@ConnectedSocket() socket: Socket,
|
|
152
|
+
@MessageBody() body: { room: string; isTyping: boolean }
|
|
153
|
+
) {
|
|
154
|
+
this.assertInRoom(socket, body.room);
|
|
155
|
+
// Relay to everyone else in the room — never echo back to the sender.
|
|
156
|
+
socket.to(body.room).emit(ServerEvent.TYPING, {
|
|
157
|
+
room: body.room,
|
|
158
|
+
userId: socket.data.user!.id,
|
|
159
|
+
name: socket.data.user!.name,
|
|
160
|
+
isTyping: body.isTyping
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@SubscribeMessage(ClientEvent.VIEWING)
|
|
165
|
+
viewing(
|
|
166
|
+
@ConnectedSocket() socket: Socket,
|
|
167
|
+
@MessageBody() body: { room: string; viewing: boolean }
|
|
168
|
+
) {
|
|
169
|
+
this.assertInRoom(socket, body.room);
|
|
170
|
+
socket.to(body.room).emit(ServerEvent.VIEWING, {
|
|
171
|
+
room: body.room,
|
|
172
|
+
userId: socket.data.user!.id,
|
|
173
|
+
viewing: body.viewing
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* A client may only signal into a room it actually occupies. Otherwise it
|
|
179
|
+
* could emit forged typing/viewing events into any room by name, including
|
|
180
|
+
* another user's `user:<id>` room.
|
|
181
|
+
*/
|
|
182
|
+
private assertInRoom(socket: Socket, room: string) {
|
|
183
|
+
if (!socket.rooms.has(room)) {
|
|
184
|
+
throw new WsException('Not in room');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
|
2
|
+
import { WsException } from '@nestjs/websockets';
|
|
3
|
+
import { Socket } from 'socket.io';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Guards `@SubscribeMessage` handlers: the socket must already carry an
|
|
7
|
+
* authenticated user (attached at connection time in `EventsGateway`). This is
|
|
8
|
+
* defense-in-depth — unauthenticated sockets are disconnected on connect, but
|
|
9
|
+
* message handlers should never trust that implicitly.
|
|
10
|
+
*/
|
|
11
|
+
@Injectable()
|
|
12
|
+
export class WsAuthGuard implements CanActivate {
|
|
13
|
+
canActivate(context: ExecutionContext): boolean {
|
|
14
|
+
const socket = context.switchToWs().getClient<Socket>();
|
|
15
|
+
if (!socket.data.user) {
|
|
16
|
+
throw new WsException('Unauthorized');
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
|
2
|
+
import { Reflector } from '@nestjs/core';
|
|
3
|
+
import { WsException } from '@nestjs/websockets';
|
|
4
|
+
import { Socket } from 'socket.io';
|
|
5
|
+
import { Permission, hasPermission } from 'lib/access';
|
|
6
|
+
import { WS_PERMISSIONS_KEY } from '../decorators/ws-permissions.decorator';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Enforces `@WsPermissions(...)` on socket message handlers.
|
|
10
|
+
*
|
|
11
|
+
* Runs after `WsAuthGuard`, which establishes *who* the socket is; this decides
|
|
12
|
+
* *what* they may do. A handler with no `@WsPermissions` metadata is allowed —
|
|
13
|
+
* identity alone is the requirement there.
|
|
14
|
+
*/
|
|
15
|
+
@Injectable()
|
|
16
|
+
export class WsPermissionsGuard implements CanActivate {
|
|
17
|
+
constructor(private readonly reflector: Reflector) {}
|
|
18
|
+
|
|
19
|
+
canActivate(context: ExecutionContext): boolean {
|
|
20
|
+
const required = this.reflector.getAllAndOverride<Permission | undefined>(
|
|
21
|
+
WS_PERMISSIONS_KEY,
|
|
22
|
+
[context.getHandler(), context.getClass()]
|
|
23
|
+
);
|
|
24
|
+
if (!required) return true;
|
|
25
|
+
|
|
26
|
+
const socket = context.switchToWs().getClient<Socket>();
|
|
27
|
+
const user = socket.data.user;
|
|
28
|
+
if (!user) throw new WsException('Unauthorized');
|
|
29
|
+
|
|
30
|
+
if (!hasPermission(user.role, required)) {
|
|
31
|
+
throw new WsException('Forbidden');
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Socket } from 'socket.io';
|
|
2
|
+
import { CustomAuthService } from 'common/services';
|
|
3
|
+
|
|
4
|
+
/** The authenticated identity we stash on `socket.data` after the handshake. */
|
|
5
|
+
export interface SocketUser {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
email: string;
|
|
9
|
+
role?: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
declare module 'socket.io' {
|
|
13
|
+
interface SocketData {
|
|
14
|
+
user?: SocketUser;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a better-auth session from a socket handshake.
|
|
20
|
+
*
|
|
21
|
+
* The browser sends the same session cookie it uses for REST, so we forward the
|
|
22
|
+
* handshake headers to better-auth's `getSession`. Returns `null` when there is
|
|
23
|
+
* no valid session — callers decide whether to disconnect.
|
|
24
|
+
*/
|
|
25
|
+
export const getSessionUserFromSocket = async (
|
|
26
|
+
socket: Socket,
|
|
27
|
+
auth: CustomAuthService
|
|
28
|
+
): Promise<SocketUser | null> => {
|
|
29
|
+
// socket.io exposes handshake headers as a plain object; better-auth wants a
|
|
30
|
+
// Fetch `Headers`. The cookie is the only header it needs.
|
|
31
|
+
const headers = new Headers();
|
|
32
|
+
const cookie = socket.handshake.headers.cookie;
|
|
33
|
+
if (cookie) headers.set('cookie', cookie);
|
|
34
|
+
|
|
35
|
+
// Also accept a bearer token (handy for native/PWA clients that can't rely on
|
|
36
|
+
// cookies), mirroring better-auth's bearer support.
|
|
37
|
+
const auth_header =
|
|
38
|
+
socket.handshake.auth?.token ?? socket.handshake.headers.authorization;
|
|
39
|
+
if (auth_header) {
|
|
40
|
+
headers.set(
|
|
41
|
+
'authorization',
|
|
42
|
+
auth_header.startsWith('Bearer ') ? auth_header : `Bearer ${auth_header}`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const session = await auth.api.getSession({ headers });
|
|
48
|
+
if (!session?.user) return null;
|
|
49
|
+
const { id, name, email, role } = session.user;
|
|
50
|
+
return { id, name, email, role };
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Global, Module } from '@nestjs/common';
|
|
2
|
+
import { EventsGateway } from './gateways/events.gateway';
|
|
3
|
+
import { PresenceService } from './services/presence.service';
|
|
4
|
+
import { RealtimeService } from './services/realtime.service';
|
|
5
|
+
import { WsAuthGuard, WsPermissionsGuard } from './guards';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Realtime (Socket.io) layer. Marked `@Global` so cross-cutting publishers —
|
|
9
|
+
* queue processors, schedulers and REST services — can
|
|
10
|
+
* inject {@link RealtimeService} without importing this module everywhere.
|
|
11
|
+
*
|
|
12
|
+
* The Redis adapter for horizontal scaling is wired separately in `main.ts`
|
|
13
|
+
* (`RedisIoAdapter`) because it must be attached to the app before `listen()`.
|
|
14
|
+
*/
|
|
15
|
+
@Global()
|
|
16
|
+
@Module({
|
|
17
|
+
providers: [
|
|
18
|
+
EventsGateway,
|
|
19
|
+
PresenceService,
|
|
20
|
+
RealtimeService,
|
|
21
|
+
WsAuthGuard,
|
|
22
|
+
WsPermissionsGuard
|
|
23
|
+
],
|
|
24
|
+
exports: [RealtimeService, PresenceService]
|
|
25
|
+
})
|
|
26
|
+
export class RealtimeModule {}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { CacheService } from 'common/services';
|
|
3
|
+
import { PRESENCE_KEY, presenceCountKey } from '../constants';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tracks which users are online across all backend replicas.
|
|
7
|
+
*
|
|
8
|
+
* A user can hold several sockets at once (multiple tabs / devices), so we keep
|
|
9
|
+
* a per-user connection counter in Redis and only flip presence when it crosses
|
|
10
|
+
* the 0↔1 boundary. State lives in Redis (not memory) so it is correct behind
|
|
11
|
+
* the horizontally-scaled Socket.io adapter.
|
|
12
|
+
*/
|
|
13
|
+
@Injectable()
|
|
14
|
+
export class PresenceService {
|
|
15
|
+
constructor(private readonly cache: CacheService) {}
|
|
16
|
+
|
|
17
|
+
private get redis() {
|
|
18
|
+
return this.cache.db;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Record a new socket for a user. Returns `true` if this was their first
|
|
23
|
+
* connection (i.e. they just came online), so the caller can broadcast it.
|
|
24
|
+
*/
|
|
25
|
+
async connect(userId: string): Promise<boolean> {
|
|
26
|
+
const count = await this.redis.incr(presenceCountKey(userId));
|
|
27
|
+
if (count === 1) {
|
|
28
|
+
await this.redis.sadd(PRESENCE_KEY, userId);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Record a socket closing for a user. Returns `true` if that was their last
|
|
36
|
+
* connection (i.e. they just went offline).
|
|
37
|
+
*/
|
|
38
|
+
async disconnect(userId: string): Promise<boolean> {
|
|
39
|
+
const count = await this.redis.decr(presenceCountKey(userId));
|
|
40
|
+
if (count <= 0) {
|
|
41
|
+
await this.redis.del(presenceCountKey(userId));
|
|
42
|
+
await this.redis.srem(PRESENCE_KEY, userId);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Ids of every currently-online user. */
|
|
49
|
+
async online(): Promise<string[]> {
|
|
50
|
+
return this.redis.smembers(PRESENCE_KEY);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async isOnline(userId: string): Promise<boolean> {
|
|
54
|
+
return (await this.redis.sismember(PRESENCE_KEY, userId)) === 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { Server } from 'socket.io';
|
|
3
|
+
import { LoggerService } from 'common/services';
|
|
4
|
+
import { FEED_ROOM, ServerEvent, channelRoom, userRoom } from '../constants';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The publish side of the realtime layer.
|
|
8
|
+
*
|
|
9
|
+
* Any module (queue processors, schedulers, REST services) can inject this to
|
|
10
|
+
* push events to connected clients without depending on the gateway — which
|
|
11
|
+
* would create a cycle and couple business logic to the transport. The gateway
|
|
12
|
+
* hands us the live Socket.io `Server` in its `afterInit`; before that (or in
|
|
13
|
+
* tests with no WS server) every emit is a safe no-op.
|
|
14
|
+
*/
|
|
15
|
+
@Injectable()
|
|
16
|
+
export class RealtimeService {
|
|
17
|
+
private server?: Server;
|
|
18
|
+
|
|
19
|
+
constructor(private readonly logger: LoggerService) {}
|
|
20
|
+
|
|
21
|
+
/** Called once by the gateway when the Socket.io server is ready. */
|
|
22
|
+
bindServer(server: Server) {
|
|
23
|
+
this.server = server;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get isReady(): boolean {
|
|
27
|
+
return !!this.server;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Deliver a notification to every socket belonging to one user. */
|
|
31
|
+
notifyUser(userId: string, notification: unknown) {
|
|
32
|
+
this.emitToRoom(userRoom(userId), ServerEvent.NOTIFICATION, notification);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Announce something to everyone watching the global feed. */
|
|
36
|
+
emitToFeed(event: string, payload: unknown) {
|
|
37
|
+
this.emitToRoom(FEED_ROOM, event, payload);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Push an update to everyone in one channel's room. */
|
|
41
|
+
emitToChannel(channelId: string, payload: unknown) {
|
|
42
|
+
this.emitToRoom(
|
|
43
|
+
channelRoom(channelId),
|
|
44
|
+
ServerEvent.CHANNEL_UPDATE,
|
|
45
|
+
payload
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Low-level: emit to a specific room. */
|
|
50
|
+
emitToRoom(room: string, event: string, payload: unknown) {
|
|
51
|
+
if (!this.server) {
|
|
52
|
+
this.logger.debug(
|
|
53
|
+
{ room, event },
|
|
54
|
+
'Realtime server not ready; dropping emit.'
|
|
55
|
+
);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this.server.to(room).emit(event, payload);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Low-level: emit to every connected client. */
|
|
62
|
+
broadcast(event: string, payload: unknown) {
|
|
63
|
+
if (!this.server) {
|
|
64
|
+
this.logger.debug(
|
|
65
|
+
{ event },
|
|
66
|
+
'Realtime server not ready; dropping broadcast.'
|
|
67
|
+
);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.server.emit(event, payload);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { TestBed, type Mocked } from '@suites/unit';
|
|
2
|
+
import { Entity } from '@test/factory/entity';
|
|
3
|
+
import { CurrentUserController } from '../controllers';
|
|
4
|
+
import { CurrentUserService } from '../services';
|
|
5
|
+
import { CurrentUserEntity, toCurrentUser } from '../entity';
|
|
6
|
+
|
|
7
|
+
describe('CurrentUserController', () => {
|
|
8
|
+
let controller: CurrentUserController;
|
|
9
|
+
let service: Mocked<CurrentUserService>;
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
const { unit, unitRef } = await TestBed.solitary(
|
|
13
|
+
CurrentUserController
|
|
14
|
+
).compile();
|
|
15
|
+
controller = unit;
|
|
16
|
+
service = unitRef.get(CurrentUserService);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should be defined', () => {
|
|
20
|
+
expect(controller).toBeDefined();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('completeOnboarding', () => {
|
|
24
|
+
it('delegates to CurrentUserService with the session user', async () => {
|
|
25
|
+
const session = Entity.userSession.build();
|
|
26
|
+
const updated = toCurrentUser(session.user);
|
|
27
|
+
service.completeOnboarding.mockResolvedValue(updated);
|
|
28
|
+
|
|
29
|
+
const result = await controller.completeOnboarding(session);
|
|
30
|
+
|
|
31
|
+
expect(service.completeOnboarding).toHaveBeenCalledWith(session.user);
|
|
32
|
+
expect(result).toBe(updated);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('getCurrentUser', () => {
|
|
37
|
+
it('should return a CurrentUserEntity from session user', () => {
|
|
38
|
+
const session = Entity.userSession.build();
|
|
39
|
+
const result = controller.getCurrentUser(session);
|
|
40
|
+
|
|
41
|
+
expect(CurrentUserEntity.safeParse(result).success).toBe(true);
|
|
42
|
+
expect(result.id).toBe(session.user.id);
|
|
43
|
+
expect(result.name).toBe(session.user.name);
|
|
44
|
+
expect(result.email).toBe(session.user.email);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// @feature:start media
|
|
49
|
+
describe('updateAvatar', () => {
|
|
50
|
+
const file = {
|
|
51
|
+
originalname: 'photo.jpg',
|
|
52
|
+
buffer: Buffer.from('photo-data'),
|
|
53
|
+
mimetype: 'image/jpeg'
|
|
54
|
+
} as Express.Multer.File;
|
|
55
|
+
|
|
56
|
+
it('should delegate to CurrentUserService.updateAvatar', async () => {
|
|
57
|
+
const session = Entity.userSession.build();
|
|
58
|
+
const fileEntity = Entity.file.build();
|
|
59
|
+
service.updateAvatar.mockResolvedValue(fileEntity);
|
|
60
|
+
|
|
61
|
+
const result = await controller.updateAvatar(session, file);
|
|
62
|
+
|
|
63
|
+
expect(service.updateAvatar).toHaveBeenCalledWith(session.user, file);
|
|
64
|
+
expect(result).toBe(fileEntity);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should pass the session user and uploaded file to the service', async () => {
|
|
68
|
+
const session = Entity.userSession.build();
|
|
69
|
+
const fileEntity = Entity.file.build();
|
|
70
|
+
service.updateAvatar.mockResolvedValue(fileEntity);
|
|
71
|
+
await controller.updateAvatar(session, file);
|
|
72
|
+
|
|
73
|
+
expect(service.updateAvatar).toHaveBeenCalledWith(session.user, file);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
// @feature:end
|
|
77
|
+
});
|