3 Commits

Author SHA1 Message Date
5b776288bd [nix] add module.nix 2026-07-11 12:23:08 +02:00
1883662b5f Update README 2026-07-04 01:04:26 +02:00
a91bda16c2 [nix] nix run now possible 2026-07-04 01:04:15 +02:00
4 changed files with 95 additions and 44 deletions

View File

@@ -1,17 +1,39 @@
## Gatekeeper - Door access system ## Gatekeeper - Door access system
Status: WIP - not prod ready #### Status: WIP - getting there o.o
Dev with `nix develop` Start prod server `nix run`<br>
sync python deps with `uv sync` Start dev server `nix run .#dev` or `nix run .#dev -- {args}`<br>
start dev server `uv run fastapi dev` Interactive dev with `nix develop`, then sync deps with `uv sync`<br>
Swagger UI @ http://127.0.0.1:8000/docs Swagger UI @ http://127.0.0.1:8000/api/v1/docs<br>
start prod server `uv run fastapi run` OpenApi @ http://127.0.0.1:8000/api/v1/openapi.json<br>
You need to set services.pcscd.enable = true; for the smartcard reader to work #### Config:
Issues: Nixos config for the card reader:
```
services.pcscd = {
enable = true;
plugins = [ pkgs.acsccid ]; #<-- Needed for the ACE1552U
};
```
This application is configured using env vars. You can use `cp .env.example .env` and then edit `.env` to change the keys to something else.
### A note on keycard scanners
I have tried two scanners while developing this project, the TWN4 MultiTech 2 from elatec and the ACR1552U from acs.<br>
##### TWN4
From the factory the TWN4 comes with firmware that emulates a HID and just inputs the serial of any scanned card. To be usable with pcscd it had to be flashed with the `TWN4_xPx520_S1SC161_Multi_CCID_1Slot_Standard.bix` firmware found in the `TWN4DevPack520` which can be downloaded under https://www.elatec-rfid.com/int/elatec-software (They send you a download link via email).
This software seems to not work under wine. I had to setup a win10 vm and pass the usb to be able to flash.<br>
Range: In my testing the TWN4 has a range of about 22mm when interfacing with our Mifare DESFire v2 cards. This is not enough for our idea of mounting the scanner on the inside of a glass window.
#### ACR1552U
The ACR1552U comes preloaded with PC/SC firmware. You need to install the acsccid package with your package-manager of choice (Nixos user see above) for the scanner to work but after that I had no problems with the hardware interfacing.<br>
Range: The range of the ACR1552U was much better at over 60mm (almost 70mm if you take of the face plate)
#### Issues:
- documentation missing - documentation missing
- `nix run` currently broken
- raspberry pi image not working - raspberry pi image not working
- no door state - no door state
- no door operations - no door operations

View File

