Skip to content

Commit

Permalink
Implement read-only mode
Browse files Browse the repository at this point in the history
  • Loading branch information
dullage authored and Gedulis12 committed Aug 7, 2023
1 parent 20725e8 commit 608a414
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 109 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Equally, the only thing flatnotes caches is the search index and that's incremen
* Advanced search functionality.
* Note "tagging" functionality.
* Light/dark themes.
* Multiple authentication options (none, username/password, 2FA).
* Multiple authentication options (none, read only, username/password, 2FA).
* Restful API.

See [the wiki](https://github.com/dullage/flatnotes/wiki) for more details.
Expand Down
13 changes: 0 additions & 13 deletions flatnotes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,3 @@ def validate_token(token: str = Depends(oauth2_scheme)):

def no_auth():
return


def get_auth(for_edit: bool = True):
if config.auth_type == AuthType.NONE:
return no_auth
elif (
config.auth_type
in [AuthType.PASSWORD_EDIT_ONLY, AuthType.TOTP_EDIT_ONLY]
and for_edit is False
):
return no_auth
else:
return validate_token
3 changes: 1 addition & 2 deletions flatnotes/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@

class AuthType(str, Enum):
NONE = "none"
READ_ONLY = "read_only"
PASSWORD = "password"
PASSWORD_EDIT_ONLY = "password_edit_only"
TOTP = "totp"
TOTP_EDIT_ONLY = "totp_edit_only"


class Config:
Expand Down
165 changes: 89 additions & 76 deletions flatnotes/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from fastapi.staticfiles import StaticFiles
from qrcode import QRCode

from auth import create_access_token, get_auth, validate_token
from auth import create_access_token, no_auth, validate_token
from config import AuthType, config
from error_responses import (
invalid_title_response,
Expand All @@ -31,6 +31,11 @@
)
last_used_totp = None

if config.auth_type in [AuthType.NONE, AuthType.READ_ONLY]:
authenticate = no_auth
else:
authenticate = validate_token

# Display TOTP QR code
if config.auth_type == AuthType.TOTP:
uri = totp.provisioning_uri(issuer_name="flatnotes", name=config.username)
Expand All @@ -43,37 +48,41 @@
qr.print_ascii()
print(f"Or manually enter this key: {totp.secret.decode('utf-8')}\n")

if config.auth_type not in [AuthType.NONE, AuthType.READ_ONLY]:

@app.post("/api/token")
def token(data: LoginModel):
global last_used_totp

username_correct = secrets.compare_digest(
config.username.lower(), data.username.lower()
)
@app.post("/api/token")
def token(data: LoginModel):
global last_used_totp

expected_password = config.password
if config.auth_type == AuthType.TOTP:
current_totp = totp.now()
expected_password += current_totp
password_correct = secrets.compare_digest(expected_password, data.password)

if not (
username_correct
and password_correct
# Prevent TOTP from being reused
and (
config.auth_type != AuthType.TOTP or current_totp != last_used_totp
username_correct = secrets.compare_digest(
config.username.lower(), data.username.lower()
)
):
raise HTTPException(
status_code=400, detail="Incorrect login credentials."

expected_password = config.password
if config.auth_type == AuthType.TOTP:
current_totp = totp.now()
expected_password += current_totp
password_correct = secrets.compare_digest(
expected_password, data.password
)

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"}
if not (
username_correct
and password_correct
# Prevent TOTP from being reused
and (
config.auth_type != AuthType.TOTP
or current_totp != last_used_totp
)
):
raise HTTPException(
status_code=400, detail="Incorrect login credentials."
)

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"}


@app.get("/")
Expand All @@ -87,26 +96,28 @@ def root(title: str = ""):
return HTMLResponse(content=html)


@app.post(
"/api/notes",
dependencies=[Depends(get_auth(for_edit=True))],
response_model=NoteModel,
)
def post_note(data: NoteModel):
"""Create a new note."""
try:
note = Note(flatnotes, data.title, new=True)
note.content = data.content
return NoteModel.dump(note, include_content=True)
except InvalidTitleError:
return invalid_title_response
except FileExistsError:
return title_exists_response
if config.auth_type != AuthType.READ_ONLY:

@app.post(
"/api/notes",
dependencies=[Depends(authenticate)],
response_model=NoteModel,
)
def post_note(data: NoteModel):
"""Create a new note."""
try:
note = Note(flatnotes, data.title, new=True)
note.content = data.content
return NoteModel.dump(note, include_content=True)
except InvalidTitleError:
return invalid_title_response
except FileExistsError:
return title_exists_response


@app.get(
"/api/notes/{title}",
dependencies=[Depends(get_auth(for_edit=False))],
dependencies=[Depends(authenticate)],
response_model=NoteModel,
)
def get_note(
Expand All @@ -123,43 +134,45 @@ def get_note(
return note_not_found_response


@app.patch(
"/api/notes/{title}",
dependencies=[Depends(get_auth(for_edit=True))],
response_model=NoteModel,
)
def patch_note(title: str, new_data: NotePatchModel):
try:
note = Note(flatnotes, title)
if new_data.new_title is not None:
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)
except InvalidTitleError:
return invalid_title_response
except FileExistsError:
return title_exists_response
except FileNotFoundError:
return note_not_found_response

if config.auth_type != AuthType.READ_ONLY:

@app.delete(
"/api/notes/{title}", dependencies=[Depends(get_auth(for_edit=True))]
)
def delete_note(title: str):
try:
note = Note(flatnotes, title)
note.delete()
except InvalidTitleError:
return invalid_title_response
except FileNotFoundError:
return note_not_found_response
@app.patch(
"/api/notes/{title}",
dependencies=[Depends(authenticate)],
response_model=NoteModel,
)
def patch_note(title: str, new_data: NotePatchModel):
try:
note = Note(flatnotes, title)
if new_data.new_title is not None:
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)
except InvalidTitleError:
return invalid_title_response
except FileExistsError:
return title_exists_response
except FileNotFoundError:
return note_not_found_response


if config.auth_type != AuthType.READ_ONLY:

@app.delete("/api/notes/{title}", dependencies=[Depends(authenticate)])
def delete_note(title: str):
try:
note = Note(flatnotes, title)
note.delete()
except InvalidTitleError:
return invalid_title_response
except FileNotFoundError:
return note_not_found_response


@app.get(
"/api/tags",
dependencies=[Depends(get_auth(for_edit=False))],
dependencies=[Depends(authenticate)],
)
def get_tags():
"""Get a list of all indexed tags."""
Expand All @@ -168,7 +181,7 @@ def get_tags():

@app.get(
"/api/search",
dependencies=[Depends(get_auth(for_edit=False))],
dependencies=[Depends(authenticate)],
response_model=List[SearchResultModel],
)
def search(
Expand Down
14 changes: 3 additions & 11 deletions flatnotes/src/components/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
v-if="currentView != views.login"
class="w-100 mb-5"
:show-logo="currentView != views.home"
:show-log-out="authType != null && authType != constants.authTypes.none"
:auth-type="authType"
:dark-theme="darkTheme"
@logout="logout()"
@toggleTheme="toggleTheme()"
Expand All @@ -29,16 +29,7 @@
<!-- Home -->
<div
v-if="currentView == views.home"
class="
home-view
align-self-center
d-flex
flex-column
justify-content-center
align-items-center
flex-grow-1
w-100
"
class="home-view align-self-center d-flex flex-column justify-content-center align-items-center flex-grow-1 w-100"
>
<Logo class="mb-3"></Logo>
<SearchInput
Expand Down Expand Up @@ -67,6 +58,7 @@
v-if="currentView == this.views.note"
class="flex-grow-1"
:titleToLoad="noteTitle"
:auth-type="authType"
@note-deleted="noteDeletedToast"
></NoteViewerEditor>
</div>
Expand Down
20 changes: 18 additions & 2 deletions flatnotes/src/components/NavBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<div class="d-flex">
<!-- Log Out -->
<button
v-if="showLogOut"
v-if="showLogOutButton"
type="button"
class="bttn"
@click="$emit('logout')"
Expand All @@ -22,6 +22,7 @@

<!-- New Note -->
<a
v-if="showNewButton"
:href="constants.basePaths.new"
class="bttn"
@click.prevent="navigate(constants.basePaths.new, $event)"
Expand Down Expand Up @@ -91,7 +92,7 @@ export default {
type: Boolean,
default: true,
},
showLogOut: { type: Boolean, default: false },
authType: { type: String, default: null },
darkTheme: { type: Boolean, default: false },
},
Expand All @@ -103,6 +104,21 @@ export default {
params.set(constants.params.showHighlights, false);
return `${constants.basePaths.search}?${params.toString()}`;
},
showLogOutButton: function () {
return (
this.authType != null &&
![constants.authTypes.none, constants.authTypes.readOnly].includes(
this.authType
)
);
},
showNewButton: function () {
return (
this.authType != null && this.authType != constants.authTypes.readOnly
);
},
},
methods: {
Expand Down
15 changes: 12 additions & 3 deletions flatnotes/src/components/NoteViewerEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<div class="d-flex">
<!-- Edit -->
<button
v-if="editMode == false && noteLoadFailed == false"
v-if="canModify && editMode == false && noteLoadFailed == false"
type="button"
class="bttn"
@click="setEditMode(true)"
Expand All @@ -47,7 +47,7 @@

<!-- Delete -->
<button
v-if="editMode == false && noteLoadFailed == false"
v-if="canModify && editMode == false && noteLoadFailed == false"
type="button"
class="bttn"
@click="deleteNote"
Expand Down Expand Up @@ -216,6 +216,7 @@ export default {
props: {
titleToLoad: { type: String, default: null },
authType: { type: String, default: null },
},
data: function () {
Expand All @@ -240,6 +241,14 @@ export default {
};
},
computed: {
canModify: function () {
return (
this.authType != null && this.authType != constants.authTypes.readOnly
);
},
},
watch: {
titleToLoad: function () {
if (this.titleToLoad !== this.currentNote?.title) {
Expand Down Expand Up @@ -553,7 +562,7 @@ export default {
// 'e' to edit
Mousetrap.bind("e", function () {
if (parent.editMode == false) {
if (parent.editMode == false && parent.canModify) {
parent.setEditMode(true);
}
});
Expand Down
Loading

0 comments on commit 608a414

Please sign in to comment.