5 Commits

Author SHA1 Message Date
alwin
4dfcc7547f wrote migrations for 001_initial.py 2024-12-04 16:14:35 -06:00
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
11 changed files with 5808 additions and 21 deletions

View File

@@ -1,5 +1,7 @@
# 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.
## Structure

View File

@@ -39,8 +39,13 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
@migrator.create_model
class IPAddress(pw.Model):
ip = pw.CharField(max_length=255, primary_key=True)
lastSeen = pw.DateTimeField()
"""wrote a generic script... hope one of you can test it for me bc its still not working on my machine"""
ip = pw.CharField(max_length=255, primary_key=True, help_text="The IP address (primary key).")
lastSeen = pw.DateTimeField(help_text="Timestamp of the last activity from this IP address.")
location = pw.CharField(max_length=100, null=True, help_text="Optional location information.")
is_blocked = pw.BooleanField(default=False, help_text="Indicates whether the IP is blocked.")
created_at = pw.DateTimeField(constraints=[pw.SQL("DEFAULT CURRENT_TIMESTAMP")], help_text="Record creation time.")
class Meta:
table_name = "ipaddress"

View File

@@ -25,25 +25,30 @@ Some examples (model - class or model name)::
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
"""
Add a 'count' field to the 'ipaddress' table.
The new field:
- Name: count
- Type: IntegerField
- Default: 0
"""
migrator.add_fields(
'ipaddress',
count=pw.IntegerField(default=0))
count=pw.IntegerField(default=0)
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
"""
Remove the 'count' field from the 'ipaddress' table.
"""
migrator.remove_fields('ipaddress', 'count')

View File

@@ -1,16 +1,42 @@
"""Miscellaneous endpoints for the Linkpulse API."""
from pathlib import Path
from typing import Any
import structlog
import toml
from fastapi import APIRouter
from fastapi_cache.decorator import cache
from linkpulse.utilities import get_db
logger = structlog.get_logger(__name__)
router = APIRouter()
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")
async def health():
"""An endpoint to check if the service is running.

View File

@@ -1,6 +1,21 @@
import re
from linkpulse.utilities import utc_now
from fastapi.testclient import TestClient
from linkpulse.app import app
def test_utcnow_tz_aware():
dt = utc_now()
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"]
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]]
name = "types-peewee"
version = "3.17.7.20241017"
@@ -1659,6 +1670,17 @@ files = [
{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]]
name = "typing-extensions"
version = "4.12.2"
@@ -1840,4 +1862,4 @@ h11 = ">=0.9.0,<1"
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "9dff2a47bb95e65f15616bb82926eb81d418456c5ec60db46dfe06056c643e31"
content-hash = "4e094b6d46fab5f7886452b22c30fc520d396c6d77f9f92dfbd4ef36257c10df"

View File

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

5700
frontend/package-lock.json generated Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -46,7 +46,7 @@
"tailwindcss": "^3.4.14",
"typescript": "~5.6.2",
"typescript-eslint": "^8.11.0",
"vite": "^5.4.10",
"vite": "^5.4.11",
"vitest": "^2.1.4"
}
}

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.
// 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 = {
detail: string;
};
@@ -31,7 +27,7 @@ type SessionResponse = {
export const getSession = async (): Promise<
Result<SessionResponse, ErrorResponse>
> => {
const response = await fetch(TARGET + "/api/session");
const response = await fetch("/api/session");
if (response.ok) {
const user = await response.json();
useUserStore.getState().setUser(user);
@@ -62,7 +58,7 @@ export const login = async (
email: string,
password: string,
): Promise<Result<LoginResponse, ErrorResponse>> => {
const response = await fetch(TARGET + "/api/login", {
const response = await fetch("/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",

View File

@@ -1,11 +1,25 @@
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({
component: () => (
<>
<Outlet />
<TanStackRouterDevtools />
<Suspense>
<TanStackRouterDevtools />
</Suspense>
</>
),
});