diff --git a/README.md b/README.md index 9f39eef..010b904 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ If you forked this repository before May 27th, you'll want to view commit `653e2 ## ❔ What -A scalable, testable, extensible, template for Sveltekit. +A scalable, testable, extensible, template for Sveltekit. Sveltekit is awesome, but sometimes you need a bit more capability in the backend than what frameworks like NextJS & Sveltekit can deliver. @@ -25,25 +25,35 @@ export const DELETE: RequestHandler = ({ request }) => app.fetch(request); export const POST: RequestHandler = ({ request }) => app.fetch(request); ``` +## Local Setup + +1. Make sure Docker is running +2. Copy the `.env.example` file and rename to `.env` +3. `pnpm install` +4. `pnpm initialize`(this will start the docker-compose and run the initial database migration.) +5. `pnpm dev` + +No additional setup is required, zero api keys, zero services to signup for, zero cost. + ## How to Use -This is **not** supposed to serve as an all batteries included ["production boilerplate"](https://github.com/ixartz/Next-js-Boilerplate) with 200 useless sponsored features that get in your way. Templates that do this are ANYTHING but "production" and "quick start". +This is **not** supposed to serve as an all batteries included ["production boilerplate"](https://github.com/ixartz/Next-js-Boilerplate) with 200 useless sponsored features that get in your way. Templates that do this are ANYTHING but "production" and "quick start". This is stack is designed to be library agnostic. The philosophy here is to boostrap the concrete, repetitive, and time consuming tasks that every application will need reguardless of what you're building. -**So - fork this repo, add your favorite libraries, and build out your own "more opinionated" personal template tailored to you**! +**So - fork this repo, add your favorite libraries, and build out your own "more opinionated" personal template tailored to you**! ## Features - 🟢 Full E2E typesafety - 🟢 RPC Client for API Requests - 🟢 Custom Fetch Wrapper -- 🔴 Deployment Template +- 🟢 Deployment Template - 🟠 Authentication - 🟢 Email/Passkey - 🔴 OAuth - 🟢 Email Update/Verifiaction - - 🔴 Rate limiter + - 🟢 Rate limiter ## Technologies diff --git a/package.json b/package.json index e96e5c0..ad303ed 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@typescript-eslint/parser": "^7.13.1", "arctic": "^1.9.1", "autoprefixer": "^10.4.19", + "bullmq": "^5.8.3", "dayjs": "^1.11.11", "dotenv-cli": "^7.4.2", "drizzle-kit": "^0.21.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ecbe293..bb4ceff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,6 +105,9 @@ importers: autoprefixer: specifier: ^10.4.19 version: 10.4.19(postcss@8.4.38) + bullmq: + specifier: ^5.8.3 + version: 5.8.3 dayjs: specifier: ^1.11.11 version: 1.11.11 @@ -764,6 +767,36 @@ packages: peerDependencies: svelte: '>=3 <5' + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + cpu: [x64] + os: [win32] + '@napi-rs/wasm-runtime@0.2.4': resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==} @@ -1507,6 +1540,9 @@ packages: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} + bullmq@5.8.3: + resolution: {integrity: sha512-RJgQu/vgSZqjOYrZ7F1UJsSAzveNx7FFpR3Tp/1TxOMXXN9TtZMSly5MT+vjzOhQX//3+YWNRbMWpC1mkqBc9w==} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1612,6 +1648,10 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -1689,6 +1729,10 @@ packages: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + devalue@5.0.0: resolution: {integrity: sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==} @@ -2442,6 +2486,10 @@ packages: peerDependencies: svelte: ^3 || ^4 || ^5.0.0-next.42 + luxon@3.4.4: + resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==} + engines: {node: '>=12'} + magic-string@0.30.10: resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} @@ -2552,6 +2600,13 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msgpackr-extract@3.0.3: + resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + hasBin: true + + msgpackr@1.10.2: + resolution: {integrity: sha512-L60rsPynBvNE+8BWipKKZ9jHcSGbtyJYIwjRq0VrIvQ08cRjntGXJYW/tmciZ2IHWIY8WEW32Qa2xbh5+SKBZA==} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -2583,6 +2638,13 @@ packages: next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} @@ -3461,6 +3523,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + valibot@0.31.1: resolution: {integrity: sha512-2YYIhPrnVSz/gfT2/iXVTrSj92HwchCt9Cga/6hX4B26iCz9zkIsGTS0HjDYTZfTi1Un0X6aRvhBi1cfqs/i0Q==} @@ -4006,6 +4072,24 @@ snapshots: nanoid: 5.0.7 svelte: 5.0.0-next.164 + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + optional: true + '@napi-rs/wasm-runtime@0.2.4': dependencies: '@emnapi/core': 1.2.0 @@ -4729,6 +4813,18 @@ snapshots: builtin-modules@3.3.0: {} + bullmq@5.8.3: + dependencies: + cron-parser: 4.9.0 + ioredis: 5.4.1 + msgpackr: 1.10.2 + node-abort-controller: 3.1.1 + semver: 7.6.2 + tslib: 2.6.3 + uuid: 9.0.1 + transitivePeerDependencies: + - supports-color + bytes@3.1.2: {} cac@6.7.14: {} @@ -4832,6 +4928,10 @@ snapshots: cookie@0.6.0: {} + cron-parser@4.9.0: + dependencies: + luxon: 3.4.4 + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -4883,6 +4983,9 @@ snapshots: detect-indent@6.1.0: {} + detect-libc@2.0.3: + optional: true + devalue@5.0.0: {} didyoumean@1.2.2: {} @@ -5729,6 +5832,8 @@ snapshots: dependencies: svelte: 5.0.0-next.164 + luxon@3.4.4: {} + magic-string@0.30.10: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 @@ -5828,6 +5933,22 @@ snapshots: ms@2.1.3: {} + msgpackr-extract@3.0.3: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + optional: true + + msgpackr@1.10.2: + optionalDependencies: + msgpackr-extract: 3.0.3 + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -5848,6 +5969,13 @@ snapshots: next-tick@1.1.0: {} + node-abort-controller@3.1.1: {} + + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.0.3 + optional: true + node-releases@2.0.14: {} nodemailer@6.9.14: {} @@ -6673,6 +6801,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@9.0.1: {} + valibot@0.31.1: optional: true diff --git a/src/lib/dtos/update-email.dto.ts b/src/lib/dtos/update-email.dto.ts index a4e2f41..94b9101 100644 --- a/src/lib/dtos/update-email.dto.ts +++ b/src/lib/dtos/update-email.dto.ts @@ -19,6 +19,6 @@ ensure that the correct data is being passed around. /* -------------------------------------------------------------------------- */ export const updateEmailDto = z.object({ - email: z.string() + email: z.string().email() }); export type UpdateEmailDto = z.infer; diff --git a/src/lib/server/api/providers/index.ts b/src/lib/server/api/providers/index.ts index 0f06262..0ac960b 100644 --- a/src/lib/server/api/providers/index.ts +++ b/src/lib/server/api/providers/index.ts @@ -1,2 +1,3 @@ export * from './database.provider'; export * from './lucia.provider'; +export * from './redis.provider'; diff --git a/src/lib/server/api/providers/redis.provider.ts b/src/lib/server/api/providers/redis.provider.ts new file mode 100644 index 0000000..26496de --- /dev/null +++ b/src/lib/server/api/providers/redis.provider.ts @@ -0,0 +1,14 @@ +import { container } from 'tsyringe'; +import RedisClient from 'ioredis' +import { config } from '../common/config'; + +// Symbol +export const RedisProvider = Symbol('REDIS_TOKEN'); + +// Type +export type RedisProvider = RedisClient; + +// Register +container.register(RedisProvider, { + useValue: new RedisClient(config.REDIS_URL) +}); diff --git a/src/lib/server/api/services/queues.service.ts b/src/lib/server/api/services/queues.service.ts new file mode 100644 index 0000000..97e7ca3 --- /dev/null +++ b/src/lib/server/api/services/queues.service.ts @@ -0,0 +1,19 @@ +import { injectable } from "tsyringe"; +import RedisClient from 'ioredis' +import { config } from "../common/config"; +import { Queue, Worker, type Processor } from 'bullmq'; + +@injectable() +export class QueuesServices { + connection = new RedisClient(config.REDIS_URL); + + constructor() { } + + createQueue(name: string) { + return new Queue(name, { connection: this.connection }) + } + + createWorker(name: string, prcoessor: Processor) { + return new Worker(name, prcoessor, { connection: this.connection }) + } +} \ No newline at end of file diff --git a/src/lib/server/api/services/tokens.service.ts b/src/lib/server/api/services/tokens.service.ts index 6412e8b..6e9cc89 100644 --- a/src/lib/server/api/services/tokens.service.ts +++ b/src/lib/server/api/services/tokens.service.ts @@ -3,24 +3,6 @@ import { generateRandomString } from "oslo/crypto"; import { TimeSpan, createDate, type TimeSpanUnit } from 'oslo'; import { HashingService } from './hashing.service'; -/* -------------------------------------------------------------------------- */ -/* Service */ -/* -------------------------------------------------------------------------- */ -/* -------------------------------------------------------------------------- */ -/* ---------------------------------- About --------------------------------- */ -/* -Services are responsible for handling business logic and data manipulation. -They genreally call on repositories or other services to complete a use-case. -*/ -/* ---------------------------------- Notes --------------------------------- */ -/* -Services should be kept as clean and simple as possible. - -Create private functions to handle complex logic and keep the public methods as -simple as possible. This makes the service easier to read, test and understand. -*/ -/* -------------------------------------------------------------------------- */ - @injectable() export class TokensService { constructor(@inject(HashingService) private readonly hashingService: HashingService) { } diff --git a/src/lib/server/api/tests/login-requests.service.test.ts b/src/lib/server/api/tests/login-requests.service.test.ts index c2b4715..efdfc36 100644 --- a/src/lib/server/api/tests/login-requests.service.test.ts +++ b/src/lib/server/api/tests/login-requests.service.test.ts @@ -1,6 +1,6 @@ import 'reflect-metadata'; import { LoginRequestsService } from '../services/login-requests.service'; -import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; import { TokensService } from '../services/tokens.service'; import { MailerService } from '../services/mailer.service'; import { UsersRepository } from '../repositories/users.repository';