From e670c571c7ab0908637809504148be72bba5b9b8 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 4 Jan 2025 18:24:23 +0800 Subject: [PATCH] :sparkles: Open ID connect --- next.config.ts | 8 ++ src/pages/_app.tsx | 3 + src/pages/api/well-known/jwks.ts | 10 ++ .../api/well-known/openid-configuration.ts | 23 ++++ src/pages/auth/authorize.tsx | 125 ++++++++++++++++++ src/pages/flow/accounts/deletion.tsx | 4 +- src/pages/flow/accounts/password-reset.tsx | 4 +- 7 files changed, 171 insertions(+), 6 deletions(-) create mode 100644 src/pages/api/well-known/jwks.ts create mode 100644 src/pages/api/well-known/openid-configuration.ts create mode 100644 src/pages/auth/authorize.tsx diff --git a/next.config.ts b/next.config.ts index 4aaad85..166505b 100644 --- a/next.config.ts +++ b/next.config.ts @@ -18,6 +18,14 @@ const nextConfig: NextConfig = { }, ], }, + async rewrites() { + return [ + { + source: '/.well-known/:path*', + destination: '/api/well-known/:path*', + }, + ] + }, } export default nextConfig diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 28fceef..ab0cb1e 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -15,6 +15,9 @@ const fontRoboto = Roboto({ const siteTheme = createTheme({ cssVariables: true, + // colorSchemes: { + // dark: true, + // }, palette: { mode: 'light', primary: { diff --git a/src/pages/api/well-known/jwks.ts b/src/pages/api/well-known/jwks.ts new file mode 100644 index 0000000..8e4d4a5 --- /dev/null +++ b/src/pages/api/well-known/jwks.ts @@ -0,0 +1,10 @@ +import axios from 'axios' +import { NextApiRequest, NextApiResponse } from 'next' + +export default async function handler(_: NextApiRequest, res: NextApiResponse) { + const solarNetworkApi = 'https://api.sn.solsynth.dev' + + const resp = await axios.get(solarNetworkApi + '/cgi/id/well-known/jwks') + + res.status(200).json(resp.data) +} diff --git a/src/pages/api/well-known/openid-configuration.ts b/src/pages/api/well-known/openid-configuration.ts new file mode 100644 index 0000000..35372ad --- /dev/null +++ b/src/pages/api/well-known/openid-configuration.ts @@ -0,0 +1,23 @@ +import axios from 'axios' +import { NextApiRequest, NextApiResponse } from 'next' + +export default async function handler(_: NextApiRequest, res: NextApiResponse) { + const siteUrl = 'https://solsynth.dev' + const solarNetworkApi = 'https://api.sn.solsynth.dev' + + const resp = await axios.get(solarNetworkApi + '/cgi/id/well-known/openid-configuration') + const out: Record = resp.data + + out['authorization_endpoint'] = siteUrl + '/auth/authorize' + out['jwks_uri'] = siteUrl + '/.well-known/jwks' + + for (let [k, v] of Object.entries(out)) { + if (typeof v === 'string') { + if (v.startsWith('https://id.solsynth.dev/api')) { + out[k] = v.replace('https://id.solsynth.dev/api', solarNetworkApi + '/cgi/id') + } + } + } + + res.status(200).json(out) +} diff --git a/src/pages/auth/authorize.tsx b/src/pages/auth/authorize.tsx new file mode 100644 index 0000000..50a2b3a --- /dev/null +++ b/src/pages/auth/authorize.tsx @@ -0,0 +1,125 @@ +import { sni } from '@/services/network' +import { Container, Box, Typography, Alert, Collapse, Button, CircularProgress, Card, CardContent } from '@mui/material' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' + +import ErrorIcon from '@mui/icons-material/Error' +import CloseIcon from '@mui/icons-material/Close' +import CheckIcon from '@mui/icons-material/Check' +import { SnAuthTicket } from '@/services/auth' + +export default function AccountAuthorize() { + const router = useRouter() + + const [thirdClient, setThirdClient] = useState(null) + + const [error, setError] = useState(null) + const [reverting, setReverting] = useState(false) + const [busy, setBusy] = useState(false) + + function doCallback(ticket: SnAuthTicket) { + const url = `${router.query['redirect_uri']}?code=${ticket.grantToken}&state=${router.query['state']}` + window.open(url, '_self') + } + + async function fetch() { + try { + setReverting(true) + const resp = await sni.get<{ ticket: SnAuthTicket; client: any }>( + '/cgi/id/auth/o/authorize' + window.location.search, + {}, + ) + if (resp.data.ticket) { + return doCallback(resp.data.ticket) + } + setThirdClient(resp.data.client) + } catch (err: any) { + setError(err.toString()) + } finally { + setReverting(false) + } + } + + useEffect(() => { + fetch() + }, []) + + async function confirm() { + try { + setBusy(true) + const resp = await sni.post<{ ticket: SnAuthTicket }>('/cgi/id/auth/o/authorize' + window.location.search) + return doCallback(resp.data.ticket) + } catch (err: any) { + setError(err.toString()) + } finally { + setBusy(false) + } + } + + function decline() { + if (window.history.length > 0) { + window.history.back() + } else { + window.close() + } + } + + return ( + + + + Connect with Solarpass + + + Connect third-party services with Solar Network + + + + } severity="error"> + {error} + + + + {reverting && ( + + + + )} + + {!reverting && ( + + + + {thirdClient?.name} + {thirdClient?.description} + + + + + + + + + )} + + + ) +} diff --git a/src/pages/flow/accounts/deletion.tsx b/src/pages/flow/accounts/deletion.tsx index 08f1402..47edc16 100644 --- a/src/pages/flow/accounts/deletion.tsx +++ b/src/pages/flow/accounts/deletion.tsx @@ -5,9 +5,7 @@ import { useState } from 'react' import ErrorIcon from '@mui/icons-material/Error' -import 'animate.css' - -export default function AccountConfirm() { +export default function AccountDeletion() { const router = useRouter() const [error, setError] = useState(null) diff --git a/src/pages/flow/accounts/password-reset.tsx b/src/pages/flow/accounts/password-reset.tsx index f6bf694..3473863 100644 --- a/src/pages/flow/accounts/password-reset.tsx +++ b/src/pages/flow/accounts/password-reset.tsx @@ -6,13 +6,11 @@ import { useForm } from 'react-hook-form' import ErrorIcon from '@mui/icons-material/Error' -import 'animate.css' - export type SnResetPasswordForm = { password: string } -export default function AccountConfirm() { +export default function AccountPasswordReset() { const router = useRouter() const { handleSubmit, register } = useForm()