first commit
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
31
app/main.py
31
app/main.py
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user