第一次提交
This commit is contained in:
4
auth-portal/src/.dockerignore
Normal file
4
auth-portal/src/.dockerignore
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
.next
|
||||
npm-debug.log
|
||||
|
||||
314
auth-portal/src/.gitignore
vendored
Normal file
314
auth-portal/src/.gitignore
vendored
Normal file
@ -0,0 +1,314 @@
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# ---> Node
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# vitepress build output
|
||||
**/.vitepress/dist
|
||||
|
||||
# vitepress cache directory
|
||||
**/.vitepress/cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
20
auth-portal/src/Dockerfile
Normal file
20
auth-portal/src/Dockerfile
Normal file
@ -0,0 +1,20 @@
|
||||
FROM node:20-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3081
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
COPY --from=builder /app ./
|
||||
EXPOSE 3081
|
||||
CMD ["npm", "run", "start"]
|
||||
|
||||
16
auth-portal/src/README.md
Normal file
16
auth-portal/src/README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# nanobot-auth-portal
|
||||
|
||||
Dedicated login/register frontend for nanobot containers.
|
||||
|
||||
## Docs
|
||||
|
||||
- API documentation: [docs/api.md](docs/api.md)
|
||||
|
||||
## Env
|
||||
|
||||
The portal now talks to the deployment control API on the server side:
|
||||
|
||||
```bash
|
||||
DEPLOY_API_BASE_URL=http://127.0.0.1:8090
|
||||
DEPLOY_API_TOKEN=change-me
|
||||
```
|
||||
50
auth-portal/src/app/api/runtime/login/route.ts
Normal file
50
auth-portal/src/app/api/runtime/login/route.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import type { TokenResponse } from '@/types/auth';
|
||||
import { HttpError, callDeployControl, callInstanceApi, normalizeTokenResponse } from '@/lib/runtime-control';
|
||||
|
||||
function errorStatus(error: unknown): number {
|
||||
if (error instanceof HttpError) {
|
||||
return error.status;
|
||||
}
|
||||
return 500;
|
||||
}
|
||||
|
||||
function errorDetail(error: unknown): string {
|
||||
if (error instanceof HttpError) {
|
||||
return error.message;
|
||||
}
|
||||
return error instanceof Error ? error.message : 'login failed';
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = (await request.json()) as {
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
const username = body.username?.trim() || '';
|
||||
const password = body.password || '';
|
||||
|
||||
if (!username || !password) {
|
||||
return NextResponse.json({ detail: 'username and password are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const routing = await callDeployControl<{
|
||||
api_base_url?: string;
|
||||
frontend_base_url?: string;
|
||||
public_url?: string;
|
||||
}>('/api/instances/resolve', { username });
|
||||
|
||||
const response = await callInstanceApi<TokenResponse>(routing.api_base_url || '', '/api/auth/login', {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
|
||||
return NextResponse.json(normalizeTokenResponse(response, routing));
|
||||
} catch (error) {
|
||||
const status = errorStatus(error);
|
||||
const detail = status === 404 || status === 401 ? '用户名或密码错误' : errorDetail(error);
|
||||
return NextResponse.json({ detail }, { status: status === 404 ? 401 : status });
|
||||
}
|
||||
}
|
||||
55
auth-portal/src/app/api/runtime/register/route.ts
Normal file
55
auth-portal/src/app/api/runtime/register/route.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import type { TokenResponse } from '@/types/auth';
|
||||
import { HttpError, callDeployControl, callInstanceApi, normalizeTokenResponse } from '@/lib/runtime-control';
|
||||
|
||||
function errorStatus(error: unknown): number {
|
||||
if (error instanceof HttpError) {
|
||||
return error.status;
|
||||
}
|
||||
return 500;
|
||||
}
|
||||
|
||||
function errorDetail(error: unknown): string {
|
||||
if (error instanceof HttpError) {
|
||||
return error.message;
|
||||
}
|
||||
return error instanceof Error ? error.message : 'registration failed';
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = (await request.json()) as {
|
||||
username?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
};
|
||||
const username = body.username?.trim() || '';
|
||||
const email = body.email?.trim() || '';
|
||||
const password = body.password || '';
|
||||
|
||||
if (!username || !password) {
|
||||
return NextResponse.json({ detail: 'username and password are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const routing = await callDeployControl<{
|
||||
api_base_url?: string;
|
||||
frontend_base_url?: string;
|
||||
public_url?: string;
|
||||
}>('/api/instances/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
const response = await callInstanceApi<TokenResponse>(routing.api_base_url || '', '/api/auth/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
return NextResponse.json(normalizeTokenResponse(response, routing));
|
||||
} catch (error) {
|
||||
return NextResponse.json({ detail: errorDetail(error) }, { status: errorStatus(error) });
|
||||
}
|
||||
}
|
||||
266
auth-portal/src/app/globals.css
Normal file
266
auth-portal/src/app/globals.css
Normal file
@ -0,0 +1,266 @@
|
||||
:root {
|
||||
--bg: #f4efe6;
|
||||
--bg-strong: #e6d8bf;
|
||||
--panel: rgba(23, 26, 31, 0.88);
|
||||
--panel-border: rgba(255, 255, 255, 0.1);
|
||||
--text: #f7f1e7;
|
||||
--muted: rgba(247, 241, 231, 0.72);
|
||||
--accent: #ff8d3a;
|
||||
--accent-strong: #ff6b00;
|
||||
--danger: #ff8787;
|
||||
--input: rgba(255, 255, 255, 0.08);
|
||||
--input-focus: rgba(255, 141, 58, 0.28);
|
||||
--shadow: 0 40px 90px rgba(26, 24, 21, 0.28);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(255, 141, 58, 0.28), transparent 28%),
|
||||
radial-gradient(circle at right center, rgba(19, 104, 93, 0.18), transparent 24%),
|
||||
linear-gradient(135deg, var(--bg) 0%, #d6c09b 45%, var(--bg-strong) 100%);
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.portal-page {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 32px 18px;
|
||||
}
|
||||
|
||||
.portal-shell {
|
||||
width: min(980px, 100%);
|
||||
display: grid;
|
||||
grid-template-columns: 1.05fr 0.95fr;
|
||||
overflow: hidden;
|
||||
border-radius: 28px;
|
||||
box-shadow: var(--shadow);
|
||||
background: rgba(255, 250, 241, 0.45);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.portal-brand {
|
||||
position: relative;
|
||||
min-height: 620px;
|
||||
padding: 48px 42px;
|
||||
color: #26180d;
|
||||
background:
|
||||
linear-gradient(160deg, rgba(255, 240, 217, 0.82), rgba(255, 220, 174, 0.64)),
|
||||
linear-gradient(135deg, #ffd7a3, #f3b15f);
|
||||
}
|
||||
|
||||
.portal-brand::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 22px;
|
||||
border-radius: 22px;
|
||||
border: 1px solid rgba(38, 24, 13, 0.08);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.portal-kicker {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(38, 24, 13, 0.08);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.portal-title {
|
||||
margin: 26px 0 14px;
|
||||
font-size: clamp(36px, 5vw, 60px);
|
||||
line-height: 0.95;
|
||||
letter-spacing: -0.06em;
|
||||
}
|
||||
|
||||
.portal-copy {
|
||||
max-width: 460px;
|
||||
font-size: 16px;
|
||||
line-height: 1.65;
|
||||
color: rgba(38, 24, 13, 0.78);
|
||||
}
|
||||
|
||||
.portal-notes {
|
||||
margin-top: 34px;
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.portal-note {
|
||||
padding: 16px 18px;
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
border: 1px solid rgba(38, 24, 13, 0.08);
|
||||
}
|
||||
|
||||
.portal-note strong {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.portal-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(23, 26, 31, 0.96), rgba(12, 14, 17, 0.94));
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: min(440px, 100%);
|
||||
padding: 34px;
|
||||
border-radius: 24px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--panel-border);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.auth-card h1 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 34px;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
.auth-card p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
margin-top: 26px;
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.field label {
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.field input {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 14px;
|
||||
color: var(--text);
|
||||
background: var(--input);
|
||||
outline: none;
|
||||
transition: border-color 140ms ease, background 140ms ease, box-shadow 140ms ease;
|
||||
}
|
||||
|
||||
.field input:focus {
|
||||
border-color: rgba(255, 141, 58, 0.65);
|
||||
background: rgba(255, 255, 255, 0.11);
|
||||
box-shadow: 0 0 0 5px var(--input-focus);
|
||||
}
|
||||
|
||||
.error-text {
|
||||
min-height: 22px;
|
||||
font-size: 14px;
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.primary-button {
|
||||
width: 100%;
|
||||
padding: 14px 18px;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
color: #26180d;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-strong));
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.01em;
|
||||
transition: transform 140ms ease, filter 140ms ease, opacity 140ms ease;
|
||||
}
|
||||
|
||||
.primary-button:hover {
|
||||
transform: translateY(-1px);
|
||||
filter: brightness(1.02);
|
||||
}
|
||||
|
||||
.primary-button:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.68;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
margin-top: 20px;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.auth-footer a {
|
||||
color: #ffd7a3;
|
||||
}
|
||||
|
||||
.status-panel {
|
||||
margin-top: 18px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
.portal-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.portal-brand {
|
||||
min-height: auto;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.portal-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.portal-brand,
|
||||
.portal-panel {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
padding: 24px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
20
auth-portal/src/app/layout.tsx
Normal file
20
auth-portal/src/app/layout.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import './globals.css';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Boardware Genius Auth Portal',
|
||||
description: 'Dedicated login and registration portal for nanobot containers.',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
100
auth-portal/src/app/login/page.tsx
Normal file
100
auth-portal/src/app/login/page.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { buildFrontendHandoffUrl, login, withNext } from '@/lib/auth-client';
|
||||
|
||||
export default function LoginPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const nextPath = searchParams?.get('next') || '/';
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const response = await login(username, password);
|
||||
window.location.replace(buildFrontendHandoffUrl(response, nextPath));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '登录失败,请稍后重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="portal-page">
|
||||
<section className="portal-shell">
|
||||
<div className="portal-brand">
|
||||
<div className="portal-kicker">Auth Portal</div>
|
||||
<h1 className="portal-title">Boardware Genius</h1>
|
||||
<p className="portal-copy">
|
||||
这个入口只负责鉴权。成功后会把你直接送到为你分配的专属实例 URL,后续前后端请求都留在那套容器里。
|
||||
</p>
|
||||
<div className="portal-notes">
|
||||
<div className="portal-note">
|
||||
<strong>容器边界</strong>
|
||||
登录注册先经过独立 auth portal,再跳到专属实例。一用户一套前后端容器不变。
|
||||
</div>
|
||||
<div className="portal-note">
|
||||
<strong>目标页面</strong>
|
||||
当前登录完成后将回到:<code>{nextPath}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="portal-panel">
|
||||
<div className="auth-card">
|
||||
<h1>登录</h1>
|
||||
<p>输入已有账号,认证完成后直接进入目标容器前端。</p>
|
||||
|
||||
<form className="auth-form" onSubmit={handleSubmit}>
|
||||
<div className="field">
|
||||
<label htmlFor="username">用户名</label>
|
||||
<input
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(event) => setUsername(event.target.value)}
|
||||
autoComplete="username"
|
||||
placeholder="例如:bwgdi"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label htmlFor="password">密码</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
autoComplete="current-password"
|
||||
placeholder="输入密码"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="error-text">{error}</div>
|
||||
|
||||
<button className="primary-button" type="submit" disabled={loading}>
|
||||
{loading ? '登录中...' : '登录并进入容器'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="auth-footer">
|
||||
还没有账号? <Link href={withNext('/register', nextPath)}>去注册</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
6
auth-portal/src/app/page.tsx
Normal file
6
auth-portal/src/app/page.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default function HomePage() {
|
||||
redirect('/login');
|
||||
}
|
||||
|
||||
134
auth-portal/src/app/register/page.tsx
Normal file
134
auth-portal/src/app/register/page.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { buildFrontendHandoffUrl, register, withNext } from '@/lib/auth-client';
|
||||
|
||||
export default function RegisterPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const nextPath = searchParams?.get('next') || '/mcp';
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
if (password !== confirmPassword) {
|
||||
throw new Error('两次输入的密码不一致');
|
||||
}
|
||||
const response = await register(username, email, password);
|
||||
window.location.replace(buildFrontendHandoffUrl(response, nextPath));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '注册失败,请稍后重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="portal-page">
|
||||
<section className="portal-shell">
|
||||
<div className="portal-brand">
|
||||
<div className="portal-kicker">Auth Portal</div>
|
||||
<h1 className="portal-title">Create Runtime</h1>
|
||||
<p className="portal-copy">
|
||||
注册不仅建立登录账号,还会触发专属实例创建和 backend 身份分配。认证完成后会直接进入你的专属 URL。
|
||||
</p>
|
||||
<div className="portal-notes">
|
||||
<div className="portal-note">
|
||||
<strong>注册结果</strong>
|
||||
deploy-control 会创建实例,AuthZ 再补齐 backend 身份,auth portal 最后把你转交到该实例前端。
|
||||
</div>
|
||||
<div className="portal-note">
|
||||
<strong>目标页面</strong>
|
||||
当前注册完成后将回到:<code>{nextPath}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="portal-panel">
|
||||
<div className="auth-card">
|
||||
<h1>注册</h1>
|
||||
<p>为当前容器创建登录账号,并完成 backend 身份初始化。</p>
|
||||
|
||||
<form className="auth-form" onSubmit={handleSubmit}>
|
||||
<div className="field">
|
||||
<label htmlFor="username">用户名</label>
|
||||
<input
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(event) => setUsername(event.target.value)}
|
||||
autoComplete="username"
|
||||
placeholder="例如:bwgdi"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label htmlFor="email">邮箱</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
autoComplete="email"
|
||||
placeholder="例如:steven@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label htmlFor="password">密码</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
autoComplete="new-password"
|
||||
placeholder="设置密码"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label htmlFor="confirmPassword">确认密码</label>
|
||||
<input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(event) => setConfirmPassword(event.target.value)}
|
||||
autoComplete="new-password"
|
||||
placeholder="再次输入密码"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="error-text">{error}</div>
|
||||
|
||||
<button className="primary-button" type="submit" disabled={loading}>
|
||||
{loading ? '注册中...' : '注册并进入容器'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="auth-footer">
|
||||
已有账号? <Link href={withNext('/login', nextPath)}>去登录</Link>
|
||||
</div>
|
||||
|
||||
<div className="status-panel">
|
||||
Portal 会先调用部署机接口创建实例,再把浏览器跳到实例自己的 URL。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
602
auth-portal/src/docs/api.md
Normal file
602
auth-portal/src/docs/api.md
Normal file
@ -0,0 +1,602 @@
|
||||
# Nanobot Auth Portal 接口文档
|
||||
|
||||
## 1. 文档范围
|
||||
|
||||
本文档覆盖 `nanobot-auth-portal` 当前实际依赖和对接的接口:
|
||||
|
||||
- Auth Portal 前端页面路由:`/`、`/login`、`/register`
|
||||
- 后端认证接口:`/api/auth/*`
|
||||
- 登录完成后的浏览器交接接口:目标业务前端 `/handoff`
|
||||
|
||||
说明:
|
||||
|
||||
- Auth Portal 自身是一个独立前端,不在本仓库内实现后端 API。
|
||||
- 文档中的后端接口来自当前联调使用的 `nanobot-backend` 实现。
|
||||
- 当前项目只直接调用 `POST /api/auth/login` 和 `POST /api/auth/register`,但为了便于联调,本文档一并补全了同一条登录链路上的 `handoff`、`me`、`logout` 接口。
|
||||
|
||||
## 2. 服务地址与环境变量
|
||||
|
||||
### 2.1 Auth Portal 页面地址
|
||||
|
||||
默认运行在当前主机 `3081` 端口。
|
||||
|
||||
### 2.2 后端 API Base URL
|
||||
|
||||
Auth Portal 按以下优先级计算后端地址:
|
||||
|
||||
1. `NEXT_PUBLIC_BACKEND_API_URL`
|
||||
2. 浏览器当前域名 + `NEXT_PUBLIC_BACKEND_API_PORT`,默认 `10000`
|
||||
3. SSR 回退地址 `http://127.0.0.1:10000`
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
NEXT_PUBLIC_BACKEND_API_URL=https://nanobot-api.bwgdi.com
|
||||
NEXT_PUBLIC_BACKEND_API_PORT=10000
|
||||
NEXT_PUBLIC_FRONTEND_PORT=3080
|
||||
```
|
||||
|
||||
### 2.3 目标业务前端 Base URL
|
||||
|
||||
登录或注册成功后,Auth Portal 会从响应里解析目标前端地址,优先级如下:
|
||||
|
||||
1. `backend_connection.frontend_base_url`
|
||||
2. `backend_connection.api_base_url`
|
||||
3. `backend_connection.public_base_url`
|
||||
4. `local_backend.public_base_url`
|
||||
|
||||
如果命中的是后 2 到 4 项,Auth Portal 会将端口替换为 `NEXT_PUBLIC_FRONTEND_PORT`,默认 `3080`。
|
||||
|
||||
## 3. 公共约定
|
||||
|
||||
### 3.1 请求格式
|
||||
|
||||
- 请求体格式:`application/json`
|
||||
- 成功响应:JSON
|
||||
- Auth Portal 前端请求超时:`8000ms`
|
||||
|
||||
### 3.2 鉴权方式
|
||||
|
||||
- `POST /api/auth/login`:无需鉴权
|
||||
- `POST /api/auth/register`:无需鉴权
|
||||
- `POST /api/auth/handoff/consume`:无需鉴权
|
||||
- `GET /api/auth/me`:需要 `Authorization: Bearer <access_token>`
|
||||
- `POST /api/auth/logout`:可带 `Authorization: Bearer <access_token>`
|
||||
|
||||
### 3.3 错误响应格式
|
||||
|
||||
后端主要返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
Auth Portal 前端收到非 `2xx` 时,会转成如下错误文案:
|
||||
|
||||
```text
|
||||
接口错误 <status>: <detail>
|
||||
```
|
||||
|
||||
### 3.4 Token 与 handoff 约定
|
||||
|
||||
- `access_token` 是后端进程内维护的 Web 会话 token。
|
||||
- `refresh_token` 当前实现始终返回空字符串 `""`。
|
||||
- 当前认证链路没有独立的 `/api/auth/refresh`。
|
||||
- `handoff_code` 是短时有效的浏览器交接码。
|
||||
- `handoff_code` 默认 TTL 为 `90` 秒。
|
||||
- `handoff_code` 被消费后默认允许 `15` 秒内的短暂重放窗口,用于前端页面刷新或重试。
|
||||
|
||||
## 4. 数据模型
|
||||
|
||||
### 4.1 BackendConnectionInfo
|
||||
|
||||
```json
|
||||
{
|
||||
"backend_id": "backend-001",
|
||||
"client_id": "client-001",
|
||||
"name": "Boardware Genius",
|
||||
"public_base_url": "https://nanobot-api.example.com",
|
||||
"api_base_url": "https://nanobot-api.example.com",
|
||||
"ws_base_url": "wss://nanobot-api.example.com",
|
||||
"frontend_base_url": "https://nanobot.example.com",
|
||||
"registered": true
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
- `backend_id`:backend 唯一标识
|
||||
- `client_id`:backend 在 AuthZ 中的客户端 ID
|
||||
- `name`:backend 名称
|
||||
- `public_base_url`:公开后端地址
|
||||
- `api_base_url`:业务 API 地址
|
||||
- `ws_base_url`:WebSocket 地址
|
||||
- `frontend_base_url`:目标业务前端入口地址
|
||||
- `registered`:当前 backend 是否已完成本地身份注册
|
||||
|
||||
### 4.2 AuthzLocalBackendStatus
|
||||
|
||||
```json
|
||||
{
|
||||
"backend_id": "backend-001",
|
||||
"client_id": "client-001",
|
||||
"name": "Boardware Genius",
|
||||
"public_base_url": "https://nanobot-api.example.com",
|
||||
"authz": {
|
||||
"enabled": true,
|
||||
"base_url": "https://authz.example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
- `backend_id`:本地 backend identity 中记录的 backend ID
|
||||
- `client_id`:本地 backend identity 中记录的 client ID
|
||||
- `name`:本地 backend 名称
|
||||
- `public_base_url`:本地 backend 对外地址
|
||||
- `authz.enabled`:是否启用 AuthZ
|
||||
- `authz.base_url`:当前 backend 使用的 AuthZ 服务地址
|
||||
|
||||
### 4.3 RegisterAuthzStatus
|
||||
|
||||
仅 `POST /api/auth/register` 响应会返回。
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"base_url": "https://authz.example.com",
|
||||
"user_registered": true,
|
||||
"backend_registered": true
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
- `enabled`:本次注册是否启用了 AuthZ
|
||||
- `base_url`:本次注册使用的 AuthZ 服务地址
|
||||
- `user_registered`:用户是否已在 AuthZ 完成注册或确认存在
|
||||
- `backend_registered`:backend 身份是否已在 AuthZ 完成注册
|
||||
|
||||
### 4.4 TokenResponse
|
||||
|
||||
登录、注册、handoff 消费都会返回 token 响应,但字段不完全相同。
|
||||
|
||||
```json
|
||||
{
|
||||
"access_token": "opaque-token",
|
||||
"refresh_token": "",
|
||||
"token_type": "bearer",
|
||||
"user_id": "bwgdi",
|
||||
"username": "bwgdi",
|
||||
"email": "steven@example.com",
|
||||
"role": "owner",
|
||||
"handoff_code": "short-lived-code",
|
||||
"handoff_expires_at": 1760000000,
|
||||
"existing_user": false,
|
||||
"authz": {
|
||||
"enabled": true,
|
||||
"base_url": "https://authz.example.com",
|
||||
"user_registered": true,
|
||||
"backend_registered": true
|
||||
},
|
||||
"backend_connection": {
|
||||
"backend_id": "backend-001",
|
||||
"client_id": "client-001",
|
||||
"name": "Boardware Genius",
|
||||
"public_base_url": "https://nanobot-api.example.com",
|
||||
"api_base_url": "https://nanobot-api.example.com",
|
||||
"ws_base_url": "wss://nanobot-api.example.com",
|
||||
"frontend_base_url": "https://nanobot.example.com",
|
||||
"registered": true
|
||||
},
|
||||
"local_backend": {
|
||||
"backend_id": "backend-001",
|
||||
"client_id": "client-001",
|
||||
"name": "Boardware Genius",
|
||||
"public_base_url": "https://nanobot-api.example.com",
|
||||
"authz": {
|
||||
"enabled": true,
|
||||
"base_url": "https://authz.example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
- `access_token`:后端签发的登录 token
|
||||
- `refresh_token`:当前固定为空字符串
|
||||
- `token_type`:当前固定为 `bearer`
|
||||
- `user_id`:当前等于用户名
|
||||
- `username`:用户名
|
||||
- `email`:仅注册响应会返回,登录响应通常没有
|
||||
- `role`:当前固定为 `owner`
|
||||
- `handoff_code`:仅登录/注册响应返回,用于跳转到目标前端
|
||||
- `handoff_expires_at`:UNIX 时间戳,单位秒
|
||||
- `existing_user`:仅注册响应返回,表示这次注册是否命中了已有用户
|
||||
- `authz`:仅注册响应返回,表示本次 AuthZ 注册结果
|
||||
- `backend_connection`:当前登录用户对应的目标 backend 路由信息
|
||||
- `local_backend`:本地 backend identity 视图
|
||||
|
||||
### 4.5 AuthUser
|
||||
|
||||
`GET /api/auth/me` 返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "bwgdi",
|
||||
"username": "bwgdi",
|
||||
"email": "",
|
||||
"role": "owner",
|
||||
"quota_tier": "single-user"
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 前端页面路由
|
||||
|
||||
### 5.1 GET /
|
||||
|
||||
用途:Portal 首页。
|
||||
|
||||
行为:立即重定向到 `/login`。
|
||||
|
||||
### 5.2 GET /login
|
||||
|
||||
用途:显示登录页面。
|
||||
|
||||
Query 参数:
|
||||
|
||||
- `next`:登录成功后希望进入的目标业务前端路径,默认 `/`
|
||||
|
||||
示例:
|
||||
|
||||
```text
|
||||
/login?next=/mcp
|
||||
```
|
||||
|
||||
成功后浏览器会跳转到:
|
||||
|
||||
```text
|
||||
<frontend_base_url>/handoff?code=<handoff_code>&next=/mcp
|
||||
```
|
||||
|
||||
### 5.3 GET /register
|
||||
|
||||
用途:显示注册页面。
|
||||
|
||||
Query 参数:
|
||||
|
||||
- `next`:注册成功后希望进入的目标业务前端路径,默认 `/mcp`
|
||||
|
||||
示例:
|
||||
|
||||
```text
|
||||
/register?next=/mcp
|
||||
```
|
||||
|
||||
## 6. 后端认证接口
|
||||
|
||||
### 6.1 POST /api/auth/login
|
||||
|
||||
用途:用户名密码登录,并返回目标 backend 路由与 handoff 信息。
|
||||
|
||||
鉴权:否。
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "bwgdi",
|
||||
"password": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
请求字段:
|
||||
|
||||
- `username`:必填,登录用户名
|
||||
- `password`:必填,登录密码
|
||||
|
||||
成功响应:`200 OK`
|
||||
|
||||
```json
|
||||
{
|
||||
"access_token": "opaque-token",
|
||||
"refresh_token": "",
|
||||
"token_type": "bearer",
|
||||
"user_id": "bwgdi",
|
||||
"username": "bwgdi",
|
||||
"role": "owner",
|
||||
"handoff_code": "short-lived-code",
|
||||
"handoff_expires_at": 1760000000,
|
||||
"backend_connection": {
|
||||
"backend_id": "backend-001",
|
||||
"client_id": "client-001",
|
||||
"name": "Boardware Genius",
|
||||
"public_base_url": "https://nanobot-api.example.com",
|
||||
"api_base_url": "https://nanobot-api.example.com",
|
||||
"ws_base_url": "wss://nanobot-api.example.com",
|
||||
"frontend_base_url": "https://nanobot.example.com",
|
||||
"registered": true
|
||||
},
|
||||
"local_backend": {
|
||||
"backend_id": "backend-001",
|
||||
"client_id": "client-001",
|
||||
"name": "Boardware Genius",
|
||||
"public_base_url": "https://nanobot-api.example.com",
|
||||
"authz": {
|
||||
"enabled": true,
|
||||
"base_url": "https://authz.example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
错误码:
|
||||
|
||||
- `400 Bad Request`:`username` 为空
|
||||
- `401 Unauthorized`:用户名或密码错误
|
||||
- `500 Internal Server Error`:本地用户文件不存在或格式非法
|
||||
|
||||
实现备注:
|
||||
|
||||
- 登录成功后,Portal 必须使用 `handoff_code` 跳到目标业务前端 `/handoff`。
|
||||
- 如果后端没有返回 `frontend_base_url` 或 `handoff_code`,Portal 会直接提示错误,不会继续跳转。
|
||||
|
||||
### 6.2 POST /api/auth/register
|
||||
|
||||
用途:创建本地登录账号,并在需要时完成 AuthZ 用户/后端注册,然后返回 handoff 信息。
|
||||
|
||||
鉴权:否。
|
||||
|
||||
当前 Portal 页面实际发送字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "bwgdi",
|
||||
"email": "steven@example.com",
|
||||
"password": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
后端支持的完整请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "bwgdi",
|
||||
"email": "steven@example.com",
|
||||
"password": "123456",
|
||||
"authz_base_url": "https://authz.example.com",
|
||||
"backend_name": "Boardware Genius",
|
||||
"backend_id": "backend-001",
|
||||
"base_url": "https://nanobot-api.example.com",
|
||||
"frontend_base_url": "https://nanobot.example.com"
|
||||
}
|
||||
```
|
||||
|
||||
请求字段:
|
||||
|
||||
- `username`:必填,用户名
|
||||
- `email`:选填,邮箱
|
||||
- `password`:必填,密码
|
||||
- `authz_base_url`:选填,覆盖后端当前配置的 AuthZ 地址
|
||||
- `backend_name`:选填,注册 backend 时使用的名称
|
||||
- `backend_id`:选填,期望写入或复用的 backend ID
|
||||
- `base_url`:选填,注册 backend 时使用的公开 API 地址
|
||||
- `frontend_base_url`:选填,注册 backend 时使用的公开前端地址
|
||||
|
||||
成功响应:当前实现为 `200 OK`
|
||||
|
||||
```json
|
||||
{
|
||||
"access_token": "opaque-token",
|
||||
"refresh_token": "",
|
||||
"token_type": "bearer",
|
||||
"user_id": "bwgdi",
|
||||
"username": "bwgdi",
|
||||
"email": "steven@example.com",
|
||||
"role": "owner",
|
||||
"handoff_code": "short-lived-code",
|
||||
"handoff_expires_at": 1760000000,
|
||||
"existing_user": false,
|
||||
"authz": {
|
||||
"enabled": true,
|
||||
"base_url": "https://authz.example.com",
|
||||
"user_registered": true,
|
||||
"backend_registered": true
|
||||
},
|
||||
"backend_connection": {
|
||||
"backend_id": "backend-001",
|
||||
"client_id": "client-001",
|
||||
"name": "Boardware Genius",
|
||||
"public_base_url": "https://nanobot-api.example.com",
|
||||
"api_base_url": "https://nanobot-api.example.com",
|
||||
"ws_base_url": "wss://nanobot-api.example.com",
|
||||
"frontend_base_url": "https://nanobot.example.com",
|
||||
"registered": true
|
||||
},
|
||||
"local_backend": {
|
||||
"backend_id": "backend-001",
|
||||
"client_id": "client-001",
|
||||
"name": "Boardware Genius",
|
||||
"public_base_url": "https://nanobot-api.example.com",
|
||||
"authz": {
|
||||
"enabled": true,
|
||||
"base_url": "https://authz.example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
错误码:
|
||||
|
||||
- `400 Bad Request`:`username` 或 `password` 缺失
|
||||
- `409 Conflict`:用户名已存在,且提交的密码与已有密码不一致
|
||||
- `4xx/5xx`:AuthZ 上游服务直接返回的错误
|
||||
- `502 Bad Gateway`:调用 AuthZ 服务失败
|
||||
|
||||
实现备注:
|
||||
|
||||
- 如果用户名已存在但密码相同,后端会把本次请求视为“继续完成配置”,不会直接报错。
|
||||
- 如果本地 `web_auth_users.json` 不存在,注册会自动创建。
|
||||
- 注册成功后同样必须依赖 `handoff_code` 进入目标业务前端。
|
||||
|
||||
### 6.3 POST /api/auth/handoff/consume
|
||||
|
||||
用途:由目标业务前端消费 `handoff_code`,换取可直接使用的 `access_token`。
|
||||
|
||||
鉴权:否。
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "short-lived-code"
|
||||
}
|
||||
```
|
||||
|
||||
成功响应:`200 OK`
|
||||
|
||||
```json
|
||||
{
|
||||
"access_token": "opaque-token",
|
||||
"refresh_token": "",
|
||||
"token_type": "bearer",
|
||||
"user_id": "bwgdi",
|
||||
"username": "bwgdi",
|
||||
"role": "owner"
|
||||
}
|
||||
```
|
||||
|
||||
错误码:
|
||||
|
||||
- `400 Bad Request`:`code` 为空
|
||||
- `401 Unauthorized`:`code` 无效、已失效,或 payload 非法
|
||||
- `410 Gone`:`code` 已过期,或已超过重放窗口
|
||||
|
||||
实现备注:
|
||||
|
||||
- 当前目标前端在 `/handoff` 页面里调用该接口。
|
||||
- 目标前端拿到 token 后会继续调用 `GET /api/auth/me` 校验登录态。
|
||||
|
||||
### 6.4 GET /api/auth/me
|
||||
|
||||
用途:获取当前登录用户信息。
|
||||
|
||||
鉴权:是,必须提供 `Authorization: Bearer <access_token>`。
|
||||
|
||||
请求头:
|
||||
|
||||
```http
|
||||
Authorization: Bearer opaque-token
|
||||
```
|
||||
|
||||
成功响应:`200 OK`
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "bwgdi",
|
||||
"username": "bwgdi",
|
||||
"email": "",
|
||||
"role": "owner",
|
||||
"quota_tier": "single-user"
|
||||
}
|
||||
```
|
||||
|
||||
错误码:
|
||||
|
||||
- `401 Unauthorized`:缺少 `Authorization` 头
|
||||
- `401 Unauthorized`:`Authorization` 头格式错误
|
||||
- `401 Unauthorized`:token 为空
|
||||
- `401 Unauthorized`:token 无效或已失效
|
||||
|
||||
### 6.5 POST /api/auth/logout
|
||||
|
||||
用途:注销当前 token。
|
||||
|
||||
鉴权:可选。带 token 时会尝试从后端内存中删除该 token。
|
||||
|
||||
请求头:
|
||||
|
||||
```http
|
||||
Authorization: Bearer opaque-token
|
||||
```
|
||||
|
||||
成功响应:`200 OK`
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
```
|
||||
|
||||
实现备注:
|
||||
|
||||
- 即使没有传 token,接口也会返回 `{"ok": true}`。
|
||||
- Portal 或业务前端通常还会同步清理浏览器本地 token。
|
||||
|
||||
## 7. 登录与跳转链路
|
||||
|
||||
### 7.1 登录链路
|
||||
|
||||
1. 用户访问目标业务前端的受保护页面,例如 `/mcp`
|
||||
2. 业务前端发现未登录,跳转到 Auth Portal:`/login?next=/mcp`
|
||||
3. Auth Portal 提交 `POST /api/auth/login`
|
||||
4. 后端返回 `handoff_code` 与 `backend_connection.frontend_base_url`
|
||||
5. Auth Portal 跳转到目标业务前端:
|
||||
|
||||
```text
|
||||
https://target-frontend.example.com/handoff?code=<handoff_code>&next=/mcp
|
||||
```
|
||||
|
||||
6. 目标业务前端在 `/handoff` 页面调用 `POST /api/auth/handoff/consume`
|
||||
7. 目标业务前端保存 token 后调用 `GET /api/auth/me`
|
||||
8. 用户最终进入 `/mcp`
|
||||
|
||||
### 7.2 注册链路
|
||||
|
||||
1. 用户访问 Auth Portal:`/register?next=/mcp`
|
||||
2. Auth Portal 提交 `POST /api/auth/register`
|
||||
3. 后端按需创建本地用户,并与 AuthZ 同步用户/后端身份
|
||||
4. 后端返回 `handoff_code` 与目标前端地址
|
||||
5. Portal 跳转到目标业务前端 `/handoff`
|
||||
6. 后续步骤与登录链路一致
|
||||
|
||||
## 8. 联调示例
|
||||
|
||||
### 8.1 登录
|
||||
|
||||
```bash
|
||||
curl -X POST 'https://nanobot-api.example.com/api/auth/login' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"username": "bwgdi",
|
||||
"password": "123456"
|
||||
}'
|
||||
```
|
||||
|
||||
### 8.2 消费 handoff_code
|
||||
|
||||
```bash
|
||||
curl -X POST 'https://nanobot-api.example.com/api/auth/handoff/consume' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"code": "short-lived-code"
|
||||
}'
|
||||
```
|
||||
|
||||
### 8.3 获取当前用户
|
||||
|
||||
```bash
|
||||
curl 'https://nanobot-api.example.com/api/auth/me' \
|
||||
-H 'Authorization: Bearer opaque-token'
|
||||
```
|
||||
|
||||
## 9. 当前实现限制
|
||||
|
||||
- `refresh_token` 目前没有实际用途,返回值固定为空字符串。
|
||||
- token 与 handoff code 都保存在后端进程内存中,后端重启后会失效。
|
||||
- 本仓库前端页面目前没有暴露 `register` 的高级可选字段,只使用基础注册参数。
|
||||
- `GET /api/auth/me` 当前返回的 `email` 固定为空字符串,不会回填注册邮箱。
|
||||
2
auth-portal/src/env_template
Normal file
2
auth-portal/src/env_template
Normal file
@ -0,0 +1,2 @@
|
||||
DEPLOY_API_BASE_URL=http://127.0.0.1:8090
|
||||
DEPLOY_API_TOKEN=change-me
|
||||
5
auth-portal/src/next-env.d.ts
vendored
Normal file
5
auth-portal/src/next-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
7
auth-portal/src/next.config.js
Normal file
7
auth-portal/src/next.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
||||
511
auth-portal/src/package-lock.json
generated
Normal file
511
auth-portal/src/package-lock.json
generated
Normal file
@ -0,0 +1,511 @@
|
||||
{
|
||||
"name": "nanobot-auth-portal",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nanobot-auth-portal",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"next": "13.5.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.6.2",
|
||||
"@types/react": "18.2.22",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "13.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.1.tgz",
|
||||
"integrity": "sha512-CIMWiOTyflFn/GFx33iYXkgLSQsMQZV4jB91qaj/TfxGaGOXxn8C1j72TaUSPIyN7ziS/AYG46kGmnvuk1oOpg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "13.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.1.tgz",
|
||||
"integrity": "sha512-Bcd0VFrLHZnMmJy6LqV1CydZ7lYaBao8YBEdQUVzV8Ypn/l5s//j5ffjfvMzpEQ4mzlAj3fIY+Bmd9NxpWhACw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "13.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.1.tgz",
|
||||
"integrity": "sha512-uvTZrZa4D0bdWa1jJ7X1tBGIxzpqSnw/ATxWvoRO9CVBvXSx87JyuISY+BWsfLFF59IRodESdeZwkWM2l6+Kjg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "13.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.1.tgz",
|
||||
"integrity": "sha512-/52ThlqdORPQt3+AlMoO+omicdYyUEDeRDGPAj86ULpV4dg+/GCFCKAmFWT0Q4zChFwsAoZUECLcKbRdcc0SNg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "13.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.1.tgz",
|
||||
"integrity": "sha512-L4qNXSOHeu1hEAeeNsBgIYVnvm0gg9fj2O2Yx/qawgQEGuFBfcKqlmIE/Vp8z6gwlppxz5d7v6pmHs1NB6R37w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "13.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.1.tgz",
|
||||
"integrity": "sha512-QVvMrlrFFYvLtABk092kcZ5Mzlmsk2+SV3xYuAu8sbTuIoh0U2+HGNhVklmuYCuM3DAAxdiMQTNlRQmNH11udw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "13.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.1.tgz",
|
||||
"integrity": "sha512-bBnr+XuWc28r9e8gQ35XBtyi5KLHLhTbEvrSgcWna8atI48sNggjIK8IyiEBO3KIrcUVXYkldAzGXPEYMnKt1g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "13.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.1.tgz",
|
||||
"integrity": "sha512-EQGeE4S5c9v06jje9gr4UlxqUEA+zrsgPi6kg9VwR+dQHirzbnVJISF69UfKVkmLntknZJJI9XpWPB6q0Z7mTg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-ia32-msvc": {
|
||||
"version": "13.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.1.tgz",
|
||||
"integrity": "sha512-1y31Q6awzofVjmbTLtRl92OX3s+W0ZfO8AP8fTnITcIo9a6ATDc/eqa08fd6tSpFu6IFpxOBbdevOjwYTGx/AQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "13.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.1.tgz",
|
||||
"integrity": "sha512-+9XBQizy7X/GuwNegq+5QkkxAPV7SBsIwapVRQd9WSvvU20YO23B3bZUpevdabi4fsd25y9RJDDncljy/V54ww==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
|
||||
"integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz",
|
||||
"integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.2.22",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.22.tgz",
|
||||
"integrity": "sha512-60fLTOLqzarLED2O3UQImc/lsNRgG0jE/a1mPW9KjMemY0LMITWEsbS4VvZ4p6rorEHd5YKxxmMKSDK505GHpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz",
|
||||
"integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/scheduler": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.26.0.tgz",
|
||||
"integrity": "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||
"dependencies": {
|
||||
"streamsearch": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001777",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz",
|
||||
"integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/client-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob-to-regexp": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
|
||||
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "13.5.1",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-13.5.1.tgz",
|
||||
"integrity": "sha512-GIudNR7ggGUZoIL79mSZcxbXK9f5pwAIPZxEM8+j2yLqv5RODg4TkmUlaKSYVqE1bPQueamXSqdC3j7axiTSEg==",
|
||||
"deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "13.5.1",
|
||||
"@swc/helpers": "0.5.2",
|
||||
"busboy": "1.6.0",
|
||||
"caniuse-lite": "^1.0.30001406",
|
||||
"postcss": "8.4.14",
|
||||
"styled-jsx": "5.1.1",
|
||||
"watchpack": "2.4.0",
|
||||
"zod": "3.21.4"
|
||||
},
|
||||
"bin": {
|
||||
"next": "dist/bin/next"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "13.5.1",
|
||||
"@next/swc-darwin-x64": "13.5.1",
|
||||
"@next/swc-linux-arm64-gnu": "13.5.1",
|
||||
"@next/swc-linux-arm64-musl": "13.5.1",
|
||||
"@next/swc-linux-x64-gnu": "13.5.1",
|
||||
"@next/swc-linux-x64-musl": "13.5.1",
|
||||
"@next/swc-win32-arm64-msvc": "13.5.1",
|
||||
"@next/swc-win32-ia32-msvc": "13.5.1",
|
||||
"@next/swc-win32-x64-msvc": "13.5.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sass": "^1.3.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.14",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
|
||||
"integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.4",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/styled-jsx": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
|
||||
"integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"client-only": "0.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@babel/core": {
|
||||
"optional": true
|
||||
},
|
||||
"babel-plugin-macros": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
||||
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||
"integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"graceful-fs": "^4.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.21.4",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
|
||||
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
auth-portal/src/package.json
Normal file
23
auth-portal/src/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "nanobot-auth-portal",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3081 -H 0.0.0.0",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3081 -H 0.0.0.0",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "13.5.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.6.2",
|
||||
"@types/react": "18.2.22",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
42
auth-portal/src/tsconfig.json
Normal file
42
auth-portal/src/tsconfig.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
46
auth-portal/src/types/auth.ts
Normal file
46
auth-portal/src/types/auth.ts
Normal file
@ -0,0 +1,46 @@
|
||||
export interface AuthzConnectionInfo {
|
||||
enabled?: boolean;
|
||||
base_url?: string | null;
|
||||
}
|
||||
|
||||
export interface BackendConnectionInfo {
|
||||
backend_id?: string | null;
|
||||
client_id?: string | null;
|
||||
name?: string | null;
|
||||
public_base_url?: string | null;
|
||||
api_base_url?: string | null;
|
||||
ws_base_url?: string | null;
|
||||
frontend_base_url?: string | null;
|
||||
registered?: boolean;
|
||||
}
|
||||
|
||||
export interface AuthzLocalBackendStatus {
|
||||
backend_id?: string | null;
|
||||
client_id?: string | null;
|
||||
name?: string | null;
|
||||
public_base_url?: string | null;
|
||||
authz?: AuthzConnectionInfo | null;
|
||||
}
|
||||
|
||||
export interface RegisterAuthzStatus {
|
||||
enabled: boolean;
|
||||
base_url?: string | null;
|
||||
user_registered: boolean;
|
||||
backend_registered: boolean;
|
||||
}
|
||||
|
||||
export interface TokenResponse {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
token_type: string;
|
||||
user_id: string;
|
||||
username: string;
|
||||
email?: string;
|
||||
role: string;
|
||||
handoff_code?: string;
|
||||
handoff_expires_at?: number;
|
||||
existing_user?: boolean;
|
||||
authz?: RegisterAuthzStatus | null;
|
||||
backend_connection?: BackendConnectionInfo | null;
|
||||
local_backend?: AuthzLocalBackendStatus | null;
|
||||
}
|
||||
Reference in New Issue
Block a user