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. 8
      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";
export type AuthUser = {
id: number;
username: string;
email: string | null;
role: string;
publicSlug: string | null;
nickname: string | null;
avatar: string | null;
};
import type { MinimalUser } from "~~/server/service/auth";
type MeResult = {
user: AuthUser;
user: MinimalUser;
};
type SessionResult = {
user: AuthUser | null;
user: MinimalUser | null;
};
export type AuthSessionState = {
initialized: boolean;
pending: boolean;
loggedIn: boolean;
user: AuthUser | null;
user: MinimalUser | null;
};
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 applyUser = (user: AuthUser | null) => {
const applyUser = (user: MinimalUser | null) => {
state.value.user = user;
state.value.loggedIn = Boolean(user);
state.value.initialized = true;
@ -88,7 +79,7 @@ export function useAuthSession() {
};
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",
body: data,
});

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

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

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 { clearSessionCookie } from "#server/service/auth/cookie";
import { toPublicAuthError } from "#server/service/auth/errors";
import { getCache, setCache, delCache, requireUser } from "#server/utils/context";
export default defineWrappedResponseHandler(async (event) => {
try {
const user = await event.context.auth.requireUser();
const user = await requireUser(event);
if (!user) {
clearSessionCookie(event);
throw createError({
@ -17,7 +18,7 @@ export default defineWrappedResponseHandler(async (event) => {
}
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);
const [row] = await dbGlobal
@ -36,7 +37,7 @@ export default defineWrappedResponseHandler(async (event) => {
avatar: row?.avatar ?? null,
},
};
await event.context.cache.set(cacheKey, result, 60);
await setCache(cacheKey, result, 60);
return R.success(result);
} catch (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 { UNAUTHORIZED_MESSAGE } from "#server/constants/auth";
import { toPublicAuthError } from "#server/service/auth/errors";
import { delCache, requireUser } from "#server/utils/context";
export default defineWrappedResponseHandler(async (event) => {
try {
const user = await event.context.auth.requireUser();
const user = await requireUser(event);
if (!user) {
throw createError({
statusCode: 401,
@ -61,7 +62,7 @@ export default defineWrappedResponseHandler(async (event) => {
.where(eq(users.id, user.id));
// Invalidate me cache
await event.context.cache.del(`auth:me:${user.id}`);
await delCache(`auth:me:${user.id}`);
}
// 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 { assertLoginRegisterCaptchaFieldsPresent } from "#server/service/captcha/validate-body";
import { assertUnderRateLimit } from "#server/utils/simple-rate-limit";
import { getConfigGlobal } from "#server/utils/context";
export default defineWrappedResponseHandler(async (event) => {
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) {
throw createError({
statusCode: 403,

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

@ -1,4 +1,5 @@
import { toPublicAuthError } from "#server/service/auth/errors";
import { getCurrentUser } from "#server/utils/context";
/**
* Cookie 访 Cookie SSR `auth.getCurrent()`
@ -6,7 +7,7 @@ import { toPublicAuthError } from "#server/service/auth/errors";
*/
export default defineWrappedResponseHandler(async (event) => {
try {
const user = await event.context.auth.getCurrent();
const user = await getCurrentUser(event);
return R.success({ user: user ?? null });
} catch (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'
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 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
const start = (page - 1) * pageSize
@ -141,6 +143,6 @@ export default defineWrappedResponseHandler(async (event) => {
const hasMore = start + pageSize < total
const result = { items, hasMore, page }
await event.context.cache.set(cacheKey, result, 300)
await setCache(cacheKey, result, 300)
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 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>([]);
export default defineWrappedResponseHandler(async (event) => {
const user = await event.context.auth.getCurrent();
const user = await getCurrentUser(event);
const isAdmin = user?.role === "admin";
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);
const keys: readonly KnownConfigKey[] = isAdmin ? KNOWN_CONFIG_KEYS : PUBLIC_GLOBAL_CONFIG_KEYS;
const entries = await Promise.all(
keys.map(async (key) => {
const value = await event.context.config.getGlobal(key);
const value = await getConfigGlobal(event, key);
const safeValue =
SECRET_MASKED_GLOBAL_CONFIG_KEYS.has(key)
? ""
@ -30,11 +31,11 @@ export default defineWrappedResponseHandler(async (event) => {
const config = Object.fromEntries(entries);
if (!isAdmin) {
await event.context.cache.set(cacheKey, { config }, 300);
await setCache(cacheKey, { config }, 300);
return R.success({ config });
}
const result = { config: { ...config } };
await event.context.cache.set(cacheKey, result, 300);
await setCache(cacheKey, result, 300);
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 { assertUnderRateLimit } from "#server/utils/simple-rate-limit";
import { getRequestIP } from "h3";
import { delCache, getConfigGlobal } from "#server/utils/context";
type UpdateGlobalConfigBody = {
key: string;
@ -28,10 +29,10 @@ export default defineWrappedResponseHandler(async (event) => {
const key = ensureKnownConfigKey(body.key);
await setGlobalConfigValue(key, body.value);
await event.context.cache.del("config:global:public");
await event.context.cache.del("config:global:admin");
await delCache("config:global:public");
await delCache("config:global:admin");
const value = await event.context.config.getGlobal(key);
const value = await getConfigGlobal(event, key);
return R.success({
key,
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 { users } from "drizzle-pkg/lib/schema/auth";
import { eq } from "drizzle-orm";
import { KNOWN_CONFIG_KEYS } from "#server/service/config/registry";
export default defineWrappedResponseHandler(async (event) => {
const user = await event.context.auth.requireUser();
const user = await requireUser(event);
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);
const entries = await Promise.all(
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;
}),
);
@ -36,6 +37,6 @@ export default defineWrappedResponseHandler(async (event) => {
},
config: Object.fromEntries(entries),
};
await event.context.cache.set(cacheKey, result, 60);
await setCache(cacheKey, result, 60);
return R.success(result);
});

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

@ -3,6 +3,7 @@ import {
setCurrentUserConfigValue,
} from "#server/service/config";
import { toPublicConfigError } from "#server/service/config/errors";
import { delCache, requireUser, getConfig } from "#server/utils/context";
type UpdateMyConfigBody = {
key: string;
@ -11,15 +12,15 @@ type UpdateMyConfigBody = {
export default defineWrappedResponseHandler(async (event) => {
try {
const user = await event.context.auth.requireUser();
const user = await requireUser(event);
const body = await readBody<UpdateMyConfigBody>(event);
const key = ensureKnownConfigKey(body.key);
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({
key,
value,

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

@ -1,3 +1,4 @@
import { getCache, setCache } from "#server/utils/context";
import { listExecutions } from "../../service/scheduler";
export default defineWrappedResponseHandler(async (event) => {
@ -6,7 +7,7 @@ export default defineWrappedResponseHandler(async (event) => {
const pageSize = query.pageSize ? Number(query.pageSize) : 20;
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)
const result = await listExecutions({
@ -16,6 +17,6 @@ export default defineWrappedResponseHandler(async (event) => {
status: query.status as string | undefined,
});
await event.context.cache.set(cacheKey, result, 60)
await setCache(cacheKey, result, 60)
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";
export default defineWrappedResponseHandler(async (event) => {
@ -6,8 +7,8 @@ export default defineWrappedResponseHandler(async (event) => {
const deleted = await deleteExecution(id);
await event.context.cache.del('scheduler:executions:1:20:all:all')
await event.context.cache.del('scheduler:stats')
await delCache('scheduler:executions:1:20:all:all')
await delCache('scheduler:stats')
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";
export default defineWrappedResponseHandler(async (event) => {
@ -6,8 +7,8 @@ export default defineWrappedResponseHandler(async (event) => {
const deleted = await deleteAllExecutions(taskId);
await event.context.cache.del('scheduler:executions:1:20:all:all')
await event.context.cache.del('scheduler:stats')
await delCache('scheduler:executions:1:20:all:all')
await delCache('scheduler:stats')
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 { getJobCount } from "../../scheduler/engine";
export default defineWrappedResponseHandler(async (event) => {
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() })
const stats = await getStats();
const result = { ...stats, activeJobs: getJobCount() }
await event.context.cache.set(cacheKey, result, 60)
await setCache(cacheKey, result, 60)
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 { removeTask } from "../../../scheduler/engine";
@ -8,8 +9,8 @@ export default defineWrappedResponseHandler(async (event) => {
removeTask(id);
await deleteTask(id);
await event.context.cache.del(`scheduler:task:${id}`)
await event.context.cache.del('scheduler:tasks:1:20:all:all')
await delCache(`scheduler:task:${id}`)
await delCache('scheduler:tasks:1:20:all:all')
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";
export default defineWrappedResponseHandler(async (event) => {
@ -5,7 +6,7 @@ export default defineWrappedResponseHandler(async (event) => {
if (!id) return R.throwError(400, "Missing id", null);
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)
const task = await getTaskById(id);
@ -14,6 +15,6 @@ export default defineWrappedResponseHandler(async (event) => {
const recentExecutions = await getRecentExecutions(id, 20);
const result = { task, recentExecutions }
await event.context.cache.set(cacheKey, result, 60)
await setCache(cacheKey, result, 60)
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 { updateTask, getTaskById } from "../../../service/scheduler";
import { reloadTask } from "../../../scheduler/engine";
@ -54,9 +55,9 @@ export default defineWrappedResponseHandler(async (event) => {
reloadTask(task.id);
}
await event.context.cache.del(`scheduler:task:${id}`)
await event.context.cache.del('scheduler:tasks:1:20:all:all')
await event.context.cache.del('scheduler:stats')
await delCache(`scheduler:task:${id}`)
await delCache('scheduler:tasks:1:20:all:all')
await delCache('scheduler:stats')
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 { reloadTask, removeTask } from "../../../../scheduler/engine";
@ -16,9 +17,9 @@ export default defineWrappedResponseHandler(async (event) => {
removeTask(id);
}
await event.context.cache.del(`scheduler:task:${id}`)
await event.context.cache.del('scheduler:tasks:1:20:all:all')
await event.context.cache.del('scheduler:stats')
await delCache(`scheduler:task:${id}`)
await delCache('scheduler:tasks:1:20:all:all')
await delCache('scheduler:stats')
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 { listRegisteredTasks } from "../../../scheduler/registry";
@ -7,7 +8,7 @@ export default defineWrappedResponseHandler(async (event) => {
const pageSize = query.pageSize ? Number(query.pageSize) : 20;
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)
const result = await listTasks({
@ -21,6 +22,6 @@ export default defineWrappedResponseHandler(async (event) => {
...result,
registeredFunctions: listRegisteredTasks(),
}
await event.context.cache.set(cacheKey, fullResult, 120)
await setCache(cacheKey, fullResult, 120)
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 { createTask } from "../../../service/scheduler";
import { addTask } from "../../../scheduler/engine";
@ -55,8 +56,8 @@ export default defineWrappedResponseHandler(async (event) => {
addTask(task.id);
}
await event.context.cache.del('scheduler:tasks:1:20:all:all')
await event.context.cache.del('scheduler:stats')
await delCache('scheduler:tasks:1:20:all:all')
await delCache('scheduler:stats')
return R.success(task);
});

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

@ -1,5 +1,6 @@
import { UNAUTHORIZED_MESSAGE } from "#server/constants/auth";
import { isAllowlistedApiPath } from "#server/utils/auth-api-routes";
import { getCurrentUser } from "#server/utils/context";
export default eventHandler(async (event) => {
const path = event.path;
@ -14,7 +15,7 @@ export default eventHandler(async (event) => {
return;
}
const user = await event.context.auth.getCurrent();
const user = await getCurrentUser(event);
if (user) {
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 { SESSION_COOKIE_NAME, UNAUTHORIZED_MESSAGE } from "#server/constants/auth";
import { getCurrentUser, type MinimalUser } from ".";
import { getSessionId } from "./cookie";
export function createAuthContext(event: H3Event) {
let currentUserPromise: Promise<MinimalUser | null> | undefined;
@ -8,7 +9,7 @@ export function createAuthContext(event: H3Event) {
const getCurrent = async () => {
if (!currentUserPromise) {
currentUserPromise = (async () => {
const sessionId = getCookie(event, SESSION_COOKIE_NAME);
const sessionId = getSessionId(event);
if (!sessionId) {
return null;
}

1
server/service/auth/cookie.ts

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

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

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

3
server/utils/admin-guard.ts

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