Sprint 006 non-blocking:
- N2: AuthContext 401 시 refresh token 자동 재발급
- N3: login 후 /me API로 실제 userId 조회
- N4: register refreshToken localStorage 저장
TASK-024: Gitea 동기화
- GiteaSyncService: syncRepos() (Project upsert + ActivityLog)
- POST /api/admin/gitea/sync + GET /api/admin/gitea/status
- GiteaSyncModule (CompositeGuard + @Roles('admin'))
TASK-025: Commit/Branch/PR API
- GiteaService 강화: getCommits, getBranches, getPulls
- GiteaCommit/GiteaBranch 인터페이스 추가
- GET /api/projects/:id/{commits,branches,pulls}
TASK-026: 프로젝트 FE
- /projects: GITEA SYNC 버튼 + sync 결과 표시
- /projects/[id]: Overview/Commits/Branches/PRs 탭
TASK-027: 활동 로그 자동 기록
- SistersService: 상태 변경 시 ActivityLog 자동 기록 (@Optional ActivityService)
- GiteaSyncService: sync 시 new_repo/sync_complete 로그
- /activities: 실제 API 연결 확인 (하드코딩 없음)
테스트 26/26 pass, FE 16 routes build 성공
123 lines
3.7 KiB
TypeScript
123 lines
3.7 KiB
TypeScript
'use client';
|
|
|
|
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { API_URL } from './config';
|
|
|
|
interface AuthUser {
|
|
userId: number;
|
|
username: string;
|
|
role: string;
|
|
}
|
|
|
|
interface AuthContextValue {
|
|
user: AuthUser | null;
|
|
loading: boolean;
|
|
login: (username: string, password: string) => Promise<void>;
|
|
logout: () => void;
|
|
isAuthenticated: boolean;
|
|
}
|
|
|
|
const AuthContext = createContext<AuthContextValue>({
|
|
user: null,
|
|
loading: true,
|
|
login: async () => {},
|
|
logout: () => {},
|
|
isAuthenticated: false,
|
|
});
|
|
|
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
const [user, setUser] = useState<AuthUser | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const loadUser = useCallback(async () => {
|
|
const token = localStorage.getItem('hanarang_access_token');
|
|
if (!token) { setLoading(false); return; }
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/api/auth/me`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setUser({ userId: data.id, username: data.username, role: data.role });
|
|
} else if (res.status === 401) {
|
|
// access token 만료 → refresh 시도
|
|
const refreshToken = localStorage.getItem('hanarang_refresh_token');
|
|
if (refreshToken) {
|
|
try {
|
|
const rRes = await fetch(`${API_URL}/api/auth/refresh`, {
|
|
method: 'POST',
|
|
headers: { 'x-refresh-token': refreshToken },
|
|
});
|
|
if (rRes.ok) {
|
|
const d = await rRes.json();
|
|
localStorage.setItem('hanarang_access_token', d.accessToken);
|
|
if (d.refreshToken) localStorage.setItem('hanarang_refresh_token', d.refreshToken);
|
|
setUser({ userId: 0, username: d.username, role: d.role });
|
|
} else {
|
|
localStorage.removeItem('hanarang_access_token');
|
|
localStorage.removeItem('hanarang_refresh_token');
|
|
}
|
|
} catch { /* silent */ }
|
|
} else {
|
|
localStorage.removeItem('hanarang_access_token');
|
|
}
|
|
}
|
|
} catch {
|
|
// silent
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => { loadUser(); }, [loadUser]);
|
|
|
|
const login = async (username: string, password: string) => {
|
|
const res = await fetch(`${API_URL}/api/auth/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, password }),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const d = await res.json();
|
|
throw new Error(d.message ?? '로그인 실패');
|
|
}
|
|
|
|
const d = await res.json();
|
|
localStorage.setItem('hanarang_access_token', d.accessToken);
|
|
if (d.refreshToken) {
|
|
localStorage.setItem('hanarang_refresh_token', d.refreshToken);
|
|
}
|
|
// me API로 실제 userId 가져오기
|
|
try {
|
|
const meRes = await fetch(`${API_URL}/api/auth/me`, {
|
|
headers: { Authorization: `Bearer ${d.accessToken}` },
|
|
});
|
|
if (meRes.ok) {
|
|
const me = await meRes.json();
|
|
setUser({ userId: me.id, username: me.username, role: me.role });
|
|
return;
|
|
}
|
|
} catch { /* fallback */ }
|
|
setUser({ userId: 0, username: d.username, role: d.role });
|
|
};
|
|
|
|
const logout = () => {
|
|
localStorage.removeItem('hanarang_access_token');
|
|
localStorage.removeItem('hanarang_refresh_token');
|
|
setUser(null);
|
|
};
|
|
|
|
return (
|
|
<AuthContext.Provider value={{ user, loading, login, logout, isAuthenticated: !!user }}>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useAuth() {
|
|
return useContext(AuthContext);
|
|
}
|