-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhelpers.ts
More file actions
201 lines (167 loc) · 6.45 KB
/
helpers.ts
File metadata and controls
201 lines (167 loc) · 6.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// Convex用ヘルパー関数
import { ConvexError } from "convex/values";
import type { Id } from "./_generated/dataModel";
import type { MutationCtx, QueryCtx } from "./_generated/server";
import { DEFAULT_POSITIONS, POSITION_COLORS, SKILL_LEVELS } from "./constants";
// セキュアなトークン生成(crypto APIを使用)
export const generateToken = () => {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
};
// authIdからユーザー(管理者)を取得
export const getUserByAuthId = async (ctx: QueryCtx | MutationCtx, authId: string) => {
const user = await ctx.db
.query("users")
.withIndex("by_auth_id", (q) => q.eq("authId", authId))
.filter((q) => q.neq(q.field("isDeleted"), true))
.first();
return user;
};
// authIdからユーザーを取得(存在しない場合はエラー)
export const requireUserByAuthId = async (ctx: QueryCtx | MutationCtx, authId: string) => {
const user = await getUserByAuthId(ctx, authId);
if (!user) {
throw new ConvexError({
message: "ユーザーが見つかりません",
code: "USER_NOT_FOUND",
});
}
return user;
};
// 店舗のオーナー(作成者)かどうかチェック
export const isShopOwner = async (ctx: QueryCtx | MutationCtx, shopId: Id<"shops">, authId: string) => {
const shop = await ctx.db.get(shopId);
if (!shop || shop.isDeleted) return false;
return shop.createdBy === authId;
};
// 店舗のオーナーであることを要求
export const requireShopOwner = async (ctx: QueryCtx | MutationCtx, shopId: Id<"shops">, authId: string) => {
const isOwner = await isShopOwner(ctx, shopId, authId);
if (!isOwner) {
throw new ConvexError({
message: "この操作を行う権限がありません",
code: "PERMISSION_DENIED",
});
}
return true;
};
// スタッフを取得(店舗ID + スタッフID)
export const getStaff = async (ctx: QueryCtx | MutationCtx, staffId: Id<"staffs">) => {
const staff = await ctx.db.get(staffId);
if (!staff || staff.isDeleted) return null;
return staff;
};
// スタッフを取得(店舗ID + メールアドレス)
export const getStaffByEmail = async (ctx: QueryCtx | MutationCtx, shopId: Id<"shops">, email: string) => {
const staff = await ctx.db
.query("staffs")
.withIndex("by_shop_and_email", (q) => q.eq("shopId", shopId).eq("email", email))
.filter((q) => q.neq(q.field("isDeleted"), true))
.first();
return staff;
};
// マジックリンクトークンからmagicLinkレコードとスタッフを取得
export const getMagicLinkByToken = async (ctx: QueryCtx | MutationCtx, token: string) => {
const magicLink = await ctx.db
.query("magicLinks")
.withIndex("by_token", (q) => q.eq("token", token))
.first();
if (!magicLink) return null;
const staff = await ctx.db.get(magicLink.staffId);
if (!staff || staff.isDeleted) return null;
return { magicLink, staff };
};
// 招待トークンでスタッフを取得
export const getStaffByInviteToken = async (ctx: QueryCtx | MutationCtx, token: string) => {
const staff = await ctx.db
.query("staffs")
.withIndex("by_invite_token", (q) => q.eq("inviteToken", token))
.filter((q) => q.neq(q.field("isDeleted"), true))
.first();
return staff;
};
// 店舗のオーナーまたはマネージャーかどうかチェック
export const isShopOwnerOrManager = async (ctx: QueryCtx | MutationCtx, shopId: Id<"shops">, authId: string) => {
// まずオーナーかどうかチェック
const isOwner = await isShopOwner(ctx, shopId, authId);
if (isOwner) return true;
// ユーザーを取得
const user = await getUserByAuthId(ctx, authId);
if (!user) return false;
// スタッフとしてマネージャーかどうかチェック
const staff = await ctx.db
.query("staffs")
.withIndex("by_shop", (q) => q.eq("shopId", shopId))
.filter((q) =>
q.and(q.eq(q.field("userId"), user._id), q.neq(q.field("isDeleted"), true), q.eq(q.field("role"), "manager")),
)
.first();
return !!staff;
};
// 店舗のオーナーまたはマネージャーであることを要求
export const requireShopOwnerOrManager = async (ctx: QueryCtx | MutationCtx, shopId: Id<"shops">, authId: string) => {
const isOwnerOrManager = await isShopOwnerOrManager(ctx, shopId, authId);
if (!isOwnerOrManager) {
throw new ConvexError({
message: "この操作を行う権限がありません",
code: "PERMISSION_DENIED",
});
}
return true;
};
// 店舗存在チェック(存在しない場合はエラー)
export const requireShop = async (ctx: QueryCtx | MutationCtx, shopId: Id<"shops">) => {
const shop = await ctx.db.get(shopId);
if (!shop || shop.isDeleted) {
throw new ConvexError({
message: "店舗が見つかりません",
code: "SHOP_NOT_FOUND",
});
}
return shop;
};
// 時刻形式バリデーション(HH:mm形式)
export const isValidTimeFormat = (time: string) => {
const timeRegex = /^([01]\d|2[0-3]):([0-5]\d)$/;
return timeRegex.test(time);
};
// 全ポジションを「未経験」で初期化したスキル配列を生成
export const createDefaultSkills = () => {
return DEFAULT_POSITIONS.map((position) => ({
position,
level: SKILL_LEVELS[0], // "未経験"
}));
};
// 店舗のデフォルトポジションを初期化
export const initializeDefaultPositions = async (ctx: MutationCtx, shopId: Id<"shops">) => {
const positionIds: Id<"shopPositions">[] = [];
for (let i = 0; i < DEFAULT_POSITIONS.length; i++) {
const positionId = await ctx.db.insert("shopPositions", {
shopId,
name: DEFAULT_POSITIONS[i],
color: POSITION_COLORS[i % POSITION_COLORS.length],
order: i,
isDeleted: false,
createdAt: Date.now(),
});
positionIds.push(positionId);
}
return positionIds;
};
// スタッフのスキルを全ポジション「未経験」で初期化
export const initializeStaffSkills = async (ctx: MutationCtx, shopId: Id<"shops">, staffId: Id<"staffs">) => {
const positions = await ctx.db
.query("shopPositions")
.withIndex("by_shop", (q) => q.eq("shopId", shopId))
.filter((q) => q.neq(q.field("isDeleted"), true))
.collect();
for (const position of positions) {
await ctx.db.insert("staffSkills", {
staffId,
positionId: position._id,
level: SKILL_LEVELS[0], // "未経験"
updatedAt: Date.now(),
});
}
};