Browse Source

feat: refactor authentication and caching logic with new context utilities

shadcn-as
npmrun 3 weeks ago
parent
commit
2c106613e6
  1. 21
      app/composables/useAuthSession.ts
  2. 6
      app/plugins/auth-session.server.ts
  3. 36
      packages/cache/readme.md
  4. BIN
      packages/drizzle-pkg/db.sqlite
  5. 7
      server/api/auth/me.get.ts
  6. 5
      server/api/auth/profile.put.ts
  7. 3
      server/api/auth/register.post.ts
  8. 3
      server/api/auth/session.get.ts
  9. 6
      server/api/cards.get.ts
  10. 11
      server/api/config/global.get.ts
  11. 7
      server/api/config/global.put.ts
  12. 9
      server/api/config/me.get.ts
  13. 7
      server/api/config/me.put.ts
  14. 5
      server/api/scheduler/executions.get.ts
  15. 5
      server/api/scheduler/executions/[id].delete.ts
  16. 5
      server/api/scheduler/executions/delete-all.post.ts
  17. 5
      server/api/scheduler/stats.get.ts
  18. 5
      server/api/scheduler/tasks/[id].delete.ts
  19. 5
      server/api/scheduler/tasks/[id].get.ts
  20. 7
      server/api/scheduler/tasks/[id].put.ts
  21. 7
      server/api/scheduler/tasks/[id]/toggle.post.ts
  22. 5
      server/api/scheduler/tasks/index.get.ts
  23. 5
      server/api/scheduler/tasks/index.post.ts
  24. 3
      server/middleware/02.auth-guard.ts
  25. 22
      server/plugins/00.cache.ts
  26. 68
      server/plugins/01.context.ts
  27. 3
      server/service/auth/context.ts
  28. 1
      server/service/auth/cookie.ts
  29. 5
      server/service/config/me/[key].delete.ts
  30. 3
      server/utils/admin-guard.ts
  31. 56
      server/utils/context.ts

21
app/composables/useAuthSession.ts

