diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..4d6abf3
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,7 @@
+{
+ "semi": false,
+ "printWidth": 120,
+ "tabWidth": 2,
+ "trailingComma": "all",
+ "singleQuote": true
+}
diff --git a/README.md b/README.md
deleted file mode 100644
index ef0e47e..0000000
--- a/README.md
+++ /dev/null
@@ -1,40 +0,0 @@
-This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/pages/api-reference/create-next-app).
-
-## Getting Started
-
-First, run the development server:
-
-```bash
-npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-# or
-bun dev
-```
-
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
-
-You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
-
-[API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
-
-The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) instead of React pages.
-
-This project uses [`next/font`](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
-
-## Learn More
-
-To learn more about Next.js, take a look at the following resources:
-
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn-pages-router) - an interactive Next.js tutorial.
-
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
-
-## Deploy on Vercel
-
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
-
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/pages/building-your-application/deploying) for more details.
diff --git a/bun.lockb b/bun.lockb
index 14d072c..08746b8 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index 8f41033..d42d8f0 100644
--- a/package.json
+++ b/package.json
@@ -13,9 +13,13 @@
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^6.3.0",
"@mui/material": "^6.3.0",
+ "axios": "^1.7.9",
+ "axios-case-converter": "^1.1.1",
+ "cookies-next": "^5.0.2",
"next": "15.1.3",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "react-hook-form": "^7.54.2"
},
"devDependencies": {
"typescript": "^5",
diff --git a/src/components/CapAppBar.tsx b/src/components/CapAppBar.tsx
index 7fc58f8..c5752cd 100644
--- a/src/components/CapAppBar.tsx
+++ b/src/components/CapAppBar.tsx
@@ -1,35 +1,35 @@
-import { AppBar, Box, IconButton, Toolbar, Typography } from "@mui/material";
-import MenuIcon from "@mui/icons-material/Menu";
-import AccountCircle from "@mui/icons-material/AccountCircle";
+import { AppBar, Box, IconButton, Toolbar, Typography } from '@mui/material'
+import MenuIcon from '@mui/icons-material/Menu'
+import AccountCircle from '@mui/icons-material/AccountCircle'
+import Link from 'next/link'
export function CapAppBar() {
return (
-
+
-
- Capital
-
-
-
-
+
+
+ Capital
+
+
+
+
+
+
+
+
- );
+ )
}
diff --git a/src/components/auth/SnLoginCheckpoint.tsx b/src/components/auth/SnLoginCheckpoint.tsx
new file mode 100644
index 0000000..e10d16d
--- /dev/null
+++ b/src/components/auth/SnLoginCheckpoint.tsx
@@ -0,0 +1,83 @@
+'use client'
+
+import { SnAuthFactor, SnAuthResult, SnAuthTicket } from '@/services/auth'
+import { sni } from '@/services/network'
+import { ArrowForward } from '@mui/icons-material'
+import { Collapse, Alert, Box, TextField, Button } from '@mui/material'
+import { useState } from 'react'
+import { useForm } from 'react-hook-form'
+
+import ErrorIcon from '@mui/icons-material/Error'
+import { setCookie } from 'cookies-next/client'
+
+export interface SnLoginCheckpointForm {
+ password: string
+}
+
+export function SnLoginCheckpoint({
+ ticket,
+ factor,
+ onNext,
+}: {
+ ticket: SnAuthTicket
+ factor: SnAuthFactor
+ onNext: (val: SnAuthTicket, done: boolean) => void
+}) {
+ const [error, setError] = useState(null)
+ const [loading, setLoading] = useState(false)
+
+ const { handleSubmit, register } = useForm()
+
+ async function onSubmit(data: any) {
+ try {
+ setLoading(true)
+ const resp = await sni.patch('/cgi/id/auth', {
+ ticket_id: ticket.id,
+ factor_id: factor.id,
+ code: data.password,
+ })
+
+ if (resp.data.isFinished) {
+ const tokenResp = await sni.post('/cgi/id/auth/token', {
+ grant_type: 'grant_token',
+ code: resp.data.ticket.grantToken!,
+ })
+ const atk: string = tokenResp.data['accessToken']
+ const rtk: string = tokenResp.data['refreshToken']
+ setCookie('nex_user_atk', atk, { path: '/', maxAge: 2592000 })
+ setCookie('nex_user_rtk', rtk, { path: '/', maxAge: 2592000 })
+ console.log('[Authenticator] User has been logged in. Result atk: ', atk)
+ }
+
+ onNext(resp.data.ticket, resp.data.isFinished)
+ } catch (err: any) {
+ setError(err.toString())
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+ <>
+
+ } severity="error">
+ {error}
+
+
+
+
+ >
+ )
+}
diff --git a/src/components/auth/SnLoginRouter.tsx b/src/components/auth/SnLoginRouter.tsx
new file mode 100644
index 0000000..54187c1
--- /dev/null
+++ b/src/components/auth/SnLoginRouter.tsx
@@ -0,0 +1,68 @@
+'use client'
+
+import { SnAuthFactor, SnAuthTicket } from '@/services/auth'
+import { sni } from '@/services/network'
+import { Collapse, Alert, Box, Button, Typography, ButtonGroup } from '@mui/material'
+import { useState } from 'react'
+
+import ErrorIcon from '@mui/icons-material/Error'
+import PasswordIcon from '@mui/icons-material/Password'
+import EmailIcon from '@mui/icons-material/Email'
+
+export function SnLoginRouter({
+ ticket,
+ factorList,
+ onNext,
+}: {
+ ticket: SnAuthTicket
+ factorList: SnAuthFactor[]
+ onNext: (val: SnAuthFactor) => void
+}) {
+ const factorTypeIcons = [, ]
+ const factorTypeLabels = ['Password', 'Email verification code']
+
+ const [error, setError] = useState(null)
+ const [loading, setLoading] = useState(false)
+
+ async function onSubmit(factor: SnAuthFactor) {
+ try {
+ setLoading(true)
+ await sni.post('/cgi/id/auth/factors/' + factor.id)
+ onNext(factor)
+ } catch (err: any) {
+ setError(err.toString())
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+ <>
+
+ } severity="error">
+ {error}
+
+
+
+
+
+ {factorList.map((factor) => (
+
+ ))}
+
+
+
+ {ticket.stepRemain} step(s) left
+
+
+ >
+ )
+}
diff --git a/src/components/auth/SnLoginStart.tsx b/src/components/auth/SnLoginStart.tsx
new file mode 100644
index 0000000..0373ae2
--- /dev/null
+++ b/src/components/auth/SnLoginStart.tsx
@@ -0,0 +1,67 @@
+'use client'
+
+import { useState } from 'react'
+import { sni } from '@/services/network'
+import { ArrowForward } from '@mui/icons-material'
+import { Alert, Box, Button, Collapse, Link, TextField, Typography } from '@mui/material'
+import { SnAuthFactor, SnAuthResult, SnAuthTicket } from '@/services/auth'
+import { useForm } from 'react-hook-form'
+
+import ErrorIcon from '@mui/icons-material/Error'
+
+export type SnLoginStartForm = {
+ username: string
+}
+
+export function SnLoginStart({ onNext }: { onNext: (val: SnAuthTicket, fcs: SnAuthFactor[]) => void }) {
+ const [error, setError] = useState(null)
+ const [loading, setLoading] = useState(false)
+
+ const { handleSubmit, register } = useForm()
+
+ async function onSubmit(data: any) {
+ try {
+ setLoading(true)
+ const resp = await sni.post('/cgi/id/auth', data)
+ const factorResp = await sni.get('/cgi/id/auth/factors', {
+ params: {
+ ticketId: resp.data.ticket.id,
+ },
+ })
+ onNext(resp.data.ticket, factorResp.data)
+ } catch (err: any) {
+ setError(err.toString())
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+ <>
+
+ } severity="error">
+ {error}
+
+
+
+
+ >
+ )
+}
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index ff5c974..57e4d4e 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -1,26 +1,26 @@
-import "@/styles/globals.css";
-import type { AppProps } from "next/app";
-import { createTheme, CssBaseline, ThemeProvider } from "@mui/material";
-import { Roboto } from "next/font/google";
-import { CapAppBar } from "@/components/CapAppBar";
+import '@/styles/globals.css'
+import type { AppProps } from 'next/app'
+import { Box, createTheme, CssBaseline, ThemeProvider } from '@mui/material'
+import { Roboto } from 'next/font/google'
+import { CapAppBar } from '@/components/CapAppBar'
const fontRoboto = Roboto({
- subsets: ["latin"],
- weight: ["400", "500", "700"],
- display: "swap",
-});
+ subsets: ['latin'],
+ weight: ['400', '500', '700'],
+ display: 'swap',
+})
const siteTheme = createTheme({
palette: {
- mode: "light",
+ mode: 'light',
primary: {
- main: "#3949ab",
+ main: '#3949ab',
},
secondary: {
- main: "#1e88e5",
+ main: '#1e88e5',
},
},
-});
+})
export default function App({ Component, pageProps }: AppProps) {
return (
@@ -35,8 +35,10 @@ export default function App({ Component, pageProps }: AppProps) {
-
+
+
+
>
- );
+ )
}
diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx
index 628a733..89f88e1 100644
--- a/src/pages/_document.tsx
+++ b/src/pages/_document.tsx
@@ -1,4 +1,4 @@
-import { Html, Head, Main, NextScript } from "next/document";
+import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
@@ -9,5 +9,5 @@ export default function Document() {