@@ -1,7 +1,6 @@
from sqlmodel import Field, Relationship, Session, SQLModel from sqlmodel import Field, Relationship, Session, SQLModel
from typing import List, Literal, Union from typing import List
from datetime import datetime, time from datetime import time
from pydantic import model_validator
class Base(SQLModel): class Base(SQLModel):
pass pass
@@ -61,49 +60,19 @@ class GroupResponse(GroupBase):
#### AccessAuthorization #### AccessAuthorization
class AccessAuthorizationBase(Base): class AccessAuthorizationBase(Base):
name: str = Field(index=True) name: str = Field(index=True)
type: Literal["timetable", "oneshot", "somefuturespec"]
is_active: bool is_active: bool
class AccessAuthorizationDB(AccessAuthorizationBase, table=True): class AccessAuthorizationDB(AccessAuthorizationBase, table=True):
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(default=None, primary_key=True)
type: str
groups: List["GroupDB"] = Relationship(back_populates="accessauths", link_model=AaGroupLink) groups: List["GroupDB"] = Relationship(back_populates="accessauths", link_model=AaGroupLink)
timetables: List["Timetable"] = Relationship(back_populates="accessauth", cascade_delete=True) timetables: List["Timetable"] = Relationship(back_populates="accessauth", cascade_delete=True)
oneshot: "OneShotAccess" = Relationship(back_populates="accessauth", cascade_delete=True)
class OneShotAccessBase(Base):
uses: int = 1
ends_at: datetime
class OneShotAccess(OneShotAccessBase, table=True):
id: int | None = Field(default=None, primary_key=True)
accessauth_id: int = Field(default=None, foreign_key="accessauthorizationdb.id")
accessauth: AccessAuthorizationDB = Relationship(back_populates="oneshot")
class AccessAuthorizationCreate(AccessAuthorizationBase): class AccessAuthorizationCreate(AccessAuthorizationBase):
timetables: List["TimetableCreate"] = [] timetables: List["TimetableCreate"]
oneshot: OneShotAccessBase | None = None
@model_validator(mode="after")
def check_type(self):
if self.type == "timetable":
if not self.timetables:
raise ValueError("timetable auths require at least one timetable object")
if self.oneshot is not None:
raise ValueError("timetable auths are not allowed oneshot objects")
elif self.type == "oneshot":
if not self.oneshot:
raise ValueError("oneshot auths require a oneshot object")
if self.timetables:
raise ValueError("oneshot auths are not allowed timetable objects")
elif self.type == "somefuturespec":
raise ValueError("somefuturespec is not jet implemented. Please do not use")
return self
class AccessAuthorizationResponse(AccessAuthorizationBase): class AccessAuthorizationResponse(AccessAuthorizationBase):
id: int id: int
timetables: List["Timetable"] = [] timetables: List["Timetable"]
oneshot: OneShotAccessBase | None = None
groups: List["GroupDB"] groups: List["GroupDB"]
class AccessAuthorizationUpdate(Base): class AccessAuthorizationUpdate(Base):

View File

@@ -25,6 +25,7 @@
outputs = outputs =
{ {
self,
nixpkgs, nixpkgs,
pyproject-nix, pyproject-nix,
uv2nix, uv2nix,
@@ -105,5 +106,23 @@
packages = forAllSystems (system: { packages = forAllSystems (system: {
default = pythonSets.${system}.mkVirtualEnv "gatekeeper" workspace.deps.default; default = pythonSets.${system}.mkVirtualEnv "gatekeeper" workspace.deps.default;
}); });
apps = forAllSystems (system: {
default = {
type = "app";
program = toString (nixpkgs.legacyPackages.${system}.writeShellScript "gatekeeper" ''
export LD_LIBRARY_PATH="${lib.getLib nixpkgs.legacyPackages.${system}.pcsclite}/lib''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
exec ${self.packages.${system}.default}/bin/fastapi run ${self}/app/main.py "$@";
'');
};
dev = {
type = "app";
program = toString (nixpkgs.legacyPackages.${system}.writeShellScript "gatekeeper" ''
export LD_LIBRARY_PATH="${lib.getLib nixpkgs.legacyPackages.${system}.pcsclite}/lib''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
exec ${self.packages.${system}.default}/bin/fastapi dev ${self}/app/main.py "$@";
'');
};
});
nixosModules = { gatekeeper = import ./module.nix; };
}; };
} }

41
module.nix Normal file
View File

@@ -0,0 +1,41 @@
{config, pkgs, lib, self, system, ... }:
let
cfg = config.services.gatekeeper;
in
{
options = {
services.gatekeeper = {
enable = lib.mkEnableOption "Enable the gatekeeper api service.";
dotenv = lib.mkOption {
type = lib.types.path;
default = null;
description = "The path to a .env file with the keys";
};
db = {
type = lib.types.path;
default = null;
description = "Where to save the database.";
};
};
};
config = lib.mkIf cfg.enable {
users.extraGroups.gatekeeper = {};
users.extraUsers.gatekeeper = {
description = "gatekeeper user";
group = "gatekeeper";
isSystemUser = true;
};
#environment.systemPackages = [self.packages.${system}.default];
systemd.services.gatekeeper = {
script = ''
export LD_LIBRARY_PATH="${lib.getLib pkgs.pcsclite}/lib''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
exec ${pkgs.python3.pkgs.uvicorn}/bin/uvicorn run ${self}/app/main.py
'';
wantedBy = ["multi-user.target"];
serviceConfig = {
User = "gatekeeper";
Restart = "on-failure";
};
};
};
}