@ -1,28 +1,19 @@
import { request, unwrapApiBody, type ApiResponse } from "../utils/http/factory"; import { request, unwrapApiBody, type ApiResponse } from "../utils/http/factory";
import type { MinimalUser } from "~~/server/service/auth";
export type AuthUser = {
id: number;
username: string;
email: string | null;
role: string;
publicSlug: string | null;
nickname: string | null;
avatar: string | null;
};
type MeResult = { type MeResult = {
user: AuthUser; user: MinimalUser;
}; };
type SessionResult = { type SessionResult = {
user: AuthUser | null; user: MinimalUser | null;
}; };
export type AuthSessionState = { export type AuthSessionState = {
initialized: boolean; initialized: boolean;
pending: boolean; pending: boolean;
loggedIn: boolean; loggedIn: boolean;
user: AuthUser | null; user: MinimalUser | null;
}; };
export const AUTH_SESSION_STATE_KEY = "auth:session"; export const AUTH_SESSION_STATE_KEY = "auth:session";
@ -49,7 +40,7 @@ export function useAuthSession() {
})); }));
const clientMeSynced = useState<boolean>(AUTH_CLIENT_ME_SYNCED_KEY, () => false); const clientMeSynced = useState<boolean>(AUTH_CLIENT_ME_SYNCED_KEY, () => false);
const applyUser = (user: AuthUser | null) => { const applyUser = (user: MinimalUser | null) => {
state.value.user = user; state.value.user = user;
state.value.loggedIn = Boolean(user); state.value.loggedIn = Boolean(user);
state.value.initialized = true; state.value.initialized = true;
@ -88,7 +79,7 @@ export function useAuthSession() {
}; };
const updateProfile = async (data: { username?: string; email?: string; nickname?: string }) => { const updateProfile = async (data: { username?: string; email?: string; nickname?: string }) => {
const payload = await request<ApiResponse<{ user: AuthUser }>>("/api/auth/profile", { const payload = await request<ApiResponse<{ user: MinimalUser }>>("/api/auth/profile", {
method: "put", method: "put",
body: data, body: data,
}); });

6
app/plugins/auth-session.server.ts

@ -3,6 +3,7 @@ import {
DEFAULT_AUTH_SESSION_STATE, DEFAULT_AUTH_SESSION_STATE,
type AuthSessionState, type AuthSessionState,
} from "../composables/useAuthSession"; } from "../composables/useAuthSession";
import { getCurrentUser } from "#server/utils/context";
export default defineNuxtPlugin(async () => { export default defineNuxtPlugin(async () => {
const event = useRequestEvent(); const event = useRequestEvent();
@ -17,12 +18,13 @@ export default defineNuxtPlugin(async () => {
if (state.value.initialized) { if (state.value.initialized) {
return; return;
} }
console.log(useCookie);
const user = await event.context.auth.getCurrent(); const user = await getCurrentUser(event);
state.value = { state.value = {
initialized: true, initialized: true,
pending: false, pending: false,
loggedIn: Boolean(user), loggedIn: Boolean(user),
user: user ?? null, user: user,
}; };
}); });

36
packages/cache/readme.md

@ -135,42 +135,6 @@ const cache = createCache({
}) })
``` ```
### 场景四:Nitro Plugin 全局注入
`server/plugins/cache.ts`:
```typescript
import { createCache } from 'cache'
export default defineNitroPlugin(() => {
const cache = createCache({
redis: {
host: '127.0.0.1',
port: 6379,
},
defaultTtl: 300,
})
// 注入到 H3 event context
event.context.cache = cache
})
```
`server/api/users.get.ts`:
```typescript
export default defineEventHandler(async (event) => {
const cache = event.context.cache
const cached = await cache.get('users:list')
if (cached) return cached
const data = await fetchUsers()
await cache.set('users:list', data)
return data
})
```
## 数据结构 ## 数据结构
所有值以 JSON 序列化存储到 Redis: 所有值以 JSON 序列化存储到 Redis:

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.

7
server/api/auth/me.get.ts

@ -4,10 +4,11 @@ import { eq } from "drizzle-orm";
import { UNAUTHORIZED_MESSAGE } from "#server/constants/auth"; import { UNAUTHORIZED_MESSAGE } from "#server/constants/auth";
import { clearSessionCookie } from "#server/service/auth/cookie"; import { clearSessionCookie } from "#server/service/auth/cookie";
import { toPublicAuthError } from "#server/service/auth/errors"; import { toPublicAuthError } from "#server/service/auth/errors";
import { getCache, setCache, delCache, requireUser } from "#server/utils/context";
export default defineWrappedResponseHandler(async (event) => { export default defineWrappedResponseHandler(async (event) => {
try { try {
const user = await event.context.auth.requireUser(); const user = await requireUser(event);
if (!user) { if (!user) {
clearSessionCookie(event); clearSessionCookie(event);
throw createError({ throw createError({
@ -17,7 +18,7 @@ export default defineWrappedResponseHandler(async (event) => {
} }
const cacheKey = `auth:me:${user.id}`; const cacheKey = `auth:me:${user.id}`;
const cached = await event.context.cache.get<{ user: { id: number, username: string, role: string, nickname: string | null, avatar: string | null } }>(cacheKey); const cached = await getCache<{ user: { id: number, username: string, role: string, nickname: string | null, avatar: string | null } }>(cacheKey);
if (cached) return R.success(cached); if (cached) return R.success(cached);
const [row] = await dbGlobal const [row] = await dbGlobal
@ -36,7 +37,7 @@ export default defineWrappedResponseHandler(async (event) => {
avatar: row?.avatar ?? null, avatar: row?.avatar ?? null,
}, },
}; };
await event.context.cache.set(cacheKey, result, 60); await setCache(cacheKey, result, 60);
return R.success(result); return R.success(result);
} catch (err) { } catch (err) {
throw toPublicAuthError(err); throw toPublicAuthError(err);

5
server/api/auth/profile.put.ts

@ -3,10 +3,11 @@ import { users } from "drizzle-pkg/lib/schema/auth";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { UNAUTHORIZED_MESSAGE } from "#server/constants/auth"; import { UNAUTHORIZED_MESSAGE } from "#server/constants/auth";
import { toPublicAuthError } from "#server/service/auth/errors"; import { toPublicAuthError } from "#server/service/auth/errors";
import { delCache, requireUser } from "#server/utils/context";
export default defineWrappedResponseHandler(async (event) => { export default defineWrappedResponseHandler(async (event) => {
try { try {
const user = await event.context.auth.requireUser(); const user = await requireUser(event);
if (!user) { if (!user) {
throw createError({ throw createError({
statusCode: 401, statusCode: 401,
@ -61,7 +62,7 @@ export default defineWrappedResponseHandler(async (event) => {
.where(eq(users.id, user.id)); .where(eq(users.id, user.id));
// Invalidate me cache // Invalidate me cache
await event.context.cache.del(`auth:me:${user.id}`); await delCache(`auth:me:${user.id}`);
} }
// Fetch updated user data // Fetch updated user data

3
server/api/auth/register.post.ts

@ -4,6 +4,7 @@ import { toPublicAuthError } from "#server/service/auth/errors";
import { captchaConsume } from "#server/service/captcha/store"; import { captchaConsume } from "#server/service/captcha/store";
import { assertLoginRegisterCaptchaFieldsPresent } from "#server/service/captcha/validate-body"; import { assertLoginRegisterCaptchaFieldsPresent } from "#server/service/captcha/validate-body";
import { assertUnderRateLimit } from "#server/utils/simple-rate-limit"; import { assertUnderRateLimit } from "#server/utils/simple-rate-limit";
import { getConfigGlobal } from "#server/utils/context";
export default defineWrappedResponseHandler(async (event) => { export default defineWrappedResponseHandler(async (event) => {
const ip = getRequestIP(event, { xForwardedFor: true }) ?? "unknown"; const ip = getRequestIP(event, { xForwardedFor: true }) ?? "unknown";
@ -18,7 +19,7 @@ export default defineWrappedResponseHandler(async (event) => {
}); });
} }
const allowRegister = await event.context.config.getGlobal("allowRegister"); const allowRegister = await getConfigGlobal("allowRegister");
if (!allowRegister) { if (!allowRegister) {
throw createError({ throw createError({
statusCode: 403, statusCode: 403,

3
server/api/auth/session.get.ts

@ -1,4 +1,5 @@
import { toPublicAuthError } from "#server/service/auth/errors"; import { toPublicAuthError } from "#server/service/auth/errors";
import { getCurrentUser } from "#server/utils/context";
/** /**
* Cookie 访 Cookie SSR `auth.getCurrent()` * Cookie 访 Cookie SSR `auth.getCurrent()`
@ -6,7 +7,7 @@ import { toPublicAuthError } from "#server/service/auth/errors";
*/ */
export default defineWrappedResponseHandler(async (event) => { export default defineWrappedResponseHandler(async (event) => {
try { try {
const user = await event.context.auth.getCurrent(); const user = await getCurrentUser(event);
return R.success({ user: user ?? null }); return R.success({ user: user ?? null });
} catch (err) { } catch (err) {
throw toPublicAuthError(err); throw toPublicAuthError(err);

6
server/api/cards.get.ts

@ -1,3 +1,5 @@
import { getCache, setCache } from "#server/utils/context";
type CardType = 'text' | 'image' | 'image-text' | 'portfolio' | 'project' type CardType = 'text' | 'image' | 'image-text' | 'portfolio' | 'project'
interface CardData { interface CardData {
@ -131,7 +133,7 @@ export default defineWrappedResponseHandler(async (event) => {
const pageSize = Math.min(30, Math.max(1, parseInt(String(query.pageSize || '12')))) const pageSize = Math.min(30, Math.max(1, parseInt(String(query.pageSize || '12'))))
const cacheKey = `cards:list:${page}:${pageSize}` const cacheKey = `cards:list:${page}:${pageSize}`
const cached = await event.context.cache.get<{ items: CardData[], hasMore: boolean, page: number }>(cacheKey) const cached = await getCache<{ items: CardData[], hasMore: boolean, page: number }>(cacheKey)
if (cached) return cached if (cached) return cached
const start = (page - 1) * pageSize const start = (page - 1) * pageSize
@ -141,6 +143,6 @@ export default defineWrappedResponseHandler(async (event) => {
const hasMore = start + pageSize < total const hasMore = start + pageSize < total
const result = { items, hasMore, page } const result = { items, hasMore, page }
await event.context.cache.set(cacheKey, result, 300) await setCache(cacheKey, result, 300)
return result return result
}) })

11
server/api/config/global.get.ts

@ -1,3 +1,4 @@
import { getCache, setCache, getCurrentUser, getConfigGlobal } from "#server/utils/context";
import { KNOWN_CONFIG_KEYS } from "#server/service/config/registry"; import { KNOWN_CONFIG_KEYS } from "#server/service/config/registry";
import type { KnownConfigKey, KnownConfigValue } from "#server/service/config/registry"; import type { KnownConfigKey, KnownConfigValue } from "#server/service/config/registry";
@ -8,18 +9,18 @@ const PUBLIC_GLOBAL_CONFIG_KEYS = [
const SECRET_MASKED_GLOBAL_CONFIG_KEYS = new Set<KnownConfigKey>([]); const SECRET_MASKED_GLOBAL_CONFIG_KEYS = new Set<KnownConfigKey>([]);
export default defineWrappedResponseHandler(async (event) => { export default defineWrappedResponseHandler(async (event) => {
const user = await event.context.auth.getCurrent(); const user = await getCurrentUser(event);
const isAdmin = user?.role === "admin"; const isAdmin = user?.role === "admin";
const cacheKey = isAdmin ? "config:global:admin" : "config:global:public"; const cacheKey = isAdmin ? "config:global:admin" : "config:global:public";
const cached = await event.context.cache.get<{ config: Record<string, unknown> }>(cacheKey); const cached = await getCache<{ config: Record<string, unknown> }>(cacheKey);
if (cached) return R.success(cached); if (cached) return R.success(cached);
const keys: readonly KnownConfigKey[] = isAdmin ? KNOWN_CONFIG_KEYS : PUBLIC_GLOBAL_CONFIG_KEYS; const keys: readonly KnownConfigKey[] = isAdmin ? KNOWN_CONFIG_KEYS : PUBLIC_GLOBAL_CONFIG_KEYS;
const entries = await Promise.all( const entries = await Promise.all(
keys.map(async (key) => { keys.map(async (key) => {
const value = await event.context.config.getGlobal(key); const value = await getConfigGlobal(event, key);
const safeValue = const safeValue =
SECRET_MASKED_GLOBAL_CONFIG_KEYS.has(key) SECRET_MASKED_GLOBAL_CONFIG_KEYS.has(key)
? "" ? ""
@ -30,11 +31,11 @@ export default defineWrappedResponseHandler(async (event) => {
const config = Object.fromEntries(entries); const config = Object.fromEntries(entries);
if (!isAdmin) { if (!isAdmin) {
await event.context.cache.set(cacheKey, { config }, 300); await setCache(cacheKey, { config }, 300);
return R.success({ config }); return R.success({ config });
} }
const result = { config: { ...config } }; const result = { config: { ...config } };
await event.context.cache.set(cacheKey, result, 300); await setCache(cacheKey, result, 300);
return R.success(result); return R.success(result);
}); });

7
server/api/config/global.put.ts

@ -6,6 +6,7 @@ import { toPublicConfigError } from "#server/service/config/errors";
import { requireAdmin } from "#server/utils/admin-guard"; import { requireAdmin } from "#server/utils/admin-guard";
import { assertUnderRateLimit } from "#server/utils/simple-rate-limit"; import { assertUnderRateLimit } from "#server/utils/simple-rate-limit";
import { getRequestIP } from "h3"; import { getRequestIP } from "h3";
import { delCache, getConfigGlobal } from "#server/utils/context";
type UpdateGlobalConfigBody = { type UpdateGlobalConfigBody = {
key: string; key: string;
@ -28,10 +29,10 @@ export default defineWrappedResponseHandler(async (event) => {
const key = ensureKnownConfigKey(body.key); const key = ensureKnownConfigKey(body.key);
await setGlobalConfigValue(key, body.value); await setGlobalConfigValue(key, body.value);
await event.context.cache.del("config:global:public"); await delCache("config:global:public");
await event.context.cache.del("config:global:admin"); await delCache("config:global:admin");
const value = await event.context.config.getGlobal(key); const value = await getConfigGlobal(event, key);
return R.success({ return R.success({
key, key,
value: toSafeResponseValue(key, value), value: toSafeResponseValue(key, value),

9
server/api/config/me.get.ts

@ -1,18 +1,19 @@
import { getCache, setCache, requireUser, getConfig } from "#server/utils/context";
import { dbGlobal } from "drizzle-pkg/lib/db"; import { dbGlobal } from "drizzle-pkg/lib/db";
import { users } from "drizzle-pkg/lib/schema/auth"; import { users } from "drizzle-pkg/lib/schema/auth";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { KNOWN_CONFIG_KEYS } from "#server/service/config/registry"; import { KNOWN_CONFIG_KEYS } from "#server/service/config/registry";
export default defineWrappedResponseHandler(async (event) => { export default defineWrappedResponseHandler(async (event) => {
const user = await event.context.auth.requireUser(); const user = await requireUser(event);
const cacheKey = `config:me:${user.id}`; const cacheKey = `config:me:${user.id}`;
const cached = await event.context.cache.get<{ user: unknown, config: unknown }>(cacheKey); const cached = await getCache<{ user: unknown, config: unknown }>(cacheKey);
if (cached) return R.success(cached); if (cached) return R.success(cached);
const entries = await Promise.all( const entries = await Promise.all(
KNOWN_CONFIG_KEYS.map(async (key) => { KNOWN_CONFIG_KEYS.map(async (key) => {
const value = await event.context.config.get(key); const value = await getConfig(event, key);
return [key, value] as const; return [key, value] as const;
}), }),
); );
@ -36,6 +37,6 @@ export default defineWrappedResponseHandler(async (event) => {
}, },
config: Object.fromEntries(entries), config: Object.fromEntries(entries),
}; };
await event.context.cache.set(cacheKey, result, 60); await setCache(cacheKey, result, 60);
return R.success(result); return R.success(result);
}); });

7
server/api/config/me.put.ts

@ -3,6 +3,7 @@ import {
setCurrentUserConfigValue, setCurrentUserConfigValue,
} from "#server/service/config"; } from "#server/service/config";
import { toPublicConfigError } from "#server/service/config/errors"; import { toPublicConfigError } from "#server/service/config/errors";
import { delCache, requireUser, getConfig } from "#server/utils/context";
type UpdateMyConfigBody = { type UpdateMyConfigBody = {
key: string; key: string;
@ -11,15 +12,15 @@ type UpdateMyConfigBody = {
export default defineWrappedResponseHandler(async (event) => { export default defineWrappedResponseHandler(async (event) => {
try { try {
const user = await event.context.auth.requireUser(); const user = await requireUser(event);
const body = await readBody<UpdateMyConfigBody>(event); const body = await readBody<UpdateMyConfigBody>(event);
const key = ensureKnownConfigKey(body.key); const key = ensureKnownConfigKey(body.key);
await setCurrentUserConfigValue(user.id, key, body.value); await setCurrentUserConfigValue(user.id, key, body.value);
await event.context.cache.del(`config:me:${user.id}`); await delCache(`config:me:${user.id}`);
const value = await event.context.config.get(key); const value = await getConfig(event, key);
return R.success({ return R.success({
key, key,
value, value,

5
server/api/scheduler/executions.get.ts

@ -1,3 +1,4 @@
import { getCache, setCache } from "#server/utils/context";
import { listExecutions } from "../../service/scheduler"; import { listExecutions } from "../../service/scheduler";
export default defineWrappedResponseHandler(async (event) => { export default defineWrappedResponseHandler(async (event) => {
@ -6,7 +7,7 @@ export default defineWrappedResponseHandler(async (event) => {
const pageSize = query.pageSize ? Number(query.pageSize) : 20; const pageSize = query.pageSize ? Number(query.pageSize) : 20;
const cacheKey = `scheduler:executions:${page}:${pageSize}:${query.taskId ?? 'all'}:${query.status ?? 'all'}` const cacheKey = `scheduler:executions:${page}:${pageSize}:${query.taskId ?? 'all'}:${query.status ?? 'all'}`
const cached = await event.context.cache.get<{ list: unknown[], total: number, page: number, pageSize: number }>(cacheKey) const cached = await getCache<{ list: unknown[], total: number, page: number, pageSize: number }>(cacheKey)
if (cached) return R.success(cached) if (cached) return R.success(cached)
const result = await listExecutions({ const result = await listExecutions({
@ -16,6 +17,6 @@ export default defineWrappedResponseHandler(async (event) => {
status: query.status as string | undefined, status: query.status as string | undefined,
}); });
await event.context.cache.set(cacheKey, result, 60) await setCache(cacheKey, result, 60)
return R.success(result); return R.success(result);
}); });

5
server/api/scheduler/executions/[id].delete.ts

@ -1,3 +1,4 @@
import { delCache } from "#server/utils/context";
import { deleteExecution } from "#server/service/scheduler"; import { deleteExecution } from "#server/service/scheduler";
export default defineWrappedResponseHandler(async (event) => { export default defineWrappedResponseHandler(async (event) => {
@ -6,8 +7,8 @@ export default defineWrappedResponseHandler(async (event) => {
const deleted = await deleteExecution(id); const deleted = await deleteExecution(id);
await event.context.cache.del('scheduler:executions:1:20:all:all') await delCache('scheduler:executions:1:20:all:all')
await event.context.cache.del('scheduler:stats') await delCache('scheduler:stats')
return R.success({ deleted }); return R.success({ deleted });
}); });

5
server/api/scheduler/executions/delete-all.post.ts

@ -1,3 +1,4 @@
import { delCache } from "#server/utils/context";
import { deleteAllExecutions } from "#server/service/scheduler"; import { deleteAllExecutions } from "#server/service/scheduler";
export default defineWrappedResponseHandler(async (event) => { export default defineWrappedResponseHandler(async (event) => {
@ -6,8 +7,8 @@ export default defineWrappedResponseHandler(async (event) => {
const deleted = await deleteAllExecutions(taskId); const deleted = await deleteAllExecutions(taskId);
await event.context.cache.del('scheduler:executions:1:20:all:all') await delCache('scheduler:executions:1:20:all:all')
await event.context.cache.del('scheduler:stats') await delCache('scheduler:stats')
return R.success({ deleted }); return R.success({ deleted });
}); });

5
server/api/scheduler/stats.get.ts

@ -1,13 +1,14 @@
import { getCache, setCache } from "#server/utils/context";
import { getStats } from "../../service/scheduler"; import { getStats } from "../../service/scheduler";
import { getJobCount } from "../../scheduler/engine"; import { getJobCount } from "../../scheduler/engine";
export default defineWrappedResponseHandler(async (event) => { export default defineWrappedResponseHandler(async (event) => {
const cacheKey = 'scheduler:stats' const cacheKey = 'scheduler:stats'
const cached = await event.context.cache.get<{ totalTasks: number, enabledTasks: number, last24hExecutions: number, activeJobs: number }>(cacheKey) const cached = await getCache<{ totalTasks: number, enabledTasks: number, last24hExecutions: number, activeJobs: number }>(cacheKey)
if (cached) return R.success({ ...cached, activeJobs: getJobCount() }) if (cached) return R.success({ ...cached, activeJobs: getJobCount() })
const stats = await getStats(); const stats = await getStats();
const result = { ...stats, activeJobs: getJobCount() } const result = { ...stats, activeJobs: getJobCount() }
await event.context.cache.set(cacheKey, result, 60) await setCache(cacheKey, result, 60)
return R.success(result); return R.success(result);
}); });

5
server/api/scheduler/tasks/[id].delete.ts

@ -1,3 +1,4 @@
import { delCache } from "#server/utils/context";
import { deleteTask } from "../../../service/scheduler"; import { deleteTask } from "../../../service/scheduler";
import { removeTask } from "../../../scheduler/engine"; import { removeTask } from "../../../scheduler/engine";
@ -8,8 +9,8 @@ export default defineWrappedResponseHandler(async (event) => {
removeTask(id); removeTask(id);
await deleteTask(id); await deleteTask(id);
await event.context.cache.del(`scheduler:task:${id}`) await delCache(`scheduler:task:${id}`)
await event.context.cache.del('scheduler:tasks:1:20:all:all') await delCache('scheduler:tasks:1:20:all:all')
return R.success(null); return R.success(null);
}); });

5
server/api/scheduler/tasks/[id].get.ts

@ -1,3 +1,4 @@
import { getCache, setCache } from "#server/utils/context";
import { getTaskById, getRecentExecutions } from "../../../service/scheduler"; import { getTaskById, getRecentExecutions } from "../../../service/scheduler";
export default defineWrappedResponseHandler(async (event) => { export default defineWrappedResponseHandler(async (event) => {
@ -5,7 +6,7 @@ export default defineWrappedResponseHandler(async (event) => {
if (!id) return R.throwError(400, "Missing id", null); if (!id) return R.throwError(400, "Missing id", null);
const cacheKey = `scheduler:task:${id}` const cacheKey = `scheduler:task:${id}`
const cached = await event.context.cache.get<{ task: unknown, recentExecutions: unknown[] }>(cacheKey) const cached = await getCache<{ task: unknown, recentExecutions: unknown[] }>(cacheKey)
if (cached) return R.success(cached) if (cached) return R.success(cached)
const task = await getTaskById(id); const task = await getTaskById(id);
@ -14,6 +15,6 @@ export default defineWrappedResponseHandler(async (event) => {
const recentExecutions = await getRecentExecutions(id, 20); const recentExecutions = await getRecentExecutions(id, 20);
const result = { task, recentExecutions } const result = { task, recentExecutions }
await event.context.cache.set(cacheKey, result, 60) await setCache(cacheKey, result, 60)
return R.success(result); return R.success(result);
}); });

7
server/api/scheduler/tasks/[id].put.ts

@ -1,3 +1,4 @@
import { delCache } from "#server/utils/context";
import { z } from "zod"; import { z } from "zod";
import { updateTask, getTaskById } from "../../../service/scheduler"; import { updateTask, getTaskById } from "../../../service/scheduler";
import { reloadTask } from "../../../scheduler/engine"; import { reloadTask } from "../../../scheduler/engine";
@ -54,9 +55,9 @@ export default defineWrappedResponseHandler(async (event) => {
reloadTask(task.id); reloadTask(task.id);
} }
await event.context.cache.del(`scheduler:task:${id}`) await delCache(`scheduler:task:${id}`)
await event.context.cache.del('scheduler:tasks:1:20:all:all') await delCache('scheduler:tasks:1:20:all:all')
await event.context.cache.del('scheduler:stats') await delCache('scheduler:stats')
return R.success(task); return R.success(task);
}); });

7
server/api/scheduler/tasks/[id]/toggle.post.ts

@ -1,3 +1,4 @@
import { delCache } from "#server/utils/context";
import { toggleTask } from "../../../../service/scheduler"; import { toggleTask } from "../../../../service/scheduler";
import { reloadTask, removeTask } from "../../../../scheduler/engine"; import { reloadTask, removeTask } from "../../../../scheduler/engine";
@ -16,9 +17,9 @@ export default defineWrappedResponseHandler(async (event) => {
removeTask(id); removeTask(id);
} }
await event.context.cache.del(`scheduler:task:${id}`) await delCache(`scheduler:task:${id}`)
await event.context.cache.del('scheduler:tasks:1:20:all:all') await delCache('scheduler:tasks:1:20:all:all')
await event.context.cache.del('scheduler:stats') await delCache('scheduler:stats')
return R.success(task); return R.success(task);
}); });

5
server/api/scheduler/tasks/index.get.ts

@ -1,3 +1,4 @@
import { getCache, setCache } from "#server/utils/context";
import { listTasks } from "../../../service/scheduler"; import { listTasks } from "../../../service/scheduler";
import { listRegisteredTasks } from "../../../scheduler/registry"; import { listRegisteredTasks } from "../../../scheduler/registry";
@ -7,7 +8,7 @@ export default defineWrappedResponseHandler(async (event) => {
const pageSize = query.pageSize ? Number(query.pageSize) : 20; const pageSize = query.pageSize ? Number(query.pageSize) : 20;
const cacheKey = `scheduler:tasks:${page}:${pageSize}:${query.type ?? 'all'}:${query.enabled ?? 'all'}` const cacheKey = `scheduler:tasks:${page}:${pageSize}:${query.type ?? 'all'}:${query.enabled ?? 'all'}`
const cached = await event.context.cache.get<{ list: unknown[], total: number, page: number, pageSize: number, registeredFunctions: unknown[] }>(cacheKey) const cached = await getCache<{ list: unknown[], total: number, page: number, pageSize: number, registeredFunctions: unknown[] }>(cacheKey)
if (cached) return R.success(cached) if (cached) return R.success(cached)
const result = await listTasks({ const result = await listTasks({
@ -21,6 +22,6 @@ export default defineWrappedResponseHandler(async (event) => {
...result, ...result,
registeredFunctions: listRegisteredTasks(), registeredFunctions: listRegisteredTasks(),
} }
await event.context.cache.set(cacheKey, fullResult, 120) await setCache(cacheKey, fullResult, 120)
return R.success(fullResult); return R.success(fullResult);
}); });

5
server/api/scheduler/tasks/index.post.ts

@ -1,3 +1,4 @@
import { delCache } from "#server/utils/context";
import { z } from "zod"; import { z } from "zod";
import { createTask } from "../../../service/scheduler"; import { createTask } from "../../../service/scheduler";
import { addTask } from "../../../scheduler/engine"; import { addTask } from "../../../scheduler/engine";
@ -55,8 +56,8 @@ export default defineWrappedResponseHandler(async (event) => {
addTask(task.id); addTask(task.id);
} }
await event.context.cache.del('scheduler:tasks:1:20:all:all') await delCache('scheduler:tasks:1:20:all:all')
await event.context.cache.del('scheduler:stats') await delCache('scheduler:stats')
return R.success(task); return R.success(task);
}); });

3
server/middleware/02.auth-guard.ts

@ -1,5 +1,6 @@
import { UNAUTHORIZED_MESSAGE } from "#server/constants/auth"; import { UNAUTHORIZED_MESSAGE } from "#server/constants/auth";
import { isAllowlistedApiPath } from "#server/utils/auth-api-routes"; import { isAllowlistedApiPath } from "#server/utils/auth-api-routes";
import { getCurrentUser } from "#server/utils/context";
export default eventHandler(async (event) => { export default eventHandler(async (event) => {
const path = event.path; const path = event.path;
@ -14,7 +15,7 @@ export default eventHandler(async (event) => {
return; return;
} }
const user = await event.context.auth.getCurrent(); const user = await getCurrentUser(event);
if (user) { if (user) {
return; return;
} }

22
server/plugins/00.cache.ts

@ -1,22 +0,0 @@
import { createCache } from "cache";
const cache = createCache({
// redis: {
// host: process.env.REDIS_HOST ?? '127.0.0.1',
// port: Number(process.env.REDIS_PORT ?? 6379),
// password: process.env.REDIS_PASSWORD,
// db: Number(process.env.REDIS_DB ?? 0),
// },
// defaultTtl: 300,
memory: true,
});
if (import.meta.dev) {
console.log("plugin: 00.cache");
}
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook("request", (event) => {
event.context.cache = cache;
});
});

68
server/plugins/01.context.ts

@ -1,68 +0,0 @@
import { createCache } from "cache";
import { createAuthContext } from "../service/auth/context";
import { getGlobalConfigValue, getMergedConfigValue, getUserConfigValue } from "../service/config";
import type { KnownConfigKey, KnownConfigValue } from "../service/config/registry";
if (import.meta.dev) {
console.log("plugin: 01.context");
}
type ConfigContext = {
getGlobal: <K extends KnownConfigKey>(key: K) => Promise<KnownConfigValue<K>>;
getUser: <K extends KnownConfigKey>(key: K) => Promise<KnownConfigValue<K> | undefined>;
get: <K extends KnownConfigKey>(key: K) => Promise<KnownConfigValue<K>>;
};
interface CachedConfigCache {
get<T>(key: string): Promise<T | null>;
set<T>(key: string, value: T, ttl?: number): Promise<void>;
del(key: string): Promise<void>;
}
declare module "h3" {
interface H3EventContext {
auth: ReturnType<typeof createAuthContext>;
config: ConfigContext;
cache: ReturnType<typeof createCache>;
}
}
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook("request", async (event) => {
event.context.auth = createAuthContext(event);
const cache = event.context.cache;
event.context.config = {
getGlobal: async <K extends KnownConfigKey>(key: K) => {
const cacheKey = `config:global:${key}`;
const cached = await cache.get<KnownConfigValue<K>>(cacheKey);
if (cached !== null) return cached;
const value = await getGlobalConfigValue(key);
await cache.set(cacheKey, value);
return value;
},
getUser: async <K extends KnownConfigKey>(key: K) => {
const user = await event.context.auth.getCurrent();
const cacheKey = `config:user:${user?.id ?? "anonymous"}:${key}`;
const cached = await cache.get<KnownConfigValue<K>>(cacheKey);
if (cached !== null) return cached;
const value = await getUserConfigValue(user?.id, key);
if (value !== undefined) {
await cache.set(cacheKey, value);
}
return value;
},
get: async <K extends KnownConfigKey>(key: K) => {
const user = await event.context.auth.getCurrent();
const cacheKey = `config:merged:${user?.id ?? "anonymous"}:${key}`;
const cached = await cache.get<KnownConfigValue<K>>(cacheKey);
if (cached !== null) return cached;
const value = await getMergedConfigValue(user?.id, key);
await cache.set(cacheKey, value);
return value;
},
};
});
})

3
server/service/auth/context.ts

@ -1,6 +1,7 @@
import type { H3Event } from "h3"; import type { H3Event } from "h3";
import { SESSION_COOKIE_NAME, UNAUTHORIZED_MESSAGE } from "#server/constants/auth"; import { SESSION_COOKIE_NAME, UNAUTHORIZED_MESSAGE } from "#server/constants/auth";
import { getCurrentUser, type MinimalUser } from "."; import { getCurrentUser, type MinimalUser } from ".";
import { getSessionId } from "./cookie";
export function createAuthContext(event: H3Event) { export function createAuthContext(event: H3Event) {
let currentUserPromise: Promise<MinimalUser | null> | undefined; let currentUserPromise: Promise<MinimalUser | null> | undefined;
@ -8,7 +9,7 @@ export function createAuthContext(event: H3Event) {
const getCurrent = async () => { const getCurrent = async () => {
if (!currentUserPromise) { if (!currentUserPromise) {
currentUserPromise = (async () => { currentUserPromise = (async () => {
const sessionId = getCookie(event, SESSION_COOKIE_NAME); const sessionId = getSessionId(event);
if (!sessionId) { if (!sessionId) {
return null; return null;
} }

1
server/service/auth/cookie.ts

@ -1,4 +1,5 @@
import type { H3Event } from "h3"; import type { H3Event } from "h3";
import { getCookie } from "h3";
import { import {
SESSION_COOKIE_NAME, SESSION_COOKIE_NAME,
SESSION_COOKIE_PATH, SESSION_COOKIE_PATH,

5
server/service/config/me/[key].delete.ts

@ -3,13 +3,14 @@ import {
resetCurrentUserConfigValue, resetCurrentUserConfigValue,
} from "#server/service/config"; } from "#server/service/config";
import { toPublicConfigError } from "#server/service/config/errors"; import { toPublicConfigError } from "#server/service/config/errors";
import { delCache, requireUser, getConfig } from "#server/utils/context";
export default defineWrappedResponseHandler(async (event) => { export default defineWrappedResponseHandler(async (event) => {
try { try {
const user = await event.context.auth.requireUser(); const user = await requireUser(event);
const key = ensureKnownConfigKey(getRouterParam(event, "key") ?? ""); const key = ensureKnownConfigKey(getRouterParam(event, "key") ?? "");
await resetCurrentUserConfigValue(user.id, key); await resetCurrentUserConfigValue(user.id, key);
const value = await event.context.config.get(key); const value = await getConfig(event, key);
return R.success({ return R.success({
key, key,
value, value,

3
server/utils/admin-guard.ts

@ -1,8 +1,9 @@
import type { H3Event } from "h3"; import type { H3Event } from "h3";
import type { MinimalUser } from "#server/service/auth"; import type { MinimalUser } from "#server/service/auth";
import { requireUser } from "#server/utils/context";
export async function requireAdmin(event: H3Event): Promise<MinimalUser> { export async function requireAdmin(event: H3Event): Promise<MinimalUser> {
const user = await event.context.auth.requireUser(); const user = await requireUser(event);
if (user.role !== "admin") { if (user.role !== "admin") {
throw createError({ throw createError({
statusCode: 403, statusCode: 403,

56
server/utils/context.ts

@ -0,0 +1,56 @@
import type { H3Event } from "h3";
import { createCache } from "cache";
import { createAuthContext } from "../service/auth/context";
import { getGlobalConfigValue, getMergedConfigValue, getUserConfigValue } from "../service/config";
import type { KnownConfigKey, KnownConfigValue } from "../service/config/registry";
// ============= Cache =============
const _cache = createCache({
memory: true,
});
export const cache = _cache;
export async function getCache<T>(key: string): Promise<T | null> {
return _cache.get<T>(key);
}
export async function setCache<T>(key: string, value: T, ttl?: number): Promise<void> {
return _cache.set(key, value, ttl);
}
export async function delCache(key: string): Promise<void> {
return _cache.del(key);
}
// ============= Auth =============
export function getAuth(event: H3Event) {
return createAuthContext(event);
}
export async function getCurrentUser(event: H3Event) {
const auth = getAuth(event);
return auth.getCurrent();
}
export async function requireUser(event: H3Event) {
const auth = getAuth(event);
return auth.requireUser();
}
// ============= Config =============
export async function getConfigGlobal<K extends KnownConfigKey>(key: K): Promise<KnownConfigValue<K>> {
return getGlobalConfigValue(key);
}
export async function getConfigUser<K extends KnownConfigKey>(event: H3Event, key: K): Promise<KnownConfigValue<K> | undefined> {
const auth = getAuth(event);
const user = await auth.getCurrent();
return getUserConfigValue(user?.id ?? undefined, key);
}
export async function getConfig<K extends KnownConfigKey>(event: H3Event, key: K): Promise<KnownConfigValue<K>> {
const auth = getAuth(event);
const user = await auth.getCurrent();
return getMergedConfigValue(user?.id ?? undefined, key);
}
Loading…
Cancel
Save