startx 1.0.4 → 1.0.8

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.
Files changed (47) hide show
  1. package/_gitignore +40 -0
  2. package/apps/core-server/Dockerfile +7 -2
  3. package/apps/core-server/src/middlewares/auth-middleware.ts +74 -30
  4. package/apps/queue-worker/Dockerfile +15 -8
  5. package/apps/queue-worker/package.json +5 -1
  6. package/apps/queue-worker/src/bullmq/board.ts +28 -0
  7. package/apps/queue-worker/src/index.ts +2 -0
  8. package/apps/startx-cli/dist/index.mjs +2 -2
  9. package/apps/startx-cli/src/commands/init.ts +2 -1
  10. package/apps/startx-cli/src/configs/scripts.ts +40 -0
  11. package/apps/web-client/react-router.config.ts +1 -0
  12. package/package.json +8 -2
  13. package/packages/@db/drizzle/drizzle.config.ts +1 -1
  14. package/packages/@db/drizzle/src/index.ts +4 -15
  15. package/packages/@repo/lib/src/cookie-module/cookie-module.ts +94 -38
  16. package/packages/@repo/lib/src/extra/index.ts +1 -0
  17. package/packages/@repo/lib/src/extra/token-module.ts +50 -21
  18. package/packages/@repo/lib/src/mail-module/nodemailer.ts +33 -21
  19. package/packages/@repo/lib/src/session-module/i-session.ts +132 -59
  20. package/packages/@repo/lib/src/session-module/index.ts +8 -2
  21. package/packages/@repo/lib/src/session-module/redis-session.ts +53 -23
  22. package/packages/@repo/lib/src/validation-module/index.ts +50 -78
  23. package/packages/@repo/model/eslint.config.ts +4 -0
  24. package/packages/@repo/model/package.json +41 -0
  25. package/packages/@repo/model/src/index.ts +0 -0
  26. package/packages/@repo/model/tsconfig.json +7 -0
  27. package/packages/@repo/model/vitest.config.ts +3 -0
  28. package/packages/common/src/time.ts +95 -22
  29. package/packages/queue/src/adapter/bullmq-adapter.ts +138 -47
  30. package/packages/queue/src/index.ts +3 -0
  31. package/packages/queue/src/queue-interface.ts +12 -5
  32. package/packages/queue/src/registry.ts +2 -2
  33. package/packages/queue/tsconfig.json +1 -1
  34. package/packages/ui/src/api/use-api/react-query/types.ts +3 -3
  35. package/packages/ui/src/api/use-api/react-query/use-api.ts +10 -11
  36. package/pnpm-workspace.yaml +4 -2
  37. package/turbo.json +20 -0
  38. /package/apps/web-client/{app → src}/app.css +0 -0
  39. /package/apps/web-client/{app → src}/components.json +0 -0
  40. /package/apps/web-client/{app → src}/config/auth/auth-state.ts +0 -0
  41. /package/apps/web-client/{app → src}/config/axios-client.ts +0 -0
  42. /package/apps/web-client/{app → src}/config/env.ts +0 -0
  43. /package/apps/web-client/{app → src}/entry.client.tsx +0 -0
  44. /package/apps/web-client/{app → src}/eslint.config.ts +0 -0
  45. /package/apps/web-client/{app → src}/root.tsx +0 -0
  46. /package/apps/web-client/{app → src}/routes/home.tsx +0 -0
  47. /package/apps/web-client/{app → src}/routes.ts +0 -0
@@ -1,44 +1,74 @@
1
- import type { SessionUser } from "@repo/common/types/users";
2
1
  import { RedisStore } from "@repo/redis";
3
- import { IUserSession, type TokenPair } from "./i-session.js";
2
+ import type { SessionRecord, SessionType } from "./i-session.js";
3
+ import { constants, IUserSession } from "./i-session.js";
4
+
5
+ type SessionIndex = string[];
4
6
 
