enable change password
This commit is contained in:
@@ -62,12 +62,22 @@ POST /api/v1/admin/videos/{sha256}/process
|
|||||||
PUT /api/v1/admin/videos/{sha256}/sentences/{index}
|
PUT /api/v1/admin/videos/{sha256}/sentences/{index}
|
||||||
PUT /api/v1/admin/videos/{sha256}/sentences/{index}/split
|
PUT /api/v1/admin/videos/{sha256}/sentences/{index}/split
|
||||||
PUT /api/v1/admin/videos/{sha256}/sentences/{index}/boundary
|
PUT /api/v1/admin/videos/{sha256}/sentences/{index}/boundary
|
||||||
|
PUT /api/v1/admin/users/password
|
||||||
DELETE /api/v1/admin/videos/{sha256}
|
DELETE /api/v1/admin/videos/{sha256}
|
||||||
```
|
```
|
||||||
|
|
||||||
Admin endpoints use `X-Admin-Key` when `ADMIN_API_KEY` is configured. Assessment
|
Admin endpoints use `X-Admin-Key` when `ADMIN_API_KEY` is configured. Assessment
|
||||||
requests use `X-Client-Key` when `CLIENT_API_KEY` is configured.
|
requests use `X-Client-Key` when `CLIENT_API_KEY` is configured.
|
||||||
|
|
||||||
|
Administrators can set a local password for an SSO-linked account with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X PUT https://your-server/api/v1/admin/users/password \
|
||||||
|
-H "X-Admin-Key: $ADMIN_API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"bobcoc","password":"new-password"}'
|
||||||
|
```
|
||||||
|
|
||||||
`POST /api/v1/audio/sentence-boundaries` accepts an audio file from an
|
`POST /api/v1/audio/sentence-boundaries` accepts an audio file from an
|
||||||
authenticated client, generates boundaries in memory/temp storage only, and
|
authenticated client, generates boundaries in memory/temp storage only, and
|
||||||
returns a sentence document. It does not register or archive the uploaded
|
returns a sentence document. It does not register or archive the uploaded
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ from .models import (
|
|||||||
SentenceBoundaryDocument,
|
SentenceBoundaryDocument,
|
||||||
SentenceExplanationRequest,
|
SentenceExplanationRequest,
|
||||||
SentenceExplanationResponse,
|
SentenceExplanationResponse,
|
||||||
|
PasswordResetRequest,
|
||||||
SentenceTextUpdate,
|
SentenceTextUpdate,
|
||||||
UserDubShareSummary,
|
UserDubShareSummary,
|
||||||
SsoExchangeRequest,
|
SsoExchangeRequest,
|
||||||
@@ -441,6 +442,19 @@ def create_app(
|
|||||||
raise HTTPException(status_code=404, detail="User was not found.")
|
raise HTTPException(status_code=404, detail="User was not found.")
|
||||||
return Response(status_code=204)
|
return Response(status_code=204)
|
||||||
|
|
||||||
|
@application.put(
|
||||||
|
"/api/v1/admin/users/password",
|
||||||
|
status_code=204,
|
||||||
|
dependencies=[Depends(require_admin)],
|
||||||
|
)
|
||||||
|
def reset_user_password(
|
||||||
|
payload: PasswordResetRequest,
|
||||||
|
) -> Response:
|
||||||
|
updated = video_repository.set_user_password(payload.username, payload.password)
|
||||||
|
if not updated:
|
||||||
|
raise HTTPException(status_code=404, detail="User was not found.")
|
||||||
|
return Response(status_code=204)
|
||||||
|
|
||||||
@application.get("/api/v1/videos", response_model=VideoListResponse)
|
@application.get("/api/v1/videos", response_model=VideoListResponse)
|
||||||
def list_videos() -> VideoListResponse:
|
def list_videos() -> VideoListResponse:
|
||||||
return VideoListResponse(
|
return VideoListResponse(
|
||||||
|
|||||||
@@ -92,6 +92,13 @@ class SentenceTextUpdate(BaseModel):
|
|||||||
language: Optional[str] = Field(default=None, max_length=32)
|
language: Optional[str] = Field(default=None, max_length=32)
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetRequest(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
username: str = Field(min_length=3, max_length=50)
|
||||||
|
password: str = Field(min_length=8, max_length=128)
|
||||||
|
|
||||||
|
|
||||||
class SentenceExplanationRequest(BaseModel):
|
class SentenceExplanationRequest(BaseModel):
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
|||||||
@@ -984,6 +984,22 @@ class VideoRepository:
|
|||||||
return None
|
return None
|
||||||
return self.get_user_by_id(row["id"])
|
return self.get_user_by_id(row["id"])
|
||||||
|
|
||||||
|
def set_user_password(self, username: str, password: str) -> bool:
|
||||||
|
"""Set a local password for either a normal or SSO-linked account."""
|
||||||
|
username = username.strip()
|
||||||
|
if not username:
|
||||||
|
return False
|
||||||
|
with self._connect() as connection:
|
||||||
|
cursor = connection.execute(
|
||||||
|
"""
|
||||||
|
UPDATE users
|
||||||
|
SET password_hash = ?
|
||||||
|
WHERE username = ? COLLATE NOCASE
|
||||||
|
""",
|
||||||
|
(hash_password(password), username),
|
||||||
|
)
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
def create_session(self, user_id: str) -> str:
|
def create_session(self, user_id: str) -> str:
|
||||||
token = new_token()
|
token = new_token()
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
@@ -1205,4 +1221,3 @@ def _is_cjk_char(char: str) -> bool:
|
|||||||
or 0xF900 <= code <= 0xFAFF
|
or 0xF900 <= code <= 0xFAFF
|
||||||
or 0x20000 <= code <= 0x2CEAF
|
or 0x20000 <= code <= 0x2CEAF
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from sentence_api.store import BoundaryStore
|
|||||||
VIDEO_HASH = "a" * 64
|
VIDEO_HASH = "a" * 64
|
||||||
|
|
||||||
|
|
||||||
def make_client(tmp_path):
|
def make_client(tmp_path, admin_api_key=""):
|
||||||
index_path = tmp_path / "boundaries.json"
|
index_path = tmp_path / "boundaries.json"
|
||||||
index_path.write_text(
|
index_path.write_text(
|
||||||
json.dumps({
|
json.dumps({
|
||||||
@@ -36,6 +36,7 @@ def make_client(tmp_path):
|
|||||||
Settings.from_env(),
|
Settings.from_env(),
|
||||||
data_dir=tmp_path / "data",
|
data_dir=tmp_path / "data",
|
||||||
legacy_boundaries_file=index_path,
|
legacy_boundaries_file=index_path,
|
||||||
|
admin_api_key=admin_api_key,
|
||||||
)
|
)
|
||||||
settings.ensure_directories()
|
settings.ensure_directories()
|
||||||
client = TestClient(
|
client = TestClient(
|
||||||
@@ -116,6 +117,27 @@ def test_adhoc_audio_boundaries_require_login(tmp_path):
|
|||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_can_reset_sso_user_password(tmp_path):
|
||||||
|
client = make_client(tmp_path, admin_api_key="test-admin-key")
|
||||||
|
repository = client.app.state.video_repository
|
||||||
|
created = repository.create_sso_user(
|
||||||
|
username="bobcoc",
|
||||||
|
nickname="Bob",
|
||||||
|
sso_provider="d1kt",
|
||||||
|
)
|
||||||
|
assert created is not None
|
||||||
|
assert repository.authenticate_user("bobcoc", "anything-123") is None
|
||||||
|
|
||||||
|
response = client.put(
|
||||||
|
"/api/v1/admin/users/password",
|
||||||
|
json={"username": "bobcoc", "password": "new-password-123"},
|
||||||
|
headers={"X-Admin-Key": "test-admin-key"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
||||||
|
assert repository.authenticate_user("bobcoc", "new-password-123") is not None
|
||||||
|
|
||||||
|
|
||||||
def test_sentence_explanation_requires_login(tmp_path):
|
def test_sentence_explanation_requires_login(tmp_path):
|
||||||
index_path = tmp_path / "boundaries.json"
|
index_path = tmp_path / "boundaries.json"
|
||||||
index_path.write_text("{}", encoding="utf-8")
|
index_path.write_text("{}", encoding="utf-8")
|
||||||
|
|||||||
Reference in New Issue
Block a user