Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Save time while creating your topo by using a simple click-editor for drawing li

## Installation

If you want to use LocalCrag for your own crag, either deploy [via docker](./docs/docker-compose-installation.md) or [helm on k8s](./helm/README.md) or join our cloud, it's up to you. If you join our cloud you will get automated updates, but you will have to pay a hosting fee (we will not make money charging this fee, it's 1:1 what our cloud provider charges us).
If you want to use LocalCrag for your own crag, either deploy [via docker](./docs/docker-compose-installation.md) or [helm on k8s](./helm/localcrag/README.md) or join our cloud, it's up to you. If you join our cloud you will get automated updates, but you will have to pay a hosting fee (we will not make money charging this fee, it's 1:1 what our cloud provider charges us).

### Configuration

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,15 @@
@if (!user.avatar) {
<p-avatar
image="assets/user.png"
styleClass="mr-2"
class="mr-2"
size="xlarge"
shape="circle"
></p-avatar>
}
@if (user.avatar) {
<p-avatar
image="{{ user.avatar.thumbnailM }}"
styleClass="mr-2"
class="mr-2"
size="xlarge"
shape="circle"
></p-avatar>
Expand Down Expand Up @@ -124,7 +124,10 @@
[popup]="true"
appendTo="body"
></p-menu>
@if (currentUser.id !== user.id && !user.admin) {
@if (
currentUser.id !== user.id &&
!(user.admin && !currentUser.superadmin)
) {
<p-button
icon="pi pi-wrench"
(click)="menu.toggle($event)"
Expand Down
4 changes: 2 additions & 2 deletions client/src/assets/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,7 @@
"reorderLinePathsDialogItemsName": "Linien",
"editTopoImageBrowserTitle": "Topo Bild bearbeiten",
"addTopoImageBrowserTitle": "Topo Bild hinzufügen",
"allCrags": "Alle Gebiete",
"orderByGrade": "Grad",
"orderByTimeCreated": "Eintragungsdatum",
"orderDescending": "absteigend",
Expand All @@ -593,7 +594,6 @@
"highPriority": "Hohe Priorität",
"mediumPriority": "Normale Priorität",
"lowPriority": "Niedrige Priorität",
"allCrags": "Alle Gebiete",
"allSectors": "Alle Sektoren",
"allAreas": "Alle Bereiche",
"time.now": "gerade eben",
Expand Down Expand Up @@ -774,10 +774,10 @@
"batchUploadTopoImageBrowserTitle": "Topo-Upload + Schnellbearbeitung",
"ascent.editAscent": "Bearbeiten",
"ascent.deleteAscent": "Löschen",
"orderByAscentDate": "Begehungsdatum",
"ascent.askReallyWantToDeleteAscent": "Willst Du die Begehung wirklich löschen? Dies kann nicht rückgängig gemacht werden. Du kannst die Begehung natürlich jederzeit danach erneut loggen.",
"ascent.yesDelete": "Ja, weg damit!",
"ascent.noDontDelete": "Nee, doch nicht.",
"orderByAscentDate": "Begehungsdatum",
"reorderAreasDialogTitle": "Bereiche ordnen",
"reorderAreasDialogItemsName": "Bereiche",
"areaFormBrowserTitle": "Neuen Bereich erstellen",
Expand Down
14 changes: 10 additions & 4 deletions client/src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@
"instanceSettings.instanceSettingsForm.defaultStartingPosition": "Start position",
"instanceSettings.instanceSettingsForm.rankingPastWeeksLabel": "Time interval for the rankings",
"instanceSettings.instanceSettingsForm.rankingPastWeeksTooltip": "Restricts rankings to ascents from the last X weeks. Select \"All weeks\" for an all-time ranking.",
"instanceSettings.instanceSettingsForm.languageLabel": "Language",
"instanceSettings.instanceSettingsForm.instanceLanguageTooltip": "The language displayed by LocalCrag is determined in the following order: 1. User preference, 2. Browser language, 3. Default language for this setting.",
"instanceSettings.instanceSettingsForm.imagesSettings": "Images",
"instanceSettings.instanceSettingsForm.logoImageLabel": "Logo",
"instanceSettings.instanceSettingsForm.faviconImageLabel": "Favicon",
Expand Down Expand Up @@ -310,6 +312,8 @@
"activateAccount.activateAccountDescription": "You are already logged in. Log out to activate another account.",
"activateAccount.activateNowButtonLabel": "Log out now",
"activateAccount.cancelButtonLabel": "Cancel",
"accountSettingsForm.languageLabel": "Language",
"accountSettingsForm.notificationsLabel": "Notifications",
"accountSettingsForm.commentReplyMailsEnabled": "Receive emails for replies to my comments",
"accountSettingsForm.saveAccountSettingsButtonLabel": "Save",
"accountForm.accountFormTitle": "Account settings",
Expand All @@ -325,7 +329,6 @@
"accountForm.emailsDontMatchAlertText": "Email addresses don't match.",
"accountForm.saveAccountSettingsButtonLabel": "Save",
"accountForm.emailAddressChangeInfoText": "You must confirm the change in your email address by using the link that we have just sent you by email.",
"accountForm.NotificationsTitle": "Notifications",
"accountForm.dangerZoneTitle": "Danger zone",
"accountForm.superadminsCannotDeleteOwnUser": "Superadmins cannot delete their own account.",
"accountForm.deleteAccountInfoText": "Deleting your account is permanent and cannot be undone.",
Expand Down Expand Up @@ -543,6 +546,9 @@
"notifications.SCALE_DELETED_MESSAGE": "Scale successfully removed",
"notifications.SCALE_DELETED_ERROR_TITLE": "Removal error",
"notifications.SCALE_DELETED_ERROR_MESSAGE": "An error occurred during removal. Is the scale still being used by a line or a hierarchical level?",
"de": "German",
"en": "English",
"it": "Italian",
"CLOSED_PROJECT": "Completed Project",
"OPEN_PROJECT": "Open Project",
"UNGRADED": "Ungraded",
Expand Down Expand Up @@ -579,6 +585,7 @@
"reorderLinePathsDialogItemsName": "Lines",
"editTopoImageBrowserTitle": "Edit Topo image",
"addTopoImageBrowserTitle": "Add Topo image",
"allCrags": "All Crags",
"orderByGrade": "Grade",
"orderByTimeCreated": "Record date",
"orderDescending": "descending",
Expand All @@ -587,7 +594,6 @@
"highPriority": "High Priority",
"mediumPriority": "Normal Priority",
"lowPriority": "Low priority",
"allCrags": "All Crags",
"allSectors": "All Sectors",
"allAreas": "All Areas",
"time.now": "just now",
Expand Down Expand Up @@ -733,7 +739,7 @@
"menu.systemCategory": "System",
"menu.menuPages": "Menu Pages",
"menu.menus": "Menus",
"menu.users": "User",
"menu.users": "Users",
"menu.scales": "Manage scales",
"menu.instanceSettings": "Settings",
"menu.accountCategory": "Account",
Expand Down Expand Up @@ -768,10 +774,10 @@
"batchUploadTopoImageBrowserTitle": "Topo Upload + Quick Edit",
"ascent.editAscent": "Edit",
"ascent.deleteAscent": "Delete",
"orderByAscentDate": "Ascent date",
"ascent.askReallyWantToDeleteAscent": "Are you sure you want to delete the ascent? This action cannot be undone. Of course, you can once again log the ascent any time afterwards.",
"ascent.yesDelete": "Yes, get on with it!",
"ascent.noDontDelete": "No, not you.",
"orderByAscentDate": "Ascent date",
"reorderAreasDialogTitle": "Reorder areas",
"reorderAreasDialogItemsName": "Areas",
"areaFormBrowserTitle": "Create new area",
Expand Down
14 changes: 10 additions & 4 deletions client/src/assets/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@
"instanceSettings.instanceSettingsForm.defaultStartingPosition": "Posizione di partenza predefinita",
"instanceSettings.instanceSettingsForm.rankingPastWeeksLabel": "Periodo classifica",
"instanceSettings.instanceSettingsForm.rankingPastWeeksTooltip": "Limita le classifica alle ripetizioni delle ultime X settimane. Seleziona \"Tutte le settimane\" per una classifica completa.",
"instanceSettings.instanceSettingsForm.languageLabel": "Lingua",
"instanceSettings.instanceSettingsForm.instanceLanguageTooltip": "La lingua visualizzata su LocalCrag viene determinata nel seguente ordine: 1. Preferenza dell'utente, 2. Lingua del browser, 3. Lingua predefinita di questa impostazione.",
"instanceSettings.instanceSettingsForm.imagesSettings": "Immagini",
"instanceSettings.instanceSettingsForm.logoImageLabel": "Logo",
"instanceSettings.instanceSettingsForm.faviconImageLabel": "Favicon",
Expand Down Expand Up @@ -310,6 +312,8 @@
"activateAccount.activateAccountDescription": "Hai già effettuato l'accesso. Effettua il logout per attivare un altro account.",
"activateAccount.activateNowButtonLabel": "Esci ora",
"activateAccount.cancelButtonLabel": "Annulla",
"accountSettingsForm.languageLabel": "Lingua",
"accountSettingsForm.notificationsLabel": "Notifiche",
"accountSettingsForm.commentReplyMailsEnabled": "Ricevi risposte ai tuoi commenti per e-mail",
"accountSettingsForm.saveAccountSettingsButtonLabel": "Salva",
"accountForm.accountFormTitle": "Impostazioni account",
Expand All @@ -325,7 +329,6 @@
"accountForm.emailsDontMatchAlertText": "Gli indirizzi e-mail non coincidono.",
"accountForm.saveAccountSettingsButtonLabel": "Salva",
"accountForm.emailAddressChangeInfoText": "Per effettuare la modifica del tuo indirizzo e-mail, devi confermare utilizzando il link che ti abbiamo appena inviato via e-mail.",
"accountForm.NotificationsTitle": "Notifiche",
"accountForm.dangerZoneTitle": "Danger Zone",
"accountForm.superadminsCannotDeleteOwnUser": "I superadmin non possono eliminare il proprio accont.",
"accountForm.deleteAccountInfoText": "L'eliminazione dell'account è permanente e non può essere annullata.",
Expand Down Expand Up @@ -543,6 +546,9 @@
"notifications.SCALE_DELETED_MESSAGE": "Scala rimossa con successo",
"notifications.SCALE_DELETED_ERROR_TITLE": "Errore durante la rimozione",
"notifications.SCALE_DELETED_ERROR_MESSAGE": "Si è verificato un errore durante la rimozione. La scala è ancora utilizzata in dei problemi o in dei livelli gerarchici?",
"de": "Tedesco",
"en": "Inglese",
"it": "Italiano",
"CLOSED_PROJECT": "Progetto chiuso",
"OPEN_PROJECT": "Progetto aperto",
"UNGRADED": "Non gradato",
Expand Down Expand Up @@ -579,6 +585,7 @@
"reorderLinePathsDialogItemsName": "Problemi",
"editTopoImageBrowserTitle": "Modifica immagine Guida",
"addTopoImageBrowserTitle": "Aggiungi immagine Guida",
"allCrags": "Tutte le zone",
"orderByGrade": "Grado",
"orderByTimeCreated": "Data di aggiunta",
"orderDescending": "decrescente",
Expand All @@ -587,7 +594,6 @@
"highPriority": "Priorità alta",
"mediumPriority": "Priorità media",
"lowPriority": "Priorità bassa",
"allCrags": "Tutte le zone",
"allSectors": "Tutti i settori",
"allAreas": "Tutte le aree",
"time.now": "poco fa",
Expand Down Expand Up @@ -733,7 +739,7 @@
"menu.systemCategory": "Sistema",
"menu.menuPages": "Pagine",
"menu.menus": "Menu",
"menu.users": "Utente",
"menu.users": "Utenti",
"menu.scales": "Gestisci scale",
"menu.instanceSettings": "Impostazioni",
"menu.accountCategory": "Account",
Expand Down Expand Up @@ -768,10 +774,10 @@
"batchUploadTopoImageBrowserTitle": "Upload Guida + elaborazione rapida",
"ascent.editAscent": "Modifica",
"ascent.deleteAscent": "Elimina",
"orderByAscentDate": "Data della ripetizione",
"ascent.askReallyWantToDeleteAscent": "Vuoi davvero eliminare la ripetizione? L'operazione non può essere annullata. Resta sempre possibile aggiungere la ripetizione di nuovo in futuro.",
"ascent.yesDelete": "Sì, elimina!",
"ascent.noDontDelete": "No, vabbè dai...",
"orderByAscentDate": "Data della ripetizione",
"reorderAreasDialogTitle": "Ordina aree",
"reorderAreasDialogItemsName": "Aree",
"areaFormBrowserTitle": "Crea una nuova area",
Expand Down
3 changes: 2 additions & 1 deletion server/src/messages/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ class ResponseMessage(Enum):
INTEGRITY_VIOLATION = "INTEGRITY_VIOLATION"
INVALID_FILETYPE_UPLOADED = "INVALID_FILETYPE_UPLOADED"
CANNOT_PROMOTE_OWN_USER = "CANNOT_PROMOTE_OWN_USER"
CANNOT_DELETE_SUPERADMIN = "CANNOT_DELETE_SUPERADMIN"
ONLY_SUPERADMINS_CAN_DELETE_OTHER_ADMINS = "ONLY_SUPERADMINS_CAN_DELETE_OTHER_ADMINS"
MIGRATION_IMPOSSIBLE = "MIGRATION_IMPOSSIBLE"
CANNOT_CHANGE_SCALES_CONFLICTING_LINES = "CANNOT_CHANGE_SCALES_CONFLICTING_LINES"
INVALID_BAR_CHART_BRACKETS = "INVALID_BAR_CHART_BRACKETS"
INVALID_STACKED_CHART_BRACKETS = "INVALID_STACKED_CHART_BRACKETS"
SUPERADMINS_CANNOT_DELETE_OWN_USER = "SUPERADMINS_CANNOT_DELETE_OWN_USER"
57 changes: 35 additions & 22 deletions server/src/migrations/util_scripts/add_superadmin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,41 @@ def add_superadmin():
Adds the initial superadmin user account.
"""
with app.app_context():
if not User.find_by_email(current_app.config["SUPERADMIN_EMAIL"]):
if (
not current_app.config["SUPERADMIN_FIRSTNAME"]
or not current_app.config["SUPERADMIN_LASTNAME"]
or not current_app.config["SUPERADMIN_EMAIL"]
):
raise ValueError(
"SUPERADMIN_FIRSTNAME, SUPERADMIN_LASTNAME, and SUPERADMIN_EMAIL must be set in the environment."
)
user_data = {
"firstname": current_app.config["SUPERADMIN_FIRSTNAME"],
"lastname": current_app.config["SUPERADMIN_LASTNAME"],
"email": current_app.config["SUPERADMIN_EMAIL"],
}
user = create_user(user_data)
user.superadmin = True
user.admin = True
user.moderator = True
user.member = True
db.session.add(user)
db.session.commit()
print("Added superadmin user.")
# Prevent creating multiple superadmins.
if User.query.filter_by(superadmin=True).first():
print("Superadmin already exists. Skipping.")
return

superadmin_email = (current_app.config.get("SUPERADMIN_EMAIL") or "").strip().lower()

# If the configured email already exists, do not create a new superadmin.
# (This avoids creating a second account and avoids implicitly elevating an existing one.)
if superadmin_email and User.find_by_email(superadmin_email):
print("Configured SUPERADMIN_EMAIL already exists. Skipping.")
return

if (
not current_app.config.get("SUPERADMIN_FIRSTNAME")
or not current_app.config.get("SUPERADMIN_LASTNAME")
or not superadmin_email
):
raise ValueError(
"SUPERADMIN_FIRSTNAME, SUPERADMIN_LASTNAME, and SUPERADMIN_EMAIL must be set in the environment."
)

user_data = {
"firstname": current_app.config["SUPERADMIN_FIRSTNAME"],
"lastname": current_app.config["SUPERADMIN_LASTNAME"],
"email": superadmin_email,
}
user = create_user(user_data)
user.superadmin = True
user.admin = True
user.moderator = True
user.member = True
db.session.add(user)
db.session.commit()
print("Added superadmin user.")


if __name__ == "__main__":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,20 @@ def upgrade():
op.add_column("instance_settings", sa.Column("language", sa.String(length=10), nullable=False, server_default="en"))
op.add_column("account_settings", sa.Column("language", sa.String(length=10), nullable=False, server_default="en"))

# set existing rows to 'de' to keep current behavior
op.execute("UPDATE instance_settings SET language='de' WHERE language IS NULL OR language='' ")
op.execute("UPDATE account_settings SET language='de' WHERE language IS NULL OR language='' ")
# set existing rows to 'de' to keep current behavior,
# but only for DBs that already have meaningful content
# (actively used instances have at least one line and are most probably German)
op.execute(
"""
DO $$
BEGIN
IF (SELECT COUNT(*) FROM lines) > 0 THEN
UPDATE instance_settings SET language='de';
UPDATE account_settings SET language='de';
END IF;
END $$;
"""
)

op.drop_column("users", "language")

Expand Down
2 changes: 1 addition & 1 deletion server/src/resources/account_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def delete(self):
"""
user = User.find_by_email(get_jwt_identity())
if user.superadmin:
raise BadRequest(ResponseMessage.CANNOT_DELETE_SUPERADMIN.value)
raise BadRequest(ResponseMessage.SUPERADMINS_CANNOT_DELETE_OWN_USER.value)

db.session.delete(user)
db.session.commit()
Expand Down
7 changes: 7 additions & 0 deletions server/src/resources/auth_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ def post(self):
user.password = User.generate_hash(data["newPassword"])
user.reset_password_hash = None
user.reset_password_hash_created = None

# If the user is not yet activated, we may need to activate him
# (could be that he used forgot password before first regular login)
user.activated = True
if not user.activated_at:
user.activated_at = datetime.now(pytz.utc)

db.session.add(user)
db.session.commit()
access_token = create_access_token(identity=user.email, additional_claims=get_access_token_claims(user))
Expand Down
11 changes: 6 additions & 5 deletions server/src/resources/user_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,16 +128,17 @@ def delete(self, user_id):
:param user_id: ID of the User to delete.
"""

user: User = User.find_by_id(user_id)
user_to_delete: User = User.find_by_id(user_id)
request_user = User.find_by_email(get_jwt_identity())

if user.email == get_jwt_identity():
if user_to_delete.id == request_user.id:
# Own user can only be deleted via account settings
raise BadRequest(ResponseMessage.CANNOT_DELETE_OWN_USER.value)

if user.superadmin:
raise BadRequest(ResponseMessage.CANNOT_DELETE_SUPERADMIN.value)
if user_to_delete.admin and not request_user.superadmin:
raise Unauthorized(ResponseMessage.ONLY_SUPERADMINS_CAN_DELETE_OTHER_ADMINS.value)

db.session.delete(user)
db.session.delete(user_to_delete)
db.session.commit()
return jsonify(None), 204

Expand Down
Loading
Loading