Add backend/frontend scaffolding with Oracle ADB wallet config
- Backend: Spring Boot 3 + WebFlux, JWT auth, Oracle ADB wallet, 8 controllers/services/repositories (Auth~Tag), DTOs, exception handling - Frontend: Next.js 15, TypeScript, Tailwind CSS, AuthContext, 7 pages (dashboard, knowledge, chat, study, todos, habits, login) - DB: V1 migration with 12 tables including VECTOR(1024) + HNSW index - Ops: PM2 ecosystem config, deploy.sh, start-backend.sh - CLAUDE.md: DB credentials replaced with env var references Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
14
sundol-frontend/eslint.config.mjs
Normal file
14
sundol-frontend/eslint.config.mjs
Normal file
@@ -0,0 +1,14 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [...compat.extends("next/core-web-vitals", "next/typescript")];
|
||||
|
||||
export default eslintConfig;
|
||||
7
sundol-frontend/next.config.ts
Normal file
7
sundol-frontend/next.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
31
sundol-frontend/package.json
Normal file
31
sundol-frontend/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "sundol-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^15.3.1",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"axios": "^1.7.9",
|
||||
"zustand": "^5.0.3",
|
||||
"react-markdown": "^9.0.3",
|
||||
"lucide-react": "^0.469.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.3",
|
||||
"@types/node": "^22.10.0",
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.0",
|
||||
"@tailwindcss/postcss": "^4.1.0",
|
||||
"tailwindcss": "^4.1.0",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-next": "^15.3.1",
|
||||
"@eslint/eslintrc": "^3"
|
||||
}
|
||||
}
|
||||
7
sundol-frontend/postcss.config.mjs
Normal file
7
sundol-frontend/postcss.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
18
sundol-frontend/src/app/chat/page.tsx
Normal file
18
sundol-frontend/src/app/chat/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import AuthGuard from "@/components/auth-guard";
|
||||
import NavBar from "@/components/nav-bar";
|
||||
|
||||
export default function ChatPage() {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<NavBar />
|
||||
<main className="max-w-7xl mx-auto px-4 py-8">
|
||||
<h1 className="text-2xl font-bold mb-6">AI Chat</h1>
|
||||
<div className="bg-[var(--color-bg-card)] rounded-xl p-6 border border-[var(--color-border)] min-h-[60vh] flex items-center justify-center">
|
||||
<p className="text-[var(--color-text-muted)]">Start a new conversation to ask questions about your knowledge base.</p>
|
||||
</div>
|
||||
</main>
|
||||
</AuthGuard>
|
||||
);
|
||||
}
|
||||
33
sundol-frontend/src/app/dashboard/page.tsx
Normal file
33
sundol-frontend/src/app/dashboard/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import AuthGuard from "@/components/auth-guard";
|
||||
import NavBar from "@/components/nav-bar";
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<NavBar />
|
||||
<main className="max-w-7xl mx-auto px-4 py-8">
|
||||
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<DashCard title="Knowledge Items" value="-" description="Total ingested items" />
|
||||
<DashCard title="Due Study Cards" value="-" description="Cards due for review" />
|
||||
<DashCard title="Active Todos" value="-" description="Pending tasks" />
|
||||
<DashCard title="Habit Streaks" value="-" description="Current active streaks" />
|
||||
<DashCard title="Chat Sessions" value="-" description="Active conversations" />
|
||||
<DashCard title="Tags" value="-" description="Knowledge categories" />
|
||||
</div>
|
||||
</main>
|
||||
</AuthGuard>
|
||||
);
|
||||
}
|
||||
|
||||
function DashCard({ title, value, description }: { title: string; value: string; description: string }) {
|
||||
return (
|
||||
<div className="bg-[var(--color-bg-card)] rounded-xl p-6 border border-[var(--color-border)]">
|
||||
<h3 className="text-sm text-[var(--color-text-muted)] mb-1">{title}</h3>
|
||||
<p className="text-3xl font-bold mb-1">{value}</p>
|
||||
<p className="text-sm text-[var(--color-text-muted)]">{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
sundol-frontend/src/app/globals.css
Normal file
21
sundol-frontend/src/app/globals.css
Normal file
@@ -0,0 +1,21 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--color-bg: #0f172a;
|
||||
--color-bg-card: #1e293b;
|
||||
--color-bg-hover: #334155;
|
||||
--color-primary: #3b82f6;
|
||||
--color-primary-hover: #2563eb;
|
||||
--color-text: #f1f5f9;
|
||||
--color-text-muted: #94a3b8;
|
||||
--color-border: #334155;
|
||||
--color-success: #22c55e;
|
||||
--color-warning: #f59e0b;
|
||||
--color-danger: #ef4444;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
23
sundol-frontend/src/app/habits/page.tsx
Normal file
23
sundol-frontend/src/app/habits/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import AuthGuard from "@/components/auth-guard";
|
||||
import NavBar from "@/components/nav-bar";
|
||||
|
||||
export default function HabitsPage() {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<NavBar />
|
||||
<main className="max-w-7xl mx-auto px-4 py-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-bold">Habits</h1>
|
||||
<button className="px-4 py-2 bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] rounded-lg transition-colors">
|
||||
+ Add Habit
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-[var(--color-bg-card)] rounded-xl p-6 border border-[var(--color-border)]">
|
||||
<p className="text-[var(--color-text-muted)]">No habits tracked yet. Start building good habits.</p>
|
||||
</div>
|
||||
</main>
|
||||
</AuthGuard>
|
||||
);
|
||||
}
|
||||
23
sundol-frontend/src/app/knowledge/page.tsx
Normal file
23
sundol-frontend/src/app/knowledge/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import AuthGuard from "@/components/auth-guard";
|
||||
import NavBar from "@/components/nav-bar";
|
||||
|
||||
export default function KnowledgePage() {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<NavBar />
|
||||
<main className="max-w-7xl mx-auto px-4 py-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-bold">Knowledge</h1>
|
||||
<button className="px-4 py-2 bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] rounded-lg transition-colors">
|
||||
+ Add Knowledge
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-[var(--color-bg-card)] rounded-xl p-6 border border-[var(--color-border)]">
|
||||
<p className="text-[var(--color-text-muted)]">No knowledge items yet. Add your first item to get started.</p>
|
||||
</div>
|
||||
</main>
|
||||
</AuthGuard>
|
||||
);
|
||||
}
|
||||
22
sundol-frontend/src/app/layout.tsx
Normal file
22
sundol-frontend/src/app/layout.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import { AuthProvider } from "@/lib/auth-context";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "SUNDOL",
|
||||
description: "Smart Unified Natural Dog-Operated Layer",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="ko">
|
||||
<body>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
50
sundol-frontend/src/app/login/page.tsx
Normal file
50
sundol-frontend/src/app/login/page.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuth } from "@/lib/auth-context";
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const { login } = useAuth();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleGoogleLogin = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// TODO: Implement Google OAuth flow
|
||||
// For now, placeholder
|
||||
alert("Google OAuth not configured yet");
|
||||
} catch (error) {
|
||||
console.error("Login failed:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="bg-[var(--color-bg-card)] rounded-2xl p-8 w-full max-w-md shadow-xl border border-[var(--color-border)]">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold mb-2">SUNDOL</h1>
|
||||
<p className="text-[var(--color-text-muted)]">
|
||||
Smart Unified Natural Dog-Operated Layer
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleGoogleLogin}
|
||||
disabled={isLoading}
|
||||
className="w-full py-3 px-4 bg-white text-gray-800 rounded-lg font-medium hover:bg-gray-100 transition-colors disabled:opacity-50 flex items-center justify-center gap-3"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"/>
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||
</svg>
|
||||
{isLoading ? "Signing in..." : "Sign in with Google"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
22
sundol-frontend/src/app/page.tsx
Normal file
22
sundol-frontend/src/app/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuth } from "@/lib/auth-context";
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
router.replace(isAuthenticated ? "/dashboard" : "/login");
|
||||
}
|
||||
}, [isAuthenticated, isLoading, router]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-[var(--color-text-muted)]">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
18
sundol-frontend/src/app/study/page.tsx
Normal file
18
sundol-frontend/src/app/study/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import AuthGuard from "@/components/auth-guard";
|
||||
import NavBar from "@/components/nav-bar";
|
||||
|
||||
export default function StudyPage() {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<NavBar />
|
||||
<main className="max-w-7xl mx-auto px-4 py-8">
|
||||
<h1 className="text-2xl font-bold mb-6">Study Cards</h1>
|
||||
<div className="bg-[var(--color-bg-card)] rounded-xl p-6 border border-[var(--color-border)] flex items-center justify-center min-h-[40vh]">
|
||||
<p className="text-[var(--color-text-muted)]">No cards due for review. Generate cards from your knowledge items.</p>
|
||||
</div>
|
||||
</main>
|
||||
</AuthGuard>
|
||||
);
|
||||
}
|
||||
23
sundol-frontend/src/app/todos/page.tsx
Normal file
23
sundol-frontend/src/app/todos/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import AuthGuard from "@/components/auth-guard";
|
||||
import NavBar from "@/components/nav-bar";
|
||||
|
||||
export default function TodosPage() {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<NavBar />
|
||||
<main className="max-w-7xl mx-auto px-4 py-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-bold">Todos</h1>
|
||||
<button className="px-4 py-2 bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] rounded-lg transition-colors">
|
||||
+ Add Todo
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-[var(--color-bg-card)] rounded-xl p-6 border border-[var(--color-border)]">
|
||||
<p className="text-[var(--color-text-muted)]">No todos yet. Create your first task.</p>
|
||||
</div>
|
||||
</main>
|
||||
</AuthGuard>
|
||||
);
|
||||
}
|
||||
30
sundol-frontend/src/components/auth-guard.tsx
Normal file
30
sundol-frontend/src/components/auth-guard.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuth } from "@/lib/auth-context";
|
||||
|
||||
export default function AuthGuard({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isAuthenticated) {
|
||||
router.replace("/login");
|
||||
}
|
||||
}, [isAuthenticated, isLoading, router]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-[var(--color-text-muted)]">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
54
sundol-frontend/src/components/nav-bar.tsx
Normal file
54
sundol-frontend/src/components/nav-bar.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useAuth } from "@/lib/auth-context";
|
||||
|
||||
const navItems = [
|
||||
{ href: "/dashboard", label: "Dashboard" },
|
||||
{ href: "/knowledge", label: "Knowledge" },
|
||||
{ href: "/chat", label: "Chat" },
|
||||
{ href: "/study", label: "Study" },
|
||||
{ href: "/todos", label: "Todos" },
|
||||
{ href: "/habits", label: "Habits" },
|
||||
];
|
||||
|
||||
export default function NavBar() {
|
||||
const pathname = usePathname();
|
||||
const { logout } = useAuth();
|
||||
|
||||
return (
|
||||
<nav className="bg-[var(--color-bg-card)] border-b border-[var(--color-border)]">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<div className="flex items-center gap-8">
|
||||
<Link href="/dashboard" className="text-xl font-bold">
|
||||
SUNDOL
|
||||
</Link>
|
||||
<div className="flex gap-1">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`px-3 py-2 rounded-lg text-sm transition-colors ${
|
||||
pathname === item.href
|
||||
? "bg-[var(--color-primary)] text-white"
|
||||
: "text-[var(--color-text-muted)] hover:text-white hover:bg-[var(--color-bg-hover)]"
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="text-sm text-[var(--color-text-muted)] hover:text-white transition-colors"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
76
sundol-frontend/src/lib/api.ts
Normal file
76
sundol-frontend/src/lib/api.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import axios from "axios";
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080",
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
// Types
|
||||
export interface LoginResponse {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export interface KnowledgeItem {
|
||||
id: string;
|
||||
userId: string;
|
||||
type: string;
|
||||
title: string;
|
||||
sourceUrl: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ChatSession {
|
||||
id: string;
|
||||
title: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
role: string;
|
||||
content: string;
|
||||
sourceChunks: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Todo {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
status: string;
|
||||
priority: string;
|
||||
dueDate: string;
|
||||
parentId: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Habit {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
habitType: string;
|
||||
targetDays: string;
|
||||
color: string;
|
||||
streakCurrent: number;
|
||||
streakBest: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface StudyCard {
|
||||
id: string;
|
||||
knowledgeItemId: string;
|
||||
front: string;
|
||||
back: string;
|
||||
easeFactor: number;
|
||||
interval: number;
|
||||
nextReviewAt: string;
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
81
sundol-frontend/src/lib/auth-context.tsx
Normal file
81
sundol-frontend/src/lib/auth-context.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import React, { createContext, useContext, useState, useCallback, useEffect } from "react";
|
||||
import { api, LoginResponse } from "./api";
|
||||
|
||||
interface AuthContextType {
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
accessToken: string | null;
|
||||
login: (response: LoginResponse) => void;
|
||||
logout: () => void;
|
||||
setAccessToken: (token: string) => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType>({
|
||||
isAuthenticated: false,
|
||||
isLoading: true,
|
||||
accessToken: null,
|
||||
login: () => {},
|
||||
logout: () => {},
|
||||
setAccessToken: () => {},
|
||||
});
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Try to restore session from refresh token cookie
|
||||
const tryRefresh = async () => {
|
||||
try {
|
||||
const res = await api.post<LoginResponse>("/api/auth/refresh");
|
||||
setAccessToken(res.data.accessToken);
|
||||
} catch {
|
||||
// No valid session
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
tryRefresh();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (accessToken) {
|
||||
api.defaults.headers.common["Authorization"] = `Bearer ${accessToken}`;
|
||||
} else {
|
||||
delete api.defaults.headers.common["Authorization"];
|
||||
}
|
||||
}, [accessToken]);
|
||||
|
||||
const login = useCallback((response: LoginResponse) => {
|
||||
setAccessToken(response.accessToken);
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
try {
|
||||
await api.post("/api/auth/logout");
|
||||
} catch {
|
||||
// Ignore logout errors
|
||||
}
|
||||
setAccessToken(null);
|
||||
window.location.href = "/login";
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
isAuthenticated: !!accessToken,
|
||||
isLoading,
|
||||
accessToken,
|
||||
login,
|
||||
logout,
|
||||
setAccessToken,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useAuth = () => useContext(AuthContext);
|
||||
36
sundol-frontend/src/lib/use-api.ts
Normal file
36
sundol-frontend/src/lib/use-api.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { api, LoginResponse } from "./api";
|
||||
import { useAuth } from "./auth-context";
|
||||
import { AxiosRequestConfig } from "axios";
|
||||
|
||||
export function useApi() {
|
||||
const { setAccessToken, logout } = useAuth();
|
||||
|
||||
const request = useCallback(
|
||||
async <T>(config: AxiosRequestConfig): Promise<T> => {
|
||||
try {
|
||||
const response = await api.request<T>(config);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
if (error.response?.status === 401) {
|
||||
try {
|
||||
const refreshRes = await api.post<LoginResponse>("/api/auth/refresh");
|
||||
setAccessToken(refreshRes.data.accessToken);
|
||||
api.defaults.headers.common["Authorization"] = `Bearer ${refreshRes.data.accessToken}`;
|
||||
const retryResponse = await api.request<T>(config);
|
||||
return retryResponse.data;
|
||||
} catch {
|
||||
logout();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[setAccessToken, logout]
|
||||
);
|
||||
|
||||
return { request };
|
||||
}
|
||||
27
sundol-frontend/tsconfig.json
Normal file
27
sundol-frontend/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user