4 Commits

Author SHA1 Message Date
5414ae80a8 Add additional README badges, add /api/version route with TOML loading, toml + types-toml 2024-11-12 15:36:44 -06:00
2f7aea9d25 Disable TanStackRouterDevtools in production, add Wakatime repository badge 2024-11-12 15:13:48 -06:00
5d8621feb5 remove now broken/unnecessary TARGET route prefix 2024-11-10 23:55:00 -06:00
f11d5af7cd Merge pull request #15 from Xevion/0.3.0
## Added

- A release checklist to the `CHANGELOG.md` file, as a reminder for procedure.
- An action workflow for invoking `pytest`, with coverage report generation in CI/CD
- backend: Login & Logout routes
- backend: Rate Limiting via custom `RateLimiter` dependency
- backend: `User` model, `Session` model with migration script
- backend: `Session` model constraints for `token` length, `expiry` & `last_used` timestamps
- backend: `SessionDependency` for easy session validation, enforcement & handling per route
- backend: provided `LOG_JSON_FORMAT` and `LOG_LEVEL` environment variable defaults in `run.sh` development script
- backend: Simple `/health` & `/api/migrations` endpoint tests
- backend: `utc_now` helper function
- backend: `pwdlib[argon2]`, `pytest` (`pytest-cov`, `pytest-xdist`), `limits`, `httpx`, `email-validator` pacakges
- frontend: Re-initialized with `vite` template, setup `@tanstack/router` & `shadcn` components.
- frontend: Added Login & Register page, added basic authentication check with redirect
- frontend: Added Zustand state management, basic login & session API functions with `true-myth` types.
- frontend: Added `zustand`, `true-myth`, `@tanstack/router`, `clsx`, `tailwind-merge` packages

## Changed

- Set `black` formatter line length to 120 characters
- backend: migration squashing threshold to 15
- backend: moved top level `app` routes to `router.misc`

## Removed

- frontend: Most old packages from initial `vite` template
- backend: `IPAddress` Model (definition + DB state via migration) & all related code
2024-11-10 23:49:49 -06:00
7 changed files with 87 additions and 10 deletions

View File

@@ -1,5 +1,7 @@
# linkpulse # linkpulse
![Website Status](https://img.shields.io/website?url=https%3A%2F%2Flinkpulse.xevion.dev&up_message=online&down_message=down&label=linkpulse) ![Python Version](https://img.shields.io/badge/python-3.12-blue) ![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Flinkpulse.xevion%2Fdev%2Fapi%2Fversion&query=%24.version&label=version) [![wakatime](https://wakatime.com/badge/github/Xevion/linkpulse.svg)](https://wakatime.com/badge/github/Xevion/linkpulse)
A project for monitoring websites, built with FastAPI and React. A project for monitoring websites, built with FastAPI and React.
## Structure ## Structure

View File

@@ -1,16 +1,42 @@
"""Miscellaneous endpoints for the Linkpulse API.""" """Miscellaneous endpoints for the Linkpulse API."""
from pathlib import Path
from typing import Any from typing import Any
import structlog
import toml
from fastapi import APIRouter from fastapi import APIRouter
from fastapi_cache.decorator import cache from fastapi_cache.decorator import cache
from linkpulse.utilities import get_db from linkpulse.utilities import get_db
logger = structlog.get_logger(__name__)
router = APIRouter() router = APIRouter()
db = get_db() db = get_db()
@router.get("/api/version")
@cache(expire=None)
async def version() -> dict[str, str]:
"""Get the version of the API.
:return: The version of the API.
:rtype: dict[str, str]
"""
pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml"
version = "unknown"
if pyproject_path.exists() and pyproject_path.is_file():
data = toml.load(pyproject_path)
version = data["tool"]["poetry"]["version"]
logger.debug("Version loaded from pyproject.toml", version=version)
else:
version = "error"
return {"version": version}
@router.get("/health") @router.get("/health")
async def health(): async def health():
"""An endpoint to check if the service is running. """An endpoint to check if the service is running.

View File

@@ -1,6 +1,21 @@
import re
from linkpulse.utilities import utc_now from linkpulse.utilities import utc_now
from fastapi.testclient import TestClient
from linkpulse.app import app
def test_utcnow_tz_aware(): def test_utcnow_tz_aware():
dt = utc_now() dt = utc_now()
dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
def test_api_version():
with TestClient(app) as client:
response = client.get("/api/version")
assert response.status_code == 200
assert "version" in response.json()
version = response.json()["version"]
assert isinstance(version, str)
assert re.match(r"^\d+\.\d+\.\d+$", version)

24
backend/poetry.lock generated
View File

@@ -1626,6 +1626,17 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi
tests = ["freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"] tests = ["freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"]
typing = ["mypy (>=1.4)", "rich", "twisted"] typing = ["mypy (>=1.4)", "rich", "twisted"]
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
[[package]] [[package]]
name = "types-peewee" name = "types-peewee"
version = "3.17.7.20241017" version = "3.17.7.20241017"
@@ -1659,6 +1670,17 @@ files = [
{file = "types_pytz-2024.2.0.20241003-py3-none-any.whl", hash = "sha256:3e22df1336c0c6ad1d29163c8fda82736909eb977281cb823c57f8bae07118b7"}, {file = "types_pytz-2024.2.0.20241003-py3-none-any.whl", hash = "sha256:3e22df1336c0c6ad1d29163c8fda82736909eb977281cb823c57f8bae07118b7"},
] ]
[[package]]
name = "types-toml"
version = "0.10.8.20240310"
description = "Typing stubs for toml"
optional = false
python-versions = ">=3.8"
files = [
{file = "types-toml-0.10.8.20240310.tar.gz", hash = "sha256:3d41501302972436a6b8b239c850b26689657e25281b48ff0ec06345b8830331"},
{file = "types_toml-0.10.8.20240310-py3-none-any.whl", hash = "sha256:627b47775d25fa29977d9c70dc0cbab3f314f32c8d8d0c012f2ef5de7aaec05d"},
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.12.2" version = "4.12.2"
@@ -1840,4 +1862,4 @@ h11 = ">=0.9.0,<1"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.12" python-versions = "^3.12"
content-hash = "9dff2a47bb95e65f15616bb82926eb81d418456c5ec60db46dfe06056c643e31" content-hash = "4e094b6d46fab5f7886452b22c30fc520d396c6d77f9f92dfbd4ef36257c10df"

View File

@@ -5,7 +5,7 @@ description = ""
authors = ["Xevion <xevion@xevion.dev>"] authors = ["Xevion <xevion@xevion.dev>"]
license = "GNU GPL v3" license = "GNU GPL v3"
readme = "README.md" readme = "README.md"
package-mode = false package-mode = true
[tool.poetry.scripts] [tool.poetry.scripts]
app = "linkpulse" app = "linkpulse"
@@ -32,6 +32,8 @@ pwdlib = {extras = ["argon2"], version = "^0.2.1"}
pytest-xdist = "^3.6.1" pytest-xdist = "^3.6.1"
email-validator = "^2.2.0" email-validator = "^2.2.0"
limits = "^3.13.0" limits = "^3.13.0"
toml = "^0.10.2"
types-toml = "^0.10.8.20240310"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]

