fix: keep workbench user ids compatible
This commit is contained in:
+19
-19
@@ -238,7 +238,7 @@ func (a *API) attendanceRecords(w http.ResponseWriter, r *http.Request, p *auth.
|
|||||||
Status: r.URL.Query().Get("status"),
|
Status: r.URL.Query().Get("status"),
|
||||||
Cursor: r.URL.Query().Get("cursor"),
|
Cursor: r.URL.Query().Get("cursor"),
|
||||||
Limit: limit(r, 50, 100),
|
Limit: limit(r, 50, 100),
|
||||||
UserID: parseInt64(r.URL.Query().Get("user_id")),
|
UserID: parseUserIDQuery(r.URL.Query().Get("user_id")),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
@@ -401,13 +401,13 @@ func (a *API) meetings(w http.ResponseWriter, r *http.Request, p *auth.Principal
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var body struct {
|
var body struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
RecorderUserID int64 `json:"recorder_user_id"`
|
RecorderUserID userID `json:"recorder_user_id"`
|
||||||
StartAt string `json:"start_at"`
|
StartAt string `json:"start_at"`
|
||||||
EndAt string `json:"end_at"`
|
EndAt string `json:"end_at"`
|
||||||
Location string `json:"location"`
|
Location string `json:"location"`
|
||||||
AttendeeUserIDs []int64 `json:"attendee_user_ids"`
|
AttendeeUserIDs []userID `json:"attendee_user_ids"`
|
||||||
ReminderMinutes []int `json:"reminder_minutes"`
|
ReminderMinutes []int `json:"reminder_minutes"`
|
||||||
}
|
}
|
||||||
if !decodeBody(w, r, &body) {
|
if !decodeBody(w, r, &body) {
|
||||||
return
|
return
|
||||||
@@ -424,9 +424,9 @@ func (a *API) meetings(w http.ResponseWriter, r *http.Request, p *auth.Principal
|
|||||||
}
|
}
|
||||||
reminders, _ := json.Marshal(body.ReminderMinutes)
|
reminders, _ := json.Marshal(body.ReminderMinutes)
|
||||||
meeting, err := a.store.CreateMeeting(r.Context(), p.UserID, store.Meeting{
|
meeting, err := a.store.CreateMeeting(r.Context(), p.UserID, store.Meeting{
|
||||||
MeetingID: "mtg_" + randomToken(12), RecorderID: body.RecorderUserID, Title: strings.TrimSpace(body.Title), StartAt: startAt, EndAt: endAt,
|
MeetingID: "mtg_" + randomToken(12), RecorderID: int64(body.RecorderUserID), Title: strings.TrimSpace(body.Title), StartAt: startAt, EndAt: endAt,
|
||||||
Location: strings.TrimSpace(body.Location), ReminderMinutes: string(reminders), IdempotencyKey: idempotencyKey,
|
Location: strings.TrimSpace(body.Location), ReminderMinutes: string(reminders), IdempotencyKey: idempotencyKey,
|
||||||
}, body.AttendeeUserIDs)
|
}, parseUserIDList(body.AttendeeUserIDs))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -459,14 +459,14 @@ func (a *API) meetingResource(w http.ResponseWriter, r *http.Request, p *auth.Pr
|
|||||||
writeSuccess(w, r, meetingPayload(*meeting, p.UserID))
|
writeSuccess(w, r, meetingPayload(*meeting, p.UserID))
|
||||||
case tail == "" && r.Method == http.MethodPatch:
|
case tail == "" && r.Method == http.MethodPatch:
|
||||||
var body struct {
|
var body struct {
|
||||||
Title *string `json:"title"`
|
Title *string `json:"title"`
|
||||||
RecorderUserID *int64 `json:"recorder_user_id"`
|
RecorderUserID optionalUserID `json:"recorder_user_id"`
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
}
|
}
|
||||||
if !decodeBody(w, r, &body) {
|
if !decodeBody(w, r, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
meeting, err := a.store.UpdateMeeting(r.Context(), p.UserID, meetingID, store.MeetingPatch{Title: body.Title, RecorderID: body.RecorderUserID, Version: body.Version})
|
meeting, err := a.store.UpdateMeeting(r.Context(), p.UserID, meetingID, store.MeetingPatch{Title: body.Title, RecorderID: body.RecorderUserID.ptr(), Version: body.Version})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -729,14 +729,14 @@ func (a *API) dailyReportReminders(w http.ResponseWriter, r *http.Request, p *au
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var body struct {
|
var body struct {
|
||||||
Date string `json:"date"`
|
Date string `json:"date"`
|
||||||
UserIDs []int64 `json:"user_ids"`
|
UserIDs []userID `json:"user_ids"`
|
||||||
}
|
}
|
||||||
if !decodeBody(w, r, &body) {
|
if !decodeBody(w, r, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !a.publishWorkbenchEvent(w, r, p, "daily_report", "daily_report.reminded", strings.TrimSpace(body.Date),
|
if !a.publishWorkbenchEvent(w, r, p, "daily_report", "daily_report.reminded", strings.TrimSpace(body.Date),
|
||||||
body.UserIDs, "日报提醒", strings.TrimSpace(body.Date),
|
parseUserIDList(body.UserIDs), "日报提醒", strings.TrimSpace(body.Date),
|
||||||
map[string]any{"date": strings.TrimSpace(body.Date), "idempotency_key": idempotencyKey, "reminded_count": len(body.UserIDs)}) {
|
map[string]any{"date": strings.TrimSpace(body.Date), "idempotency_key": idempotencyKey, "reminded_count": len(body.UserIDs)}) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1000,8 +1000,8 @@ func limit(r *http.Request, fallback, max int) int {
|
|||||||
return parsed
|
return parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseInt64(value string) int64 {
|
func parseUserIDQuery(value string) int64 {
|
||||||
parsed, _ := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
|
parsed, _ := parseUserID(value)
|
||||||
return parsed
|
return parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-11
@@ -54,7 +54,7 @@ func organizationMemberPayloads(members []store.OrganizationMember) []map[string
|
|||||||
|
|
||||||
func organizationMemberPayload(member store.OrganizationMember) map[string]any {
|
func organizationMemberPayload(member store.OrganizationMember) map[string]any {
|
||||||
payload := map[string]any{
|
payload := map[string]any{
|
||||||
"user_id": member.UserID,
|
"user_id": formatUserID(member.UserID),
|
||||||
"department_id": member.DepartmentID,
|
"department_id": member.DepartmentID,
|
||||||
"display_name": member.DisplayName,
|
"display_name": member.DisplayName,
|
||||||
"position": member.Position,
|
"position": member.Position,
|
||||||
@@ -133,7 +133,7 @@ func attendanceRecordPayload(record *store.AttendanceRecord) map[string]any {
|
|||||||
}
|
}
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"record_id": record.RecordID,
|
"record_id": record.RecordID,
|
||||||
"user_id": record.UserID,
|
"user_id": formatUserID(record.UserID),
|
||||||
"date": record.Date.Format("2006-01-02"),
|
"date": record.Date.Format("2006-01-02"),
|
||||||
"rule_id": record.RuleID,
|
"rule_id": record.RuleID,
|
||||||
"shift_id": record.ShiftID,
|
"shift_id": record.ShiftID,
|
||||||
@@ -195,7 +195,7 @@ func attendanceAppealPayload(appeal *store.AttendanceAppeal) map[string]any {
|
|||||||
data := map[string]any{
|
data := map[string]any{
|
||||||
"appeal_id": appeal.AppealID,
|
"appeal_id": appeal.AppealID,
|
||||||
"record_id": appeal.RecordID,
|
"record_id": appeal.RecordID,
|
||||||
"user_id": appeal.UserID,
|
"user_id": formatUserID(appeal.UserID),
|
||||||
"appeal_type": appeal.AppealType,
|
"appeal_type": appeal.AppealType,
|
||||||
"reason": appeal.Reason,
|
"reason": appeal.Reason,
|
||||||
"target_action": appeal.TargetAction,
|
"target_action": appeal.TargetAction,
|
||||||
@@ -229,9 +229,9 @@ func meetingPayload(meeting store.Meeting, viewer int64) map[string]any {
|
|||||||
data := map[string]any{
|
data := map[string]any{
|
||||||
"meeting_id": meeting.MeetingID,
|
"meeting_id": meeting.MeetingID,
|
||||||
"title": meeting.Title,
|
"title": meeting.Title,
|
||||||
"organizer_id": meeting.OrganizerID,
|
"organizer_id": formatUserID(meeting.OrganizerID),
|
||||||
"organizer": userSummary(meeting.OrganizerID),
|
"organizer": userSummary(meeting.OrganizerID),
|
||||||
"recorder_user_id": recorderID,
|
"recorder_user_id": formatUserID(recorderID),
|
||||||
"recorder": userSummary(recorderID),
|
"recorder": userSummary(recorderID),
|
||||||
"start_at": meeting.StartAt.UTC().Format(time.RFC3339Nano),
|
"start_at": meeting.StartAt.UTC().Format(time.RFC3339Nano),
|
||||||
"end_at": meeting.EndAt.UTC().Format(time.RFC3339Nano),
|
"end_at": meeting.EndAt.UTC().Format(time.RFC3339Nano),
|
||||||
@@ -263,7 +263,7 @@ func meetingPayload(meeting store.Meeting, viewer int64) map[string]any {
|
|||||||
func meetingParticipantPayloads(participants []store.MeetingParticipant) []map[string]any {
|
func meetingParticipantPayloads(participants []store.MeetingParticipant) []map[string]any {
|
||||||
items := make([]map[string]any, 0, len(participants))
|
items := make([]map[string]any, 0, len(participants))
|
||||||
for _, participant := range participants {
|
for _, participant := range participants {
|
||||||
item := map[string]any{"user_id": participant.UserID, "response": participant.Response}
|
item := map[string]any{"user_id": formatUserID(participant.UserID), "response": participant.Response}
|
||||||
if participant.ResponseAt != nil {
|
if participant.ResponseAt != nil {
|
||||||
item["response_at"] = participant.ResponseAt.UTC().Format(time.RFC3339Nano)
|
item["response_at"] = participant.ResponseAt.UTC().Format(time.RFC3339Nano)
|
||||||
}
|
}
|
||||||
@@ -318,7 +318,7 @@ func meetingNoteFilePayload(file store.MeetingNoteFile, viewer int64) map[string
|
|||||||
"preview_status": file.PreviewStatus,
|
"preview_status": file.PreviewStatus,
|
||||||
"preview_mode": file.PreviewMode,
|
"preview_mode": file.PreviewMode,
|
||||||
"preview_file_id": file.PreviewFileID,
|
"preview_file_id": file.PreviewFileID,
|
||||||
"uploader_user_id": file.UploaderID,
|
"uploader_user_id": formatUserID(file.UploaderID),
|
||||||
"uploader": userSummary(file.UploaderID),
|
"uploader": userSummary(file.UploaderID),
|
||||||
"version": file.Version,
|
"version": file.Version,
|
||||||
"permissions": map[string]any{
|
"permissions": map[string]any{
|
||||||
@@ -343,7 +343,7 @@ func dailyReportPayloads(reports []store.DailyReport) []map[string]any {
|
|||||||
func dailyReportPayload(report store.DailyReport) map[string]any {
|
func dailyReportPayload(report store.DailyReport) map[string]any {
|
||||||
data := map[string]any{
|
data := map[string]any{
|
||||||
"report_id": report.ReportID,
|
"report_id": report.ReportID,
|
||||||
"author_id": report.AuthorID,
|
"author_id": formatUserID(report.AuthorID),
|
||||||
"report_date": report.ReportDate.Format("2006-01-02"),
|
"report_date": report.ReportDate.Format("2006-01-02"),
|
||||||
"content": report.Content,
|
"content": report.Content,
|
||||||
"content_short": preview(report.Content),
|
"content_short": preview(report.Content),
|
||||||
@@ -372,7 +372,7 @@ func monthlyReportPayloads(reports []store.MonthlyReport) []map[string]any {
|
|||||||
func monthlyReportPayload(report store.MonthlyReport) map[string]any {
|
func monthlyReportPayload(report store.MonthlyReport) map[string]any {
|
||||||
data := map[string]any{
|
data := map[string]any{
|
||||||
"report_id": report.ReportID,
|
"report_id": report.ReportID,
|
||||||
"author_id": report.AuthorID,
|
"author_id": formatUserID(report.AuthorID),
|
||||||
"month": report.Month,
|
"month": report.Month,
|
||||||
"work_summary": report.WorkSummary,
|
"work_summary": report.WorkSummary,
|
||||||
"next_plan": report.NextPlan,
|
"next_plan": report.NextPlan,
|
||||||
@@ -408,7 +408,7 @@ func mailboxConfigPayload(config store.MailboxConfig) map[string]any {
|
|||||||
func mailboxSubmissionPayload(submission store.MailboxSubmission) map[string]any {
|
func mailboxSubmissionPayload(submission store.MailboxSubmission) map[string]any {
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"submission_id": submission.SubmissionID,
|
"submission_id": submission.SubmissionID,
|
||||||
"sender_id": submission.SenderID,
|
"sender_id": formatUserID(submission.SenderID),
|
||||||
"subject": submission.Subject,
|
"subject": submission.Subject,
|
||||||
"anonymous": submission.Anonymous,
|
"anonymous": submission.Anonymous,
|
||||||
"attachment_file_ids": jsonList(submission.AttachmentFileIDs),
|
"attachment_file_ids": jsonList(submission.AttachmentFileIDs),
|
||||||
@@ -439,7 +439,7 @@ func meetingNotePermissions(meeting store.Meeting, viewer int64) map[string]any
|
|||||||
}
|
}
|
||||||
|
|
||||||
func userSummary(userID int64) map[string]any {
|
func userSummary(userID int64) map[string]any {
|
||||||
return map[string]any{"user_id": userID, "display_name": userID}
|
return map[string]any{"user_id": formatUserID(userID), "display_name": formatUserID(userID)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func jsonList(raw string) []any {
|
func jsonList(raw string) []any {
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package httpapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userID int64
|
||||||
|
|
||||||
|
func (id *userID) UnmarshalJSON(raw []byte) error {
|
||||||
|
value, err := parseUserIDJSON(raw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*id = userID(value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type optionalUserID struct {
|
||||||
|
value int64
|
||||||
|
set bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id *optionalUserID) UnmarshalJSON(raw []byte) error {
|
||||||
|
if strings.TrimSpace(string(raw)) == "null" {
|
||||||
|
id.set = true
|
||||||
|
id.value = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
value, err := parseUserIDJSON(raw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
id.set = true
|
||||||
|
id.value = value
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id optionalUserID) ptr() *int64 {
|
||||||
|
if !id.set {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &id.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUserIDJSON(raw []byte) (int64, error) {
|
||||||
|
var text string
|
||||||
|
if err := json.Unmarshal(raw, &text); err == nil {
|
||||||
|
return parseUserID(text)
|
||||||
|
}
|
||||||
|
var number int64
|
||||||
|
if err := json.Unmarshal(raw, &number); err == nil {
|
||||||
|
return number, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("invalid user id")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUserID(value string) (int64, error) {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return 0, fmt.Errorf("empty user id")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(value, "usr") {
|
||||||
|
decoded, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(strings.TrimPrefix(value, "usr"))
|
||||||
|
if err != nil || len(decoded) < 8 {
|
||||||
|
return 0, fmt.Errorf("invalid user id")
|
||||||
|
}
|
||||||
|
return int64(binary.LittleEndian.Uint64(decoded[:8])), nil
|
||||||
|
}
|
||||||
|
return strconv.ParseInt(value, 10, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatUserID(id int64) string {
|
||||||
|
if id == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
src := make([]byte, 8)
|
||||||
|
dst := make([]byte, base64.URLEncoding.WithPadding(base64.NoPadding).EncodedLen(8))
|
||||||
|
binary.LittleEndian.PutUint64(src, uint64(id))
|
||||||
|
base64.URLEncoding.WithPadding(base64.NoPadding).Encode(dst, src)
|
||||||
|
return "usr" + string(dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUserIDList(values []userID) []int64 {
|
||||||
|
items := make([]int64, 0, len(values))
|
||||||
|
seen := make(map[int64]bool, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
id := int64(value)
|
||||||
|
if id == 0 || seen[id] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[id] = true
|
||||||
|
items = append(items, id)
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package httpapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserIDJSONAcceptsIMUserID(t *testing.T) {
|
||||||
|
var body struct {
|
||||||
|
RecorderUserID userID `json:"recorder_user_id"`
|
||||||
|
AttendeeUserIDs []userID `json:"attendee_user_ids"`
|
||||||
|
}
|
||||||
|
raw := []byte(`{"recorder_user_id":"usrag7w9xJ6hto","attendee_user_ids":["usr1RqTFgHSenM"]}`)
|
||||||
|
if err := json.Unmarshal(raw, &body); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if body.RecorderUserID == 0 || len(body.AttendeeUserIDs) != 1 || body.AttendeeUserIDs[0] == 0 {
|
||||||
|
t.Fatalf("decoded zero ids: %+v", body)
|
||||||
|
}
|
||||||
|
if got := formatUserID(int64(body.RecorderUserID)); got != "usrag7w9xJ6hto" {
|
||||||
|
t.Fatalf("roundtrip recorder=%q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserIDJSONStillAcceptsNumericID(t *testing.T) {
|
||||||
|
var id userID
|
||||||
|
if err := json.Unmarshal([]byte(`1573862062872858624`), &id); err != nil {
|
||||||
|
t.Fatalf("unmarshal numeric: %v", err)
|
||||||
|
}
|
||||||
|
if id != 1573862062872858624 {
|
||||||
|
t.Fatalf("id=%d", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user