145 lines
6.5 KiB
TypeScript
145 lines
6.5 KiB
TypeScript
export interface VisibilityWhere {
|
||
clause: string;
|
||
params: unknown[];
|
||
}
|
||
|
||
export interface VisibilityOptions {
|
||
/**
|
||
* 行がスペースに属する場合の space_id 列名(例 `'space_id'`、spaces 自身は `'id'`)。
|
||
* 指定かつ非 admin のとき、可視性節に「スペースのメンバーなら閲覧可」の OR ブランチを足す。
|
||
* **コード制御のリテラルのみ**(tableAlias と同じ信頼レベル)。ユーザー入力を渡してはならない。
|
||
*/
|
||
spaceColumn?: string;
|
||
}
|
||
|
||
export function buildVisibilityWhere(
|
||
user: Express.User,
|
||
tableAlias: string,
|
||
opts?: VisibilityOptions,
|
||
): VisibilityWhere {
|
||
if (user.role === 'admin') {
|
||
return { clause: '1=1', params: [] };
|
||
}
|
||
// Synthetic users (no-auth 'local' user, internal callers) may omit orgIds —
|
||
// treat as no org membership rather than throwing on `.length`.
|
||
const orgIds = user.orgIds ?? [];
|
||
const orgPlaceholders = orgIds.length > 0
|
||
? orgIds.map(() => '?').join(',')
|
||
: 'NULL';
|
||
// 既存の params 順は [user.id, ...user.orgIds]。membership の user.id は必ず末尾に足す(順序が重要)。
|
||
const params: unknown[] = [user.id, ...orgIds];
|
||
let membershipBranch = '';
|
||
if (opts?.spaceColumn) {
|
||
// スペース行の閲覧資格 = 明示メンバー(space_members)OR スペースの根オーナー
|
||
// (spaces.owner_id)。根オーナーは通常 space_members 行を持たないため、
|
||
// owner サブクエリを足さないと自分のスペースの「他メンバーの行」が見えない(gap-1)。
|
||
// user.id を clause 出現順(members → spaces)に 2 回 push する。
|
||
membershipBranch =
|
||
` OR (${tableAlias}.${opts.spaceColumn} IN (SELECT space_id FROM space_members WHERE user_id = ?)` +
|
||
` OR ${tableAlias}.${opts.spaceColumn} IN (SELECT id FROM spaces WHERE owner_id = ?))`;
|
||
params.push(user.id, user.id);
|
||
}
|
||
return {
|
||
clause: `(
|
||
${tableAlias}.owner_id = ?
|
||
OR ${tableAlias}.visibility = 'public'
|
||
OR (${tableAlias}.visibility = 'org' AND ${tableAlias}.visibility_scope_org_id IN (${orgPlaceholders}))${membershipBranch}
|
||
)`.replace(/\s+/g, ' ').trim(),
|
||
params,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* `spaces` テーブル専用の可視性 WHERE。通常の行可視性と決定的に違うのは
|
||
* **個人スペース(kind='personal')は所有者本人にしか見えない(admin も例外でない)**
|
||
* という点。これがないと admin の listSpaces が他ユーザーの個人スペースを拾い、
|
||
* UI が「最初に見つかった personal」を自分のワークスペースと誤認する(誤った
|
||
* members-owner / AGENTS.md / MCP / 設定で動く root cause)。
|
||
*
|
||
* - `kind='personal'` 行: `s.owner_id = ?`(viewer.id)のみ。role を問わない。
|
||
* - `kind='case'` 行: 従来の `buildVisibilityWhere`(admin=1=1 / owner / org /
|
||
* public / space メンバーシップ OR ブランチ)をそのまま適用。
|
||
*
|
||
* param 順は SQL の出現順に合わせる: 先頭に personal-owner の viewer.id、続いて
|
||
* `buildVisibilityWhere` が返す params(admin のときは空)。`tableAlias` は呼び出し
|
||
* 側のコード制御リテラルのみ(buildVisibilityWhere と同じ信頼レベル)。
|
||
*/
|
||
export function buildSpaceVisibilityWhere(
|
||
user: Express.User,
|
||
tableAlias: string,
|
||
): VisibilityWhere {
|
||
const base = buildVisibilityWhere(user, tableAlias, { spaceColumn: 'id' });
|
||
const clause =
|
||
`(
|
||
(${tableAlias}.kind = 'personal' AND ${tableAlias}.owner_id = ?)
|
||
OR (${tableAlias}.kind != 'personal' AND ${base.clause})
|
||
)`.replace(/\s+/g, ' ').trim();
|
||
// 出現順: personal-owner の viewer.id → base の params。
|
||
return { clause, params: [user.id, ...base.params] };
|
||
}
|
||
|
||
export function canEditEntity(user: Express.User, entity: { ownerId: string | null }): boolean {
|
||
return user.role === 'admin' || entity.ownerId === user.id;
|
||
}
|
||
|
||
export type SpaceMemberRole = 'owner' | 'editor' | 'viewer';
|
||
|
||
/**
|
||
* スペースの管理操作(メンバー管理・スペース設定変更・archive)を許可するか。
|
||
* admin / 根オーナー(space.ownerId)/ member.role==='owner' なら true。
|
||
* `memberRole` は Repository.getSpaceMemberRole で事前取得したロール(DB アクセスは Repository に置く)。
|
||
*/
|
||
export function canManageSpace(
|
||
user: Express.User,
|
||
space: { ownerId: string | null },
|
||
memberRole?: SpaceMemberRole | null,
|
||
): boolean {
|
||
return (
|
||
user.role === 'admin' ||
|
||
(space.ownerId !== null && space.ownerId === user.id) ||
|
||
memberRole === 'owner'
|
||
);
|
||
}
|
||
|
||
/**
|
||
* スペース配下の行編集(タスク継続/削除・カレンダー CRUD・ファイル upload 等)を
|
||
* スペースのメンバーシップ起点で許可するか。canManageSpace に加え editor も許可。
|
||
* viewer は read のみ(buildVisibilityWhere が扱う)で書き込み不可。
|
||
*/
|
||
export function canEditInSpace(
|
||
user: Express.User,
|
||
space: { ownerId: string | null },
|
||
memberRole?: SpaceMemberRole | null,
|
||
): boolean {
|
||
return canManageSpace(user, space, memberRole) || memberRole === 'editor';
|
||
}
|
||
|
||
/**
|
||
* 指定 user が指定タスクを閲覧できるか判定する。SQL ではなくロード済みオブジェクト
|
||
* に対して使う。`buildVisibilityWhere` と同じセマンティクスをコード上で再現する。
|
||
*
|
||
* - admin: 常に true
|
||
* - owner: 常に true
|
||
* - public: 常に true
|
||
* - org: user.orgIds に visibilityScopeOrgId が含まれているかで判定
|
||
* - スペースメンバー/根オーナー: `spaceAccessible === true` のとき true
|
||
* (呼び出し側が `repo.userCanViewSpace(task.spaceId, user)` で算出して渡す。
|
||
* この関数自体は DB を触らず純粋に保つ)
|
||
* - private: 上記いずれにも該当しなければ false
|
||
*/
|
||
export function canUserSeeTask(
|
||
user: Express.User,
|
||
task: { ownerId: string | null; visibility: 'private' | 'org' | 'public'; visibilityScopeOrgId: string | null },
|
||
spaceAccessible?: boolean,
|
||
): boolean {
|
||
if (user.role === 'admin') return true;
|
||
if (task.ownerId !== null && task.ownerId === user.id) return true;
|
||
if (task.visibility === 'public') return true;
|
||
if (task.visibility === 'org' && task.visibilityScopeOrgId !== null) {
|
||
if (user.orgIds.includes(task.visibilityScopeOrgId)) return true;
|
||
}
|
||
// スペースメンバーシップ起点の閲覧許可(buildVisibilityWhere の OR ブランチに対応)。
|
||
if (spaceAccessible === true) return true;
|
||
return false;
|
||
}
|