Merge pull request 'fix(auth): 비자동승인 도메인 Google OAuth PENDING 안내 UX 개선' (#15) from feature/auth-system into develop
This commit is contained in:
커밋
62546144fa
@ -45,9 +45,14 @@ router.post('/oauth/google', async (req, res) => {
|
||||
const ipAddr = (req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() || req.ip || ''
|
||||
const userAgent = req.headers['user-agent'] || ''
|
||||
|
||||
const userInfo = await googleLogin(credential, ipAddr, userAgent, res)
|
||||
const result = await googleLogin(credential, ipAddr, userAgent, res)
|
||||
|
||||
res.json({ success: true, user: userInfo })
|
||||
if (result.type === 'pending') {
|
||||
res.json({ success: true, pending: true, message: result.message })
|
||||
return
|
||||
}
|
||||
|
||||
res.json({ success: true, user: result.user })
|
||||
} catch (err) {
|
||||
if (err instanceof AuthError) {
|
||||
res.status(err.status).json({ error: err.message })
|
||||
|
||||
@ -16,12 +16,18 @@ interface GoogleProfile {
|
||||
hd?: string // hosted domain (Google Workspace)
|
||||
}
|
||||
|
||||
export interface GoogleLoginResult {
|
||||
type: 'success' | 'pending'
|
||||
user?: Awaited<ReturnType<typeof getUserInfo>>
|
||||
message?: string
|
||||
}
|
||||
|
||||
export async function googleLogin(
|
||||
credential: string,
|
||||
ipAddr: string,
|
||||
userAgent: string,
|
||||
res: Response
|
||||
) {
|
||||
): Promise<GoogleLoginResult> {
|
||||
const profile = await verifyGoogleToken(credential)
|
||||
|
||||
// 1. OAUTH_SUB로 기존 사용자 조회
|
||||
@ -38,7 +44,7 @@ export async function googleLogin(
|
||||
userId = user.user_id
|
||||
|
||||
if (user.user_stts_cd === 'PENDING') {
|
||||
throw new AuthError('계정이 승인 대기 중입니다. 관리자 승인 후 로그인할 수 있습니다.', 403)
|
||||
return { type: 'pending', message: '계정이 승인 대기 중입니다. 관리자 승인 후 로그인할 수 있습니다.' }
|
||||
}
|
||||
if (user.user_stts_cd === 'LOCKED') {
|
||||
throw new AuthError('계정이 잠겨있습니다. 관리자에게 문의하세요.', 403)
|
||||
@ -66,16 +72,28 @@ export async function googleLogin(
|
||||
['GOOGLE', profile.sub, profile.email, userId]
|
||||
)
|
||||
|
||||
if (user.user_stts_cd === 'PENDING') {
|
||||
return { type: 'pending', message: '계정이 승인 대기 중입니다. 관리자 승인 후 로그인할 수 있습니다.' }
|
||||
}
|
||||
if (user.user_stts_cd !== 'ACTIVE') {
|
||||
throw new AuthError('계정이 활성 상태가 아닙니다. 관리자에게 문의하세요.', 403)
|
||||
}
|
||||
} else {
|
||||
// 신규 사용자 생성
|
||||
userId = await createOAuthUser(profile)
|
||||
|
||||
// PENDING 상태로 생성된 경우 등록 완료 안내
|
||||
const statusResult = await authPool.query(
|
||||
'SELECT USER_STTS_CD as user_stts_cd FROM AUTH_USER WHERE USER_ID = $1',
|
||||
[userId]
|
||||
)
|
||||
if (statusResult.rows[0]?.user_stts_cd === 'PENDING') {
|
||||
return { type: 'pending', message: '계정이 등록되었습니다. 관리자 승인 후 로그인할 수 있습니다.' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 로그인 처리
|
||||
// ACTIVE 사용자 로그인 처리
|
||||
await authPool.query(
|
||||
'UPDATE AUTH_USER SET LAST_LOGIN_DTM = NOW(), MDFCN_DTM = NOW() WHERE USER_ID = $1',
|
||||
[userId]
|
||||
@ -85,17 +103,6 @@ export async function googleLogin(
|
||||
|
||||
const userInfo = await getUserInfo(userId)
|
||||
|
||||
// PENDING 사용자는 JWT 발급하지 않음
|
||||
if (userInfo.roles.length === 0) {
|
||||
const userStatus = await authPool.query(
|
||||
'SELECT USER_STTS_CD as user_stts_cd FROM AUTH_USER WHERE USER_ID = $1',
|
||||
[userId]
|
||||
)
|
||||
if (userStatus.rows[0]?.user_stts_cd === 'PENDING') {
|
||||
throw new AuthError('계정이 승인 대기 중입니다. 관리자 승인 후 로그인할 수 있습니다.', 403)
|
||||
}
|
||||
}
|
||||
|
||||
const token = signToken({
|
||||
sub: userInfo.id,
|
||||
acnt: userInfo.account,
|
||||
@ -104,7 +111,7 @@ export async function googleLogin(
|
||||
})
|
||||
|
||||
setTokenCookie(res, token)
|
||||
return userInfo
|
||||
return { type: 'success', user: userInfo }
|
||||
}
|
||||
|
||||
async function verifyGoogleToken(credential: string): Promise<GoogleProfile> {
|
||||
|
||||
@ -11,7 +11,7 @@ export function LoginPage() {
|
||||
const [userId, setUserId] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [remember, setRemember] = useState(false)
|
||||
const { login, googleLogin, isLoading, error, clearError } = useAuthStore()
|
||||
const { login, googleLogin, isLoading, error, pendingMessage, clearError } = useAuthStore()
|
||||
const GOOGLE_ENABLED = !!import.meta.env.VITE_GOOGLE_CLIENT_ID
|
||||
|
||||
useEffect(() => {
|
||||
@ -213,6 +213,21 @@ export function LoginPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Pending approval */}
|
||||
{pendingMessage && (
|
||||
<div style={{
|
||||
padding: '10px 12px', marginBottom: 16, borderRadius: 6,
|
||||
background: 'rgba(6,182,212,0.08)', border: '1px solid rgba(6,182,212,0.2)',
|
||||
fontSize: 11, color: '#67e8f9', fontFamily: 'var(--fK)',
|
||||
display: 'flex', alignItems: 'flex-start', gap: 8,
|
||||
}}>
|
||||
<span style={{ fontSize: 14, flexShrink: 0, marginTop: 1 }}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
</span>
|
||||
<span>{pendingMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div style={{
|
||||
|
||||
@ -13,6 +13,8 @@ export interface AuthUser {
|
||||
interface LoginResponse {
|
||||
success: boolean
|
||||
user: AuthUser
|
||||
pending?: boolean
|
||||
message?: string
|
||||
}
|
||||
|
||||
export async function loginApi(account: string, password: string): Promise<AuthUser> {
|
||||
@ -20,8 +22,18 @@ export async function loginApi(account: string, password: string): Promise<AuthU
|
||||
return response.data.user
|
||||
}
|
||||
|
||||
export class PendingApprovalError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = 'PendingApprovalError'
|
||||
}
|
||||
}
|
||||
|
||||
export async function googleLoginApi(credential: string): Promise<AuthUser> {
|
||||
const response = await api.post<LoginResponse>('/auth/oauth/google', { credential })
|
||||
if (response.data.pending) {
|
||||
throw new PendingApprovalError(response.data.message || '관리자 승인 후 로그인할 수 있습니다.')
|
||||
}
|
||||
return response.data.user
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { create } from 'zustand'
|
||||
import { loginApi, googleLoginApi, logoutApi, fetchMe } from '../services/authApi'
|
||||
import { loginApi, googleLoginApi, logoutApi, fetchMe, PendingApprovalError } from '../services/authApi'
|
||||
import type { AuthUser } from '../services/authApi'
|
||||
|
||||
interface AuthState {
|
||||
@ -7,6 +7,7 @@ interface AuthState {
|
||||
isAuthenticated: boolean
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
pendingMessage: string | null
|
||||
login: (account: string, password: string) => Promise<void>
|
||||
googleLogin: (credential: string) => Promise<void>
|
||||
logout: () => Promise<void>
|
||||
@ -20,6 +21,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
isAuthenticated: false,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
pendingMessage: null,
|
||||
|
||||
login: async (account: string, password: string) => {
|
||||
set({ isLoading: true, error: null })
|
||||
@ -34,11 +36,15 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
},
|
||||
|
||||
googleLogin: async (credential: string) => {
|
||||
set({ isLoading: true, error: null })
|
||||
set({ isLoading: true, error: null, pendingMessage: null })
|
||||
try {
|
||||
const user = await googleLoginApi(credential)
|
||||
set({ user, isAuthenticated: true, isLoading: false })
|
||||
} catch (err) {
|
||||
if (err instanceof PendingApprovalError) {
|
||||
set({ isLoading: false, pendingMessage: err.message })
|
||||
return
|
||||
}
|
||||
const message = (err as { message?: string })?.message || 'Google 로그인에 실패했습니다.'
|
||||
set({ isLoading: false, error: message })
|
||||
throw err
|
||||
@ -70,5 +76,5 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
return user.permissions.includes(resource)
|
||||
},
|
||||
|
||||
clearError: () => set({ error: null }),
|
||||
clearError: () => set({ error: null, pendingMessage: null }),
|
||||
}))
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user