View File

@@ -8,10 +8,6 @@ import { useUserStore } from "@/lib/state";
// If any protected API call suddenly fails with a 401 status code, the user store is reset, a logout message is displayed, and the user is redirected to the login page. // If any protected API call suddenly fails with a 401 status code, the user store is reset, a logout message is displayed, and the user is redirected to the login page.
// All redirects to the login page will carry a masked URL parameter that can be used to redirect the user back to the page they were on after logging in. // All redirects to the login page will carry a masked URL parameter that can be used to redirect the user back to the page they were on after logging in.
const TARGET = !import.meta.env.DEV
? `http://${import.meta.env.VITE_BACKEND_TARGET}`
: "";
type ErrorResponse = { type ErrorResponse = {
detail: string; detail: string;
}; };
@@ -31,7 +27,7 @@ type SessionResponse = {
export const getSession = async (): Promise< export const getSession = async (): Promise<
Result<SessionResponse, ErrorResponse> Result<SessionResponse, ErrorResponse>
> => { > => {
const response = await fetch(TARGET + "/api/session"); const response = await fetch("/api/session");
if (response.ok) { if (response.ok) {
const user = await response.json(); const user = await response.json();
useUserStore.getState().setUser(user); useUserStore.getState().setUser(user);
@@ -62,7 +58,7 @@ export const login = async (
email: string, email: string,
password: string, password: string,
): Promise<Result<LoginResponse, ErrorResponse>> => { ): Promise<Result<LoginResponse, ErrorResponse>> => {
const response = await fetch(TARGET + "/api/login", { const response = await fetch("/api/login", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

View File

@@ -1,11 +1,25 @@
import { createRootRoute, Outlet } from "@tanstack/react-router"; import { createRootRoute, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools"; import { lazy, Suspense } from "react";
const TanStackRouterDevtools =
process.env.NODE_ENV === "production"
? () => null // Render nothing in production
: lazy(() =>
// Lazy load in development
import("@tanstack/router-devtools").then((res) => ({
default: res.TanStackRouterDevtools,
// For Embedded Mode
// default: res.TanStackRouterDevtoolsPanel
})),
);
export const Route = createRootRoute({ export const Route = createRootRoute({
component: () => ( component: () => (
<> <>
<Outlet /> <Outlet />
<Suspense>
<TanStackRouterDevtools /> <TanStackRouterDevtools />
</Suspense>
</> </>
), ),
}); });