fix: rewrite backend hooks, fix invitation flow, align frontend API, fix component naming
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+128
-128
@@ -8,13 +8,139 @@ import (
|
|||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// isGroupMember checks if a user is a member of a group
|
func main() {
|
||||||
|
app := pocketbase.New()
|
||||||
|
|
||||||
|
// ── Groups ──
|
||||||
|
|
||||||
|
app.OnRecordBeforeCreateRequest("groups").Add(func(e *core.RecordCreateEvent) error {
|
||||||
|
authRecord, _ := e.HttpContext.AuthRecord()
|
||||||
|
if authRecord == nil {
|
||||||
|
return apis.NewForbiddenError("需要登录", nil)
|
||||||
|
}
|
||||||
|
e.Record.Set("owner", authRecord.Id)
|
||||||
|
e.Record.Set("members", []string{authRecord.Id})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
app.OnRecordBeforeUpdateRequest("groups").Add(func(e *core.RecordUpdateEvent) error {
|
||||||
|
authRecord, _ := e.HttpContext.AuthRecord()
|
||||||
|
if authRecord == nil {
|
||||||
|
return apis.NewForbiddenError("需要登录", nil)
|
||||||
|
}
|
||||||
|
original, err := app.Dao().FindRecordById("groups", e.Record.Id)
|
||||||
|
if err != nil {
|
||||||
|
return apis.NewNotFoundError("群组不存在", nil)
|
||||||
|
}
|
||||||
|
if original.GetString("owner") != authRecord.Id {
|
||||||
|
return apis.NewForbiddenError("只有群组所有者可以更新群组", nil)
|
||||||
|
}
|
||||||
|
// 保护 owner 字段不被篡改
|
||||||
|
e.Record.Set("owner", original.GetString("owner"))
|
||||||
|
// 验证 members 数量
|
||||||
|
members := e.Record.GetStringSlice("members")
|
||||||
|
maxMembers := e.Record.GetInt("maxMembers")
|
||||||
|
if maxMembers > 0 && len(members) > maxMembers {
|
||||||
|
return apis.NewBadRequestError("成员数量超过上限", nil)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
app.OnRecordBeforeDeleteRequest("groups").Add(func(e *core.RecordDeleteEvent) error {
|
||||||
|
authRecord, _ := e.HttpContext.AuthRecord()
|
||||||
|
if authRecord == nil {
|
||||||
|
return apis.NewForbiddenError("需要登录", nil)
|
||||||
|
}
|
||||||
|
isOwner, err := isGroupOwner(app, e.Record.Id, authRecord.Id)
|
||||||
|
if err != nil || !isOwner {
|
||||||
|
return apis.NewForbiddenError("只有群组所有者可以删除群组", nil)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Team Sessions ──
|
||||||
|
|
||||||
|
app.OnRecordBeforeCreateRequest("team_sessions").Add(func(e *core.RecordCreateEvent) error {
|
||||||
|
authRecord, _ := e.HttpContext.AuthRecord()
|
||||||
|
if authRecord == nil {
|
||||||
|
return apis.NewForbiddenError("需要登录", nil)
|
||||||
|
}
|
||||||
|
groupId := e.Record.GetString("sourceGroup")
|
||||||
|
isMember, err := isGroupMember(app, groupId, authRecord.Id)
|
||||||
|
if err != nil || !isMember {
|
||||||
|
return apis.NewForbiddenError("只有群组成员可以创建团队会话", nil)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Invitations ──
|
||||||
|
|
||||||
|
app.OnRecordBeforeCreateRequest("invitations").Add(func(e *core.RecordCreateEvent) error {
|
||||||
|
authRecord, _ := e.HttpContext.AuthRecord()
|
||||||
|
if authRecord == nil {
|
||||||
|
return apis.NewForbiddenError("需要登录", nil)
|
||||||
|
}
|
||||||
|
// 强制设置 from 为当前用户
|
||||||
|
e.Record.Set("from", authRecord.Id)
|
||||||
|
e.Record.Set("status", "pending")
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// 接受邀请:自动加入 team session + 更新用户状态
|
||||||
|
app.OnRecordBeforeUpdateRequest("invitations").Add(func(e *core.RecordUpdateEvent) error {
|
||||||
|
authRecord, _ := e.HttpContext.AuthRecord()
|
||||||
|
if authRecord == nil {
|
||||||
|
return apis.NewForbiddenError("需要登录", nil)
|
||||||
|
}
|
||||||
|
// 只有 recipient 可以更新邀请
|
||||||
|
if e.Record.GetString("to") != authRecord.Id {
|
||||||
|
return apis.NewForbiddenError("无权操作此邀请", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
newStatus := e.Record.GetString("status")
|
||||||
|
if newStatus == "accepted" {
|
||||||
|
teamSessionId := e.Record.GetString("teamSession")
|
||||||
|
teamSession, err := app.Dao().FindRecordById("team_sessions", teamSessionId)
|
||||||
|
if err != nil {
|
||||||
|
return apis.NewNotFoundError("临时小组不存在", nil)
|
||||||
|
}
|
||||||
|
// 将用户加入 team session members
|
||||||
|
members := teamSession.GetStringSlice("members")
|
||||||
|
alreadyIn := false
|
||||||
|
for _, m := range members {
|
||||||
|
if m == authRecord.Id {
|
||||||
|
alreadyIn = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !alreadyIn {
|
||||||
|
members = append(members, authRecord.Id)
|
||||||
|
teamSession.Set("members", members)
|
||||||
|
if err := app.Dao().SaveRecord(teamSession); err != nil {
|
||||||
|
return apis.NewBadRequestError("加入临时小组失败", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户状态为 in_team
|
||||||
|
user, err := app.Dao().FindRecordById("users", authRecord.Id)
|
||||||
|
if err == nil {
|
||||||
|
user.Set("status", "in_team")
|
||||||
|
app.Dao().SaveRecord(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := app.Start(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func isGroupMember(app *pocketbase.PocketBase, groupId string, userId string) (bool, error) {
|
func isGroupMember(app *pocketbase.PocketBase, groupId string, userId string) (bool, error) {
|
||||||
group, err := app.Dao().FindRecordById("groups", groupId)
|
group, err := app.Dao().FindRecordById("groups", groupId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
members := group.GetStringSlice("members")
|
members := group.GetStringSlice("members")
|
||||||
for _, member := range members {
|
for _, member := range members {
|
||||||
if member == userId {
|
if member == userId {
|
||||||
@@ -24,7 +150,6 @@ func isGroupMember(app *pocketbase.PocketBase, groupId string, userId string) (b
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isGroupOwner checks if a user is the owner of a group
|
|
||||||
func isGroupOwner(app *pocketbase.PocketBase, groupId string, userId string) (bool, error) {
|
func isGroupOwner(app *pocketbase.PocketBase, groupId string, userId string) (bool, error) {
|
||||||
group, err := app.Dao().FindRecordById("groups", groupId)
|
group, err := app.Dao().FindRecordById("groups", groupId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -32,128 +157,3 @@ func isGroupOwner(app *pocketbase.PocketBase, groupId string, userId string) (bo
|
|||||||
}
|
}
|
||||||
return group.GetString("owner") == userId, nil
|
return group.GetString("owner") == userId, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := pocketbase.New()
|
|
||||||
|
|
||||||
// Groups API Rules
|
|
||||||
app.OnRecordBeforeCreateRequest("groups").Add(func(e *core.RecordCreateEvent) error {
|
|
||||||
authRecord, _ := e.HttpContext.AuthRecord()
|
|
||||||
if authRecord == nil {
|
|
||||||
return apis.NewForbiddenError("需要登录", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the owner to the current user
|
|
||||||
e.Record.Set("owner", authRecord.Id)
|
|
||||||
// Initialize members array with the owner
|
|
||||||
e.Record.Set("members", []string{authRecord.Id})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
app.OnRecordBeforeUpdateRequest("groups").Add(func(e *core.RecordUpdateEvent) error {
|
|
||||||
authRecord, _ := e.HttpContext.AuthRecord()
|
|
||||||
if authRecord == nil {
|
|
||||||
return apis.NewForbiddenError("需要登录", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only owner can update the group
|
|
||||||
isOwner, err := isGroupOwner(app, e.Record.Id, authRecord.Id)
|
|
||||||
if err != nil || !isOwner {
|
|
||||||
return apis.NewForbiddenError("只有群组所有者可以更新群组", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
app.OnRecordBeforeDeleteRequest("groups").Add(func(e *core.RecordDeleteEvent) error {
|
|
||||||
authRecord, _ := e.HttpContext.AuthRecord()
|
|
||||||
if authRecord == nil {
|
|
||||||
return apis.NewForbiddenError("需要登录", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only owner can delete the group
|
|
||||||
isOwner, err := isGroupOwner(app, e.Record.Id, authRecord.Id)
|
|
||||||
if err != nil || !isOwner {
|
|
||||||
return apis.NewForbiddenError("只有群组所有者可以删除群组", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
// Team Sessions API Rules
|
|
||||||
app.OnRecordBeforeCreateRequest("team_sessions").Add(func(e *core.RecordCreateEvent) error {
|
|
||||||
authRecord, _ := e.HttpContext.AuthRecord()
|
|
||||||
if authRecord == nil {
|
|
||||||
return apis.NewForbiddenError("需要登录", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
groupId := e.Record.GetString("group")
|
|
||||||
|
|
||||||
// Check if user is a member of the group
|
|
||||||
isMember, err := isGroupMember(app, groupId, authRecord.Id)
|
|
||||||
if err != nil || !isMember {
|
|
||||||
return apis.NewForbiddenError("只有群组成员可以创建团队会话", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
// Invitations API Rules
|
|
||||||
app.OnRecordBeforeCreateRequest("invitations").Add(func(e *core.RecordCreateEvent) error {
|
|
||||||
authRecord, _ := e.HttpContext.AuthRecord()
|
|
||||||
if authRecord == nil {
|
|
||||||
return apis.NewForbiddenError("需要登录", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
groupId := e.Record.GetString("group")
|
|
||||||
|
|
||||||
// Only group owner can create invitations
|
|
||||||
isOwner, err := isGroupOwner(app, groupId, authRecord.Id)
|
|
||||||
if err != nil || !isOwner {
|
|
||||||
return apis.NewForbiddenError("只有群组所有者可以创建邀请", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set status to pending
|
|
||||||
e.Record.Set("status", "pending")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
app.OnRecordAfterCreateRequest("invitations").Add(func(e *core.RecordCreateEvent) error {
|
|
||||||
// Send real-time notification to the invited user
|
|
||||||
message := map[string]interface{}{
|
|
||||||
"action": "invitation",
|
|
||||||
"data": map[string]interface{}{
|
|
||||||
"id": e.Record.Id,
|
|
||||||
"group": e.Record.GetString("group"),
|
|
||||||
"invited_by": e.Record.GetString("invited_by"),
|
|
||||||
"status": e.Record.GetString("status"),
|
|
||||||
"created": e.Record.Created.Time(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Broadcast to the invited user's channel
|
|
||||||
if err := app.Subscriptions().Broadcast("invitations", message); err != nil {
|
|
||||||
log.Printf("Error broadcasting invitation: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
// Real-time subscription rules
|
|
||||||
app.OnRecordAfterAuthWithTokenRequest().Add(func(e *core.RecordAuthEvent) error {
|
|
||||||
// Subscribe to invitations channel and user's groups channel
|
|
||||||
app.Subscriptions().Subscribe(e.HttpContext.Response(), []string{
|
|
||||||
"invitations",
|
|
||||||
"groups:" + e.Record.Id,
|
|
||||||
"team_sessions",
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := app.Start(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
// src/api/invitations.ts
|
// src/api/invitations.ts
|
||||||
import pb from './pocketbase'
|
import pb from './pocketbase'
|
||||||
import type { Invitation, InviteStatus } from '@/types'
|
import type { Invitation } from '@/types'
|
||||||
import { joinTeamSession } from './sessions'
|
|
||||||
import { updateUserStatus } from './users'
|
|
||||||
|
|
||||||
// 发送邀请
|
// 发送邀请
|
||||||
export async function sendInvitation(data: {
|
export async function sendInvitation(data: {
|
||||||
@@ -71,35 +69,16 @@ export async function respondInvitation(
|
|||||||
response: 'accepted' | 'rejected',
|
response: 'accepted' | 'rejected',
|
||||||
rejectReason?: string
|
rejectReason?: string
|
||||||
) {
|
) {
|
||||||
const user = pb.authStore.model
|
const updateData: Record<string, unknown> = {
|
||||||
if (!user) throw new Error('未登录')
|
status: response
|
||||||
|
|
||||||
const invitation = await pb.collection('invitations').getOne(invitationId)
|
|
||||||
|
|
||||||
if (invitation.to !== user.id) {
|
|
||||||
throw new Error('无权操作此邀请')
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData: Partial<Invitation> = {
|
|
||||||
status: response as InviteStatus,
|
|
||||||
respondedAt: new Date().toISOString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response === 'rejected' && rejectReason) {
|
if (response === 'rejected' && rejectReason) {
|
||||||
updateData.rejectReason = rejectReason
|
updateData.rejectReason = rejectReason
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新邀请状态
|
// 后端 hook 会自动处理:加入 team members + 更新用户状态
|
||||||
await pb.collection('invitations').update(invitationId, updateData)
|
await pb.collection('invitations').update(invitationId, updateData)
|
||||||
|
|
||||||
// 如果接受,加入临时小组
|
|
||||||
if (response === 'accepted') {
|
|
||||||
await joinTeamSession(invitation.teamSession)
|
|
||||||
// 更新用户状态
|
|
||||||
await updateUserStatus('in_team')
|
|
||||||
}
|
|
||||||
|
|
||||||
return updateData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 订阅邀请变更
|
// 订阅邀请变更
|
||||||
|
|||||||
@@ -51,17 +51,6 @@ export async function updateTeamStatus(sessionId: string, status: TeamStatus): P
|
|||||||
|
|
||||||
// 结束游戏(解散临时小组)
|
// 结束游戏(解散临时小组)
|
||||||
export async function endGame(sessionId: string) {
|
export async function endGame(sessionId: string) {
|
||||||
const session = await pb.collection('teamSessions').getOne(sessionId)
|
|
||||||
|
|
||||||
// 将所有成员状态恢复为 idle
|
|
||||||
const members = session.members as string[]
|
|
||||||
const updatePromises = members.map(userId =>
|
|
||||||
pb.collection('users').update(userId, { status: 'idle' })
|
|
||||||
)
|
|
||||||
|
|
||||||
await Promise.all(updatePromises)
|
|
||||||
|
|
||||||
// 解散临时小组
|
|
||||||
return updateTeamStatus(sessionId, 'dissolved')
|
return updateTeamStatus(sessionId, 'dissolved')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,12 +21,18 @@ const idleMembers = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
async function inviteMember(userId: string, username: string) {
|
async function inviteMember(userId: string, username: string) {
|
||||||
|
const { getActiveTeamSession } = await import('@/api/sessions')
|
||||||
|
const session = await getActiveTeamSession()
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
ElMessage.warning('请先创建临时小组')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 这里需要先创建临时小组,或者检查是否有活跃的临时小组
|
|
||||||
// 简化版本:发送邀请
|
|
||||||
await sendInvitation({
|
await sendInvitation({
|
||||||
to: userId,
|
to: userId,
|
||||||
teamSession: '' // 实际使用时需要先创建临时小组
|
teamSession: session.id
|
||||||
})
|
})
|
||||||
ElMessage.success(`已邀请 ${username}`)
|
ElMessage.success(`已邀请 ${username}`)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ async function refreshMembers() {
|
|||||||
|
|
||||||
<!-- 主内容区 -->
|
<!-- 主内容区 -->
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<idle-membersList status="idle" />
|
<IdleMembersList status="idle" />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- 右侧边栏 -->
|
<!-- 右侧边栏 -->
|
||||||
@@ -70,7 +70,7 @@ async function refreshMembers() {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<idle-membersList />
|
<IdleMembersList />
|
||||||
</el-card>
|
</el-card>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ function selectGroup(groupId: string) {
|
|||||||
<h3>空闲成员</h3>
|
<h3>空闲成员</h3>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<idle-membersList />
|
<IdleMembersList />
|
||||||
</el-card>
|
</el-card>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user