import secrets
-from typing import List, Literal
+from typing import List, Literal, Union
import pyotp
from fastapi import Depends, FastAPI, HTTPException
from models import (
ConfigModel,
LoginModel,
- NoteModel,
+ NoteResponseModel,
+ NoteContentResponseModel,
NotePatchModel,
+ NotePostModel,
SearchResultModel,
+ TokenModel,
)
app = FastAPI()
if config.auth_type not in [AuthType.NONE, AuthType.READ_ONLY]:
- @app.post("/api/token")
+ @app.post("/api/token", response_model=TokenModel)
def token(data: LoginModel):
global last_used_totp
access_token = create_access_token(data={"sub": config.username})
if config.auth_type == AuthType.TOTP:
last_used_totp = current_totp
- return {"access_token": access_token, "token_type": "bearer"}
+ return TokenModel(access_token=access_token)
-@app.get("/")
-@app.get("/login")
-@app.get("/search")
-@app.get("/new")
-@app.get("/note/{title}")
+@app.get("/", include_in_schema=False)
+@app.get("/login", include_in_schema=False)
+@app.get("/search", include_in_schema=False)
+@app.get("/new", include_in_schema=False)
+@app.get("/note/{title}", include_in_schema=False)
def root(title: str = ""):
with open("flatnotes/dist/index.html", "r", encoding="utf-8") as f:
html = f.read()
@app.post(
"/api/notes",
dependencies=[Depends(authenticate)],
- response_model=NoteModel,
+ response_model=NoteContentResponseModel,
)
- def post_note(data: NoteModel):
+ def post_note(data: NotePostModel):
"""Create a new note."""
try:
note = Note(flatnotes, data.title, new=True)
note.content = data.content
- return NoteModel.dump(note, include_content=True)
+ return NoteContentResponseModel.model_validate(note)
except InvalidTitleError:
return invalid_title_response
except FileExistsError:
@app.get(
"/api/notes/{title}",
dependencies=[Depends(authenticate)],
- response_model=NoteModel,
+ response_model=Union[NoteContentResponseModel, NoteResponseModel],
)
def get_note(
title: str,
"""Get a specific note."""
try:
note = Note(flatnotes, title)
- return NoteModel.dump(note, include_content=include_content)
+ if include_content:
+ return NoteContentResponseModel.model_validate(note)
+ else:
+ return NoteResponseModel.model_validate(note)
except InvalidTitleError:
return invalid_title_response
except FileNotFoundError:
@app.patch(
"/api/notes/{title}",
dependencies=[Depends(authenticate)],
- response_model=NoteModel,
+ response_model=NoteContentResponseModel,
)
def patch_note(title: str, new_data: NotePatchModel):
try:
note.title = new_data.new_title
if new_data.new_content is not None:
note.content = new_data.new_content
- return NoteModel.dump(note, include_content=True)
+ return NoteContentResponseModel.model_validate(note)
except InvalidTitleError:
return invalid_title_response
except FileExistsError:
if config.auth_type != AuthType.READ_ONLY:
- @app.delete("/api/notes/{title}", dependencies=[Depends(authenticate)])
+ @app.delete(
+ "/api/notes/{title}",
+ dependencies=[Depends(authenticate)],
+ response_model=None,
+ )
def delete_note(title: str):
try:
note = Note(flatnotes, title)
@app.get(
"/api/tags",
dependencies=[Depends(authenticate)],
+ response_model=List[str],
)
def get_tags():
"""Get a list of all indexed tags."""
if sort == "lastModified":
sort = "last_modified"
return [
- SearchResultModel.dump(note_hit)
+ SearchResultModel.model_validate(note_hit)
for note_hit in flatnotes.search(
term, sort=sort, order=order, limit=limit
)
@app.get("/api/config", response_model=ConfigModel)
def get_config():
"""Retrieve server-side config required for the UI."""
- return ConfigModel.dump(config)
+ return ConfigModel.model_validate(config)
app.mount("/", StaticFiles(directory="flatnotes/dist"), name="dist")
-from typing import Dict, List, Optional
+from typing import List, Optional
-from config import AuthType, Config
-from flatnotes import Note, SearchResult
-from helpers import CamelCaseBaseModel
+from pydantic import BaseModel, Field
+from config import AuthType
+from helpers import camel_case
-class LoginModel(CamelCaseBaseModel):
+
+class TokenModel(BaseModel):
+ # Use of BaseModel instead of CustomBaseModel is intentional as OAuth
+ # requires keys to be snake_case
+ access_token: str
+ token_type: str = Field("bearer")
+
+
+class CustomBaseModel(BaseModel):
+ class Config:
+ alias_generator = camel_case
+ populate_by_name = True
+ from_attributes = True
+
+
+class LoginModel(CustomBaseModel):
username: str
password: str
-class NoteModel(CamelCaseBaseModel):
+class NotePostModel(CustomBaseModel):
title: str
- last_modified: Optional[int]
- content: Optional[str]
+ content: Optional[str] = Field(None)
- @classmethod
- def dump(cls, note: Note, include_content: bool = True) -> Dict:
- return {
- "title": note.title,
- "lastModified": note.last_modified,
- "content": note.content if include_content else None,
- }
+class NoteResponseModel(CustomBaseModel):
+ title: str
+ last_modified: int
-class NotePatchModel(CamelCaseBaseModel):
- new_title: Optional[str]
- new_content: Optional[str]
+class NoteContentResponseModel(NoteResponseModel):
+ content: Optional[str] = Field(None)
-class SearchResultModel(CamelCaseBaseModel):
- score: Optional[float]
+
+class NotePatchModel(CustomBaseModel):
+ new_title: Optional[str] = Field(None)
+ new_content: Optional[str] = Field(None)
+
+
+class SearchResultModel(CustomBaseModel):
+ score: Optional[float] = Field(None)
title: str
last_modified: int
- title_highlights: Optional[str]
- content_highlights: Optional[str]
- tag_matches: Optional[List[str]]
-
- @classmethod
- def dump(self, search_result: SearchResult) -> Dict:
- return {
- "score": search_result.score,
- "title": search_result.title,
- "lastModified": search_result.last_modified,
- "titleHighlights": search_result.title_highlights,
- "contentHighlights": search_result.content_highlights,
- "tagMatches": search_result.tag_matches,
- }
-
-
-class ConfigModel(CamelCaseBaseModel):
- auth_type: AuthType
+ title_highlights: Optional[str] = Field(None)
+ content_highlights: Optional[str] = Field(None)
+ tag_matches: Optional[List[str]] = Field(None)
+
- @classmethod
- def dump(self, config: Config) -> Dict:
- return {
- "authType": config.auth_type.value,
- }
+class ConfigModel(CustomBaseModel):
+ auth_type: AuthType