Skip to content

Commit

Permalink
Merge pull request #96 from openzim/single-user
Browse files Browse the repository at this point in the history
Allow Single User
  • Loading branch information
rgaudin authored Sep 13, 2024
2 parents 2f6e54f + e254f34 commit 0e8c4db
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 8 deletions.
8 changes: 8 additions & 0 deletions backend/api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import uuid
from dataclasses import dataclass, field
from pathlib import Path
from uuid import UUID

import humanfriendly
from rq import Retry
Expand All @@ -31,6 +32,9 @@ class BackendConf:
illustration_quota: int = 0
api_version_prefix: str = "/v1" # our API

# single-user mode (Kiwix only)
single_user_id: str = os.getenv("SINGLE_USER_ID", "").strip() or ""

# Database
postgres_uri: str = os.getenv("POSTGRES_URI") or "nodb"

Expand Down Expand Up @@ -130,6 +134,10 @@ def __post_init__(self):
os.getenv("ZIMFARM_TASK_DISK") or "200MiB"
)

@property
def single_user(self) -> UUID:
return UUID(self.single_user_id)


constants = BackendConf()
logger = constants.logger
18 changes: 16 additions & 2 deletions backend/api/database/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import datetime
from uuid import UUID

from sqlalchemy import select
from sqlalchemy import func, select

from api.database import Session as DBSession
from api.database.models import File, Project
from api.database.models import File, Project, User


def get_file_by_id(file_id: UUID) -> File:
Expand All @@ -26,3 +27,16 @@ def get_project_by_id(project_id: UUID) -> Project:
raise ValueError(f"Project not found: {project_id}")
session.expunge(project)
return project


def ensure_user_with(id_: UUID) -> bool:
"""whether such a user has been created"""
with DBSession.begin() as session:
stmt = select(func.count()).select_from(User).filter_by(id=id_)
if session.scalars(stmt).one() > 0:
return False
user = User(created_on=datetime.datetime.now(tz=datetime.UTC), projects=[])
session.add(user)
user.id = id_
session.add(user)
return True
5 changes: 5 additions & 0 deletions backend/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@

from api import __description__, __titile__, __version__
from api.constants import constants, determine_mandatory_environment_variables
from api.database.utils import ensure_user_with
from api.routes import archives, files, projects, users, utils


@asynccontextmanager
async def lifespan(_: FastAPI):
determine_mandatory_environment_variables()

if constants.single_user_id:
# make sure said user is present in DB (creates otherwise)
ensure_user_with(id_=constants.single_user)
yield


Expand Down
1 change: 0 additions & 1 deletion backend/api/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ async def validated_user(
)
stmt = select(User).filter_by(id=user_id)
user = session.execute(stmt).scalar()
stmt = select(User)
if not user:
# using delete_cookie to construct the cookie header
# but passing it to HTTPException as FastAPI middleware creates Response for it
Expand Down
14 changes: 10 additions & 4 deletions backend/api/routes/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from fastapi import APIRouter, Depends, Response
from pydantic import BaseModel, ConfigDict
from sqlalchemy import select
from sqlalchemy.orm import Session

