mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-08 15:33:53 -05:00
Add server_images logic, env vars & login photo support
[backend] added login_photo_set column to server_settings [backend] added ENVIRONMENT env variable [backend] added server_images [backend] added protections to server_images and user_images logic [backend] fixed issue on gpx laps processing with no variables initialized [backend] CORS now in production only accept requests from ENDURAIN_HOST variable [docs] updated docs with new env variable and new volume [frontend] added support to new column on server settings store
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -14,6 +14,11 @@ backend/app/*.pyc
|
||||
backend/app/logs/*.log
|
||||
backend/app/*.log
|
||||
|
||||
# server image folder images
|
||||
backend/app/server_images/*.jpeg
|
||||
backend/app/server_images/*.png
|
||||
backend/app/server_images/*.jpg
|
||||
|
||||
# user image folder images
|
||||
backend/app/user_images/*.jpeg
|
||||
backend/app/user_images/*.png
|
||||
|
||||
@@ -31,6 +31,30 @@ def upgrade() -> None:
|
||||
comment="0 - public, 1 - followers, 2 - private",
|
||||
),
|
||||
)
|
||||
# Add login_photo_set column to server_settings table
|
||||
op.add_column(
|
||||
"server_settings",
|
||||
sa.Column(
|
||||
"login_photo_set",
|
||||
sa.Boolean(),
|
||||
nullable=True,
|
||||
comment="Is login photo set (true - yes, false - no)",
|
||||
),
|
||||
)
|
||||
# Set default value for existing records
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE server_settings
|
||||
SET login_photo_set = FALSE
|
||||
"""
|
||||
)
|
||||
# Make the column non-nullable now that all rows have a value
|
||||
op.alter_column(
|
||||
"server_settings",
|
||||
"login_photo_set",
|
||||
existing_type=sa.Boolean(),
|
||||
nullable=False,
|
||||
)
|
||||
# Create activities_laps table
|
||||
op.create_table(
|
||||
"activity_laps",
|
||||
@@ -297,7 +321,10 @@ def upgrade() -> None:
|
||||
"notes", sa.String(length=250), nullable=True, comment="Workout step notes"
|
||||
),
|
||||
sa.Column(
|
||||
"exercise_category", sa.Integer(), nullable=True, comment="Workout step exercise category"
|
||||
"exercise_category",
|
||||
sa.Integer(),
|
||||
nullable=True,
|
||||
comment="Workout step exercise category",
|
||||
),
|
||||
sa.Column(
|
||||
"exercise_name", sa.Integer(), nullable=True, comment="Exercise name ID"
|
||||
@@ -366,11 +393,12 @@ def upgrade() -> None:
|
||||
nullable=False,
|
||||
comment="Workout set start date (DATETIME)",
|
||||
),
|
||||
sa.Column("category", sa.Integer(), nullable=True, comment="Category name"),
|
||||
sa.Column(
|
||||
"category", sa.Integer(), nullable=True, comment="Category name"
|
||||
),
|
||||
sa.Column(
|
||||
"category_subtype", sa.Integer(), nullable=True, comment="Category sub type number"
|
||||
"category_subtype",
|
||||
sa.Integer(),
|
||||
nullable=True,
|
||||
comment="Category sub type number",
|
||||
),
|
||||
sa.ForeignKeyConstraint(["activity_id"], ["activities.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
@@ -391,9 +419,29 @@ def upgrade() -> None:
|
||||
existing_nullable=False,
|
||||
)
|
||||
# Add column tennis_gear_id to users_default_gear table
|
||||
op.add_column('users_default_gear', sa.Column('tennis_gear_id', sa.Integer(), nullable=True, comment='Gear ID that the default tennis activity type belongs'))
|
||||
op.create_index(op.f('ix_users_default_gear_tennis_gear_id'), 'users_default_gear', ['tennis_gear_id'], unique=False)
|
||||
op.create_foreign_key(None, 'users_default_gear', 'gear', ['tennis_gear_id'], ['id'], ondelete='SET NULL')
|
||||
op.add_column(
|
||||
"users_default_gear",
|
||||
sa.Column(
|
||||
"tennis_gear_id",
|
||||
sa.Integer(),
|
||||
nullable=True,
|
||||
comment="Gear ID that the default tennis activity type belongs",
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_users_default_gear_tennis_gear_id"),
|
||||
"users_default_gear",
|
||||
["tennis_gear_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_foreign_key(
|
||||
None,
|
||||
"users_default_gear",
|
||||
"gear",
|
||||
["tennis_gear_id"],
|
||||
["id"],
|
||||
ondelete="SET NULL",
|
||||
)
|
||||
# Create migration record
|
||||
op.execute(
|
||||
"""
|
||||
@@ -414,9 +462,11 @@ def downgrade() -> None:
|
||||
"""
|
||||
)
|
||||
# Drop foreign key constraint from users_default_gear table
|
||||
op.drop_constraint(None, 'users_default_gear', type_='foreignkey')
|
||||
op.drop_index(op.f('ix_users_default_gear_tennis_gear_id'), table_name='users_default_gear')
|
||||
op.drop_column('users_default_gear', 'tennis_gear_id')
|
||||
op.drop_constraint(None, "users_default_gear", type_="foreignkey")
|
||||
op.drop_index(
|
||||
op.f("ix_users_default_gear_tennis_gear_id"), table_name="users_default_gear"
|
||||
)
|
||||
op.drop_column("users_default_gear", "tennis_gear_id")
|
||||
# Alter gear column gear_type to remove racquet from comment
|
||||
op.alter_column(
|
||||
"gear",
|
||||
|
||||
@@ -9,6 +9,7 @@ LICENSE_IDENTIFIER = "AGPL-3.0-or-later"
|
||||
LICENSE_URL = "https://spdx.org/licenses/AGPL-3.0-or-later.html"
|
||||
ROOT_PATH = "/api/v1"
|
||||
FRONTEND_DIR = "/app/frontend/dist"
|
||||
ENVIRONMENT = os.getenv("ENVIRONMENT")
|
||||
|
||||
|
||||
def check_required_env_vars():
|
||||
@@ -33,6 +34,7 @@ def check_required_env_vars():
|
||||
"JAGGER_PORT",
|
||||
"ENDURAIN_HOST",
|
||||
"GEOCODES_MAPS_API",
|
||||
"ENVIRONMENT",
|
||||
]
|
||||
|
||||
for var in required_env_vars:
|
||||
|
||||
@@ -34,7 +34,35 @@ def api_not_found():
|
||||
def user_img_return(
|
||||
user_img: str,
|
||||
):
|
||||
return core_utils.return_user_img_path(user_img)
|
||||
path = core_utils.return_user_img_path(user_img)
|
||||
|
||||
# If the path is None, raise a 404 error
|
||||
if path is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User image not found",
|
||||
)
|
||||
|
||||
# Return the user image path
|
||||
return path
|
||||
|
||||
|
||||
@router.get("/server_images/{server_img}", include_in_schema=False)
|
||||
def server_img_return(
|
||||
server_img: str,
|
||||
):
|
||||
# Get the server image path
|
||||
path = core_utils.return_server_img_path(server_img)
|
||||
|
||||
# If the path is None, raise a 404 error
|
||||
if path is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Server image not found",
|
||||
)
|
||||
|
||||
# Return the server image path
|
||||
return path
|
||||
|
||||
|
||||
@router.get("/{path:path}", include_in_schema=False)
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
from fastapi.responses import FileResponse
|
||||
import os
|
||||
|
||||
|
||||
def return_frontend_index(path: str):
|
||||
return FileResponse("/app/frontend/dist/" + path)
|
||||
|
||||
|
||||
def return_user_img_path(user_img: str):
|
||||
return FileResponse("/app/backend/user_images/" + user_img)
|
||||
file_path = "/app/backend/user_images/" + user_img
|
||||
if not os.path.isfile(file_path):
|
||||
return None
|
||||
return FileResponse(file_path)
|
||||
|
||||
|
||||
def return_server_img_path(server_img: str):
|
||||
file_path = "/app/backend/server_images/" + server_img
|
||||
if not os.path.isfile(file_path):
|
||||
return None
|
||||
return FileResponse(file_path)
|
||||
|
||||
@@ -510,6 +510,12 @@ def generate_activity_laps(
|
||||
lap_hr_waypoints = filter_waypoints(hr_waypoints, start_time, end_time)
|
||||
lap_cad_waypoints = filter_waypoints(cad_waypoints, start_time, end_time)
|
||||
lap_vel_waypoints = filter_waypoints(vel_waypoints, start_time, end_time)
|
||||
ele_gain, ele_loss = None, None
|
||||
avg_hr, max_hr = None, None
|
||||
avg_cadence, max_cadence = None, None
|
||||
avg_speed, max_speed = None, None
|
||||
avg_power, max_power, np = None, None, None
|
||||
|
||||
|
||||
# Calculate total ascent and descent
|
||||
if lap_ele_waypoints:
|
||||
|
||||
@@ -77,15 +77,21 @@ def create_app() -> FastAPI:
|
||||
|
||||
# Add CORS middleware to allow requests from the frontend
|
||||
origins = [
|
||||
"http://localhost",
|
||||
"http://localhost:8080",
|
||||
"http://localhost:5173",
|
||||
os.environ.get("ENDURAIN_HOST"),
|
||||
]
|
||||
|
||||
print(core_config.ENVIRONMENT)
|
||||
print("CORS Origins:", origins)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_origins=(
|
||||
origins
|
||||
if core_config.ENVIRONMENT == "development"
|
||||
else os.environ.get("ENDURAIN_HOST")
|
||||
),
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
@@ -96,6 +102,9 @@ def create_app() -> FastAPI:
|
||||
|
||||
# Add a route to serve the user images
|
||||
app.mount("/user_images", StaticFiles(directory="user_images"), name="user_images")
|
||||
app.mount(
|
||||
"/server_images", StaticFiles(directory="server_images"), name="server_images"
|
||||
)
|
||||
app.mount(
|
||||
"/", StaticFiles(directory=core_config.FRONTEND_DIR, html=True), name="frontend"
|
||||
)
|
||||
|
||||
0
backend/app/server_images/__init__.py
Normal file
0
backend/app/server_images/__init__.py
Normal file
@@ -24,5 +24,11 @@ class ServerSettings(Base):
|
||||
default=False,
|
||||
comment="Allow show user info on public shareable links (true - yes, false - no)",
|
||||
)
|
||||
login_photo_set = Column(
|
||||
Boolean,
|
||||
nullable=False,
|
||||
default=False,
|
||||
comment="Is login photo set (true - yes, false - no)",
|
||||
)
|
||||
|
||||
__table_args__ = (CheckConstraint("id = 1", name="single_row_check"),)
|
||||
|
||||
@@ -6,6 +6,7 @@ class ServerSettings(BaseModel):
|
||||
units: int
|
||||
public_shareable_links: bool
|
||||
public_shareable_links_user_info: bool
|
||||
login_photo_set: bool
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
@@ -58,7 +58,8 @@ ENV UID=1000 \
|
||||
JAGGER_PORT=4317 \
|
||||
ENDURAIN_HOST="http://localhost:8080" \
|
||||
GEOCODES_MAPS_API="changeme" \
|
||||
BEHIND_PROXY=false
|
||||
BEHIND_PROXY=false \
|
||||
ENVIRONMENT="production"
|
||||
|
||||
# Set the working directory to /app/frontend
|
||||
WORKDIR /app/frontend
|
||||
|
||||
@@ -43,6 +43,7 @@ Environment variable | Default value | Optional | Notes |
|
||||
| JAEGER_HOST | jaeger | Yes | N/A |
|
||||
| JAGGER_PORT | 4317 | Yes | N/A |
|
||||
| BEHIND_PROXY | false | Yes | Change to true if behind reverse proxy |
|
||||
| ENVIRONMENT | production | Yes | "production" and "development" allowed. "development" allows connections from localhost:8080 and localhost:5173 at the CORS level |
|
||||
|
||||
Table below shows the obligatory environment variables for mariadb container. You should set them based on what was also set for the Endurain container.
|
||||
|
||||
@@ -79,6 +80,7 @@ Docker image uses a non-root user, so ensure target folders are not owned by roo
|
||||
| `<local_path>/endurain/backend/files/bulk_import:/app/backend/files/bulk_import` | Necessary to enable bulk import of activities. Place here your activities files |
|
||||
| `<local_path>/endurain/backend/files/processed:/app/backend/files/processed` | Necessary for processed original files persistence on container image updates |
|
||||
| `<local_path>/endurain/backend/user_images:/app/backend/user_images` | Necessary for user image persistence on container image updates |
|
||||
| `<local_path>/endurain/backend/server_images:/app/backend/server_images` | Necessary for server image persistence on container image updates |
|
||||
| `<local_path>/endurain/backend/logs:/app/backend/logs` | Log files for the backend |
|
||||
|
||||
## Bulk import and file upload
|
||||
@@ -86,5 +88,11 @@ Docker image uses a non-root user, so ensure target folders are not owned by roo
|
||||
.fit files are preferred. I noticed that Strava/Garmin Connect process of converting .fit to .gpx introduces additional data to the activity file leading to minor variances in the data, like for example additional meters in distance and elevation gain.
|
||||
Some notes:
|
||||
|
||||
- After the files are processed, the files are moved to the processed folder.
|
||||
- GEOCODES API has a limit of 1 Request/Second on the free plan, so if you have a large number of files, it might not be possible to import all in the same action.
|
||||
- After the files are processed, the files are moved to the processed folder
|
||||
- GEOCODES API has a limit of 1 Request/Second on the free plan, so if you have a large number of files, it might not be possible to import all in the same action
|
||||
|
||||
## Image personalization
|
||||
|
||||
It is possible (v0.10.0 or higher) to personalize the login image in the login page. To do that, map the server_images directory and:
|
||||
- Place the image there with the name "login.png"
|
||||
- A square image is expected. Default one uses 1000px vs 1000px
|
||||
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
@@ -8,6 +8,7 @@ export const useServerSettingsStore = defineStore('serverSettings', {
|
||||
units: 1,
|
||||
public_shareable_links: false,
|
||||
public_shareable_links_user_info: false,
|
||||
login_photo_set: false,
|
||||
},
|
||||
}),
|
||||
actions: {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="bg-body-tertiary shadow-sm rounded p-3">
|
||||
<div class="row justify-content-center align-items-center">
|
||||
<div class="col d-none d-lg-block">
|
||||
<img width="auto" height="auto" src="../assets/sports_images/run_triton.png" alt="João at the finish line in Triton 1 Lisbon" class="img-fluid rounded">
|
||||
<img width="auto" height="auto" :src="loginPhotoUrl" alt="Square login image" class="img-fluid rounded" />
|
||||
</div>
|
||||
<div class="col form-signin text-center m-3">
|
||||
<form @submit.prevent="submitForm">
|
||||
@@ -45,6 +45,7 @@ import { useI18n } from "vue-i18n";
|
||||
import { push } from "notivue";
|
||||
// Importing the stores
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
import { useServerSettingsStore } from "@/stores/serverSettingsStore";
|
||||
// Importing the services for the login
|
||||
import { session } from "@/services/sessionService";
|
||||
import { profile } from "@/services/profileService";
|
||||
@@ -60,7 +61,13 @@ export default {
|
||||
const username = ref("");
|
||||
const password = ref("");
|
||||
const authStore = useAuthStore();
|
||||
const serverSettingsStore = useServerSettingsStore();
|
||||
const showPassword = ref(false);
|
||||
const loginPhotoUrl = serverSettingsStore.serverSettings.login_photo_set
|
||||
? `${import.meta.env.VITE_ENDURAIN_HOST}/server_images/login.png`
|
||||
: "/src/assets/login.png";
|
||||
|
||||
console.log("loginPhotoUrl", loginPhotoUrl);
|
||||
|
||||
// Toggle password visibility
|
||||
const togglePasswordVisibility = () => {
|
||||
@@ -124,6 +131,7 @@ export default {
|
||||
togglePasswordVisibility,
|
||||
submitForm,
|
||||
t,
|
||||
loginPhotoUrl,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user