modify provisioning..
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "autonomy-studio-launcher",
|
||||
"version": "1.0.24",
|
||||
"version": "1.0.25",
|
||||
"description": "Autonomy Studio v2 Frontend",
|
||||
"main": "./out/main/index.js",
|
||||
"author": {
|
||||
|
||||
+58
-12
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import Login from './pages/Login'
|
||||
import Dashboard from './pages/Dashboard'
|
||||
import AppWindowShell from './pages/AppWindowShell'
|
||||
@@ -16,6 +17,7 @@ function App(): React.JSX.Element {
|
||||
return null
|
||||
}
|
||||
})
|
||||
const [sessionExpiredOpen, setSessionExpiredOpen] = useState(false)
|
||||
|
||||
const meta = window.appWindowMeta
|
||||
if (meta?.isAppWindow) {
|
||||
@@ -23,13 +25,20 @@ function App(): React.JSX.Element {
|
||||
}
|
||||
|
||||
const handleSessionUpdate = useCallback((session: AuthSession): void => {
|
||||
setAuthSession(session)
|
||||
if (window.localStorage.getItem(AUTH_SESSION_KEY)) {
|
||||
window.localStorage.setItem(AUTH_SESSION_KEY, JSON.stringify(session))
|
||||
} else if (window.sessionStorage.getItem(AUTH_SESSION_KEY)) {
|
||||
window.sessionStorage.setItem(AUTH_SESSION_KEY, JSON.stringify(session))
|
||||
const nextSession: AuthSession = {
|
||||
...session,
|
||||
user: {
|
||||
...session.user,
|
||||
loginMethod: session.user.loginMethod ?? authSession?.user.loginMethod,
|
||||
},
|
||||
}
|
||||
}, [])
|
||||
setAuthSession(nextSession)
|
||||
if (window.localStorage.getItem(AUTH_SESSION_KEY)) {
|
||||
window.localStorage.setItem(AUTH_SESSION_KEY, JSON.stringify(nextSession))
|
||||
} else if (window.sessionStorage.getItem(AUTH_SESSION_KEY)) {
|
||||
window.sessionStorage.setItem(AUTH_SESSION_KEY, JSON.stringify(nextSession))
|
||||
}
|
||||
}, [authSession?.user.loginMethod])
|
||||
|
||||
const requestLogout = useCallback((accessToken?: string): void => {
|
||||
void fetch(`${LAUNCHER_API_BASE_URL}/api/auth/logout`, {
|
||||
@@ -48,6 +57,17 @@ function App(): React.JSX.Element {
|
||||
setAuthSession(null)
|
||||
}, [authSession?.accessToken, requestLogout])
|
||||
|
||||
const handleSessionExpired = useCallback((): void => {
|
||||
setSessionExpiredOpen(true)
|
||||
}, [])
|
||||
|
||||
const confirmSessionExpired = useCallback((): void => {
|
||||
window.localStorage.removeItem(AUTH_SESSION_KEY)
|
||||
window.sessionStorage.removeItem(AUTH_SESSION_KEY)
|
||||
setSessionExpiredOpen(false)
|
||||
setAuthSession(null)
|
||||
}, [])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!authSession?.accessToken) return
|
||||
@@ -79,20 +99,46 @@ function App(): React.JSX.Element {
|
||||
if (session?.accessToken) {
|
||||
handleSessionUpdate(session)
|
||||
} else {
|
||||
handleLogout()
|
||||
handleSessionExpired()
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
handleLogout()
|
||||
handleSessionExpired()
|
||||
})
|
||||
}, refreshDelayMs)
|
||||
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [authSession, handleLogout, handleSessionUpdate])
|
||||
}, [authSession, handleSessionExpired, handleSessionUpdate])
|
||||
|
||||
return authSession
|
||||
? <Dashboard authSession={authSession} onSessionUpdate={handleSessionUpdate} onLogout={handleLogout} />
|
||||
: <Login onLogin={setAuthSession} />
|
||||
return (
|
||||
<>
|
||||
{authSession
|
||||
? <Dashboard authSession={authSession} onSessionUpdate={handleSessionUpdate} onLogout={handleLogout} onSessionExpired={handleSessionExpired} />
|
||||
: <Login onLogin={setAuthSession} />}
|
||||
{sessionExpiredOpen && createPortal(
|
||||
<div className="fixed inset-0 z-[20000] flex items-center justify-center bg-[rgba(2,6,18,0.5)] px-4 backdrop-blur-[6px]">
|
||||
<section className="w-full max-w-[400px] overflow-hidden rounded-lg border border-[rgba(255,255,255,0.18)] bg-[rgba(22,27,39,0.86)] shadow-[0_28px_90px_rgba(0,0,0,0.58),inset_0_1px_0_rgba(255,255,255,0.12)] backdrop-blur-2xl">
|
||||
<div className="border-b border-[rgba(255,255,255,0.1)] px-5 py-4">
|
||||
<h2 className="text-[16px] font-bold text-[var(--text-primary)]">로그인이 필요합니다</h2>
|
||||
</div>
|
||||
<div className="px-5 py-5 text-[13px] leading-6 text-[var(--text-muted)]">
|
||||
로그인 세션이 만료되었거나 유효하지 않습니다. 다시 로그인해주세요.
|
||||
</div>
|
||||
<div className="flex justify-end border-t border-[rgba(255,255,255,0.1)] bg-[rgba(255,255,255,0.025)] px-5 py-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={confirmSessionExpired}
|
||||
className="rounded-md bg-[var(--accent)] px-4 py-2 text-[13px] font-semibold text-white transition-colors hover:bg-[#6aa0ff]"
|
||||
>
|
||||
확인
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
@@ -72,6 +72,7 @@ interface SidebarProps {
|
||||
launcherUpdateMessage: string
|
||||
onLauncherUpdate: () => void
|
||||
authAccessToken: string
|
||||
authLoginMethod?: 'GOOGLE' | 'PASSWORD'
|
||||
tailscaleIssue?: {
|
||||
state?: 'checking' | 'warning'
|
||||
title: string
|
||||
@@ -105,7 +106,7 @@ function isTailscaleMissingStatus(status: Awaited<ReturnType<NonNullable<typeof
|
||||
)
|
||||
}
|
||||
|
||||
function Sidebar({ apps, userName, userEmail, userRole, userCreatedAt, userLastLoginAt, primaryAffiliation, currentLicense, activeApp, onAppChange, onLogout, collapsed, onToggleCollapse, launcherUpdateAvailable, launcherUpdating, launcherUpdateMessage, onLauncherUpdate, authAccessToken, tailscaleIssue, onTailscaleRetry }: SidebarProps): React.JSX.Element {
|
||||
function Sidebar({ apps, userName, userEmail, userRole, userCreatedAt, userLastLoginAt, primaryAffiliation, currentLicense, activeApp, onAppChange, onLogout, collapsed, onToggleCollapse, launcherUpdateAvailable, launcherUpdating, launcherUpdateMessage, onLauncherUpdate, authAccessToken, authLoginMethod, tailscaleIssue, onTailscaleRetry }: SidebarProps): React.JSX.Element {
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const [modal, setModal] = useState<UserModal>(null)
|
||||
const [avatarVersion, setAvatarVersion] = useState(() => Date.now())
|
||||
@@ -396,6 +397,7 @@ function Sidebar({ apps, userName, userEmail, userRole, userCreatedAt, userLastL
|
||||
primaryAffiliation={primaryAffiliation}
|
||||
currentLicense={currentLicense}
|
||||
accessToken={authAccessToken}
|
||||
loginMethod={authLoginMethod}
|
||||
avatarObjectUrl={avatarObjectUrl}
|
||||
avatarMissing={avatarMissing}
|
||||
onAvatarMissingChange={setAvatarMissing}
|
||||
@@ -408,7 +410,7 @@ function Sidebar({ apps, userName, userEmail, userRole, userCreatedAt, userLastL
|
||||
)}
|
||||
|
||||
{modal === 'platforms' && (
|
||||
<PlatformManagementModal onClose={() => setModal(null)} />
|
||||
<PlatformManagementModal accessToken={authAccessToken} onClose={() => setModal(null)} />
|
||||
)}
|
||||
|
||||
{modal === 'settings' && (
|
||||
@@ -424,13 +426,18 @@ function Sidebar({ apps, userName, userEmail, userRole, userCreatedAt, userLastL
|
||||
)
|
||||
}
|
||||
|
||||
function PlatformManagementModal({ onClose }: { onClose: () => void }): React.JSX.Element {
|
||||
function PlatformManagementModal({ accessToken, onClose }: { accessToken: string; onClose: () => void }): React.JSX.Element {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [installing, setInstalling] = useState(false)
|
||||
const [tailscaleMissing, setTailscaleMissing] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [platforms, setPlatforms] = useState<VpnPlatformPeer[]>([])
|
||||
const [selectedId, setSelectedId] = useState('')
|
||||
const [registrationCode, setRegistrationCode] = useState('')
|
||||
const [registrationExpiresAt, setRegistrationExpiresAt] = useState('')
|
||||
const [registrationNow, setRegistrationNow] = useState(() => Date.now())
|
||||
const [registrationLoading, setRegistrationLoading] = useState(false)
|
||||
const [authorizationError, setAuthorizationError] = useState('')
|
||||
|
||||
const loadPlatforms = useCallback(async (cancelledRef?: { current: boolean }): Promise<void> => {
|
||||
setLoading(true)
|
||||
@@ -483,6 +490,39 @@ function PlatformManagementModal({ onClose }: { onClose: () => void }): React.JS
|
||||
}
|
||||
}, [loadPlatforms])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
const loadRegistrationCode = async (): Promise<void> => {
|
||||
try {
|
||||
const response = await fetch(`${LAUNCHER_API_BASE_URL}/api/provision/device-authorizations/current`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
credentials: 'include',
|
||||
})
|
||||
if (!response.ok) return
|
||||
const payload = await response.json().catch(() => null) as { code?: string; expiresAt?: string } | null
|
||||
if (!cancelled && payload?.code) {
|
||||
setRegistrationCode(payload.code)
|
||||
setRegistrationExpiresAt(payload.expiresAt || '')
|
||||
}
|
||||
} catch {
|
||||
// A registration code is optional; leave the modal usable when lookup fails.
|
||||
}
|
||||
}
|
||||
|
||||
void loadRegistrationCode()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [accessToken])
|
||||
|
||||
useEffect(() => {
|
||||
if (!registrationExpiresAt) return
|
||||
setRegistrationNow(Date.now())
|
||||
const timer = window.setInterval(() => setRegistrationNow(Date.now()), 1000)
|
||||
return () => window.clearInterval(timer)
|
||||
}, [registrationExpiresAt])
|
||||
|
||||
const installTailscale = async (): Promise<void> => {
|
||||
if (!window.launcherTailscale?.install) return
|
||||
setInstalling(true)
|
||||
@@ -501,6 +541,55 @@ function PlatformManagementModal({ onClose }: { onClose: () => void }): React.JS
|
||||
|
||||
const selectedPlatform = platforms.find((platform) => platform.id === selectedId) ?? platforms[0]
|
||||
|
||||
const createAgentRegistrationCode = async (): Promise<void> => {
|
||||
if (registrationLoading) return
|
||||
|
||||
setRegistrationLoading(true)
|
||||
setAuthorizationError('')
|
||||
try {
|
||||
const response = await fetch(`${LAUNCHER_API_BASE_URL}/api/provision/device-authorizations`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
})
|
||||
const payload = await response.json().catch(() => null) as { code?: string; expiresAt?: string; message?: string } | null
|
||||
if (!response.ok) {
|
||||
throw new Error(payload?.message || '등록 키를 만들지 못했습니다.')
|
||||
}
|
||||
if (!payload?.code) throw new Error('등록 키 응답이 올바르지 않습니다.')
|
||||
setRegistrationCode(payload.code)
|
||||
setRegistrationExpiresAt(payload.expiresAt || '')
|
||||
} catch (createError) {
|
||||
setAuthorizationError(createError instanceof Error ? createError.message : '등록 키를 만들지 못했습니다.')
|
||||
} finally {
|
||||
setRegistrationLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const cancelAgentRegistrationCode = async (): Promise<void> => {
|
||||
if (!registrationCode || registrationLoading) return
|
||||
|
||||
setRegistrationLoading(true)
|
||||
setAuthorizationError('')
|
||||
try {
|
||||
const response = await fetch(`${LAUNCHER_API_BASE_URL}/api/provision/device-authorizations/current/cancel`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
credentials: 'include',
|
||||
})
|
||||
if (!response.ok) throw new Error('등록 키를 취소하지 못했습니다.')
|
||||
setRegistrationCode('')
|
||||
setRegistrationExpiresAt('')
|
||||
} catch (cancelError) {
|
||||
setAuthorizationError(cancelError instanceof Error ? cancelError.message : '등록 키를 취소하지 못했습니다.')
|
||||
} finally {
|
||||
setRegistrationLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalShell
|
||||
title="플랫폼 관리"
|
||||
@@ -569,6 +658,41 @@ function PlatformManagementModal({ onClose }: { onClose: () => void }): React.JS
|
||||
</aside>
|
||||
|
||||
<section className="min-h-0 overflow-y-auto px-6 py-5">
|
||||
<section className="mb-5 rounded-md border border-[rgba(129,199,255,0.18)] bg-[rgba(77,151,255,0.06)] p-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-[13px] font-bold text-white/86">플랫폼 등록</p>
|
||||
<p className="mt-1 text-[11px] font-medium leading-5 text-white/48">에이전트 설치 화면에서 현재 계정 이메일과 등록 키를 입력하세요.</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void createAgentRegistrationCode()}
|
||||
disabled={registrationLoading}
|
||||
className="h-8 shrink-0 cursor-pointer rounded-md bg-[#5588ff] px-3.5 text-[12px] font-bold text-white transition-colors hover:bg-[#4778ea] disabled:cursor-default disabled:opacity-50"
|
||||
>
|
||||
키 발급
|
||||
</button>
|
||||
</div>
|
||||
{registrationCode && (
|
||||
<div className="mt-3 flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<span className="rounded-md border border-[#78a7ff]/35 bg-[#5588ff]/10 px-3 py-2 font-mono text-[16px] font-bold tracking-[0.18em] text-[#c5d7ff]">
|
||||
{registrationCode}
|
||||
</span>
|
||||
{registrationExpiresAt && <p className="shrink-0 text-[11px] font-medium leading-5 text-white/42">{formatRemainingTime(registrationExpiresAt, registrationNow)}</p>}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void cancelAgentRegistrationCode()}
|
||||
disabled={registrationLoading}
|
||||
className="h-8 cursor-pointer rounded-md border border-white/14 px-3 text-[11px] font-bold text-white/65 transition-colors hover:border-white/24 hover:bg-white/[0.06] hover:text-white disabled:cursor-default disabled:opacity-50"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{authorizationError && <p className="mt-3 text-[11px] font-medium leading-5 text-[#fca5a5]">{authorizationError}</p>}
|
||||
</section>
|
||||
{tailscaleMissing ? (
|
||||
<div className="grid h-full place-items-center px-8 text-center">
|
||||
<div>
|
||||
@@ -585,7 +709,7 @@ function PlatformManagementModal({ onClose }: { onClose: () => void }): React.JS
|
||||
</div>
|
||||
</div>
|
||||
) : !selectedPlatform ? (
|
||||
<div className="grid h-full place-items-center text-[13px] font-semibold text-white/45">플랫폼을 선택하세요.</div>
|
||||
<div className="grid place-items-center py-8 text-[13px] font-semibold text-white/45">플랫폼을 선택하세요.</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-5 flex items-start justify-between gap-4">
|
||||
@@ -661,7 +785,7 @@ function ModalShell({ title, icon, children, footer, panelClassName = 'max-h-[82
|
||||
)
|
||||
}
|
||||
|
||||
function ProfileModal({ initial, userName, userEmail, createdAt, lastLoginAt, primaryAffiliation, currentLicense, accessToken, avatarObjectUrl, avatarMissing, onAvatarMissingChange, onAvatarUpdated, onClose }: {
|
||||
function ProfileModal({ initial, userName, userEmail, createdAt, lastLoginAt, primaryAffiliation, currentLicense, accessToken, loginMethod, avatarObjectUrl, avatarMissing, onAvatarMissingChange, onAvatarUpdated, onClose }: {
|
||||
initial: string
|
||||
userName: string
|
||||
userEmail: string
|
||||
@@ -671,6 +795,7 @@ function ProfileModal({ initial, userName, userEmail, createdAt, lastLoginAt, pr
|
||||
primaryAffiliation?: SidebarProps['primaryAffiliation']
|
||||
currentLicense?: SidebarProps['currentLicense']
|
||||
accessToken: string
|
||||
loginMethod?: 'GOOGLE' | 'PASSWORD'
|
||||
avatarObjectUrl: string | null
|
||||
avatarMissing: boolean
|
||||
onAvatarMissingChange: (missing: boolean) => void
|
||||
@@ -698,6 +823,7 @@ function ProfileModal({ initial, userName, userEmail, createdAt, lastLoginAt, pr
|
||||
const licensePeriod = currentLicense
|
||||
? `${formatDate(currentLicense.startsAt)} ~ ${currentLicense.expiresAt ? formatDate(currentLicense.expiresAt) : '무기한'}`
|
||||
: '-'
|
||||
const isGoogleLogin = loginMethod === 'GOOGLE'
|
||||
|
||||
const resetPasswordForm = (): void => {
|
||||
setCurrentPassword('')
|
||||
@@ -916,8 +1042,10 @@ function ProfileModal({ initial, userName, userEmail, createdAt, lastLoginAt, pr
|
||||
<SecurityAction
|
||||
icon={<KeyRound size={15} />}
|
||||
title="비밀번호 변경"
|
||||
description="현재 비밀번호 확인 후 새 비밀번호 설정."
|
||||
description={isGoogleLogin ? 'Google 로그인 계정은 비밀번호를 변경할 수 없습니다.' : '현재 비밀번호 확인 후 새 비밀번호 설정.'}
|
||||
disabled={isGoogleLogin}
|
||||
onClick={() => {
|
||||
if (isGoogleLogin) return
|
||||
resetPasswordForm()
|
||||
setPasswordMode(true)
|
||||
}}
|
||||
@@ -1116,9 +1244,9 @@ function ProfileRow({ label, value }: { label: string; value: string }): React.J
|
||||
)
|
||||
}
|
||||
|
||||
function SecurityAction({ icon, title, description, status, onClick }: { icon: React.ReactNode; title: string; description: string; status?: string; onClick?: () => void }): React.JSX.Element {
|
||||
function SecurityAction({ icon, title, description, status, disabled = false, onClick }: { icon: React.ReactNode; title: string; description: string; status?: string; disabled?: boolean; onClick?: () => void }): React.JSX.Element {
|
||||
return (
|
||||
<button type="button" onClick={onClick} className="flex w-full items-center gap-3 border-t border-[rgba(255,255,255,0.07)] py-3 text-left cursor-pointer hover:bg-white/[0.03]">
|
||||
<button type="button" onClick={onClick} disabled={disabled} className={`flex w-full items-center gap-3 border-t border-[rgba(255,255,255,0.07)] py-3 text-left ${disabled ? 'cursor-not-allowed opacity-45' : 'cursor-pointer hover:bg-white/[0.03]'}`}>
|
||||
<span className="text-white/55">{icon}</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block text-[13px] font-semibold text-[var(--text-primary)]">{title}</span>
|
||||
@@ -1197,6 +1325,13 @@ function formatOptionalDateTime(value?: string): string {
|
||||
return formatDateTime(value)
|
||||
}
|
||||
|
||||
function formatRemainingTime(expiresAt: string, now: number): string {
|
||||
const remainingSeconds = Math.max(0, Math.floor((new Date(expiresAt).getTime() - now) / 1000))
|
||||
const minutes = Math.floor(remainingSeconds / 60)
|
||||
const seconds = remainingSeconds % 60
|
||||
return `${minutes}분 ${String(seconds).padStart(2, '0')}초`
|
||||
}
|
||||
|
||||
async function resizeImageToWebp(file: File, size: number): Promise<Blob> {
|
||||
const dataUrl = await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
|
||||
@@ -12,6 +12,7 @@ interface DashboardProps {
|
||||
authSession: AuthSession
|
||||
onSessionUpdate: (session: AuthSession) => void
|
||||
onLogout: () => void
|
||||
onSessionExpired: () => void
|
||||
}
|
||||
|
||||
const LAUNCHER_API_BASE_URL = (import.meta.env.VITE_ATNMY_STDO_API_BASE_URL || 'http://localhost:4200').replace(/\/+$/, '')
|
||||
@@ -124,7 +125,7 @@ function extractProvisionErrorMessage(payload: unknown): string {
|
||||
}
|
||||
}
|
||||
|
||||
function Dashboard({ authSession, onSessionUpdate, onLogout }: DashboardProps): React.JSX.Element {
|
||||
function Dashboard({ authSession, onSessionUpdate, onLogout, onSessionExpired }: DashboardProps): React.JSX.Element {
|
||||
const [apps, setApps] = useState<LauncherApp[]>([])
|
||||
const [activeApp, setActiveApp] = useState('')
|
||||
const [appNotices, setAppNotices] = useState<AppNotice[]>([])
|
||||
@@ -211,13 +212,13 @@ function Dashboard({ authSession, onSessionUpdate, onLogout }: DashboardProps):
|
||||
|
||||
const refreshed = await refreshSession()
|
||||
if (!refreshed) {
|
||||
onLogout()
|
||||
onSessionExpired()
|
||||
return response
|
||||
}
|
||||
|
||||
response = await request(refreshed.accessToken)
|
||||
return response
|
||||
}, [authSession.accessToken, onLogout, refreshSession])
|
||||
}, [authSession.accessToken, onSessionExpired, refreshSession])
|
||||
|
||||
useEffect(() => {
|
||||
const launcherTailscale = window.launcherTailscale
|
||||
@@ -373,7 +374,10 @@ function Dashboard({ authSession, onSessionUpdate, onLogout }: DashboardProps):
|
||||
if (!cancelled && payload?.user) {
|
||||
const nextSession = {
|
||||
...session,
|
||||
user: payload.user,
|
||||
user: {
|
||||
...payload.user,
|
||||
loginMethod: payload.user.loginMethod ?? authSession.user.loginMethod,
|
||||
},
|
||||
}
|
||||
if (JSON.stringify(nextSession.user) === JSON.stringify(authSession.user) && nextSession.accessToken === authSession.accessToken) {
|
||||
return
|
||||
@@ -727,8 +731,7 @@ function Dashboard({ authSession, onSessionUpdate, onLogout }: DashboardProps):
|
||||
onSessionUpdate(nextSession)
|
||||
response = await requestHandoff(nextSession.accessToken)
|
||||
} else {
|
||||
alert('로그인 세션이 만료되었습니다. 다시 로그인해주세요.')
|
||||
onLogout()
|
||||
onSessionExpired()
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -742,7 +745,7 @@ function Dashboard({ authSession, onSessionUpdate, onLogout }: DashboardProps):
|
||||
|
||||
const handoff = await response.json() as { handoffToken: string }
|
||||
return handoff.handoffToken
|
||||
}, [authSession.accessToken, onLogout, onSessionUpdate])
|
||||
}, [authSession.accessToken, onSessionExpired, onSessionUpdate])
|
||||
|
||||
const handleLaunch = useCallback(async () => {
|
||||
if (!appConfig || !canLaunch || launchingApp) return
|
||||
@@ -840,6 +843,7 @@ function Dashboard({ authSession, onSessionUpdate, onLogout }: DashboardProps):
|
||||
launcherUpdateMessage={launcherUpdateMessage}
|
||||
onLauncherUpdate={() => handleUpdate('launcher', launcherStatus?.latestVersion, launcherStatus?.downloadUrl)}
|
||||
authAccessToken={authSession.accessToken}
|
||||
authLoginMethod={authSession.user.loginMethod}
|
||||
tailscaleIssue={tailscaleIssue}
|
||||
onTailscaleRetry={() => setTailscaleRetryNonce((value) => value + 1)}
|
||||
/>
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface AuthSession {
|
||||
email: string
|
||||
name: string | null
|
||||
role: string
|
||||
loginMethod?: 'GOOGLE' | 'PASSWORD'
|
||||
roles?: string[]
|
||||
createdAt?: string
|
||||
lastLoginAt?: string | null
|
||||
@@ -87,7 +88,8 @@ function LoginForm({ onLogin, onGoSignup, onGoPasswordReset, onLoadingChange, in
|
||||
if (initialEmail) setEmail(initialEmail)
|
||||
}, [initialEmail])
|
||||
|
||||
const completeLogin = async (session: AuthSession, loginEmail: string, persistLogin: boolean, startedAt: number): Promise<void> => {
|
||||
const completeLogin = async (session: AuthSession, loginEmail: string, persistLogin: boolean, startedAt: number, loginMethod: 'GOOGLE' | 'PASSWORD'): Promise<void> => {
|
||||
session.user.loginMethod = loginMethod
|
||||
storeLastLoginEmail(loginEmail.trim())
|
||||
if (persistLogin) {
|
||||
window.localStorage.setItem(AUTH_SESSION_KEY, JSON.stringify(session))
|
||||
@@ -129,7 +131,7 @@ function LoginForm({ onLogin, onGoSignup, onGoPasswordReset, onLoadingChange, in
|
||||
if (!response.ok) throw new Error('Google 로그인 세션을 확인할 수 없습니다.')
|
||||
|
||||
const session = await response.json() as AuthSession
|
||||
await completeLogin(session, session.user.email, keepLogin, startedAt)
|
||||
await completeLogin(session, session.user.email, keepLogin, startedAt, 'GOOGLE')
|
||||
} catch (oauthError) {
|
||||
setError(oauthError instanceof Error ? oauthError.message : 'Google 로그인에 실패했습니다.')
|
||||
} finally {
|
||||
@@ -183,7 +185,7 @@ function LoginForm({ onLogin, onGoSignup, onGoPasswordReset, onLoadingChange, in
|
||||
throw new Error('이메일 또는 비밀번호를 확인하세요.')
|
||||
}
|
||||
const session = await response.json() as AuthSession
|
||||
await completeLogin(session, email, keepLogin, startedAt)
|
||||
await completeLogin(session, email, keepLogin, startedAt, 'PASSWORD')
|
||||
} catch (submitError) {
|
||||
setError(submitError instanceof Error ? submitError.message : '로그인에 실패했습니다.')
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user