5
7
  export class RedisUserSession extends IUserSession {
6
- private userStore: RedisStore<SessionUser>;
7
- private tokenStore: RedisStore<TokenPair>;
8
+ private sessionStore: RedisStore<SessionRecord>;
9
+ private sessionIndexStore: RedisStore<SessionIndex>;
8
10
 
9
- constructor() {
11
+ constructor(type: SessionType) {
10
12
  super();
11
- this.userStore = new RedisStore<SessionUser>({
12
- namespace: "user-session",
13
+
14
+ this.type = type;
15
+
16
+ this.sessionStore = new RedisStore<SessionRecord>({
17
+ namespace: "auth-session",
13
18
  });
14
- this.tokenStore = new RedisStore<TokenPair>({
15
- namespace: "user-tokens",
19
+
20
+ this.sessionIndexStore = new RedisStore<SessionIndex>({
21
+ namespace: "auth-session-index",
16
22
  });
17
23
  }
18
24
 
19
- protected async setSessionData(key: string, data: SessionUser, ttl: number): Promise<void> {
20
- await this.userStore.set(key, data, ttl);
25
+ protected type: SessionType;
26
+ protected async setSession(sessionId: string, data: SessionRecord, ttl: number): Promise<void> {
27
+ await this.sessionStore.set(sessionId, data, ttl);
21
28
  }
22
29
 
23
- protected async getSessionData(key: string): Promise<SessionUser | null> {
24
- const data = await this.userStore.get(key);
25
- return data ?? null;
30
+ protected async getSession(sessionId: string): Promise<SessionRecord | null> {
31
+ const session = await this.sessionStore.get(sessionId);
32
+ return session ?? null;
26
33
  }
27
34
 
28
- protected async deleteSessionData(key: string): Promise<void> {
29
- await this.userStore.del(key);
35
+ protected async deleteSession(sessionId: string): Promise<void> {
36
+ await this.sessionStore.del(sessionId);
30
37
  }
31
38
 
32
- protected async setTokenData(key: string, data: TokenPair, ttl: number): Promise<void> {
33
- await this.tokenStore.set(key, data, ttl);
39
+ protected async addUserSession(userId: string, sessionId: string): Promise<void> {
40
+ const key = this.userSessionsKey(userId);
41
+
42
+ const sessions = (await this.sessionIndexStore.get(key)) ?? [];
43
+
44
+ if (!sessions.includes(sessionId)) {
45
+ sessions.push(sessionId);
46
+ }
47
+
48
+ await this.sessionIndexStore.set(key, sessions, constants.sessionDuration);
49
+ }
50
+
51
+ protected async removeUserSession(userId: string, sessionId: string): Promise<void> {
52
+ const key = this.userSessionsKey(userId);
53
+
54
+ const sessions = (await this.sessionIndexStore.get(key)) ?? [];
55
+
56
+ const filtered = sessions.filter(id => id !== sessionId);
57
+
58
+ if (filtered.length === 0) {
59
+ await this.sessionIndexStore.del(key);
60
+ return;
61
+ }
62
+
63
+ await this.sessionIndexStore.set(key, filtered, constants.sessionDuration);
34
64
  }
35
65
 
36
- protected async getTokenData(key: string): Promise<TokenPair | null> {
37
- const data = await this.tokenStore.get(key);
38
- return data ?? null;
66
+ protected async getUserSessions(userId: string): Promise<string[]> {
67
+ const key = this.userSessionsKey(userId);
68
+ return (await this.sessionIndexStore.get(key)) ?? [];
39
69
  }
40
70
 
41
- protected async deleteTokenData(key: string): Promise<void> {
42
- await this.tokenStore.del(key);
71
+ protected async clearUserSessions(userId: string): Promise<void> {
72
+ await this.sessionIndexStore.del(this.userSessionsKey(userId));
43
73
  }
44
74
  }
@@ -1,15 +1,11 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { logger } from "@repo/logger";
1
3
  import vine from "@vinejs/vine";
2
4
  import type { Infer, SchemaTypes } from "@vinejs/vine/types";
3
5
  import type { NextFunction, Request, Response } from "express";
4
6
 
5
7
  import { ErrorResponse } from "../error-handlers-module/index.js";
6
- import { logger } from "@repo/logger";
7
8
 
8
- type ExpressHandler<P = unknown, ResBody = unknown, ReqBody = unknown, Query = unknown> = (
9
- req: Request<P, ResBody, ReqBody, Query>,
10
- res: Response,
11
- next: NextFunction
12
- ) => unknown;
13
9
  export async function validateBody<T extends SchemaTypes>(
14
10
  schema: T,
15
11
  payload: unknown
@@ -30,17 +26,20 @@ export async function validateBody<T extends SchemaTypes>(
30
26
  return { error: ["Validation failed"] };
31
27
  }
32
28
  }
29
+
33
30
  export function bodyValidator<T extends SchemaTypes>(schema: T) {
34
- return function <F extends ExpressHandler<unknown, unknown, Infer<T>, unknown>>(
35
- _target: unknown,
36
- _propertyKey: string,
37
- descriptor: TypedPropertyDescriptor<F>
38
- ) {
31
+ return function <
32
+ F extends (
33
+ req: Request<any, any, Infer<T>, any, Record<string, any>>,
34
+ res: Response<any, Record<string, any>>,
35
+ next: NextFunction
36
+ ) => Promise<any> | void,
37
+ >(_target: unknown, _propertyKey: string, descriptor: TypedPropertyDescriptor<F>) {
39
38
  const originalMethod = descriptor.value!;
40
39
 
41
40
  descriptor.value = async function (
42
41
  this: unknown,
43
- req: Request<unknown, unknown, Infer<T>>,
42
+ req: Request<any, any, Infer<T>, any, Record<string, any>>,
44
43
  res: Response,
45
44
  next: NextFunction
46
45
  ) {
@@ -57,23 +56,26 @@ export function bodyValidator<T extends SchemaTypes>(schema: T) {
57
56
 
58
57
  req.body = data;
59
58
 
60
- return originalMethod.call(this, req, res, next);
61
- } as F;
59
+ return await originalMethod.call(this, req, res, next);
60
+ } as unknown as F;
62
61
 
63
62
  return descriptor;
64
63
  };
65
64
  }
65
+
66
66
  export function paramsValidator<T extends SchemaTypes>(schema: T) {
67
- return function <F extends ExpressHandler<Infer<T>, unknown, unknown, unknown>>(
68
- _target: unknown,
69
- _propertyKey: string,
70
- descriptor: TypedPropertyDescriptor<F>
71
- ) {
67
+ return function <
68
+ F extends (
69
+ req: Request<any, any, Infer<T>, any, Record<string, any>>,
70
+ res: Response<any, Record<string, any>>,
71
+ next: NextFunction
72
+ ) => Promise<any> | void,
73
+ >(_target: unknown, _propertyKey: string, descriptor: TypedPropertyDescriptor<F>) {
72
74
  const originalMethod = descriptor.value!;
73
75
 
74
76
  descriptor.value = async function (
75
77
  this: unknown,
76
- req: Request<Infer<T>>,
78
+ req: Request<Infer<T>, any, any, any, Record<string, any>>,
77
79
  res: Response,
78
80
  next: NextFunction
79
81
  ) {
@@ -84,26 +86,28 @@ export function paramsValidator<T extends SchemaTypes>(schema: T) {
84
86
  return res.status(422).json({ message: error.join("\n") });
85
87
  }
86
88
 
87
- req.params = data;
89
+ Object.assign(req.query, data);
88
90
 
89
- return originalMethod.call(this, req, res, next);
90
- } as F;
91
+ return await originalMethod.call(this, req, res, next);
92
+ } as unknown as F;
91
93
 
92
94
  return descriptor;
93
95
  };
94
96
  }
95
97
 
96
98
  export function queryValidator<T extends SchemaTypes>(schema: T) {
97
- return function <F extends ExpressHandler<unknown, unknown, unknown, Infer<T>>>(
98
- _target: unknown,
99
- _propertyKey: string,
100
- descriptor: TypedPropertyDescriptor<F>
101
- ) {
99
+ return function <
100
+ F extends (
101
+ req: Request<any, any, Infer<T>, any, Record<string, any>>,
102
+ res: Response<any, Record<string, any>>,
103
+ next: NextFunction
104
+ ) => Promise<any> | void,
105
+ >(_target: unknown, _propertyKey: string, descriptor: TypedPropertyDescriptor<F>) {
102
106
  const originalMethod = descriptor.value!;
103
107
 
104
108
  descriptor.value = async function (
105
109
  this: unknown,
106
- req: Request<unknown, unknown, unknown, Infer<T>>,
110
+ req: Request<any, any, any, Infer<T>, Record<string, any>>,
107
111
  res: Response,
108
112
  next: NextFunction
109
113
  ) {
@@ -114,56 +118,27 @@ export function queryValidator<T extends SchemaTypes>(schema: T) {
114
118
  return res.status(422).json({ message: error.join("\n") });
115
119
  }
116
120
 
117
- req.query = data;
118
-
119
- return originalMethod.call(this, req, res, next);
120
- } as F;
121
+ Object.assign(req.query, data);
122
+ return await originalMethod.call(this, req, res, next);
123
+ } as unknown as F;
121
124
 
122
125
  return descriptor;
123
126
  };
124
127
  }
125
- // export function authValidator({ optional = false }: { optional?: boolean } | undefined = {}) {
126
- // return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
127
- // const originalMethod = descriptor.value;
128
- // descriptor.value = async function (req: Request, res: Response, next: NextFunction) {
129
- // try {
130
- // const accessToken = req.headers["authorization"]?.split(" ")[1];
131
- // if (optional && (!accessToken || !TokenModule.verifyAccessToken(accessToken))) {
132
- // return originalMethod.apply(this, [req, res, next]);
133
- // }
134
- // if (!accessToken) {
135
- // res.status(401).json({ message: "access token missing" });
136
- // return;
137
- // }
138
- // const payload = TokenModule.verifyAccessToken(accessToken);
139
-
140
- // if (!payload) {
141
- // res.status(401).json({ message: "invalid access token" });
142
- // return;
143
- // }
144
- // req.user = {
145
- // id: payload.userID,
146
- // email: payload.email
147
- // };
148
- // return originalMethod.apply(this, [req, res, next]);
149
- // } catch (error) {
150
- // next(error);
151
- // }
152
- // };
153
- // };
154
- // }
155
128
 
156
129
  export function mediaBodyValidator<T extends SchemaTypes>(schema: T, optional = false) {
157
- return function <F extends ExpressHandler<unknown, unknown, Infer<T>, unknown>>(
158
- _target: unknown,
159
- _propertyKey: string,
160
- descriptor: TypedPropertyDescriptor<F>
161
- ) {
130
+ return function <
131
+ F extends (
132
+ req: Request<any, any, Infer<T>, any, Record<string, any>> & { files?: any },
133
+ res: Response<any, Record<string, any>>,
134
+ next: NextFunction
135
+ ) => Promise<any> | void,
136
+ >(_target: unknown, _propertyKey: string, descriptor: TypedPropertyDescriptor<F>) {
162
137
  const originalMethod = descriptor.value!;
163
138
 
164
139
  descriptor.value = async function (
165
140
  this: unknown,
166
- req: Request<unknown, unknown, Infer<T>>,
141
+ req: Request<any, any, Infer<T>, any, Record<string, any>> & { files?: any },
167
142
  res: Response,
168
143
  next: NextFunction
169
144
  ) {
@@ -184,9 +159,7 @@ export function mediaBodyValidator<T extends SchemaTypes>(schema: T, optional =
184
159
  }
185
160
  };
186
161
 
187
- const parsedData = Object.fromEntries(
188
- Object.entries(req.body).map(([k, v]) => [k, isJSON(v)])
189
- );
162
+ const parsedData = Object.fromEntries(Object.entries(req.body).map(([k, v]) => [k, isJSON(v)]));
190
163
 
191
164
  const { error, data } = await validateBody(schema, parsedData);
192
165
 
@@ -198,12 +171,13 @@ export function mediaBodyValidator<T extends SchemaTypes>(schema: T, optional =
198
171
  req.body = data;
199
172
  req.files = files;
200
173
 
201
- return originalMethod.call(this, req, res, next);
202
- } as F;
174
+ return await originalMethod.call(this, req, res, next);
175
+ } as unknown as F;
203
176
 
204
177
  return descriptor;
205
178
  };
206
179
  }
180
+
207
181
  export const validateId = vine.object({
208
182
  id: vine.string().uuid(),
209
183
  });
@@ -224,10 +198,8 @@ export const paginationValidator = vine.object({
224
198
  .parse(e => (!e ? "" : e))
225
199
  .optional(),
226
200
  });
227
- export async function validate<T extends SchemaTypes>(
228
- schema: T,
229
- payload: Infer<T>
230
- ): Promise<Infer<T>> {
201
+
202
+ export async function validate<T extends SchemaTypes>(schema: T, payload: Infer<T>): Promise<Infer<T>> {
231
203
  const result = await validateBody(schema, payload);
232
204
 
233
205
  if (result.error.length) {
@@ -0,0 +1,4 @@
1
+ import { baseConfig } from "eslint-config/base";
2
+ import { extend } from "eslint-config/extend";
3
+
4
+ export default extend(baseConfig);
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@repo/model",
3
+ "type": "module",
4
+ "scripts": {
5
+ "lint": "eslint .",
6
+ "lint:fix": "eslint . src/**/*.ts --fix",
7
+ "clean": "rimraf dist build .turbo",
8
+ "deep:clean": "rimraf node_modules dist build .turbo",
9
+ "typecheck": "tsc --noEmit",
10
+ "format": "prettier --write .",
11
+ "format:check": "prettier --check .",
12
+ "test": "vitest run"
13
+ },
14
+ "exports": {
15
+ ".": "./src/index.ts",
16
+ "./*": "./src/*/index.ts"
17
+ },
18
+ "dependencies": {
19
+ "@repo/lib": "workspace:^",
20
+ "@repo/logger": "workspace:^"
21
+ },
22
+ "devDependencies": {
23
+ "typescript-config": "workspace:^",
24
+ "eslint-config": "workspace:^",
25
+ "vitest-config": "workspace:^"
26
+ },
27
+ "startx": {
28
+ "iTags": [
29
+ "node"
30
+ ],
31
+ "gTags": [],
32
+ "tags": [],
33
+ "requiredDeps": [
34
+ "@repo/env",
35
+ "@repo/lib"
36
+ ],
37
+ "requiredDevDeps": [
38
+ "typescript-config"
39
+ ]
40
+ }
41
+ }
File without changes
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "typescript-config/tsconfig.node.json",
3
+ "compilerOptions": {
4
+ // "baseUrl": "./src"
5
+ },
6
+ "include": ["src/**/*.ts"]
7
+ }
@@ -0,0 +1,3 @@
1
+ import vitestConfig from "vitest-config/node";
2
+
3
+ export default vitestConfig;
@@ -1,23 +1,96 @@
1
- /**
2
- * Convert time from any time unit to any other unit
3
- */
4
- export const Time = {
5
- milliseconds: {
6
- toMinutes: 1 / (60 * 1000),
7
- toSeconds: 1 / 1000,
8
- },
9
- seconds: {
10
- toMilliseconds: 1000,
11
- },
12
- minutes: {
13
- toMilliseconds: 60 * 1000,
14
- },
15
- hours: {
16
- toMilliseconds: 60 * 60 * 1000,
17
- toSeconds: 60 * 60,
18
- },
19
- days: {
20
- toSeconds: 24 * 60 * 60,
21
- toMilliseconds: 24 * 60 * 60 * 1000,
22
- },
1
+ type TimeUnit = "milliseconds" | "seconds" | "minutes" | "hours" | "days" | "weeks";
2
+
3
+ const UNIT_IN_MS: Record<TimeUnit, number> = {
4
+ milliseconds: 1,
5
+ seconds: 1000,
6
+ minutes: 60 * 1000,
7
+ hours: 60 * 60 * 1000,
8
+ days: 24 * 60 * 60 * 1000,
9
+ weeks: 7 * 24 * 60 * 60 * 1000,
23
10
  };
11
+
12
+ export class Time {
13
+ private ms: number;
14
+
15
+ constructor(value: number, unit: TimeUnit = "milliseconds") {
16
+ this.ms = value * UNIT_IN_MS[unit];
17
+ }
18
+
19
+ // factory methods
20
+ static ms(value: number) {
21
+ return new Time(value, "milliseconds");
22
+ }
23
+
24
+ static seconds(value: number) {
25
+ return new Time(value, "seconds");
26
+ }
27
+
28
+ static minutes(value: number) {
29
+ return new Time(value, "minutes");
30
+ }
31
+
32
+ static hours(value: number) {
33
+ return new Time(value, "hours");
34
+ }
35
+
36
+ static days(value: number) {
37
+ return new Time(value, "days");
38
+ }
39
+
40
+ static weeks(value: number) {
41
+ return new Time(value, "weeks");
42
+ }
43
+
44
+ // generic converter
45
+ to(unit: TimeUnit): number {
46
+ return this.ms / UNIT_IN_MS[unit];
47
+ }
48
+
49
+ // convenience getters
50
+ get milliseconds() {
51
+ return this.to("milliseconds");
52
+ }
53
+
54
+ get seconds() {
55
+ return this.to("seconds");
56
+ }
57
+
58
+ get minutes() {
59
+ return this.to("minutes");
60
+ }
61
+
62
+ get hours() {
63
+ return this.to("hours");
64
+ }
65
+
66
+ get days() {
67
+ return this.to("days");
68
+ }
69
+
70
+ get weeks() {
71
+ return this.to("weeks");
72
+ }
73
+
74
+ // arithmetic
75
+ add(value: number, unit: TimeUnit) {
76
+ this.ms += value * UNIT_IN_MS[unit];
77
+ return this;
78
+ }
79
+
80
+ subtract(value: number, unit: TimeUnit) {
81
+ this.ms -= value * UNIT_IN_MS[unit];
82
+ return this;
83
+ }
84
+
85
+ format() {
86
+ if (this.ms < UNIT_IN_MS.seconds) return `${this.ms}ms`;
87
+ if (this.ms < UNIT_IN_MS.minutes) return `${this.seconds}s`;
88
+ if (this.ms < UNIT_IN_MS.hours) return `${this.minutes}m`;
89
+ if (this.ms < UNIT_IN_MS.days) return `${this.hours}h`;
90
+ return `${this.days}d`;
91
+ }
92
+
93
+ valueOf() {
94
+ return this.ms;
95
+ }
96
+ }