from api.constants import constants
Expand All @@ -25,10 +26,15 @@ async def create_user(
response: Response, session: Session = Depends(gen_session)
) -> UserModel:
"""Post this endpoint to create a user."""
new_user = User(created_on=datetime.datetime.now(tz=datetime.UTC), projects=[])
session.add(new_user)
session.flush()
session.refresh(new_user)
if constants.single_user_id:
new_user: User = session.execute(
select(User).filter_by(id=constants.single_user)
).scalar_one()
else:
new_user = User(created_on=datetime.datetime.now(tz=datetime.UTC), projects=[])
session.add(new_user)
session.flush()
session.refresh(new_user)
response.set_cookie(
key=constants.authentication_cookie_name,
value=str(new_user.id),
Expand Down
2 changes: 2 additions & 0 deletions dev/reload-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ services:
- NAUTILUS_FILE_QUOTA=${NAUTILUS_FILE_QUOTA}
- NAUTILUS_PROJECT_QUOTA=${NAUTILUS_PROJECT_QUOTA}
- NAUTILUS_FILE_REFRESH_EVERY_MS=${NAUTILUS_FILE_REFRESH_EVERY_MS}
- NAUTILUS_IS_SINGLE_USER=1
- DEBUG=1
depends_on:
- backend
Expand Down Expand Up @@ -97,6 +98,7 @@ services:
- MAILGUN_API_KEY=${MAILGUN_API_KEY}
- MAILGUN_API_URL=${MAILGUN_API_URL}
- MAILGUN_FROM=${MAILGUN_FROM}
- SINGLE_USER_ID=ec7c62b2-65f5-46d0-be28-51c5e6d206ea
depends_on:
database:
condition: service_healthy
Expand Down
1 change: 1 addition & 0 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ COPY entrypoint.sh /app/
RUN ls /app

ENV NAUTILUS_WEB_API http://localhost:8080/v1
ENV NAUTILUS_IS_SINGLE_USER ""
EXPOSE 80

ENTRYPOINT [ "/app/entrypoint.sh" ]
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export interface Environ {
NAUTILUS_FILE_QUOTA: number
NAUTILUS_PROJECT_QUOTA: number
NAUTILUS_FILE_REFRESH_EVERY_MS: number
NAUTILUS_IS_SINGLE_USER: boolean
}

export interface AlertMessage {
Expand Down Expand Up @@ -165,7 +166,8 @@ export const EmptyConstants = new Constants({
NAUTILUS_WEB_API: 'noapi',
NAUTILUS_FILE_QUOTA: 100000000,
NAUTILUS_PROJECT_QUOTA: 100000000,
NAUTILUS_FILE_REFRESH_EVERY_MS: 1000
NAUTILUS_FILE_REFRESH_EVERY_MS: 1000,
NAUTILUS_IS_SINGLE_USER: false
})

// using iec to be consistent accross tools (MiB): jedec renders MiB as MB
Expand Down
60 changes: 60 additions & 0 deletions frontend/src/views/SingleUserHome.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<template>
<div class="d-flex flex-column vh-100">
<div class="flex-shrink-1">
<p>Retrieving single user & projects…</p>
</div>
</div>
</template>

<script setup lang="ts">
import { type Project } from '@/constants'
import type { User } from '@/constants'
import { useAppStore, useProjectStore } from '@/stores/stores'
import { createNewProject } from '@/utils'
import router from '@/router'
const storeProject = useProjectStore()
const storeApp = useAppStore()
async function restrieveUser(): Promise<User | null> {
var user: User | null = null
try {
const createUserRespone = await storeApp.axiosInstance.post<User>('/users')
user = createUserRespone.data
} catch (error: any) {
console.log('Unable to create a new user.', error)
storeApp.alertsError('Unable to create a new user.')
}
return user
}
async function retrieveProjects(): Promise<Project[]> {
var projects: Project[] = []
try {
const response = await storeApp.axiosInstance.get<Project[]>('/projects')
console.log(response.data)
projects = response.data
} catch (error: unknown) {
console.log('Unable to retrieve projects info', error)
storeApp.alertsError('Unable to retrieve projects info')
}
if (projects.length == 0) {
let project: Project | null = await createNewProject('First Project')
if (project !== null) {
projects.push(project)
}
}
return projects
}
await restrieveUser()
const projects = await retrieveProjects()
storeProject.setProjects(projects)
if (projects.length) {
storeProject.setLastProjectId(projects[projects.length - 1].id)
}
router.push('/collections')
</script>
4 changes: 4 additions & 0 deletions frontend/src/views/StartView.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<template>
<div class="d-flex flex-column vh-100">
<div class="flex-shrink-1">
<div v-if="storeApp.constants.env.NAUTILUS_IS_SINGLE_USER">
<SingleUserHome />
</div>
<Suspense>
<CollectionsView v-if="isValidProjectId" />
<HomeView v-else />
Expand All @@ -11,6 +14,7 @@
</template>

<script setup lang="ts">
import SingleUserHome from '@/views/SingleUserHome.vue'
import FooterComponent from '@/components/FooterComponent.vue'
import CollectionsView from './CollectionsView.vue'
import HomeView from '@/views/HomeView.vue'
Expand Down

0 comments on commit 0e8c4db

Please sign in to comment.