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} + + + +
+ + + + + + + By continuing means you agree to our Terms of Service and{' '} + Privacy Policy + + +
+ + ) +} 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() { - ); + ) } diff --git a/src/pages/auth/login.tsx b/src/pages/auth/login.tsx new file mode 100644 index 0000000..f9d1863 --- /dev/null +++ b/src/pages/auth/login.tsx @@ -0,0 +1,79 @@ +import { SnLoginCheckpoint } from '@/components/auth/SnLoginCheckpoint' +import { SnLoginRouter } from '@/components/auth/SnLoginRouter' +import { SnLoginStart } from '@/components/auth/SnLoginStart' +import { SnAuthFactor, SnAuthTicket } from '@/services/auth' +import { Box, Container, Typography } from '@mui/material' +import { useRouter } from 'next/router' +import { useState } from 'react' + +export default function Login() { + const [period, setPeriod] = useState(0) + const [ticket, setTicket] = useState(null) + const [factorList, setFactorList] = useState([]) + const [factor, setFactor] = useState(null) + + const router = useRouter() + + function renderForm() { + switch (period) { + case 1: + return ( + { + setPeriod(period + 1) + setFactor(val) + }} + /> + ) + case 2: + return ( + { + if (!done) { + setTicket(val) + setPeriod(1) + return + } + router.push('/') + }} + /> + ) + default: + return ( + { + setPeriod(period + 1) + setTicket(val) + setFactorList(fcs) + }} + /> + ) + } + } + + return ( + + + + Login + + + Login via Solarpass + + + {renderForm()} + + + ) +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index fe0a7c3..85e85d1 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,4 +1,4 @@ -import { Container, Typography } from "@mui/material"; +import { Container, Typography } from '@mui/material' export default function Home() { return ( @@ -10,5 +10,5 @@ export default function Home() { - ); + ) } diff --git a/src/services/auth.ts b/src/services/auth.ts new file mode 100644 index 0000000..1d3e6ea --- /dev/null +++ b/src/services/auth.ts @@ -0,0 +1,34 @@ +export interface SnAuthResult { + isFinished: boolean + ticket: SnAuthTicket +} + +export interface SnAuthTicket { + id: number + createdAt: Date + updatedAt: Date + deletedAt?: Date | null + stepRemain: number + grantToken?: string | null + accessToken?: string | null + refreshToken?: string | null + ipAddress: string + location: string + userAgent: string + expiredAt?: Date | null + lastGrantAt?: Date | null + availableAt?: Date | null + nonce?: string | null + accountId?: number | null + factorTrail: number[] +} + +export interface SnAuthFactor { + id: number + createdAt: Date + updatedAt: Date + deletedAt?: Date | null + type: number + config?: Record | null + accountId?: number | null +} diff --git a/src/services/network.ts b/src/services/network.ts new file mode 100644 index 0000000..755a0ab --- /dev/null +++ b/src/services/network.ts @@ -0,0 +1,12 @@ +import axios from 'axios' +import applyCaseMiddleware from 'axios-case-converter' + +export let sni = applyCaseMiddleware( + axios.create({ + baseURL: 'https://api.sn.solsynth.dev', + }), + { + ignoreParams: true, + ignoreHeaders: true, + }, +)