@podosoft/podokit 0.2.0 → 0.3.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/README.md +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/api/package.json +9 -9
- package/dist/templates/fullstack-nest-svelte/apps/api/src/app.module.ts +3 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/package.json +5 -4
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/alert/alert-action.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/alert/alert-description.svelte +23 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/alert/alert-title.svelte +23 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/alert/alert.svelte +43 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/alert/index.ts +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/avatar/avatar-badge.svelte +26 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/avatar/avatar-fallback.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/avatar/avatar-group-count.svelte +23 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/avatar/avatar-group.svelte +23 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/avatar/avatar-image.svelte +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/avatar/avatar.svelte +26 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/avatar/index.ts +22 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/badge/badge.svelte +49 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/badge/index.ts +2 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/dialog-close.svelte +11 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/dialog-content.svelte +48 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/dialog-description.svelte +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/dialog-footer.svelte +32 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/dialog-header.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/dialog-overlay.svelte +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/dialog-portal.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/dialog-title.svelte +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/dialog-trigger.svelte +11 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/dialog.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/index.ts +34 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +44 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte +31 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte +24 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +16 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +34 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/index.ts +54 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/index.ts +37 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-content.svelte +45 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-group-heading.svelte +21 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-group.svelte +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-item.svelte +40 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-label.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-portal.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-scroll-down-button.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-scroll-up-button.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-separator.svelte +18 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-trigger.svelte +29 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select.svelte +11 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/separator/index.ts +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/separator/separator.svelte +23 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/skeleton/index.ts +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/skeleton/skeleton.svelte +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/sonner/index.ts +1 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/sonner/sonner.svelte +34 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/table/index.ts +28 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/table/table-body.svelte +15 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/table/table-caption.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/table/table-cell.svelte +15 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/table/table-footer.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/table/table-head.svelte +15 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/table/table-header.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/table/table-row.svelte +15 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/table/table.svelte +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tabs/index.ts +18 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tabs/tabs-content.svelte +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tabs/tabs-list.svelte +40 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tabs/tabs-trigger.svelte +23 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tabs/tabs.svelte +19 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tooltip/index.ts +19 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tooltip/tooltip-content.svelte +52 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tooltip/tooltip-portal.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tooltip/tooltip-provider.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tooltip/tooltip-trigger.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tooltip/tooltip.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/ko.ts +5 -2
- package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key.decorator.ts +9 -0
- package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key.guard.ts +29 -0
- package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key.module.ts +9 -0
- package/dist/templates/modules/api-key-auth/files/apps/api/src/machine/machine.controller.ts +14 -0
- package/dist/templates/modules/api-key-auth/module.manifest.json +19 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-log.entity.ts +25 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.controller.ts +16 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.interceptor.ts +39 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +13 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/migrations/1720300000000-InitAuditLogs.ts +25 -0
- package/dist/templates/modules/audit-log/module.manifest.json +16 -0
- package/dist/templates/modules/auth/files/apps/api/src/account/account.controller.ts +13 -0
- package/dist/templates/modules/auth/files/apps/api/src/account/account.module.ts +5 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +36 -0
- package/dist/templates/modules/auth/files/apps/api/src/health/health.controller.ts +26 -0
- package/dist/templates/modules/auth/module.manifest.json +70 -0
- package/dist/templates/modules/bullmq/module.manifest.json +11 -3
- package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.ts +29 -0
- package/dist/templates/modules/logging/module.manifest.json +27 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +26 -0
- package/dist/templates/modules/rate-limit/module.manifest.json +25 -0
- package/dist/templates/todo/apps/api/package.json +9 -9
- package/dist/templates/todo/apps/api/src/app.module.ts +3 -0
- package/dist/templates/todo/apps/web/package.json +5 -4
- package/dist/templates/todo/apps/web/src/lib/components/ui/alert/alert-action.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/alert/alert-description.svelte +23 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/alert/alert-title.svelte +23 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/alert/alert.svelte +43 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/alert/index.ts +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/avatar/avatar-badge.svelte +26 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/avatar/avatar-fallback.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/avatar/avatar-group-count.svelte +23 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/avatar/avatar-group.svelte +23 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/avatar/avatar-image.svelte +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/avatar/avatar.svelte +26 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/avatar/index.ts +22 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/badge/badge.svelte +49 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/badge/index.ts +2 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/dialog-close.svelte +11 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/dialog-content.svelte +48 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/dialog-description.svelte +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/dialog-footer.svelte +32 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/dialog-header.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/dialog-overlay.svelte +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/dialog-portal.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/dialog-title.svelte +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/dialog-trigger.svelte +11 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/dialog.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/index.ts +34 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +44 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte +31 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte +24 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +16 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +34 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/index.ts +54 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/index.ts +37 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select-content.svelte +45 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select-group-heading.svelte +21 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select-group.svelte +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select-item.svelte +40 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select-label.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select-portal.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select-scroll-down-button.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select-scroll-up-button.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select-separator.svelte +18 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select-trigger.svelte +29 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select.svelte +11 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/separator/index.ts +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/separator/separator.svelte +23 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/skeleton/index.ts +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/skeleton/skeleton.svelte +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/sonner/index.ts +1 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/sonner/sonner.svelte +34 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/table/index.ts +28 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/table/table-body.svelte +15 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/table/table-caption.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/table/table-cell.svelte +15 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/table/table-footer.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/table/table-head.svelte +15 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/table/table-header.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/table/table-row.svelte +15 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/table/table.svelte +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tabs/index.ts +18 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tabs/tabs-content.svelte +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tabs/tabs-list.svelte +40 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tabs/tabs-trigger.svelte +23 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tabs/tabs.svelte +19 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tooltip/index.ts +19 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tooltip/tooltip-content.svelte +52 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tooltip/tooltip-portal.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tooltip/tooltip-provider.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tooltip/tooltip-trigger.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tooltip/tooltip.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/i18n/ko.ts +5 -2
- package/package.json +2 -2
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/auth.controller.ts +0 -31
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/auth.module.ts +0 -22
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/auth.service.ts +0 -44
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/dto/login.dto.ts +0 -12
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/dto/register.dto.ts +0 -13
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/jwt-auth.guard.ts +0 -5
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/jwt.strategy.ts +0 -23
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/user.entity.ts +0 -16
- package/dist/templates/modules/auth-jwt/files/apps/api/src/migrations/1720200000000-InitUsers.ts +0 -23
- package/dist/templates/modules/auth-jwt/module.manifest.json +0 -31
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Module } from "@nestjs/common";
|
|
2
|
+
import { MachineController } from "../machine/machine.controller";
|
|
3
|
+
import { ApiKeyGuard } from "./api-key.guard";
|
|
4
|
+
|
|
5
|
+
@Module({
|
|
6
|
+
controllers: [MachineController],
|
|
7
|
+
providers: [ApiKeyGuard],
|
|
8
|
+
})
|
|
9
|
+
export class ApiKeyModule {}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Controller, Get } from "@nestjs/common";
|
|
2
|
+
import { ApiTags } from "@nestjs/swagger";
|
|
3
|
+
import { ApiKeyProtected } from "../api-key/api-key.decorator";
|
|
4
|
+
|
|
5
|
+
@ApiTags("machine")
|
|
6
|
+
@Controller("machine")
|
|
7
|
+
export class MachineController {
|
|
8
|
+
// Reachable with a valid X-API-Key, no user session required.
|
|
9
|
+
@ApiKeyProtected()
|
|
10
|
+
@Get("ping")
|
|
11
|
+
ping(): { ok: true; via: "api-key" } {
|
|
12
|
+
return { ok: true, via: "api-key" };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "api-key-auth",
|
|
3
|
+
"description": "API-key auth for machine/service clients (X-API-Key), separate from user sessions. Provides an ApiKeyGuard and @ApiKeyProtected() to open a route to key holders. Requires auth.",
|
|
4
|
+
"requires": ["auth"],
|
|
5
|
+
"targetApp": "api",
|
|
6
|
+
"env": [
|
|
7
|
+
"# api keys for machine clients (comma-separated)",
|
|
8
|
+
"API_KEYS=dev-key-please-change"
|
|
9
|
+
],
|
|
10
|
+
"inject": [
|
|
11
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:imports", "text": "import { ApiKeyModule } from \"./api-key/api-key.module\";" },
|
|
12
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:module-imports", "text": "ApiKeyModule," }
|
|
13
|
+
],
|
|
14
|
+
"instructions": [
|
|
15
|
+
"Set API_KEYS in .env (comma-separated).",
|
|
16
|
+
"Protect a route for machine clients with @ApiKeyProtected() (bypasses the user session, requires X-API-Key).",
|
|
17
|
+
"Call it: curl -H 'x-api-key: dev-key-please-change' localhost:3000/machine/ping"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from "typeorm";
|
|
2
|
+
|
|
3
|
+
@Entity("audit_logs")
|
|
4
|
+
export class AuditLog {
|
|
5
|
+
@PrimaryGeneratedColumn("uuid")
|
|
6
|
+
id!: string;
|
|
7
|
+
|
|
8
|
+
@Column({ type: "varchar", nullable: true })
|
|
9
|
+
userId!: string | null;
|
|
10
|
+
|
|
11
|
+
@Column({ type: "varchar", length: 10 })
|
|
12
|
+
method!: string;
|
|
13
|
+
|
|
14
|
+
@Column({ type: "varchar", length: 2048 })
|
|
15
|
+
path!: string;
|
|
16
|
+
|
|
17
|
+
@Column({ type: "int" })
|
|
18
|
+
statusCode!: number;
|
|
19
|
+
|
|
20
|
+
@Column({ type: "varchar", nullable: true })
|
|
21
|
+
ip!: string | null;
|
|
22
|
+
|
|
23
|
+
@CreateDateColumn({ type: "timestamptz" })
|
|
24
|
+
createdAt!: Date;
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Controller, Get } from "@nestjs/common";
|
|
2
|
+
import { InjectRepository } from "@nestjs/typeorm";
|
|
3
|
+
import { ApiTags } from "@nestjs/swagger";
|
|
4
|
+
import { Repository } from "typeorm";
|
|
5
|
+
import { AuditLog } from "./audit-log.entity";
|
|
6
|
+
|
|
7
|
+
@ApiTags("audit")
|
|
8
|
+
@Controller("audit-logs")
|
|
9
|
+
export class AuditController {
|
|
10
|
+
constructor(@InjectRepository(AuditLog) private readonly logs: Repository<AuditLog>) {}
|
|
11
|
+
|
|
12
|
+
@Get()
|
|
13
|
+
recent(): Promise<AuditLog[]> {
|
|
14
|
+
return this.logs.find({ order: { createdAt: "DESC" }, take: 50 });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type CallHandler, type ExecutionContext, Injectable, type NestInterceptor } from "@nestjs/common";
|
|
2
|
+
import { InjectRepository } from "@nestjs/typeorm";
|
|
3
|
+
import { fromNodeHeaders } from "better-auth/node";
|
|
4
|
+
import type { Request, Response } from "express";
|
|
5
|
+
import { type Observable, tap } from "rxjs";
|
|
6
|
+
import { Repository } from "typeorm";
|
|
7
|
+
import { auth } from "../auth/auth";
|
|
8
|
+
import { AuditLog } from "./audit-log.entity";
|
|
9
|
+
|
|
10
|
+
const AUDITED_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
11
|
+
|
|
12
|
+
// Records who performed each mutating request. Skips the auth endpoints.
|
|
13
|
+
@Injectable()
|
|
14
|
+
export class AuditInterceptor implements NestInterceptor {
|
|
15
|
+
constructor(@InjectRepository(AuditLog) private readonly logs: Repository<AuditLog>) {}
|
|
16
|
+
|
|
17
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
|
18
|
+
const req = context.switchToHttp().getRequest<Request>();
|
|
19
|
+
const path = req.originalUrl ?? req.url;
|
|
20
|
+
if (!AUDITED_METHODS.has(req.method) || path.startsWith("/api/auth")) {
|
|
21
|
+
return next.handle();
|
|
22
|
+
}
|
|
23
|
+
const res = context.switchToHttp().getResponse<Response>();
|
|
24
|
+
return next.handle().pipe(tap(() => void this.record(req, res, path)));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private async record(req: Request, res: Response, path: string): Promise<void> {
|
|
28
|
+
let userId: string | null = null;
|
|
29
|
+
try {
|
|
30
|
+
const session = await auth.api.getSession({ headers: fromNodeHeaders(req.headers) });
|
|
31
|
+
userId = session?.user?.id ?? null;
|
|
32
|
+
} catch {
|
|
33
|
+
userId = null;
|
|
34
|
+
}
|
|
35
|
+
await this.logs.save(
|
|
36
|
+
this.logs.create({ userId, method: req.method, path, statusCode: res.statusCode, ip: req.ip ?? null }),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Module } from "@nestjs/common";
|
|
2
|
+
import { APP_INTERCEPTOR } from "@nestjs/core";
|
|
3
|
+
import { TypeOrmModule } from "@nestjs/typeorm";
|
|
4
|
+
import { AuditController } from "./audit.controller";
|
|
5
|
+
import { AuditInterceptor } from "./audit.interceptor";
|
|
6
|
+
import { AuditLog } from "./audit-log.entity";
|
|
7
|
+
|
|
8
|
+
@Module({
|
|
9
|
+
imports: [TypeOrmModule.forFeature([AuditLog])],
|
|
10
|
+
controllers: [AuditController],
|
|
11
|
+
providers: [{ provide: APP_INTERCEPTOR, useClass: AuditInterceptor }],
|
|
12
|
+
})
|
|
13
|
+
export class AuditModule {}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class InitAuditLogs1720300000000 implements MigrationInterface {
|
|
4
|
+
name = "InitAuditLogs1720300000000";
|
|
5
|
+
|
|
6
|
+
async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "pgcrypto"`);
|
|
8
|
+
await queryRunner.query(`
|
|
9
|
+
CREATE TABLE "audit_logs" (
|
|
10
|
+
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
11
|
+
"userId" character varying,
|
|
12
|
+
"method" character varying(10) NOT NULL,
|
|
13
|
+
"path" character varying(2048) NOT NULL,
|
|
14
|
+
"statusCode" integer NOT NULL,
|
|
15
|
+
"ip" character varying,
|
|
16
|
+
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
17
|
+
CONSTRAINT "PK_audit_logs_id" PRIMARY KEY ("id")
|
|
18
|
+
)
|
|
19
|
+
`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async down(queryRunner: QueryRunner): Promise<void> {
|
|
23
|
+
await queryRunner.query(`DROP TABLE "audit_logs"`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "audit-log",
|
|
3
|
+
"description": "Audit logging: a global interceptor records who did each mutating request (who/method/path/status) to an audit_logs table. Requires auth.",
|
|
4
|
+
"requires": ["auth"],
|
|
5
|
+
"targetApp": "api",
|
|
6
|
+
"env": [],
|
|
7
|
+
"inject": [
|
|
8
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:imports", "text": "import { AuditModule } from \"./audit/audit.module\";" },
|
|
9
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:module-imports", "text": "AuditModule," }
|
|
10
|
+
],
|
|
11
|
+
"instructions": [
|
|
12
|
+
"Run migrations to create the audit_logs table: npm run migration:run -w <app>-api",
|
|
13
|
+
"Mutating requests (POST/PUT/PATCH/DELETE) are recorded with the acting user.",
|
|
14
|
+
"List recent entries (requires a session): curl -b cookies.txt localhost:3000/audit-logs"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Controller, Get } from "@nestjs/common";
|
|
2
|
+
import { Session, type UserSession } from "@thallesp/nestjs-better-auth";
|
|
3
|
+
import { ApiTags } from "@nestjs/swagger";
|
|
4
|
+
|
|
5
|
+
// Protected by the global AuthGuard. Use @Session() to read the current user.
|
|
6
|
+
@ApiTags("account")
|
|
7
|
+
@Controller("account")
|
|
8
|
+
export class AccountController {
|
|
9
|
+
@Get("me")
|
|
10
|
+
me(@Session() session: UserSession) {
|
|
11
|
+
return session.user;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { betterAuth, type BetterAuthOptions } from "better-auth";
|
|
2
|
+
import { twoFactor } from "better-auth/plugins";
|
|
3
|
+
import { Pool } from "pg";
|
|
4
|
+
|
|
5
|
+
// OAuth providers are enabled only when their credentials are present.
|
|
6
|
+
function socialProviders(): NonNullable<BetterAuthOptions["socialProviders"]> {
|
|
7
|
+
const providers: NonNullable<BetterAuthOptions["socialProviders"]> = {};
|
|
8
|
+
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
|
|
9
|
+
providers.google = {
|
|
10
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
11
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) {
|
|
15
|
+
providers.github = {
|
|
16
|
+
clientId: process.env.GITHUB_CLIENT_ID,
|
|
17
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return providers;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const auth = betterAuth({
|
|
24
|
+
database: new Pool({
|
|
25
|
+
host: process.env.POSTGRES_HOST ?? "localhost",
|
|
26
|
+
port: Number(process.env.POSTGRES_PORT ?? 5432),
|
|
27
|
+
user: process.env.POSTGRES_USER ?? "podokit",
|
|
28
|
+
password: process.env.POSTGRES_PASSWORD ?? "podokit",
|
|
29
|
+
database: process.env.POSTGRES_DB ?? "podokit",
|
|
30
|
+
}),
|
|
31
|
+
emailAndPassword: { enabled: true },
|
|
32
|
+
socialProviders: socialProviders(),
|
|
33
|
+
plugins: process.env.AUTH_TWO_FACTOR === "true" ? [twoFactor()] : [],
|
|
34
|
+
secret: process.env.BETTER_AUTH_SECRET ?? "change-me-in-production-min-32-characters",
|
|
35
|
+
baseURL: process.env.BETTER_AUTH_URL ?? "http://localhost:3000",
|
|
36
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Controller, Get } from "@nestjs/common";
|
|
2
|
+
import { InjectDataSource } from "@nestjs/typeorm";
|
|
3
|
+
import { DataSource } from "typeorm";
|
|
4
|
+
import { Public } from "@thallesp/nestjs-better-auth";
|
|
5
|
+
|
|
6
|
+
// Health checks stay public under the global auth guard.
|
|
7
|
+
@Public()
|
|
8
|
+
@Controller("health")
|
|
9
|
+
export class HealthController {
|
|
10
|
+
constructor(@InjectDataSource() private readonly dataSource: DataSource) {}
|
|
11
|
+
|
|
12
|
+
@Get()
|
|
13
|
+
liveness(): { status: string; uptime: number; timestamp: string } {
|
|
14
|
+
return { status: "ok", uptime: process.uptime(), timestamp: new Date().toISOString() };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Get("ready")
|
|
18
|
+
async readiness(): Promise<{ status: string; db: string }> {
|
|
19
|
+
try {
|
|
20
|
+
await this.dataSource.query("SELECT 1");
|
|
21
|
+
return { status: "ready", db: "up" };
|
|
22
|
+
} catch {
|
|
23
|
+
return { status: "degraded", db: "down" };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "auth",
|
|
3
|
+
"description": "Full authentication (better-auth): email/password, sessions, and \u2014 via config \u2014 OAuth and 2FA. Installs a global guard so the API is secure by default; health and /api/auth stay public.",
|
|
4
|
+
"requires": [],
|
|
5
|
+
"targetApp": "api",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"better-auth": "^1.6.23",
|
|
8
|
+
"@thallesp/nestjs-better-auth": "^2.7.0",
|
|
9
|
+
"pg": "^8.12.0"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/pg": "^8.11.10"
|
|
13
|
+
},
|
|
14
|
+
"env": [
|
|
15
|
+
"# auth (better-auth) \u2014 set a strong secret (>= 32 chars) in production",
|
|
16
|
+
"BETTER_AUTH_SECRET=change-me-in-production-min-32-characters",
|
|
17
|
+
"BETTER_AUTH_URL=http://localhost:3000",
|
|
18
|
+
"# 2FA (TOTP): set true to enable the plugin",
|
|
19
|
+
"AUTH_TWO_FACTOR=false",
|
|
20
|
+
"# OAuth (optional) \u2014 set a provider's id+secret to enable it",
|
|
21
|
+
"# GOOGLE_CLIENT_ID=",
|
|
22
|
+
"# GOOGLE_CLIENT_SECRET=",
|
|
23
|
+
"# GITHUB_CLIENT_ID=",
|
|
24
|
+
"# GITHUB_CLIENT_SECRET="
|
|
25
|
+
],
|
|
26
|
+
"inject": [
|
|
27
|
+
{
|
|
28
|
+
"file": "apps/api/src/app.module.ts",
|
|
29
|
+
"marker": "// podokit:imports",
|
|
30
|
+
"text": "import { APP_GUARD } from \"@nestjs/core\";"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"file": "apps/api/src/app.module.ts",
|
|
34
|
+
"marker": "// podokit:imports",
|
|
35
|
+
"text": "import { AuthModule, AuthGuard } from \"@thallesp/nestjs-better-auth\";"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"file": "apps/api/src/app.module.ts",
|
|
39
|
+
"marker": "// podokit:imports",
|
|
40
|
+
"text": "import { auth } from \"./auth/auth\";"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"file": "apps/api/src/app.module.ts",
|
|
44
|
+
"marker": "// podokit:imports",
|
|
45
|
+
"text": "import { AccountModule } from \"./account/account.module\";"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"file": "apps/api/src/app.module.ts",
|
|
49
|
+
"marker": "// podokit:module-imports",
|
|
50
|
+
"text": "AuthModule.forRoot(auth),"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"file": "apps/api/src/app.module.ts",
|
|
54
|
+
"marker": "// podokit:module-imports",
|
|
55
|
+
"text": "AccountModule,"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"file": "apps/api/src/app.module.ts",
|
|
59
|
+
"marker": "// podokit:providers",
|
|
60
|
+
"text": "{ provide: APP_GUARD, useClass: AuthGuard },"
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"instructions": [
|
|
64
|
+
"Set BETTER_AUTH_SECRET in .env (>= 32 chars).",
|
|
65
|
+
"Create the auth tables: npx @better-auth/cli migrate -y --config apps/api/src/auth/auth.ts",
|
|
66
|
+
"Sign up: curl -c cookies.txt -XPOST localhost:3000/api/auth/sign-up/email -H 'content-type: application/json' -d '{\"email\":\"a@example.com\",\"password\":\"password123\",\"name\":\"A\"}'",
|
|
67
|
+
"Call a protected route with the session: curl -b cookies.txt localhost:3000/account/me",
|
|
68
|
+
"Everything is now protected by default; mark public routes with @Public(). Enable OAuth by setting a provider's env vars; enable 2FA with AUTH_TWO_FACTOR=true."
|
|
69
|
+
]
|
|
70
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"requires": [],
|
|
5
5
|
"targetApp": "api",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@nestjs/bullmq": "^
|
|
7
|
+
"@nestjs/bullmq": "^11.0.4",
|
|
8
8
|
"bullmq": "^5.34.0"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
@@ -15,8 +15,16 @@
|
|
|
15
15
|
"# bullmq uses REDIS_HOST / REDIS_PORT (already defined above)"
|
|
16
16
|
],
|
|
17
17
|
"inject": [
|
|
18
|
-
{
|
|
19
|
-
|
|
18
|
+
{
|
|
19
|
+
"file": "apps/api/src/app.module.ts",
|
|
20
|
+
"marker": "// podokit:imports",
|
|
21
|
+
"text": "import { JobsModule } from \"./jobs/jobs.module\";"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"file": "apps/api/src/app.module.ts",
|
|
25
|
+
"marker": "// podokit:module-imports",
|
|
26
|
+
"text": "JobsModule,"
|
|
27
|
+
}
|
|
20
28
|
],
|
|
21
29
|
"instructions": [
|
|
22
30
|
"Ensure Redis is running: docker compose -f infra/docker/docker-compose.yml up -d",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Module } from "@nestjs/common";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
4
|
+
import { LoggerModule } from "nestjs-pino";
|
|
5
|
+
|
|
6
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
7
|
+
|
|
8
|
+
// Structured request logging with a correlation id. Importing this module
|
|
9
|
+
// installs the pino-http middleware for all routes.
|
|
10
|
+
@Module({
|
|
11
|
+
imports: [
|
|
12
|
+
LoggerModule.forRoot({
|
|
13
|
+
pinoHttp: {
|
|
14
|
+
level: process.env.LOG_LEVEL ?? "info",
|
|
15
|
+
// Pretty, single-line logs in dev; JSON in production.
|
|
16
|
+
transport: isProduction ? undefined : { target: "pino-pretty", options: { singleLine: true } },
|
|
17
|
+
autoLogging: true,
|
|
18
|
+
// One id per request; honor an inbound x-request-id and echo it back.
|
|
19
|
+
genReqId: (req: IncomingMessage, res: ServerResponse): string => {
|
|
20
|
+
const header = req.headers["x-request-id"];
|
|
21
|
+
const id = (Array.isArray(header) ? header[0] : header) ?? randomUUID();
|
|
22
|
+
res.setHeader("x-request-id", id);
|
|
23
|
+
return id;
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
}),
|
|
27
|
+
],
|
|
28
|
+
})
|
|
29
|
+
export class LoggingModule {}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "logging",
|
|
3
|
+
"description": "Structured request logging (nestjs-pino) with a per-request correlation id (x-request-id). Pretty in dev, JSON in production.",
|
|
4
|
+
"requires": [],
|
|
5
|
+
"targetApp": "api",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"nestjs-pino": "^4.3.0",
|
|
8
|
+
"pino-http": "^10.3.0"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"pino-pretty": "^13.0.0"
|
|
12
|
+
},
|
|
13
|
+
"env": [
|
|
14
|
+
"# logging",
|
|
15
|
+
"LOG_LEVEL=info"
|
|
16
|
+
],
|
|
17
|
+
"inject": [
|
|
18
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:imports", "text": "import { LoggingModule } from \"./logging/logging.module\";" },
|
|
19
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:module-imports", "text": "LoggingModule," }
|
|
20
|
+
],
|
|
21
|
+
"instructions": [
|
|
22
|
+
"Every HTTP request is now logged as structured JSON with a correlation id.",
|
|
23
|
+
"Make a request and watch the API logs: curl localhost:3000/health",
|
|
24
|
+
"Set LOG_LEVEL (debug|info|warn|error) in .env. Logs are pretty in dev and JSON when NODE_ENV=production.",
|
|
25
|
+
"To route Nest's own logs through pino too, add to main.ts: NestFactory.create(AppModule, { bufferLogs: true }) then app.useLogger(app.get(Logger))."
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Module } from "@nestjs/common";
|
|
2
|
+
import { APP_GUARD } from "@nestjs/core";
|
|
3
|
+
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
|
|
4
|
+
import { ThrottlerStorageRedisService } from "@nest-lab/throttler-storage-redis";
|
|
5
|
+
import Redis from "ioredis";
|
|
6
|
+
|
|
7
|
+
const ttlSeconds = Number(process.env.RATE_LIMIT_TTL ?? 60);
|
|
8
|
+
const limit = Number(process.env.RATE_LIMIT_MAX ?? 100);
|
|
9
|
+
|
|
10
|
+
// Distributed rate limiting: the counter lives in Redis, so the limit holds
|
|
11
|
+
// across API replicas. Registers a global guard (429 when exceeded).
|
|
12
|
+
@Module({
|
|
13
|
+
imports: [
|
|
14
|
+
ThrottlerModule.forRoot({
|
|
15
|
+
throttlers: [{ ttl: ttlSeconds * 1000, limit }],
|
|
16
|
+
storage: new ThrottlerStorageRedisService(
|
|
17
|
+
new Redis({
|
|
18
|
+
host: process.env.REDIS_HOST ?? "localhost",
|
|
19
|
+
port: Number(process.env.REDIS_PORT ?? 6379),
|
|
20
|
+
}),
|
|
21
|
+
),
|
|
22
|
+
}),
|
|
23
|
+
],
|
|
24
|
+
providers: [{ provide: APP_GUARD, useClass: ThrottlerGuard }],
|
|
25
|
+
})
|
|
26
|
+
export class RateLimitModule {}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rate-limit",
|
|
3
|
+
"description": "Rate limiting with @nestjs/throttler backed by Redis (correct across API replicas). Installs a global throttler guard; configurable via env.",
|
|
4
|
+
"requires": ["redis"],
|
|
5
|
+
"targetApp": "api",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@nestjs/throttler": "^6.5.0",
|
|
8
|
+
"@nest-lab/throttler-storage-redis": "^1.2.0"
|
|
9
|
+
},
|
|
10
|
+
"env": [
|
|
11
|
+
"# rate limit (window in seconds, max requests per window per client)",
|
|
12
|
+
"RATE_LIMIT_TTL=60",
|
|
13
|
+
"RATE_LIMIT_MAX=100"
|
|
14
|
+
],
|
|
15
|
+
"inject": [
|
|
16
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:imports", "text": "import { RateLimitModule } from \"./rate-limit/rate-limit.module\";" },
|
|
17
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:module-imports", "text": "RateLimitModule," }
|
|
18
|
+
],
|
|
19
|
+
"instructions": [
|
|
20
|
+
"Ensure Redis is running: docker compose -f infra/docker/docker-compose.yml up -d",
|
|
21
|
+
"Tune RATE_LIMIT_TTL / RATE_LIMIT_MAX in .env.",
|
|
22
|
+
"Exceeding the limit returns HTTP 429. Skip a route with @SkipThrottle() or tune it with @Throttle().",
|
|
23
|
+
"The limit is shared across API replicas because it is stored in Redis."
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
"migration:generate": "npm run typeorm -- migration:generate"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@nestjs/common": "^
|
|
18
|
-
"@nestjs/config": "^
|
|
19
|
-
"@nestjs/core": "^
|
|
20
|
-
"@nestjs/platform-express": "^
|
|
21
|
-
"@nestjs/swagger": "^
|
|
22
|
-
"@nestjs/typeorm": "^
|
|
17
|
+
"@nestjs/common": "^11.1.27",
|
|
18
|
+
"@nestjs/config": "^4.0.4",
|
|
19
|
+
"@nestjs/core": "^11.1.27",
|
|
20
|
+
"@nestjs/platform-express": "^11.1.27",
|
|
21
|
+
"@nestjs/swagger": "^11.4.5",
|
|
22
|
+
"@nestjs/typeorm": "^11.0.3",
|
|
23
23
|
"class-transformer": "^0.5.1",
|
|
24
24
|
"class-validator": "^0.14.1",
|
|
25
25
|
"dotenv": "^16.4.5",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"zod": "^3.23.8"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@nestjs/cli": "^
|
|
34
|
-
"@nestjs/testing": "^
|
|
35
|
-
"@types/express": "^
|
|
33
|
+
"@nestjs/cli": "^11.0.23",
|
|
34
|
+
"@nestjs/testing": "^11.1.27",
|
|
35
|
+
"@types/express": "^5.0.1",
|
|
36
36
|
"@types/node": "^20.17.0",
|
|
37
37
|
"@types/supertest": "^6.0.2",
|
|
38
38
|
"jest": "^29.7.0",
|
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
"test": "echo \"add Playwright tests here\" && exit 0"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"
|
|
14
|
+
"@internationalized/date": "^3.12.2",
|
|
15
|
+
"@lucide/svelte": "^1.23.0",
|
|
15
16
|
"bits-ui": "^2.18.1",
|
|
16
17
|
"clsx": "^2.1.1",
|
|
18
|
+
"mode-watcher": "^1.1.0",
|
|
19
|
+
"svelte-sonner": "^1.1.1",
|
|
17
20
|
"tailwind-merge": "^3.6.0",
|
|
18
|
-
"tailwind-variants": "^3.2.2"
|
|
19
|
-
"@lucide/svelte": "^1.23.0",
|
|
20
|
-
"@internationalized/date": "^3.12.2"
|
|
21
|
+
"tailwind-variants": "^3.2.2"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
24
|
"@sveltejs/adapter-node": "^5.2.0",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
ref = $bindable(null),
|
|
7
|
+
class: className,
|
|
8
|
+
children,
|
|
9
|
+
...restProps
|
|
10
|
+
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<div
|
|
14
|
+
bind:this={ref}
|
|
15
|
+
data-slot="alert-action"
|
|
16
|
+
class={cn("absolute top-2 right-2", className)}
|
|
17
|
+
{...restProps}
|
|
18
|
+
>
|
|
19
|
+
{@render children?.()}
|
|
20
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
ref = $bindable(null),
|
|
7
|
+
class: className,
|
|
8
|
+
children,
|
|
9
|
+
...restProps
|
|
10
|
+
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<div
|
|
14
|
+
bind:this={ref}
|
|
15
|
+
data-slot="alert-description"
|
|
16
|
+
class={cn(
|
|
17
|
+
"text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...restProps}
|
|
21
|
+
>
|
|
22
|
+
{@render children?.()}
|
|
23
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
ref = $bindable(null),
|
|
7
|
+
class: className,
|
|
8
|
+
children,
|
|
9
|
+
...restProps
|
|
10
|
+
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<div
|
|
14
|
+
bind:this={ref}
|
|
15
|
+
data-slot="alert-title"
|
|
16
|
+
class={cn(
|
|
17
|
+
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...restProps}
|
|
21
|
+
>
|
|
22
|
+
{@render children?.()}
|
|
23
|
+
</div>
|