Add /careerbot base path for www.ityb.me/careerbot deployment
- Add BASE_PATH config, include all routers with prefix
- Inject {{ base }} Jinja2 global for all template URLs
- Add window.BASE_PATH for static JS files
- Update Nginx to proxy /careerbot/ path
- Add OPS_MANUAL.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
96997daed0
commit
501f8985ec
396
OPS_MANUAL.md
Normal file
396
OPS_MANUAL.md
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
# CareerBot 线上运维手册
|
||||||
|
|
||||||
|
## 1. 服务器信息
|
||||||
|
|
||||||
|
| 项目 | 详情 |
|
||||||
|
|------|------|
|
||||||
|
| 云服务商 | 阿里云 ECS |
|
||||||
|
| 公网 IP | `39.106.14.107` |
|
||||||
|
| 操作系统 | Alibaba Cloud Linux 3 (OpenAnolis Edition) |
|
||||||
|
| 配置 | 2 核 CPU / 2GB 内存 / 40GB 磁盘 |
|
||||||
|
| Python 版本 | 3.11.13 |
|
||||||
|
| Nginx 版本 | 1.20.1 |
|
||||||
|
| Gitea 版本 | 1.22.6 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 账号信息
|
||||||
|
|
||||||
|
### 2.1 服务器账号
|
||||||
|
|
||||||
|
| 账号 | 用途 | 登录方式 |
|
||||||
|
|------|------|---------|
|
||||||
|
| `root` | 系统管理 | SSH 密钥 (`~/.ssh/id_ed25519`) |
|
||||||
|
| `deploy` | 应用部署和运行 | SSH 密钥(同上),sudo 免密 |
|
||||||
|
| `gitea` | Gitea 服务运行 | 系统用户,不可直接登录 |
|
||||||
|
| `nginx` | Nginx 服务运行 | 系统用户,不可直接登录 |
|
||||||
|
|
||||||
|
**本地 SSH 快捷连接**(已配置在 `~/.ssh/config`):
|
||||||
|
```bash
|
||||||
|
ssh ecs # 以 deploy 用户连接
|
||||||
|
ssh root@ecs # 以 root 用户连接(需将 config 中 User 临时改为 root,或直接用 IP)
|
||||||
|
ssh root@39.106.14.107 # root 直连
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 应用账号
|
||||||
|
|
||||||
|
| 系统 | 地址 | 账号 | 密码 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| CareerBot 管理后台 | `http://39.106.14.107/admin/login` | `ln0422@gmail.com` | `qshs123456` |
|
||||||
|
| Gitea 代码管理 | `http://39.106.14.107:3000` | `ln0422` | `Qshs123456_` |
|
||||||
|
|
||||||
|
### 2.3 ECS 用户密码
|
||||||
|
|
||||||
|
| 用户 | 密码 |
|
||||||
|
|------|------|
|
||||||
|
| `deploy` | `CareerBot2026!` |
|
||||||
|
|
||||||
|
> **注意**:日常运维通过 SSH 密钥登录,密码仅在密钥不可用时作为备用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 服务架构
|
||||||
|
|
||||||
|
```
|
||||||
|
外部访问
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ Nginx (:80) │
|
||||||
|
│ 配置: /etc/nginx/sites-enabled/ │
|
||||||
|
│ 日志: /var/log/nginx/ │
|
||||||
|
├─────────────────────────────────────────┤
|
||||||
|
│ http://39.106.14.107/ │
|
||||||
|
│ → proxy_pass http://127.0.0.1:8000 │
|
||||||
|
│ → CareerBot (uvicorn) │
|
||||||
|
├─────────────────────────────────────────┤
|
||||||
|
│ http://39.106.14.107:3000 │
|
||||||
|
│ → Gitea (直接监听,未经 Nginx 代理) │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 端口使用
|
||||||
|
|
||||||
|
| 端口 | 服务 | 监听地址 | 防火墙 | 安全组 |
|
||||||
|
|------|------|---------|--------|--------|
|
||||||
|
| 22 | SSH | 0.0.0.0 | 已放行 | 已放行 |
|
||||||
|
| 80 | Nginx → CareerBot | 0.0.0.0 | 已放行 | 已放行 |
|
||||||
|
| 443 | HTTPS (预留) | - | 已放行 | 已放行 |
|
||||||
|
| 3000 | Gitea | 0.0.0.0 | 已放行 | 已放行 |
|
||||||
|
| 8000 | CareerBot (uvicorn) | 127.0.0.1 | 内部 | 无需 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 文件目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
/home/deploy/apps/CareerBot/ ← 项目根目录
|
||||||
|
├── venv/ ← Python 虚拟环境
|
||||||
|
├── app/ ← 应用代码
|
||||||
|
├── templates/ ← 页面模板
|
||||||
|
├── careerbot.db ← SQLite 数据库(重要数据!)
|
||||||
|
├── uploads/ ← 用户上传文件
|
||||||
|
├── requirements.txt
|
||||||
|
├── init_data.py
|
||||||
|
├── start.bat / stop.bat ← Windows 本地启动脚本(线上不用)
|
||||||
|
├── DESIGN.md ← 设计文档
|
||||||
|
└── OPS_MANUAL.md ← 本文档
|
||||||
|
|
||||||
|
/etc/systemd/system/
|
||||||
|
├── careerbot.service ← CareerBot systemd 服务
|
||||||
|
└── gitea.service ← Gitea systemd 服务
|
||||||
|
|
||||||
|
/etc/nginx/
|
||||||
|
├── nginx.conf ← Nginx 主配置
|
||||||
|
├── sites-available/
|
||||||
|
│ └── careerbot.conf ← CareerBot 站点配置
|
||||||
|
└── sites-enabled/
|
||||||
|
└── careerbot.conf → ../sites-available/careerbot.conf
|
||||||
|
|
||||||
|
/usr/local/bin/gitea ← Gitea 二进制文件
|
||||||
|
/etc/gitea/app.ini ← Gitea 配置文件
|
||||||
|
/var/lib/gitea/ ← Gitea 数据目录(仓库、数据库)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 常用运维命令
|
||||||
|
|
||||||
|
### 5.1 服务管理
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ── CareerBot ──
|
||||||
|
sudo systemctl status careerbot # 查看状态
|
||||||
|
sudo systemctl restart careerbot # 重启
|
||||||
|
sudo systemctl stop careerbot # 停止
|
||||||
|
sudo systemctl start careerbot # 启动
|
||||||
|
sudo journalctl -u careerbot -f # 实时查看日志
|
||||||
|
sudo journalctl -u careerbot --since "1 hour ago" # 查看最近1小时日志
|
||||||
|
|
||||||
|
# ── Nginx ──
|
||||||
|
sudo systemctl status nginx
|
||||||
|
sudo systemctl reload nginx # 重载配置(不中断连接)
|
||||||
|
sudo systemctl restart nginx # 重启
|
||||||
|
sudo nginx -t # 测试配置文件语法
|
||||||
|
|
||||||
|
# ── Gitea ──
|
||||||
|
sudo systemctl status gitea
|
||||||
|
sudo systemctl restart gitea
|
||||||
|
sudo journalctl -u gitea -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 代码更新部署
|
||||||
|
|
||||||
|
**从本地推送代码并部署到线上(标准流程):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 本地:提交并推送到 Gitea
|
||||||
|
cd D:/Files/Projects/VibeCoding/CareerBot
|
||||||
|
git add .
|
||||||
|
git commit -m "描述改动"
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
# 2. 线上:拉取并重启
|
||||||
|
ssh ecs "cd ~/apps/CareerBot && git pull && sudo systemctl restart careerbot"
|
||||||
|
```
|
||||||
|
|
||||||
|
**一键部署脚本(在本地执行):**
|
||||||
|
```bash
|
||||||
|
ssh ecs "cd ~/apps/CareerBot && git pull origin main && sudo systemctl restart careerbot && sleep 2 && sudo systemctl is-active careerbot"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 数据库操作
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 连接到 SQLite 数据库
|
||||||
|
ssh ecs "cd ~/apps/CareerBot && sqlite3 careerbot.db"
|
||||||
|
|
||||||
|
# 常用 SQL
|
||||||
|
.tables # 查看所有表
|
||||||
|
SELECT * FROM admin_user; # 查看管理员
|
||||||
|
SELECT token, note, max_questions, used_questions, is_active FROM access_token; # 查看令牌
|
||||||
|
SELECT * FROM recruiter_message ORDER BY created_at DESC LIMIT 10; # 最近招聘消息
|
||||||
|
SELECT COUNT(*) FROM chat_history; # 对话记录总数
|
||||||
|
.quit # 退出
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 系统监控
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看内存使用
|
||||||
|
ssh ecs "free -h"
|
||||||
|
|
||||||
|
# 查看磁盘使用
|
||||||
|
ssh ecs "df -h /"
|
||||||
|
|
||||||
|
# 查看各服务内存占用
|
||||||
|
ssh ecs "ps aux --sort=-rss | head -10"
|
||||||
|
|
||||||
|
# 查看端口监听
|
||||||
|
ssh ecs "ss -tlnp"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 配置文件详情
|
||||||
|
|
||||||
|
### 6.1 CareerBot systemd 服务
|
||||||
|
|
||||||
|
文件:`/etc/systemd/system/careerbot.service`
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=CareerBot Web Application
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=deploy
|
||||||
|
WorkingDirectory=/home/deploy/apps/CareerBot
|
||||||
|
ExecStart=/home/deploy/apps/CareerBot/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
Environment=PATH=/home/deploy/apps/CareerBot/venv/bin:/usr/bin
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
关键参数:
|
||||||
|
- `--host 127.0.0.1`:只监听本地,由 Nginx 代理外部访问
|
||||||
|
- `Restart=always`:崩溃后自动重启
|
||||||
|
- `RestartSec=5`:重启间隔 5 秒
|
||||||
|
|
||||||
|
### 6.2 Nginx 站点配置
|
||||||
|
|
||||||
|
文件:`/etc/nginx/sites-available/careerbot.conf`
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name 39.106.14.107;
|
||||||
|
|
||||||
|
client_max_body_size 10M;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
# SSE 流式响应支持(关键!关闭缓冲)
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_cache off;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /uploads/ {
|
||||||
|
alias /home/deploy/apps/CareerBot/uploads/;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
关键配置说明:
|
||||||
|
- `proxy_buffering off` + `proxy_cache off`:**必须关闭**,否则 SSE 流式对话无法实时返回
|
||||||
|
- `proxy_read_timeout 300s`:LLM 长回复可能需要较长时间
|
||||||
|
- `client_max_body_size 10M`:允许上传最大 10MB 文件
|
||||||
|
|
||||||
|
### 6.3 防火墙规则
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看当前规则
|
||||||
|
sudo firewall-cmd --list-ports
|
||||||
|
# 输出: 22/tcp 80/tcp 443/tcp 3000/tcp
|
||||||
|
|
||||||
|
# 添加新端口(如未来部署新项目)
|
||||||
|
sudo firewall-cmd --permanent --add-port=PORT/tcp
|
||||||
|
sudo firewall-cmd --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意**:开放端口还需要在阿里云控制台的**安全组**中同步添加入方向规则,否则外部仍无法访问。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 备份策略
|
||||||
|
|
||||||
|
### 7.1 数据库备份
|
||||||
|
|
||||||
|
CareerBot 的所有业务数据存储在 SQLite 文件中:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 手动备份
|
||||||
|
ssh ecs "cp ~/apps/CareerBot/careerbot.db ~/apps/CareerBot/careerbot.db.bak.$(date +%Y%m%d)"
|
||||||
|
|
||||||
|
# 下载到本地
|
||||||
|
scp ecs:~/apps/CareerBot/careerbot.db ./careerbot_backup_$(date +%Y%m%d).db
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Gitea 数据备份
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Gitea 数据和仓库
|
||||||
|
ssh ecs "sudo tar czf /tmp/gitea-backup.tar.gz /var/lib/gitea /etc/gitea"
|
||||||
|
scp ecs:/tmp/gitea-backup.tar.gz ./gitea-backup.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 需要备份的关键文件
|
||||||
|
|
||||||
|
| 文件/目录 | 说明 | 重要程度 |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `~/apps/CareerBot/careerbot.db` | CareerBot 全部业务数据 | **极高** |
|
||||||
|
| `~/apps/CareerBot/uploads/` | 用户上传的文件 | 高 |
|
||||||
|
| `/var/lib/gitea/` | Gitea 所有仓库和数据 | **极高** |
|
||||||
|
| `/etc/gitea/app.ini` | Gitea 配置 | 中 |
|
||||||
|
| `/etc/nginx/sites-available/` | Nginx 站点配置 | 中 |
|
||||||
|
| `/etc/systemd/system/careerbot.service` | 服务配置 | 低(可重建) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 故障排查
|
||||||
|
|
||||||
|
### 8.1 CareerBot 无法访问
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 检查服务状态
|
||||||
|
sudo systemctl status careerbot
|
||||||
|
|
||||||
|
# 2. 查看错误日志
|
||||||
|
sudo journalctl -u careerbot --since "10 min ago"
|
||||||
|
|
||||||
|
# 3. 检查端口
|
||||||
|
ss -tlnp | grep 8000
|
||||||
|
|
||||||
|
# 4. 检查 Nginx
|
||||||
|
sudo nginx -t
|
||||||
|
sudo systemctl status nginx
|
||||||
|
|
||||||
|
# 5. 手动启动测试
|
||||||
|
cd ~/apps/CareerBot
|
||||||
|
source venv/bin/activate
|
||||||
|
uvicorn app.main:app --host 127.0.0.1 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 AI 对话不工作
|
||||||
|
|
||||||
|
- 检查管理后台 LLM Config 是否已配置(API URL、API Key、Model Name)
|
||||||
|
- 点击 "Test Connection" 测试连通性
|
||||||
|
- 确认 ECS 能访问外网:`curl https://api.deepseek.com`
|
||||||
|
|
||||||
|
### 8.3 Gitea 无法访问
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl status gitea
|
||||||
|
sudo journalctl -u gitea --since "10 min ago"
|
||||||
|
# 检查 3000 端口和安全组
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.4 磁盘/内存不足
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看磁盘
|
||||||
|
df -h /
|
||||||
|
|
||||||
|
# 清理日志
|
||||||
|
sudo journalctl --vacuum-time=7d
|
||||||
|
|
||||||
|
# 查看内存
|
||||||
|
free -h
|
||||||
|
|
||||||
|
# 查看占用最多的进程
|
||||||
|
ps aux --sort=-rss | head -10
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 新项目部署指南
|
||||||
|
|
||||||
|
后续在同一台 ECS 上部署新项目的标准流程:
|
||||||
|
|
||||||
|
1. **Gitea 上创建仓库**:`http://39.106.14.107:3000` → New Repository
|
||||||
|
|
||||||
|
2. **本地推送代码**
|
||||||
|
3. **ECS 上克隆并配置**:
|
||||||
|
```bash
|
||||||
|
cd ~/apps
|
||||||
|
git clone http://39.106.14.107:3000/ln0422/新项目.git
|
||||||
|
# 安装依赖、初始化等
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **创建 systemd 服务**(参考 careerbot.service,注意更换端口号)
|
||||||
|
|
||||||
|
5. **添加 Nginx 站点配置**:
|
||||||
|
```bash
|
||||||
|
sudo vim /etc/nginx/sites-available/新项目.conf
|
||||||
|
sudo ln -s /etc/nginx/sites-available/新项目.conf /etc/nginx/sites-enabled/
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **如需新端口**:防火墙 + 阿里云安全组同步放行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 安全注意事项
|
||||||
|
|
||||||
|
1. **SSH 密钥**:本地私钥 `~/.ssh/id_ed25519` 请妥善保管,丢失需重新生成并更新服务器
|
||||||
|
2. **Gitea 端口**:3000 端口对外开放,建议设置强密码,或后续通过 Nginx 代理并限制访问
|
||||||
|
3. **数据库文件**:`careerbot.db` 不应对外暴露,uvicorn 仅监听 127.0.0.1 已确保安全
|
||||||
|
4. **定期更新**:定期执行 `sudo dnf update -y` 更新系统安全补丁
|
||||||
|
5. **HTTPS**:后续绑定域名后建议配置 Let's Encrypt SSL 证书
|
||||||
@ -8,6 +8,7 @@ class Settings(BaseSettings):
|
|||||||
ALGORITHM: str = "HS256"
|
ALGORITHM: str = "HS256"
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 480
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 480
|
||||||
UPLOAD_DIR: str = "./uploads"
|
UPLOAD_DIR: str = "./uploads"
|
||||||
|
BASE_PATH: str = "/careerbot"
|
||||||
|
|
||||||
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
|
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
|
||||||
|
|
||||||
|
|||||||
29
app/main.py
29
app/main.py
@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
@ -9,18 +10,30 @@ from app.models import AdminUser, Profile
|
|||||||
from app.routers import auth, public, admin, chat
|
from app.routers import auth, public, admin, chat
|
||||||
from app.routers.auth import hash_password
|
from app.routers.auth import hash_password
|
||||||
|
|
||||||
|
BASE = settings.BASE_PATH
|
||||||
|
|
||||||
app = FastAPI(title="CareerBot", description="Personal Career Showcase with AI Assistant")
|
app = FastAPI(title="CareerBot", description="Personal Career Showcase with AI Assistant")
|
||||||
|
|
||||||
# Mount static files and uploads
|
# Mount static files and uploads under BASE_PATH
|
||||||
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
app.mount(f"{BASE}/static", StaticFiles(directory="app/static"), name="static")
|
||||||
os.makedirs(settings.UPLOAD_DIR, exist_ok=True)
|
os.makedirs(settings.UPLOAD_DIR, exist_ok=True)
|
||||||
app.mount("/uploads", StaticFiles(directory=settings.UPLOAD_DIR), name="uploads")
|
app.mount(f"{BASE}/uploads", StaticFiles(directory=settings.UPLOAD_DIR), name="uploads")
|
||||||
|
|
||||||
# Include routers
|
# Include routers with BASE_PATH prefix
|
||||||
app.include_router(auth.router)
|
app.include_router(auth.router, prefix=BASE)
|
||||||
app.include_router(public.router)
|
app.include_router(public.router, prefix=BASE)
|
||||||
app.include_router(admin.router)
|
app.include_router(admin.router, prefix=BASE)
|
||||||
app.include_router(chat.router)
|
app.include_router(chat.router, prefix=BASE)
|
||||||
|
|
||||||
|
# Set Jinja2 template globals for BASE_PATH
|
||||||
|
admin.templates.env.globals["base"] = BASE
|
||||||
|
public.templates.env.globals["base"] = BASE
|
||||||
|
|
||||||
|
|
||||||
|
# Root redirect to CareerBot
|
||||||
|
@app.get("/")
|
||||||
|
def root_redirect():
|
||||||
|
return RedirectResponse(url=f"{BASE}/", status_code=302)
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
|
|||||||
@ -9,6 +9,9 @@ from pydantic import BaseModel
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
|
|
||||||
|
BASE = settings.BASE_PATH
|
||||||
|
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Profile, Skill, Education, WorkExperience,
|
Profile, Skill, Education, WorkExperience,
|
||||||
@ -95,7 +98,7 @@ async def upload_photo(
|
|||||||
if not p:
|
if not p:
|
||||||
p = Profile()
|
p = Profile()
|
||||||
db.add(p)
|
db.add(p)
|
||||||
p.photo_url = f"/uploads/{filename}"
|
p.photo_url = f"{BASE}/uploads/{filename}"
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"photo_url": p.photo_url}
|
return {"photo_url": p.photo_url}
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from app.database import get_db
|
|||||||
from app.models import AdminUser, AccessToken
|
from app.models import AdminUser, AccessToken
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/admin/login", auto_error=False)
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.BASE_PATH}/api/admin/login", auto_error=False)
|
||||||
|
|
||||||
|
|
||||||
def hash_password(password: str) -> str:
|
def hash_password(password: str) -> str:
|
||||||
|
|||||||
@ -3,9 +3,12 @@ from fastapi.responses import HTMLResponse, RedirectResponse
|
|||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.config import settings
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.models import Profile, Skill, Education, WorkExperience, AccessToken
|
from app.models import Profile, Skill, Education, WorkExperience, AccessToken
|
||||||
|
|
||||||
|
BASE = settings.BASE_PATH
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
@ -14,14 +17,14 @@ templates = Jinja2Templates(directory="templates")
|
|||||||
def index_page(request: Request, db: Session = Depends(get_db)):
|
def index_page(request: Request, db: Session = Depends(get_db)):
|
||||||
token = request.cookies.get("visitor_token")
|
token = request.cookies.get("visitor_token")
|
||||||
if not token:
|
if not token:
|
||||||
return RedirectResponse(url="/login", status_code=302)
|
return RedirectResponse(url=f"{BASE}/login", status_code=302)
|
||||||
# Allow both valid tokens and anonymous
|
# Allow both valid tokens and anonymous
|
||||||
if token != "__anonymous__":
|
if token != "__anonymous__":
|
||||||
record = db.query(AccessToken).filter(
|
record = db.query(AccessToken).filter(
|
||||||
AccessToken.token == token, AccessToken.is_active == True
|
AccessToken.token == token, AccessToken.is_active == True
|
||||||
).first()
|
).first()
|
||||||
if not record:
|
if not record:
|
||||||
return RedirectResponse(url="/login", status_code=302)
|
return RedirectResponse(url=f"{BASE}/login", status_code=302)
|
||||||
return templates.TemplateResponse(request, "index.html")
|
return templates.TemplateResponse(request, "index.html")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,36 +1,37 @@
|
|||||||
// ── Admin Panel JavaScript ──
|
// ── Admin Panel JavaScript ──
|
||||||
|
const BASE_PATH = window.BASE_PATH || '';
|
||||||
|
|
||||||
function authHeaders() {
|
function authHeaders() {
|
||||||
return { 'Authorization': 'Bearer ' + localStorage.getItem('admin_token') };
|
return { 'Authorization': 'Bearer ' + localStorage.getItem('admin_token') };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function apiGet(url) {
|
async function apiGet(url) {
|
||||||
const resp = await fetch(url, { headers: authHeaders() });
|
const resp = await fetch(BASE_PATH + url, { headers: authHeaders() });
|
||||||
if (resp.status === 401) { window.location.href = '/admin/login'; return null; }
|
if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return null; }
|
||||||
return resp.json();
|
return resp.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function apiPost(url, data) {
|
async function apiPost(url, data) {
|
||||||
const resp = await fetch(url, {
|
const resp = await fetch(BASE_PATH + url, {
|
||||||
method: 'POST', headers: { ...authHeaders(), 'Content-Type': 'application/json' },
|
method: 'POST', headers: { ...authHeaders(), 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
if (resp.status === 401) { window.location.href = '/admin/login'; return null; }
|
if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return null; }
|
||||||
return resp.json();
|
return resp.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function apiPut(url, data) {
|
async function apiPut(url, data) {
|
||||||
const resp = await fetch(url, {
|
const resp = await fetch(BASE_PATH + url, {
|
||||||
method: 'PUT', headers: { ...authHeaders(), 'Content-Type': 'application/json' },
|
method: 'PUT', headers: { ...authHeaders(), 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
if (resp.status === 401) { window.location.href = '/admin/login'; return null; }
|
if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return null; }
|
||||||
return resp.json();
|
return resp.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function apiDelete(url) {
|
async function apiDelete(url) {
|
||||||
const resp = await fetch(url, { method: 'DELETE', headers: authHeaders() });
|
const resp = await fetch(BASE_PATH + url, { method: 'DELETE', headers: authHeaders() });
|
||||||
if (resp.status === 401) { window.location.href = '/admin/login'; return null; }
|
if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return null; }
|
||||||
return resp.json();
|
return resp.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +69,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
function adminLogout() {
|
function adminLogout() {
|
||||||
localStorage.removeItem('admin_token');
|
localStorage.removeItem('admin_token');
|
||||||
window.location.href = '/admin/login';
|
window.location.href = BASE_PATH + '/admin/login';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Dashboard ──
|
// ── Dashboard ──
|
||||||
@ -113,10 +114,10 @@ async function uploadPhoto() {
|
|||||||
if (!input.files[0]) return;
|
if (!input.files[0]) return;
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', input.files[0]);
|
formData.append('file', input.files[0]);
|
||||||
const resp = await fetch('/api/admin/profile/photo', {
|
const resp = await fetch(BASE_PATH + '/api/admin/profile/photo', {
|
||||||
method: 'POST', headers: authHeaders(), body: formData,
|
method: 'POST', headers: authHeaders(), body: formData,
|
||||||
});
|
});
|
||||||
if (resp.status === 401) { window.location.href = '/admin/login'; return; }
|
if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return; }
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.photo_url) {
|
if (data.photo_url) {
|
||||||
document.getElementById('photo-preview').innerHTML = `<img src="${data.photo_url}" style="max-width:120px;border-radius:8px;">`;
|
document.getElementById('photo-preview').innerHTML = `<img src="${data.photo_url}" style="max-width:120px;border-radius:8px;">`;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
// ── Chat Widget JavaScript ──
|
// ── Chat Widget JavaScript ──
|
||||||
|
const BASE_PATH = window.BASE_PATH || '';
|
||||||
|
|
||||||
let chatSessionId = sessionStorage.getItem('chat_session');
|
let chatSessionId = sessionStorage.getItem('chat_session');
|
||||||
if (!chatSessionId) {
|
if (!chatSessionId) {
|
||||||
@ -66,7 +67,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
async function loadChatConfig() {
|
async function loadChatConfig() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/chat/config');
|
const resp = await fetch(BASE_PATH + '/api/chat/config');
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
maxQuestions = data.max_questions || 10;
|
maxQuestions = data.max_questions || 10;
|
||||||
@ -96,7 +97,7 @@ async function loadChatConfig() {
|
|||||||
|
|
||||||
async function loadChatHistory() {
|
async function loadChatHistory() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/api/chat/history/${chatSessionId}`);
|
const resp = await fetch(BASE_PATH + `/api/chat/history/${chatSessionId}`);
|
||||||
if (!resp.ok) return;
|
if (!resp.ok) return;
|
||||||
const history = await resp.json();
|
const history = await resp.json();
|
||||||
for (const msg of history) {
|
for (const msg of history) {
|
||||||
@ -147,7 +148,7 @@ async function sendMessage() {
|
|||||||
formData.append('message', message);
|
formData.append('message', message);
|
||||||
if (file) formData.append('file', file);
|
if (file) formData.append('file', file);
|
||||||
|
|
||||||
const response = await fetch('/api/chat', {
|
const response = await fetch(BASE_PATH + '/api/chat', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
@ -243,7 +244,7 @@ async function enterToken() {
|
|||||||
const token = prompt('请输入访问令牌 (Access Token):');
|
const token = prompt('请输入访问令牌 (Access Token):');
|
||||||
if (!token || !token.trim()) return;
|
if (!token || !token.trim()) return;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/verify-token', {
|
const resp = await fetch(BASE_PATH + '/api/verify-token', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ token: token.trim() }),
|
body: JSON.stringify({ token: token.trim() }),
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
// ── Main Page JavaScript ──
|
// ── Main Page JavaScript ──
|
||||||
|
const BASE_PATH = window.BASE_PATH || '';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
loadProfile();
|
loadProfile();
|
||||||
@ -8,9 +9,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function apiFetch(url) {
|
async function apiFetch(url) {
|
||||||
const resp = await fetch(url);
|
const resp = await fetch(BASE_PATH + url);
|
||||||
if (resp.status === 401) {
|
if (resp.status === 401) {
|
||||||
window.location.href = '/login';
|
window.location.href = BASE_PATH + '/login';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return resp.json();
|
return resp.json();
|
||||||
|
|||||||
@ -4,22 +4,22 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>CareerBot Admin - Dashboard</title>
|
<title>CareerBot Admin - Dashboard</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="admin-layout">
|
<div class="admin-layout">
|
||||||
<aside class="admin-sidebar">
|
<aside class="admin-sidebar">
|
||||||
<div class="logo">CareerBot Admin</div>
|
<div class="logo">CareerBot Admin</div>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
<a href="{{ base }}/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||||
<a href="/admin/profile"><span class="icon">☺</span> Profile</a>
|
<a href="{{ base }}/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||||
<a href="/admin/education"><span class="icon">☆</span> Education</a>
|
<a href="{{ base }}/admin/education"><span class="icon">☆</span> Education</a>
|
||||||
<a href="/admin/experience"><span class="icon">✎</span> Experience</a>
|
<a href="{{ base }}/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||||
<a href="/admin/skills"><span class="icon">★</span> Skills</a>
|
<a href="{{ base }}/admin/skills"><span class="icon">★</span> Skills</a>
|
||||||
<a href="/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
<a href="{{ base }}/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||||
<a href="/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
<a href="{{ base }}/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||||
<a href="/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
<a href="{{ base }}/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||||
<a href="/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
<a href="{{ base }}/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||||
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
@ -47,6 +47,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/js/admin.js"></script>
|
<script>window.BASE_PATH = "{{ base }}";</script>
|
||||||
|
<script src="{{ base }}/static/js/admin.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -4,22 +4,22 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>CareerBot Admin - Education</title>
|
<title>CareerBot Admin - Education</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="admin-layout">
|
<div class="admin-layout">
|
||||||
<aside class="admin-sidebar">
|
<aside class="admin-sidebar">
|
||||||
<div class="logo">CareerBot Admin</div>
|
<div class="logo">CareerBot Admin</div>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
<a href="{{ base }}/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||||
<a href="/admin/profile"><span class="icon">☺</span> Profile</a>
|
<a href="{{ base }}/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||||
<a href="/admin/education"><span class="icon">☆</span> Education</a>
|
<a href="{{ base }}/admin/education"><span class="icon">☆</span> Education</a>
|
||||||
<a href="/admin/experience"><span class="icon">✎</span> Experience</a>
|
<a href="{{ base }}/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||||
<a href="/admin/skills"><span class="icon">★</span> Skills</a>
|
<a href="{{ base }}/admin/skills"><span class="icon">★</span> Skills</a>
|
||||||
<a href="/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
<a href="{{ base }}/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||||
<a href="/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
<a href="{{ base }}/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||||
<a href="/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
<a href="{{ base }}/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||||
<a href="/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
<a href="{{ base }}/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||||
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
@ -60,6 +60,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/admin.js"></script>
|
<script>window.BASE_PATH = "{{ base }}";</script>
|
||||||
|
<script src="{{ base }}/static/js/admin.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -4,22 +4,22 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>CareerBot Admin - Experience</title>
|
<title>CareerBot Admin - Experience</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="admin-layout">
|
<div class="admin-layout">
|
||||||
<aside class="admin-sidebar">
|
<aside class="admin-sidebar">
|
||||||
<div class="logo">CareerBot Admin</div>
|
<div class="logo">CareerBot Admin</div>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
<a href="{{ base }}/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||||
<a href="/admin/profile"><span class="icon">☺</span> Profile</a>
|
<a href="{{ base }}/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||||
<a href="/admin/education"><span class="icon">☆</span> Education</a>
|
<a href="{{ base }}/admin/education"><span class="icon">☆</span> Education</a>
|
||||||
<a href="/admin/experience"><span class="icon">✎</span> Experience</a>
|
<a href="{{ base }}/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||||
<a href="/admin/skills"><span class="icon">★</span> Skills</a>
|
<a href="{{ base }}/admin/skills"><span class="icon">★</span> Skills</a>
|
||||||
<a href="/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
<a href="{{ base }}/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||||
<a href="/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
<a href="{{ base }}/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||||
<a href="/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
<a href="{{ base }}/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||||
<a href="/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
<a href="{{ base }}/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||||
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
@ -61,6 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/admin.js"></script>
|
<script>window.BASE_PATH = "{{ base }}";</script>
|
||||||
|
<script src="{{ base }}/static/js/admin.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -4,22 +4,22 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>CareerBot Admin - LLM Configuration</title>
|
<title>CareerBot Admin - LLM Configuration</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="admin-layout">
|
<div class="admin-layout">
|
||||||
<aside class="admin-sidebar">
|
<aside class="admin-sidebar">
|
||||||
<div class="logo">CareerBot Admin</div>
|
<div class="logo">CareerBot Admin</div>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
<a href="{{ base }}/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||||
<a href="/admin/profile"><span class="icon">☺</span> Profile</a>
|
<a href="{{ base }}/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||||
<a href="/admin/education"><span class="icon">☆</span> Education</a>
|
<a href="{{ base }}/admin/education"><span class="icon">☆</span> Education</a>
|
||||||
<a href="/admin/experience"><span class="icon">✎</span> Experience</a>
|
<a href="{{ base }}/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||||
<a href="/admin/skills"><span class="icon">★</span> Skills</a>
|
<a href="{{ base }}/admin/skills"><span class="icon">★</span> Skills</a>
|
||||||
<a href="/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
<a href="{{ base }}/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||||
<a href="/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
<a href="{{ base }}/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||||
<a href="/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
<a href="{{ base }}/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||||
<a href="/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
<a href="{{ base }}/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||||
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
@ -76,6 +76,7 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/admin.js"></script>
|
<script>window.BASE_PATH = "{{ base }}";</script>
|
||||||
|
<script src="{{ base }}/static/js/admin.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>CareerBot Admin - Login</title>
|
<title>CareerBot Admin - Login</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
@ -18,6 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
const BASE = "{{ base }}";
|
||||||
document.getElementById('password').addEventListener('keydown', e => {
|
document.getElementById('password').addEventListener('keydown', e => {
|
||||||
if (e.key === 'Enter') doLogin();
|
if (e.key === 'Enter') doLogin();
|
||||||
});
|
});
|
||||||
@ -35,7 +36,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/admin/login', {
|
const resp = await fetch(BASE + '/api/admin/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ email, password }),
|
body: JSON.stringify({ email, password }),
|
||||||
@ -43,7 +44,7 @@
|
|||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
localStorage.setItem('admin_token', data.access_token);
|
localStorage.setItem('admin_token', data.access_token);
|
||||||
window.location.href = '/admin/dashboard';
|
window.location.href = BASE + '/admin/dashboard';
|
||||||
} else {
|
} else {
|
||||||
errEl.textContent = 'Invalid email or password';
|
errEl.textContent = 'Invalid email or password';
|
||||||
errEl.style.display = 'block';
|
errEl.style.display = 'block';
|
||||||
|
|||||||
@ -4,22 +4,22 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>CareerBot Admin - Messages</title>
|
<title>CareerBot Admin - Messages</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="admin-layout">
|
<div class="admin-layout">
|
||||||
<aside class="admin-sidebar">
|
<aside class="admin-sidebar">
|
||||||
<div class="logo">CareerBot Admin</div>
|
<div class="logo">CareerBot Admin</div>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
<a href="{{ base }}/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||||
<a href="/admin/profile"><span class="icon">☺</span> Profile</a>
|
<a href="{{ base }}/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||||
<a href="/admin/education"><span class="icon">☆</span> Education</a>
|
<a href="{{ base }}/admin/education"><span class="icon">☆</span> Education</a>
|
||||||
<a href="/admin/experience"><span class="icon">✎</span> Experience</a>
|
<a href="{{ base }}/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||||
<a href="/admin/skills"><span class="icon">★</span> Skills</a>
|
<a href="{{ base }}/admin/skills"><span class="icon">★</span> Skills</a>
|
||||||
<a href="/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
<a href="{{ base }}/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||||
<a href="/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
<a href="{{ base }}/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||||
<a href="/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
<a href="{{ base }}/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||||
<a href="/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
<a href="{{ base }}/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||||
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
@ -37,6 +37,7 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/admin.js"></script>
|
<script>window.BASE_PATH = "{{ base }}";</script>
|
||||||
|
<script src="{{ base }}/static/js/admin.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -4,22 +4,22 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>CareerBot Admin - Profile</title>
|
<title>CareerBot Admin - Profile</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="admin-layout">
|
<div class="admin-layout">
|
||||||
<aside class="admin-sidebar">
|
<aside class="admin-sidebar">
|
||||||
<div class="logo">CareerBot Admin</div>
|
<div class="logo">CareerBot Admin</div>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
<a href="{{ base }}/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||||
<a href="/admin/profile"><span class="icon">☺</span> Profile</a>
|
<a href="{{ base }}/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||||
<a href="/admin/education"><span class="icon">☆</span> Education</a>
|
<a href="{{ base }}/admin/education"><span class="icon">☆</span> Education</a>
|
||||||
<a href="/admin/experience"><span class="icon">✎</span> Experience</a>
|
<a href="{{ base }}/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||||
<a href="/admin/skills"><span class="icon">★</span> Skills</a>
|
<a href="{{ base }}/admin/skills"><span class="icon">★</span> Skills</a>
|
||||||
<a href="/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
<a href="{{ base }}/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||||
<a href="/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
<a href="{{ base }}/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||||
<a href="/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
<a href="{{ base }}/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||||
<a href="/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
<a href="{{ base }}/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||||
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
@ -75,6 +75,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/js/admin.js"></script>
|
<script>window.BASE_PATH = "{{ base }}";</script>
|
||||||
|
<script src="{{ base }}/static/js/admin.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>CareerBot Admin - Resume Generator</title>
|
<title>CareerBot Admin - Resume Generator</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
.resume-layout { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
|
.resume-layout { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
|
||||||
.resume-output {
|
.resume-output {
|
||||||
@ -28,15 +28,15 @@
|
|||||||
<aside class="admin-sidebar">
|
<aside class="admin-sidebar">
|
||||||
<div class="logo">CareerBot Admin</div>
|
<div class="logo">CareerBot Admin</div>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
<a href="{{ base }}/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||||
<a href="/admin/profile"><span class="icon">☺</span> Profile</a>
|
<a href="{{ base }}/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||||
<a href="/admin/education"><span class="icon">☆</span> Education</a>
|
<a href="{{ base }}/admin/education"><span class="icon">☆</span> Education</a>
|
||||||
<a href="/admin/experience"><span class="icon">✎</span> Experience</a>
|
<a href="{{ base }}/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||||
<a href="/admin/skills"><span class="icon">★</span> Skills</a>
|
<a href="{{ base }}/admin/skills"><span class="icon">★</span> Skills</a>
|
||||||
<a href="/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
<a href="{{ base }}/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||||
<a href="/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
<a href="{{ base }}/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||||
<a href="/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
<a href="{{ base }}/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||||
<a href="/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
<a href="{{ base }}/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||||
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
@ -78,8 +78,10 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/admin.js"></script>
|
<script>window.BASE_PATH = "{{ base }}";</script>
|
||||||
|
<script src="{{ base }}/static/js/admin.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
const BASE = window.BASE_PATH || '';
|
||||||
let resumeMarkdown = '';
|
let resumeMarkdown = '';
|
||||||
|
|
||||||
async function generateResume() {
|
async function generateResume() {
|
||||||
@ -105,13 +107,13 @@ async function generateResume() {
|
|||||||
if (file) formData.append('file', file);
|
if (file) formData.append('file', file);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/admin/resume/generate', {
|
const resp = await fetch(BASE + '/api/admin/resume/generate', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('admin_token') },
|
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('admin_token') },
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (resp.status === 401) { window.location.href = '/admin/login'; return; }
|
if (resp.status === 401) { window.location.href = BASE + '/admin/login'; return; }
|
||||||
|
|
||||||
output.innerHTML = '';
|
output.innerHTML = '';
|
||||||
const reader = resp.body.getReader();
|
const reader = resp.body.getReader();
|
||||||
@ -159,7 +161,7 @@ async function downloadResume() {
|
|||||||
formData.append('content', resumeMarkdown);
|
formData.append('content', resumeMarkdown);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/admin/resume/download', {
|
const resp = await fetch(BASE + '/api/admin/resume/download', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('admin_token') },
|
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('admin_token') },
|
||||||
body: formData,
|
body: formData,
|
||||||
|
|||||||
@ -4,22 +4,22 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>CareerBot Admin - Skills</title>
|
<title>CareerBot Admin - Skills</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="admin-layout">
|
<div class="admin-layout">
|
||||||
<aside class="admin-sidebar">
|
<aside class="admin-sidebar">
|
||||||
<div class="logo">CareerBot Admin</div>
|
<div class="logo">CareerBot Admin</div>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
<a href="{{ base }}/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||||
<a href="/admin/profile"><span class="icon">☺</span> Profile</a>
|
<a href="{{ base }}/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||||
<a href="/admin/education"><span class="icon">☆</span> Education</a>
|
<a href="{{ base }}/admin/education"><span class="icon">☆</span> Education</a>
|
||||||
<a href="/admin/experience"><span class="icon">✎</span> Experience</a>
|
<a href="{{ base }}/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||||
<a href="/admin/skills"><span class="icon">★</span> Skills</a>
|
<a href="{{ base }}/admin/skills"><span class="icon">★</span> Skills</a>
|
||||||
<a href="/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
<a href="{{ base }}/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||||
<a href="/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
<a href="{{ base }}/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||||
<a href="/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
<a href="{{ base }}/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||||
<a href="/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
<a href="{{ base }}/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||||
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
@ -52,6 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/admin.js"></script>
|
<script>window.BASE_PATH = "{{ base }}";</script>
|
||||||
|
<script src="{{ base }}/static/js/admin.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -4,22 +4,22 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>CareerBot Admin - Access Tokens</title>
|
<title>CareerBot Admin - Access Tokens</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="admin-layout">
|
<div class="admin-layout">
|
||||||
<aside class="admin-sidebar">
|
<aside class="admin-sidebar">
|
||||||
<div class="logo">CareerBot Admin</div>
|
<div class="logo">CareerBot Admin</div>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
<a href="{{ base }}/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||||
<a href="/admin/profile"><span class="icon">☺</span> Profile</a>
|
<a href="{{ base }}/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||||
<a href="/admin/education"><span class="icon">☆</span> Education</a>
|
<a href="{{ base }}/admin/education"><span class="icon">☆</span> Education</a>
|
||||||
<a href="/admin/experience"><span class="icon">✎</span> Experience</a>
|
<a href="{{ base }}/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||||
<a href="/admin/skills"><span class="icon">★</span> Skills</a>
|
<a href="{{ base }}/admin/skills"><span class="icon">★</span> Skills</a>
|
||||||
<a href="/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
<a href="{{ base }}/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||||
<a href="/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
<a href="{{ base }}/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||||
<a href="/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
<a href="{{ base }}/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||||
<a href="/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
<a href="{{ base }}/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||||
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
@ -74,6 +74,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/admin.js"></script>
|
<script>window.BASE_PATH = "{{ base }}";</script>
|
||||||
|
<script src="{{ base }}/static/js/admin.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>CareerBot</title>
|
<title>CareerBot</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@ -74,7 +74,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/main.js"></script>
|
<script>window.BASE_PATH = "{{ base }}";</script>
|
||||||
<script src="/static/js/chat.js"></script>
|
<script src="{{ base }}/static/js/main.js"></script>
|
||||||
|
<script src="{{ base }}/static/js/chat.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>CareerBot - Access</title>
|
<title>CareerBot - Access</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
@ -21,6 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
const BASE = "{{ base }}";
|
||||||
document.getElementById('token-input').addEventListener('keydown', e => {
|
document.getElementById('token-input').addEventListener('keydown', e => {
|
||||||
if (e.key === 'Enter') verifyToken();
|
if (e.key === 'Enter') verifyToken();
|
||||||
});
|
});
|
||||||
@ -31,13 +32,13 @@
|
|||||||
const errEl = document.getElementById('error-msg');
|
const errEl = document.getElementById('error-msg');
|
||||||
errEl.style.display = 'none';
|
errEl.style.display = 'none';
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/verify-token', {
|
const resp = await fetch(BASE + '/api/verify-token', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ token }),
|
body: JSON.stringify({ token }),
|
||||||
});
|
});
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
window.location.href = '/';
|
window.location.href = BASE + '/';
|
||||||
} else {
|
} else {
|
||||||
errEl.textContent = '无效或已过期的访问令牌';
|
errEl.textContent = '无效或已过期的访问令牌';
|
||||||
errEl.style.display = 'block';
|
errEl.style.display = 'block';
|
||||||
@ -50,9 +51,9 @@
|
|||||||
|
|
||||||
async function enterAnonymous() {
|
async function enterAnonymous() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/anonymous-entry', { method: 'POST' });
|
const resp = await fetch(BASE + '/api/anonymous-entry', { method: 'POST' });
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
window.location.href = '/';
|
window.location.href = BASE + '/';
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
document.getElementById('error-msg').textContent = '网络错误,请稍后重试';
|
document.getElementById('error-msg').textContent = '网络错误,请稍后重试';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user