first commit

This commit is contained in:
龙澳
2026-03-23 14:32:15 +08:00
parent 7045866ef3
commit 9ce8858562
3 changed files with 26 additions and 13 deletions

View File

@@ -2,7 +2,7 @@ AUTH_DB_PATH=~/.nanobot/auth_service.sqlite3
AUTH_JWT_SECRET=change-this-secret AUTH_JWT_SECRET=change-this-secret
AUTH_TOKEN_TTL_HOURS=24 AUTH_TOKEN_TTL_HOURS=24
AUTH_CORS_ORIGINS=* AUTH_CORS_ORIGINS=*
AUTH_INVITE_CODES=invite-a,invite-b AUTH_VERIFICATION_CODES=code-a,code-b
AUTH_ADMIN_KEY=change-this-admin-key AUTH_ADMIN_KEY=change-this-admin-key
AUTH_HOST=0.0.0.0 AUTH_HOST=0.0.0.0
AUTH_PORT=9100 AUTH_PORT=9100

View File

@@ -4,7 +4,7 @@ Standalone phone/password auth service for nanobot web chat.
## Features ## Features
- `POST /auth/register` (phone + password + invite code; returns pending) - `POST /auth/register` (phone + password + verification code; returns pending)
- `POST /auth/login` - `POST /auth/login`
- `GET /auth/me` (Bearer token) - `GET /auth/me` (Bearer token)
- `GET /auth/register/status/{request_id}` - `GET /auth/register/status/{request_id}`
@@ -30,7 +30,7 @@ uvicorn app.main:app --host ${AUTH_HOST:-0.0.0.0} --port ${AUTH_PORT:-9100}
- `AUTH_JWT_SECRET`: JWT signing secret - `AUTH_JWT_SECRET`: JWT signing secret
- `AUTH_TOKEN_TTL_HOURS`: access token ttl - `AUTH_TOKEN_TTL_HOURS`: access token ttl
- `AUTH_CORS_ORIGINS`: comma-separated origins or `*` - `AUTH_CORS_ORIGINS`: comma-separated origins or `*`
- `AUTH_INVITE_CODES`: comma-separated whitelist (empty means no whitelist check) - `AUTH_VERIFICATION_CODES`: comma-separated whitelist (empty means no whitelist check)
- `AUTH_ADMIN_KEY`: required by admin endpoints - `AUTH_ADMIN_KEY`: required by admin endpoints
- `AUTH_HOST`: bind host (run command) - `AUTH_HOST`: bind host (run command)
- `AUTH_PORT`: bind port (run command) - `AUTH_PORT`: bind port (run command)
@@ -43,7 +43,7 @@ uvicorn app.main:app --host ${AUTH_HOST:-0.0.0.0} --port ${AUTH_PORT:-9100}
{ {
"phone": "13800000000", "phone": "13800000000",
"password": "secret123", "password": "secret123",
"invite_code": "invite-a" "verification_code": "code-a"
} }
``` ```

View File

@@ -27,6 +27,11 @@ AUTH_INVITE_CODES = {
for item in os.getenv("AUTH_INVITE_CODES", "").split(",") for item in os.getenv("AUTH_INVITE_CODES", "").split(",")
if item.strip() if item.strip()
} }
AUTH_VERIFICATION_CODES = {
item.strip()
for item in os.getenv("AUTH_VERIFICATION_CODES", "").split(",")
if item.strip()
}
AUTH_ADMIN_KEY = os.getenv("AUTH_ADMIN_KEY", "") AUTH_ADMIN_KEY = os.getenv("AUTH_ADMIN_KEY", "")
@@ -74,7 +79,8 @@ class AuthPayload(BaseModel):
class RegisterPayload(AuthPayload): class RegisterPayload(AuthPayload):
invite_code: str invite_code: str = ""
verification_code: str = ""
class TokenResponse(BaseModel): class TokenResponse(BaseModel):
@@ -170,12 +176,17 @@ def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat() return datetime.now(timezone.utc).isoformat()
def _require_invite_code(invite_code: str) -> str: def _allowed_verification_codes() -> set[str]:
code = str(invite_code).strip() return AUTH_VERIFICATION_CODES or AUTH_INVITE_CODES
def _require_verification_code(code_value: str) -> str:
code = str(code_value).strip()
if not code: if not code:
raise HTTPException(status_code=400, detail="invite code is required") raise HTTPException(status_code=400, detail="verification code is required")
if AUTH_INVITE_CODES and code not in AUTH_INVITE_CODES: allowed = _allowed_verification_codes()
raise HTTPException(status_code=400, detail="invalid invite code") if allowed and code not in allowed:
raise HTTPException(status_code=400, detail="invalid verification code")
return code return code
@@ -268,7 +279,7 @@ def health() -> dict:
"ok": True, "ok": True,
"service": "auth", "service": "auth",
"db": str(DB_PATH), "db": str(DB_PATH),
"invite_code_check": bool(AUTH_INVITE_CODES), "verification_code_check": bool(_allowed_verification_codes()),
} }
@@ -276,7 +287,9 @@ def health() -> dict:
def register(payload: RegisterPayload): def register(payload: RegisterPayload):
phone = _normalize_phone(payload.phone) phone = _normalize_phone(payload.phone)
_validate_password(payload.password) _validate_password(payload.password)
invite_code = _require_invite_code(payload.invite_code) verification_code = _require_verification_code(
payload.verification_code or payload.invite_code
)
salt = secrets.token_bytes(16) salt = secrets.token_bytes(16)
password_hash = _hash_password(payload.password, salt) password_hash = _hash_password(payload.password, salt)
@@ -308,7 +321,7 @@ def register(payload: RegisterPayload):
INSERT INTO registration_requests(phone, password_hash, salt, invite_code, status, created_at) INSERT INTO registration_requests(phone, password_hash, salt, invite_code, status, created_at)
VALUES(?, ?, ?, ?, 'pending', ?) VALUES(?, ?, ?, ?, 'pending', ?)
""", """,
(phone, password_hash, base64.b64encode(salt).decode("ascii"), invite_code, now), (phone, password_hash, base64.b64encode(salt).decode("ascii"), verification_code, now),
) )
request_id = int(cur.lastrowid) request_id = int(cur.lastrowid)
conn.commit() conn.commit()