Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/pages/repair/EventDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { components } from "../../types/saturday"
import { saturdayClient } from "../../utils/client"
import { Textarea, Input, Chip, Skeleton } from "@heroui/react"
import type { PublicMember } from "../../store/member"
import dayjs from "dayjs"
import { EventStatus, UserEventAction } from "../../types/event"
import { formatDateTime } from "../../utils/date"

type PublicEvent = components["schemas"]["PublicEvent"]
type EventLog = components["schemas"]["EventLog"]
Expand Down Expand Up @@ -36,7 +36,7 @@ function EventLogItem(props: {
</div>
</div>
<div className="flex flex-col gap-2 items-center mt-1 text-gray-600">
{dayjs(props.eventLog.gmtCreate).format("YYYY-MM-DD HH:mm")}
{formatDateTime(props.eventLog.gmtCreate)}
</div>
</div>

Expand Down
27 changes: 9 additions & 18 deletions src/pages/repair/RepairAdmin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ import { useAsyncList } from "@react-stately/data"
import type { components } from "../../types/saturday"
import { saturdayClient } from "../../utils/client"
import EventDetail, { EventStatusChip, type EventDetailRef } from "./EventDetail"
import dayjs from "dayjs"
import { EventStatus, UserEventStatus, type RepairEvent } from "../../types/event"
import { makeLogtoClient } from "../../utils/auth"
import type { PublicMember } from "../../store/member"
import type { UserInfoResponse } from "@logto/browser"
import { getAvailableEventActions, type EventAction, type IdentityContext } from "./EventAction"
import { ExportExcelModal } from "./ExportEventDialog"
import { requireRepairRole } from "../../utils/repair"
import { formatDateTime } from "../../utils/date"

type PublicEvent = components["schemas"]["PublicEvent"]

Expand Down Expand Up @@ -181,11 +182,6 @@ function TicketDetailDrawer(props: {
)
}

export const validateRepairRole = (roles: string[]) => {
const acceptableRoles = ["repair admin", "repair member"]
return roles.some(role => acceptableRoles.includes(role.toLowerCase()))
}

export default function App() {
const [isLoading, setIsLoading] = useState(true)
const [page, setPage] = useState(1)
Expand All @@ -201,20 +197,15 @@ export default function App() {
useEffect(() => {
const check = async () => {
const adminPath = "/repair/admin"
const authenticated = await makeLogtoClient().isAuthenticated()
if (!authenticated) {
window.location.href = `/repair/login-hint?redirectUrl=${adminPath}`
const userInfo = await requireRepairRole(adminPath)

if (!userInfo) {
return
}
const res = await makeLogtoClient().getIdTokenClaims()

setUserInfo(userInfo)
const token = await makeLogtoClient().getAccessToken()
setToken(token)
const hasRole = validateRepairRole(res.roles)
if (!hasRole) {
window.location.href = `/repair/login-hint?redirectUrl=${adminPath}`
return
}
setUserInfo(res)

const { data } = await saturdayClient.GET("/member", {
params: {
Expand Down Expand Up @@ -353,7 +344,7 @@ export default function App() {
<div className="h-18">
<div className="flex items-center gap-2 text-sm text-gray-600">
<div className="">
{ dayjs(event.gmtCreate).format("YYYY-MM-DD HH:mm") }
{ formatDateTime(event.gmtCreate) }
</div>

{event.model && (
Expand Down Expand Up @@ -414,7 +405,7 @@ export default function App() {
case "gmtCreate":
return (
<span>
{dayjs(cellValue).format("YYYY-MM-DD HH:mm")}
{formatDateTime(cellValue)}
</span>
)
case "status":
Expand Down
8 changes: 4 additions & 4 deletions src/pages/repair/RepairHistoryCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Card, CardBody, Chip } from "@heroui/react"
import { EventStatusChip } from "./EventDetail"
import dayjs from "dayjs"
import type { components } from "../../types/saturday"
import { formatDate, formatDateTime, formatShortDateTime } from "../../utils/date"

type PublicEvent = components["schemas"]["PublicEvent"]

Expand Down Expand Up @@ -29,7 +29,7 @@ export default function RepairHistoryCard({ event, onViewDetail }: RepairHistory
{event.size && <Chip size="sm">{event.size}</Chip>}
</div>
<span className="text-xs text-gray-400">
{dayjs(event.gmtCreate).format("YYYY-MM-DD")}
{formatDate(event.gmtCreate)}
</span>
</div>

Expand All @@ -41,10 +41,10 @@ export default function RepairHistoryCard({ event, onViewDetail }: RepairHistory
</div>

<div className="text-xs text-gray-400">
创建于 {dayjs(event.gmtCreate).format("YYYY-MM-DD HH:mm")}
创建于 {formatDateTime(event.gmtCreate)}
{event.gmtModified !== event.gmtCreate && (
<span className="ml-2">
更新于 {dayjs(event.gmtModified).format("MM-DD HH:mm")}
更新于 {formatShortDateTime(event.gmtModified)}
</span>
)}
</div>
Expand Down
17 changes: 3 additions & 14 deletions src/pages/repair/RepairHistoryPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Button, Spinner } from "@heroui/react"
import { makeLogtoClient } from "../../utils/auth"
import UserRepairHistory from "./UserRepairHistory"
import type { UserInfoResponse } from "@logto/browser"
import { checkAuthAndRedirect } from "../../utils/repair"

export default function RepairHistoryPage() {
const [userInfo, setUserInfo] = useState<UserInfoResponse | null>(null)
Expand All @@ -14,22 +15,10 @@ export default function RepairHistoryPage() {

const checkAuthStatus = async () => {
try {
const logtoClient = makeLogtoClient()
const authenticated = await logtoClient.isAuthenticated()

if (authenticated) {
const claims = await logtoClient.getIdTokenClaims()
const claims = await checkAuthAndRedirect("/repair/history")
if (claims) {
setUserInfo(claims)
}
else {
// Redirect to login if not authenticated
window.location.href = "/repair/login-hint?redirectUrl=/repair/history"
}
}
catch (error) {
console.error("Error checking auth status:", error)
// Redirect to login on error
window.location.href = "/repair/login-hint?redirectUrl=/repair/history"
}
finally {
setLoading(false)
Expand Down
20 changes: 4 additions & 16 deletions src/pages/repair/TicketDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import EventDetail from "./EventDetail"
import EditRepairModal from "./EditRepairModal"
import type { UserInfoResponse } from "@logto/browser"
import type { components } from "../../types/saturday"
import { checkAuthAndRedirect } from "../../utils/repair"

type PublicEvent = components["schemas"]["PublicEvent"]

Expand All @@ -32,24 +33,11 @@ export default function TicketDetail() {

const checkAuthStatus = async () => {
try {
const logtoClient = makeLogtoClient()
const authenticated = await logtoClient.isAuthenticated()

if (authenticated) {
const claims = await logtoClient.getIdTokenClaims()
const redirectUrl = `/repair/ticket-detail${window.location.search}`
const claims = await checkAuthAndRedirect(redirectUrl)
if (claims) {
setUserInfo(claims)
}
else {
// Redirect to login if not authenticated
const redirectUrl = `/repair/ticket-detail${window.location.search}`
window.location.href = `/repair/login-hint?redirectUrl=${encodeURIComponent(redirectUrl)}`
}
}
catch (error) {
console.error("Error checking auth status:", error)
// Redirect to login on error
const redirectUrl = `/repair/ticket-detail${window.location.search}`
window.location.href = `/repair/login-hint?redirectUrl=${encodeURIComponent(redirectUrl)}`
}
finally {
setLoading(false)
Expand Down
16 changes: 4 additions & 12 deletions src/pages/repair/TicketForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { makeLogtoClient } from "../../utils/auth"
import type { UserInfoResponse } from "@logto/browser"
import { Alert, Form, Input, Button, Textarea } from "@heroui/react"
import { saturdayClient } from "../../utils/client"
import { safe } from "../../utils/safe"
import { checkAuthAndRedirect } from "../../utils/repair"

type TicketFormData = {
model?: string
Expand Down Expand Up @@ -275,17 +275,9 @@ export default function App() {
useEffect(() => {
const check = async () => {
const createRepairPath = "/repair/create-ticket"
try {
const [res, err] = await safe(makeLogtoClient().getIdTokenClaims())
if (err) {
window.location.href = `/repair/login-hint?redirectUrl=${createRepairPath}`
return
}
setUserInfo(res)
}
catch (error) {
console.error("Error checking authentication:", error)
window.location.href = `/repair/login-hint?redirectUrl=${createRepairPath}`
const claims = await checkAuthAndRedirect(createRepairPath)
if (claims) {
setUserInfo(claims)
}
}
check()
Expand Down
34 changes: 34 additions & 0 deletions src/utils/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import dayjs from "dayjs"

/**
* Common date format patterns used across the application
*/
export const DateFormats = {
/** Format: YYYY-MM-DD HH:mm (e.g., 2024-01-15 13:45) */
DATETIME: "YYYY-MM-DD HH:mm",
/** Format: YYYY-MM-DD (e.g., 2024-01-15) */
DATE: "YYYY-MM-DD",
/** Format: MM-DD HH:mm (e.g., 01-15 13:45) */
SHORT_DATETIME: "MM-DD HH:mm",
} as const

/**
* Formats a date string or Date object to datetime format (YYYY-MM-DD HH:mm)
*/
export const formatDateTime = (date: string | Date): string => {
return dayjs(date).format(DateFormats.DATETIME)
}

/**
* Formats a date string or Date object to date format (YYYY-MM-DD)
*/
export const formatDate = (date: string | Date): string => {
return dayjs(date).format(DateFormats.DATE)
}

/**
* Formats a date string or Date object to short datetime format (MM-DD HH:mm)
*/
export const formatShortDateTime = (date: string | Date): string => {
return dayjs(date).format(DateFormats.SHORT_DATETIME)
}
62 changes: 62 additions & 0 deletions src/utils/repair.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { makeLogtoClient } from "./auth"
import type { UserInfoResponse } from "@logto/browser"

/**
* Validates if the user has repair admin or member role
*/
export const validateRepairRole = (roles: string[]): boolean => {
const acceptableRoles = ["repair admin", "repair member"]
return roles.some(role => acceptableRoles.includes(role.toLowerCase()))
}

/**
* Checks if user is authenticated and redirects to login-hint if not
* @param redirectUrl - The URL to redirect back to after login
* @returns UserInfoResponse if authenticated, undefined if redirected
* @throws Never throws - all errors are handled internally with redirect
*/
export const checkAuthAndRedirect = async (
redirectUrl: string,
): Promise<UserInfoResponse | undefined> => {
try {
const logtoClient = makeLogtoClient()
const authenticated = await logtoClient.isAuthenticated()

if (!authenticated) {
window.location.href = `/repair/login-hint?redirectUrl=${encodeURIComponent(redirectUrl)}`
return undefined
}

const claims = await logtoClient.getIdTokenClaims()
return claims
}
catch (error) {
console.error("Error checking auth status:", error)
window.location.href = `/repair/login-hint?redirectUrl=${encodeURIComponent(redirectUrl)}`
return undefined
}
}

/**
* Checks if user is authenticated and has required repair role, redirects if not
* @param redirectUrl - The URL to redirect back to after login
* @returns UserInfoResponse if authenticated and has role, undefined if redirected
* @throws Never throws - all errors are handled internally with redirect
*/
export const requireRepairRole = async (
redirectUrl: string,
): Promise<UserInfoResponse | undefined> => {
const userInfo = await checkAuthAndRedirect(redirectUrl)

if (!userInfo) {
return undefined
}

const hasRole = validateRepairRole(userInfo.roles)
if (!hasRole) {
window.location.href = `/repair/login-hint?redirectUrl=${encodeURIComponent(redirectUrl)}`
return undefined
}

return userInfo
}