nixos/lasuite-drive: init (#409878)
This commit is contained in:
@@ -132,6 +132,8 @@
|
||||
|
||||
- [Howdy](https://github.com/boltgolt/howdy), a Windows Hello™ style facial authentication program for Linux.
|
||||
|
||||
- [SuiteNumérique Drive](https://github.com/suitenumerique/drive), a collaborative file sharing and document management platform that scales. Built with Django and React. Open source alternative to Sharepoint or Google Drive.
|
||||
|
||||
- [linux-enable-ir-emitter](https://github.com/EmixamPP/linux-enable-ir-emitter), a tool used to set up IR cameras, used with Howdy.
|
||||
|
||||
- [udp-over-tcp](https://github.com/mullvad/udp-over-tcp), a tunnel for proxying UDP traffic over a TCP stream. Available as `services.udp-over-tcp`.
|
||||
|
||||
@@ -1697,6 +1697,7 @@
|
||||
./services/web-apps/komga.nix
|
||||
./services/web-apps/lanraragi.nix
|
||||
./services/web-apps/lasuite-docs.nix
|
||||
./services/web-apps/lasuite-drive.nix
|
||||
./services/web-apps/lasuite-meet.nix
|
||||
./services/web-apps/lauti.nix
|
||||
./services/web-apps/lemmy.nix
|
||||
|
||||
@@ -0,0 +1,542 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
utils,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
concatMapStringsSep
|
||||
concatStringsSep
|
||||
escapeShellArg
|
||||
getExe
|
||||
hasSuffix
|
||||
mapAttrs
|
||||
match
|
||||
mkEnableOption
|
||||
mkIf
|
||||
mkPackageOption
|
||||
mkOption
|
||||
types
|
||||
optional
|
||||
optionalString
|
||||
;
|
||||
|
||||
cfg = config.services.lasuite-drive;
|
||||
|
||||
pythonEnvironment = mapAttrs (
|
||||
_: value:
|
||||
if value == null then
|
||||
"None"
|
||||
else if value == true then
|
||||
"True"
|
||||
else if value == false then
|
||||
"False"
|
||||
else
|
||||
toString value
|
||||
) cfg.settings;
|
||||
|
||||
commonServiceConfig = {
|
||||
RuntimeDirectory = "lasuite-drive";
|
||||
StateDirectory = "lasuite-drive";
|
||||
WorkingDirectory = "/var/lib/lasuite-drive";
|
||||
|
||||
User = "lasuite-drive";
|
||||
DynamicUser = true;
|
||||
SupplementaryGroups = mkIf cfg.redis.createLocally [
|
||||
config.services.redis.servers.lasuite-drive.group
|
||||
];
|
||||
# hardening
|
||||
AmbientCapabilities = "";
|
||||
CapabilityBoundingSet = [ "" ];
|
||||
DevicePolicy = "closed";
|
||||
LockPersonality = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateDevices = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = true;
|
||||
ProcSubset = "pid";
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
ProtectSystem = "strict";
|
||||
RemoveIPC = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_UNIX"
|
||||
];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
UMask = "0077";
|
||||
EnvironmentFile = cfg.environmentFiles;
|
||||
};
|
||||
|
||||
proxySuffix = if match "unix:.*" cfg.bind != null then ":" else "";
|
||||
|
||||
# Convert environment variables to be used as systemd-run arguments
|
||||
envArgs = lib.concatStringsSep " " (
|
||||
lib.mapAttrsToList (name: value: "-E ${escapeShellArg "${name}=${value}"}") pythonEnvironment
|
||||
);
|
||||
|
||||
# Easier usage of django manage.py stuff
|
||||
manage = pkgs.writeShellScriptBin "lasuite-drive-manage" ''
|
||||
exec ${lib.getExe' config.systemd.package "systemd-run"} \
|
||||
-p User=${commonServiceConfig.User} \
|
||||
-p DynamicUser=yes \
|
||||
-p StateDirectory=${commonServiceConfig.StateDirectory} \
|
||||
${optionalString cfg.redis.createLocally "-p SupplementaryGroups=${config.services.redis.servers.lasuite-drive.group} \\"}
|
||||
${concatMapStringsSep "\n" (envFile: "-p EnvironmentFile=${envFile} \\") cfg.environmentFiles}
|
||||
--working-directory=${commonServiceConfig.WorkingDirectory} \
|
||||
--quiet --collect --pipe --pty \
|
||||
${envArgs} ${lib.getExe cfg.package} "$@"
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.services.lasuite-drive = {
|
||||
enable = mkEnableOption "SuiteNumérique Drive";
|
||||
|
||||
package = mkPackageOption pkgs "lasuite-drive" { };
|
||||
|
||||
bind = mkOption {
|
||||
type = types.str;
|
||||
default = "unix:/run/lasuite-drive/gunicorn.sock";
|
||||
example = "127.0.0.1:8000";
|
||||
description = ''
|
||||
The path, host/port or file descriptior to bind the gunicorn socket to.
|
||||
|
||||
See <https://docs.gunicorn.org/en/stable/settings.html#bind> for possible options.
|
||||
'';
|
||||
};
|
||||
|
||||
enableNginx = mkEnableOption "enable and configure Nginx for reverse proxying" // {
|
||||
default = true;
|
||||
};
|
||||
|
||||
secretKeyPath = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
Path to the Django secret key.
|
||||
|
||||
The key can be generated using:
|
||||
```
|
||||
python3 -c 'import secrets; print(secrets.token_hex())'
|
||||
```
|
||||
|
||||
:::{.note}
|
||||
If not specified, a secret key is automatically generated and stored in the state directory.
|
||||
:::
|
||||
'';
|
||||
};
|
||||
|
||||
s3Url = mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
URL of the S3 bucket.
|
||||
'';
|
||||
};
|
||||
|
||||
postgresql = {
|
||||
createLocally = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Configure local PostgreSQL database server for drive.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
redis = {
|
||||
createLocally = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Configure local Redis cache server for drive.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
gunicorn = {
|
||||
extraArgs = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [
|
||||
"--name=drive"
|
||||
"--workers=3"
|
||||
];
|
||||
description = ''
|
||||
Extra arguments to pass to the gunicorn process.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
celery = {
|
||||
extraArgs = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = ''
|
||||
Extra arguments to pass to the celery process.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
domain = mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
Domain name of the drive instance.
|
||||
'';
|
||||
};
|
||||
|
||||
settings = mkOption {
|
||||
type = types.submodule {
|
||||
freeformType = types.attrsOf (
|
||||
types.nullOr (
|
||||
types.oneOf [
|
||||
types.str
|
||||
types.bool
|
||||
types.path
|
||||
types.int
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
options = {
|
||||
DJANGO_CONFIGURATION = mkOption {
|
||||
type = types.str;
|
||||
internal = true;
|
||||
default = "Production";
|
||||
description = "The configuration that Django will use";
|
||||
};
|
||||
|
||||
DJANGO_SETTINGS_MODULE = mkOption {
|
||||
type = types.str;
|
||||
internal = true;
|
||||
default = "drive.settings";
|
||||
description = "The configuration module that Django will use";
|
||||
};
|
||||
|
||||
DJANGO_SECRET_KEY_FILE = mkOption {
|
||||
type = types.path;
|
||||
default =
|
||||
if cfg.secretKeyPath == null then "/var/lib/lasuite-drive/django_secret_key" else cfg.secretKeyPath;
|
||||
description = "The path to the file containing Django's secret key";
|
||||
};
|
||||
|
||||
DATA_DIR = mkOption {
|
||||
type = types.path;
|
||||
default = "/var/lib/lasuite-drive";
|
||||
description = "Path to the data directory";
|
||||
readOnly = true;
|
||||
};
|
||||
|
||||
DJANGO_ALLOWED_HOSTS = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default =
|
||||
if cfg.enableNginx then
|
||||
[
|
||||
"localhost"
|
||||
"127.0.0.1"
|
||||
cfg.domain
|
||||
]
|
||||
else
|
||||
[ ];
|
||||
defaultText = lib.literalExpression ''
|
||||
if cfg.enableNginx then [ "localhost" "127.0.0.1" cfg.domain ] else [ ]
|
||||
'';
|
||||
apply = list: concatStringsSep "," list;
|
||||
description = "Comma-separated list of hosts that are able to connect to the server";
|
||||
};
|
||||
|
||||
DB_NAME = mkOption {
|
||||
type = types.str;
|
||||
default = "lasuite-drive";
|
||||
description = "Name of the database";
|
||||
};
|
||||
|
||||
DB_USER = mkOption {
|
||||
type = types.str;
|
||||
default = "lasuite-drive";
|
||||
description = "User of the database";
|
||||
};
|
||||
|
||||
DB_HOST = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = if cfg.postgresql.createLocally then "/run/postgresql" else null;
|
||||
description = "Host of the database";
|
||||
};
|
||||
|
||||
REDIS_URL = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default =
|
||||
if cfg.redis.createLocally then
|
||||
"unix://${config.services.redis.servers.lasuite-drive.unixSocket}?db=1"
|
||||
else
|
||||
null;
|
||||
description = "URL of the redis backend";
|
||||
};
|
||||
|
||||
CELERY_BROKER_URL = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default =
|
||||
if cfg.redis.createLocally then
|
||||
"redis+socket://${config.services.redis.servers.lasuite-drive.unixSocket}?db=2"
|
||||
else
|
||||
null;
|
||||
description = "URL of the redis backend for celery";
|
||||
};
|
||||
};
|
||||
};
|
||||
default = { };
|
||||
example = ''
|
||||
{
|
||||
AWS_S3_ENDPOINT_URL = "https://s3.us-west.amazonaws.com";
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Configuration options of drive.
|
||||
|
||||
See <https://github.com/suitenumerique/drive/blob/v${cfg.package.version}/docs/env.md>
|
||||
|
||||
`REDIS_URL` and `CELERY_BROKER_URL` are set if `services.lasuite-drive.redis.createLocally` is true.
|
||||
`DB_HOST` is set if `services.lasuite-drive.postgresql.createLocally` is true.
|
||||
'';
|
||||
};
|
||||
|
||||
environmentFiles = mkOption {
|
||||
type = types.listOf types.path;
|
||||
default = [ ];
|
||||
description = ''
|
||||
Path to environment files.
|
||||
|
||||
This can be useful to pass secrets to drive via tools like `agenix` or `sops`.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
warnings = mkIf (cfg.enableNginx && !(hasSuffix "/" cfg.s3Url)) [
|
||||
''
|
||||
services.lasuite-drive.s3Url should end with a trailing slash (/).
|
||||
This could break the HTTP requests by nginx to the S3 backend.
|
||||
''
|
||||
];
|
||||
|
||||
environment.systemPackages = [ manage ];
|
||||
|
||||
systemd.services.lasuite-drive = {
|
||||
description = "Drive from SuiteNumérique";
|
||||
after = [
|
||||
"network-online.target"
|
||||
]
|
||||
++ (optional cfg.postgresql.createLocally "postgresql.service")
|
||||
++ (optional cfg.redis.createLocally "redis-lasuite-drive.service");
|
||||
wants =
|
||||
(optional cfg.postgresql.createLocally "postgresql.service")
|
||||
++ (optional cfg.redis.createLocally "redis-lasuite-drive.service");
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
preStart = ''
|
||||
if [ ! -f .version ]; then
|
||||
touch .version
|
||||
fi
|
||||
|
||||
${optionalString (cfg.secretKeyPath == null) ''
|
||||
if [[ ! -f /var/lib/lasuite-drive/django_secret_key ]]; then
|
||||
(
|
||||
umask 0377
|
||||
tr -dc A-Za-z0-9 < /dev/urandom | head -c64 | ${pkgs.moreutils}/bin/sponge /var/lib/lasuite-drive/django_secret_key
|
||||
)
|
||||
fi
|
||||
''}
|
||||
if [ "${cfg.package.version}" != "$(cat .version)" ]; then
|
||||
${getExe cfg.package} migrate
|
||||
echo -n "${cfg.package.version}" > .version
|
||||
fi
|
||||
'';
|
||||
|
||||
environment = pythonEnvironment;
|
||||
|
||||
serviceConfig = {
|
||||
BindReadOnlyPaths = "${cfg.package}/share/static:/var/lib/lasuite-drive/static";
|
||||
|
||||
ExecStart = utils.escapeSystemdExecArgs (
|
||||
[
|
||||
(lib.getExe' cfg.package "gunicorn")
|
||||
"--bind=${cfg.bind}"
|
||||
]
|
||||
++ cfg.gunicorn.extraArgs
|
||||
++ [ "drive.wsgi:application" ]
|
||||
);
|
||||
}
|
||||
// commonServiceConfig;
|
||||
};
|
||||
|
||||
systemd.services.lasuite-drive-celery = {
|
||||
description = "Docs Celery broker from SuiteNumérique";
|
||||
after = [
|
||||
"network-online.target"
|
||||
]
|
||||
++ (optional cfg.postgresql.createLocally "postgresql.service")
|
||||
++ (optional cfg.redis.createLocally "redis-lasuite-drive.service");
|
||||
wants =
|
||||
(optional cfg.postgresql.createLocally "postgresql.service")
|
||||
++ (optional cfg.redis.createLocally "redis-lasuite-drive.service");
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
environment = pythonEnvironment;
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = utils.escapeSystemdExecArgs (
|
||||
[ (lib.getExe' cfg.package "celery") ]
|
||||
++ cfg.celery.extraArgs
|
||||
++ [
|
||||
"--app=drive.celery_app"
|
||||
"worker"
|
||||
]
|
||||
);
|
||||
}
|
||||
// commonServiceConfig;
|
||||
};
|
||||
|
||||
systemd.services.lasuite-drive-beat = {
|
||||
description = "Docs Celery beat from SuiteNumérique";
|
||||
after = [
|
||||
"network-online.target"
|
||||
]
|
||||
++ (optional cfg.postgresql.createLocally "postgresql.service")
|
||||
++ (optional cfg.redis.createLocally "redis-lasuite-drive.service");
|
||||
wants =
|
||||
(optional cfg.postgresql.createLocally "postgresql.service")
|
||||
++ (optional cfg.redis.createLocally "redis-lasuite-drive.service");
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
environment = pythonEnvironment;
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = utils.escapeSystemdExecArgs (
|
||||
[ (lib.getExe' cfg.package "celery") ]
|
||||
++ cfg.celery.extraArgs
|
||||
++ [
|
||||
"--app=drive.celery_app"
|
||||
"beat"
|
||||
]
|
||||
);
|
||||
}
|
||||
// commonServiceConfig;
|
||||
};
|
||||
|
||||
services.postgresql = mkIf cfg.postgresql.createLocally {
|
||||
enable = true;
|
||||
ensureDatabases = [ "lasuite-drive" ];
|
||||
ensureUsers = [
|
||||
{
|
||||
name = "lasuite-drive";
|
||||
ensureDBOwnership = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
services.redis.servers.lasuite-drive = mkIf cfg.redis.createLocally { enable = true; };
|
||||
|
||||
services.nginx = mkIf cfg.enableNginx {
|
||||
enable = true;
|
||||
|
||||
virtualHosts.${cfg.domain} = {
|
||||
extraConfig = ''
|
||||
error_page 401 /401.html;
|
||||
error_page 403 /403.html;
|
||||
error_page 404 /404.html;
|
||||
'';
|
||||
|
||||
root = cfg.package.frontend;
|
||||
|
||||
locations."/" = {
|
||||
tryFiles = "$uri $uri.html index.html $uri/ =404";
|
||||
};
|
||||
|
||||
locations."~ '^/explorer/items/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/?$'" =
|
||||
{
|
||||
tryFiles = "$uri /explorer/items/[id].html";
|
||||
};
|
||||
|
||||
locations."~ '^/explorer/items/files/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/?$'" =
|
||||
{
|
||||
tryFiles = "$uri /explorer/items/files/[id].html";
|
||||
};
|
||||
|
||||
locations."~ '^/wopi/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/?$'" = {
|
||||
tryFiles = "$uri /wopi/[id].html";
|
||||
};
|
||||
|
||||
locations."/static/" = {
|
||||
alias = "${cfg.package}/share/static/";
|
||||
};
|
||||
|
||||
locations."/api" = {
|
||||
proxyPass = "http://${cfg.bind}";
|
||||
recommendedProxySettings = true;
|
||||
};
|
||||
|
||||
locations."/admin" = {
|
||||
proxyPass = "http://${cfg.bind}";
|
||||
recommendedProxySettings = true;
|
||||
};
|
||||
|
||||
locations."/media-auth" = {
|
||||
proxyPass = "http://${cfg.bind}${proxySuffix}/api/v1.0/items/media-auth/";
|
||||
recommendedProxySettings = true;
|
||||
extraConfig = ''
|
||||
proxy_set_header X-Original-URL $request_uri;
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
proxy_set_header X-Original-Method $request_method;
|
||||
'';
|
||||
};
|
||||
|
||||
locations."/media/" = {
|
||||
proxyPass = cfg.s3Url;
|
||||
extraConfig = ''
|
||||
auth_request /media-auth;
|
||||
auth_request_set $authHeader $upstream_http_authorization;
|
||||
auth_request_set $authDate $upstream_http_x_amz_date;
|
||||
auth_request_set $authContentSha256 $upstream_http_x_amz_content_sha256;
|
||||
|
||||
proxy_set_header Authorization $authHeader;
|
||||
proxy_set_header X-Amz-Date $authDate;
|
||||
proxy_set_header X-Amz-Content-SHA256 $authContentSha256;
|
||||
|
||||
add_header Content-Disposition "attachment";
|
||||
'';
|
||||
};
|
||||
|
||||
locations."/media/preview/" = {
|
||||
proxyPass = cfg.s3Url;
|
||||
extraConfig = ''
|
||||
auth_request /media-auth;
|
||||
auth_request_set $authHeader $upstream_http_authorization;
|
||||
auth_request_set $authDate $upstream_http_x_amz_date;
|
||||
auth_request_set $authContentSha256 $upstream_http_x_amz_content_sha256;
|
||||
|
||||
proxy_set_header Authorization $authHeader;
|
||||
proxy_set_header X-Amz-Date $authDate;
|
||||
proxy_set_header X-Amz-Content-SHA256 $authContentSha256;
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
meta = {
|
||||
buildDocsInSandbox = false;
|
||||
maintainers = [ lib.maintainers.soyouzpanda ];
|
||||
};
|
||||
}
|
||||
@@ -872,6 +872,7 @@ in
|
||||
languagetool = runTest ./languagetool.nix;
|
||||
lanraragi = runTest ./lanraragi.nix;
|
||||
lasuite-docs = runTest ./web-apps/lasuite-docs.nix;
|
||||
lasuite-drive = runTest ./web-apps/lasuite-drive.nix;
|
||||
lasuite-meet = runTest ./web-apps/lasuite-meet.nix;
|
||||
latestKernel.login = runTest {
|
||||
imports = [ ./login.nix ];
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
{ lib, ... }:
|
||||
let
|
||||
domain = "drive.local";
|
||||
oidcAddr = "127.0.0.1:8080";
|
||||
s3Addr = "127.0.0.1:9000";
|
||||
|
||||
garageAccessKey = "GKaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||
garageSecretKey = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||
in
|
||||
|
||||
{
|
||||
name = "lasuite-drive";
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
soyouzpanda
|
||||
];
|
||||
|
||||
containers.machine =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
networking.hosts."127.0.0.1" = [ domain ];
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
awscli2
|
||||
jq
|
||||
];
|
||||
|
||||
services.lasuite-drive = {
|
||||
enable = true;
|
||||
enableNginx = true;
|
||||
redis.createLocally = true;
|
||||
postgresql.createLocally = true;
|
||||
|
||||
inherit domain;
|
||||
s3Url = "http://${s3Addr}/lasuite-drive/";
|
||||
|
||||
settings = {
|
||||
DJANGO_SECRET_KEY_FILE = pkgs.writeText "django-secret-file" ''
|
||||
8540db59c03943d48c3ed1a0f96ce3b560e0f45274f120f7ee4dace3cc366a6b
|
||||
'';
|
||||
|
||||
OIDC_OP_JWKS_ENDPOINT = "http://${oidcAddr}/dex/keys";
|
||||
OIDC_OP_AUTHORIZATION_ENDPOINT = "http://${oidcAddr}/dex/auth/mock";
|
||||
OIDC_OP_TOKEN_ENDPOINT = "http://${oidcAddr}/dex/token";
|
||||
OIDC_OP_USER_ENDPOINT = "http://${oidcAddr}/dex/userinfo";
|
||||
OIDC_RP_CLIENT_ID = "lasuite-drive";
|
||||
OIDC_RP_SIGN_ALGO = "RS256";
|
||||
OIDC_RP_SCOPES = "openid email";
|
||||
OIDC_RP_CLIENT_SECRET = "lasuitedriveclientsecret";
|
||||
|
||||
LOGIN_REDIRECT_URL = "http://${domain}";
|
||||
LOGIN_REDIRECT_URL_FAILURE = "http://${domain}";
|
||||
LOGOUT_REDIRECT_URL = "http://${domain}";
|
||||
|
||||
AWS_S3_ENDPOINT_URL = "http://${s3Addr}";
|
||||
AWS_S3_ACCESS_KEY_ID = garageAccessKey;
|
||||
AWS_S3_SECRET_ACCESS_KEY = garageSecretKey;
|
||||
AWS_STORAGE_BUCKET_NAME = "lasuite-drive";
|
||||
AWS_S3_REGION_NAME = "garage";
|
||||
MEDIA_BASE_URL = "http://${domain}";
|
||||
|
||||
# Disable HTTPS feature in tests because we're running on a HTTP connection
|
||||
DJANGO_SECURE_PROXY_SSL_HEADER = "";
|
||||
DJANGO_SECURE_SSL_REDIRECT = false;
|
||||
DJANGO_CSRF_COOKIE_SECURE = false;
|
||||
DJANGO_SESSION_COOKIE_SECURE = false;
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS = "http://*";
|
||||
};
|
||||
};
|
||||
|
||||
services.dex = {
|
||||
enable = true;
|
||||
settings = {
|
||||
issuer = "http://${oidcAddr}/dex";
|
||||
storage = {
|
||||
type = "postgres";
|
||||
config.host = "/var/run/postgresql";
|
||||
};
|
||||
web.http = "127.0.0.1:8080";
|
||||
oauth2.skipApprovalScreen = true;
|
||||
staticClients = [
|
||||
{
|
||||
id = "lasuite-drive";
|
||||
name = "Drive";
|
||||
redirectURIs = [ "http://${domain}/api/v1.0/callback/" ];
|
||||
secretFile = "/etc/dex/lasuite-drive";
|
||||
}
|
||||
];
|
||||
connectors = [
|
||||
{
|
||||
type = "mockPassword";
|
||||
id = "mock";
|
||||
name = "Example";
|
||||
config = {
|
||||
username = "admin";
|
||||
password = "password";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
services.garage = {
|
||||
enable = true;
|
||||
package = pkgs.garage_2;
|
||||
settings = {
|
||||
rpc_bind_addr = "127.0.0.1:3901";
|
||||
rpc_public_addr = "127.0.0.1:3901";
|
||||
rpc_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||
replication_factor = 1;
|
||||
|
||||
s3_api = {
|
||||
s3_region = "garage";
|
||||
api_bind_addr = s3Addr;
|
||||
};
|
||||
};
|
||||
};
|
||||
environment.etc."dex/lasuite-drive" = {
|
||||
mode = "0400";
|
||||
user = "dex";
|
||||
text = "lasuitedriveclientsecret";
|
||||
};
|
||||
|
||||
services.postgresql = {
|
||||
enable = true;
|
||||
ensureDatabases = [ "dex" ];
|
||||
ensureUsers = [
|
||||
{
|
||||
name = "dex";
|
||||
ensureDBOwnership = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
import json
|
||||
|
||||
with subtest("Wait for units to start"):
|
||||
machine.wait_for_unit("dex.service")
|
||||
machine.wait_for_unit("garage.service")
|
||||
machine.wait_for_unit("lasuite-drive.service")
|
||||
machine.wait_for_unit("lasuite-drive-celery.service")
|
||||
machine.wait_for_unit("lasuite-drive-beat.service")
|
||||
|
||||
with subtest("Create S3 bucket"):
|
||||
machine.wait_for_open_port(3901)
|
||||
garage_node_id = machine.succeed("garage status | tail -n1 | awk '{ print $1 }'")
|
||||
machine.succeed(f"garage layout assign -c 100MB -z garage {garage_node_id}")
|
||||
machine.succeed("garage layout apply --version 1")
|
||||
machine.succeed("garage key import ${garageAccessKey} ${garageSecretKey} --yes")
|
||||
machine.succeed("garage bucket create lasuite-drive")
|
||||
machine.succeed("garage bucket allow --read --write --owner lasuite-drive --key ${garageAccessKey}")
|
||||
machine.succeed("AWS_SECRET_ACCESS_KEY=${garageSecretKey} "
|
||||
"AWS_ACCESS_KEY_ID=${garageAccessKey} "
|
||||
"aws --endpoint http://${s3Addr} "
|
||||
"s3api put-bucket-cors "
|
||||
"--bucket lasuite-drive --cors-configuration "
|
||||
"""'{"CORSRules":[{"AllowedHeaders":["*"],"AllowedMethods":["PUT"],"AllowedOrigins":["http://${domain}"]}]}'""")
|
||||
|
||||
with subtest("Wait for web servers to start"):
|
||||
machine.wait_until_succeeds("curl -fs "
|
||||
"'http://${domain}/api/v1.0/authenticate/'",
|
||||
timeout=120)
|
||||
machine.wait_until_succeeds("curl -fs "
|
||||
"'${oidcAddr}/dex/auth/mock?client_id=lasuite-drive&response_type=code&redirect_uri=http://${domain}/api/v1.0/callback/&scope=openid'",
|
||||
timeout=120)
|
||||
|
||||
with subtest("Login"):
|
||||
state, nonce = machine.succeed("curl -fs -c cjar "
|
||||
"'http://${domain}/api/v1.0/authenticate/' "
|
||||
"-w '%{redirect_url}' "
|
||||
"| sed -n 's/.*state=\\(.*\\)&nonce=\\(.*\\)/\\1 \\2/p'").strip().split(' ')
|
||||
|
||||
oidc_state = machine.succeed("curl -fs "
|
||||
f"'${oidcAddr}/dex/auth/mock?client_id=lasuite-drive&response_type=code&redirect_uri=http://${domain}/api/v1.0/callback/&scope=openid+email&state={state}&nonce={nonce}' "
|
||||
"| sed -n 's/.*state=\\(.*\\)\">.*/\\1/p'").strip()
|
||||
|
||||
code = machine.succeed("curl -fs "
|
||||
f"'${oidcAddr}/dex/auth/mock/login?back=&state={oidc_state}' "
|
||||
"-d 'login=admin&password=password' "
|
||||
"-w '%{redirect_url}' "
|
||||
"| sed -n 's/.*code=\\(.*\\)&.*/\\1/p'").strip()
|
||||
|
||||
print(f"Got approval code {code}")
|
||||
|
||||
machine.succeed(f"curl -fs -c cjar -b cjar 'http://${domain}/api/v1.0/callback/?code={code}&state={state}'")
|
||||
|
||||
with subtest("Upload a document"):
|
||||
csrf_token = machine.succeed("grep csrftoken cjar | cut -f 7 | tr -d '\n'")
|
||||
upload = json.loads(
|
||||
machine.succeed("curl -fs -c cjar -b cjar "
|
||||
"'http://${domain}/api/v1.0/items/' "
|
||||
"-X POST "
|
||||
f"-H 'X-CSRFToken: {csrf_token}' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
"-H 'Referer: http://${domain}/explorer/items/my-files' "
|
||||
"--data-raw '{\"type\":\"file\",\"filename\":\"text.txt\"}'"
|
||||
)
|
||||
)
|
||||
|
||||
print(f"Created file with id {upload['id']}")
|
||||
|
||||
machine.succeed("curl -fs "
|
||||
f"'{upload['policy']}' "
|
||||
"-X PUT "
|
||||
"-H 'Origin: http://${domain}' "
|
||||
"-H 'X-amz-acl: private' "
|
||||
"-H 'Content-Type: text/plain' "
|
||||
"-H 'Content-Length: 8' "
|
||||
"--data-raw 'sometext'")
|
||||
|
||||
print("Uploaded file")
|
||||
|
||||
csrf_token = machine.succeed("grep csrftoken cjar | cut -f 7 | tr -d '\n'")
|
||||
machine.succeed("curl -fs -c cjar -b cjar "
|
||||
f"'http://${domain}/api/v1.0/items/{upload['id']}/upload-ended/' "
|
||||
"-X POST "
|
||||
f"-H 'X-CSRFToken: {csrf_token}' "
|
||||
"-H 'Referer: http://${domain}/explorer/items/my-files'")
|
||||
|
||||
with subtest("Download a document"):
|
||||
csrf_token = machine.succeed("grep csrftoken cjar | cut -f 7 | tr -d '\n'")
|
||||
items = json.loads(
|
||||
machine.succeed("curl -fs -c cjar -b cjar "
|
||||
f"'http://${domain}/api/v1.0/items/' "
|
||||
"-X GET "
|
||||
f"-H 'X-CSRFToken: {csrf_token}' "
|
||||
"-H 'Referer: http://${domain}/explorer/items/my-files'"
|
||||
)
|
||||
)
|
||||
|
||||
assert items["count"] == 1
|
||||
|
||||
url = items["results"][0]["url_permalink"]
|
||||
|
||||
data = machine.succeed("curl -fs -c cjar -b cjar -L "
|
||||
f"'{url}' "
|
||||
"-X GET "
|
||||
"-H 'Referer: http://${domain}/explorer/items/my-files'")
|
||||
|
||||
assert data == "sometext"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
src,
|
||||
version,
|
||||
meta,
|
||||
stdenv,
|
||||
fetchYarnDeps,
|
||||
nodejs,
|
||||
fixup-yarn-lock,
|
||||
yarn,
|
||||
yarnConfigHook,
|
||||
yarnBuildHook,
|
||||
}:
|
||||
|
||||
stdenv.mkDerivation (finalAttrs: {
|
||||
pname = "lasuite-drive-frontend";
|
||||
inherit src version;
|
||||
|
||||
sourceRoot = "${finalAttrs.src.name}/src/frontend";
|
||||
|
||||
offlineCache = fetchYarnDeps {
|
||||
yarnLock = "${finalAttrs.src}/src/frontend/yarn.lock";
|
||||
hash = "sha256-yUKJp6yUTxpvkaA+YuQC3r1t4LBvuYMv1xesLewbK/U=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
nodejs
|
||||
fixup-yarn-lock
|
||||
yarn
|
||||
yarnConfigHook
|
||||
yarnBuildHook
|
||||
];
|
||||
|
||||
strictDeps = true;
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
cp -r apps/drive/out/ $out
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
__structuredAttrs = true;
|
||||
|
||||
meta = meta // {
|
||||
description = "A collaborative file sharing and document management platform that scales. Built with Django and React. Opensource alternative to Sharepoint or Google Drive";
|
||||
};
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
src,
|
||||
version,
|
||||
meta,
|
||||
stdenv,
|
||||
fetchYarnDeps,
|
||||
nodejs,
|
||||
yarnConfigHook,
|
||||
yarnBuildHook,
|
||||
}:
|
||||
stdenv.mkDerivation (finalAttrs: {
|
||||
pname = "lasuite-drive-mail";
|
||||
inherit src version;
|
||||
|
||||
sourceRoot = "${finalAttrs.src.name}/src/mail";
|
||||
|
||||
postPatch = ''
|
||||
substituteInPlace bin/html-to-plain-text bin/mjml-to-html \
|
||||
--replace-fail \
|
||||
'../backend/core/templates/mail' \
|
||||
'${placeholder "out"}'
|
||||
'';
|
||||
|
||||
offlineCache = fetchYarnDeps {
|
||||
yarnLock = "${finalAttrs.src}/src/mail/yarn.lock";
|
||||
hash = "sha256-UPIb9QJk+zC8wYeBeDnmlGLhHDhsEOoT+qquFM1XyqU=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
nodejs
|
||||
yarnConfigHook
|
||||
yarnBuildHook
|
||||
];
|
||||
|
||||
dontInstall = true;
|
||||
|
||||
__structuredAttrs = true;
|
||||
|
||||
meta = meta // {
|
||||
description = "HTML mail templates for LaSuite Drive";
|
||||
};
|
||||
})
|
||||
@@ -0,0 +1,156 @@
|
||||
{
|
||||
callPackage,
|
||||
lib,
|
||||
python3,
|
||||
stdenv,
|
||||
fetchFromGitHub,
|
||||
nixosTests,
|
||||
}:
|
||||
let
|
||||
version = "0.18.0";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "suitenumerique";
|
||||
repo = "drive";
|
||||
tag = "v${version}";
|
||||
hash = "sha256-JoOHbwZR4salfLB9Gg7kfRMDcDA/Srn8qwUqLAZtsz8=";
|
||||
};
|
||||
|
||||
meta = {
|
||||
homepage = "https://github.com/suitenumerique/drive";
|
||||
changelog = "https://github.com/suitenumerique/drive/blob/${src.tag}/CHANGELOG.md";
|
||||
license = lib.licenses.mit;
|
||||
maintainers = with lib.maintainers; [ soyouzpanda ];
|
||||
platforms = lib.platforms.linux;
|
||||
};
|
||||
|
||||
mail = callPackage ./mail.nix { inherit src version meta; };
|
||||
frontend = callPackage ./frontend.nix { inherit src version meta; };
|
||||
|
||||
python = python3.override {
|
||||
self = python;
|
||||
packageOverrides = (self: super: { django = super.django_5; });
|
||||
};
|
||||
|
||||
in
|
||||
python.pkgs.buildPythonApplication (finalAttrs: {
|
||||
pname = "lasuite-drive";
|
||||
pyproject = true;
|
||||
inherit version src;
|
||||
|
||||
sourceRoot = "${finalAttrs.src.name}/src/backend";
|
||||
|
||||
patches = [
|
||||
# Support configuration throught environment variables for SECURE_*
|
||||
./secure_settings.patch
|
||||
# Fix some build fields on pyproject
|
||||
./pyproject_build.patch
|
||||
];
|
||||
|
||||
build-system = with python.pkgs; [ uv-build ];
|
||||
|
||||
dependencies =
|
||||
with python.pkgs;
|
||||
[
|
||||
boto3
|
||||
brotli
|
||||
celery
|
||||
defusedxml
|
||||
dj-database-url
|
||||
django
|
||||
django-configurations
|
||||
django-cors-headers
|
||||
django-countries
|
||||
django-debug-toolbar
|
||||
django-extensions
|
||||
django-filter
|
||||
django-lasuite
|
||||
django-ltree
|
||||
django-parler
|
||||
django-pydantic-field
|
||||
django-redis
|
||||
django-storages
|
||||
django-timezone-field
|
||||
djangorestframework
|
||||
djangorestframework-api-key
|
||||
dockerflow
|
||||
drf-spectacular
|
||||
drf-spectacular-sidecar
|
||||
drf-standardized-errors
|
||||
easy-thumbnails
|
||||
factory-boy
|
||||
gunicorn
|
||||
jsonschema
|
||||
markdown
|
||||
mozilla-django-oidc
|
||||
nested-multipart-parser
|
||||
posthog
|
||||
psycopg
|
||||
pydantic
|
||||
pyjwt
|
||||
python-magic
|
||||
redis
|
||||
requests
|
||||
sentry-sdk
|
||||
url-normalize
|
||||
whitenoise
|
||||
]
|
||||
++ celery.optional-dependencies.redis
|
||||
++ django-storages.optional-dependencies.s3;
|
||||
|
||||
pythonRelaxDeps = true;
|
||||
|
||||
postPatch = ''
|
||||
# Put assets inside a data directory
|
||||
# so uv will copy the assets directory
|
||||
# entirely
|
||||
mkdir data
|
||||
mv assets data
|
||||
''
|
||||
+ (lib.optionalString stdenv.hostPlatform.isDarwin ''
|
||||
substituteInPlace impress/settings.py \
|
||||
--replace-fail \
|
||||
"gethostname()" \
|
||||
"gethostname() + '.local'"
|
||||
'');
|
||||
__darwinAllowLocalNetworking = true;
|
||||
|
||||
postBuild = ''
|
||||
export DATA_DIR=$(pwd)/data
|
||||
${python.pythonOnBuildForHost.interpreter} manage.py collectstatic --no-input --clear
|
||||
'';
|
||||
|
||||
postInstall =
|
||||
let
|
||||
pythonPath = python.pkgs.makePythonPath finalAttrs.passthru.dependencies;
|
||||
in
|
||||
''
|
||||
mkdir -p $out/{bin,share}
|
||||
cp ./manage.py $out/bin/.manage.py
|
||||
cp -r data/static $out/share
|
||||
chmod +x $out/bin/.manage.py
|
||||
makeWrapper $out/bin/.manage.py $out/bin/drive \
|
||||
--prefix PYTHONPATH : "${pythonPath}"
|
||||
makeWrapper ${lib.getExe python.pkgs.celery} $out/bin/celery \
|
||||
--prefix PYTHONPATH : "${pythonPath}:$out/${python.sitePackages}"
|
||||
makeWrapper ${lib.getExe python.pkgs.gunicorn} $out/bin/gunicorn \
|
||||
--prefix PYTHONPATH : "${pythonPath}:$out/${python.sitePackages}"
|
||||
|
||||
mkdir -p $out/${python.sitePackages}/core/templates
|
||||
ln -sv ${mail}/ $out/${python.sitePackages}/core/templates/mail
|
||||
'';
|
||||
|
||||
passthru = {
|
||||
inherit mail frontend;
|
||||
tests = {
|
||||
login-upload-and-download-file = nixosTests.lasuite-drive;
|
||||
};
|
||||
};
|
||||
|
||||
__structuredAttrs = true;
|
||||
|
||||
meta = meta // {
|
||||
description = "A collaborative file sharing and document management platform that scales. Built with Django and React. Opensource alternative to Sharepoint or Google Drive";
|
||||
mainProgram = "drive";
|
||||
};
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
diff --git a/pyproject.toml b/pyproject.toml
|
||||
index ba8f5362..ddc40c83 100644
|
||||
--- a/pyproject.toml
|
||||
+++ b/pyproject.toml
|
||||
@@ -2,7 +2,7 @@
|
||||
# drive package
|
||||
#
|
||||
[build-system]
|
||||
-requires = ["uv_build>=0.11.7,<0.12.0"]
|
||||
+requires = ["uv_build"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
[project]
|
||||
@@ -102,6 +102,9 @@ constraint-dependencies = [
|
||||
|
||||
[tool.uv.build-backend]
|
||||
module-root = ""
|
||||
+module-name = ["core", "demo", "drive", "e2e", "wopi"]
|
||||
+data = ["data"]
|
||||
+namespace = true
|
||||
source-exclude = [
|
||||
"**/tests/**",
|
||||
"**/test_*.py",
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
diff --git a/drive/settings.py b/drive/settings.py
|
||||
index 82f4d31..3d66c23 100755
|
||||
--- a/drive/settings.py
|
||||
+++ b/drive/settings.py
|
||||
@@ -775,19 +775,24 @@ class Production(Base):
|
||||
#
|
||||
# In other cases, you should comment the following line to avoid security issues.
|
||||
# SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
- SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
- SECURE_HSTS_SECONDS = 60
|
||||
- SECURE_HSTS_PRELOAD = True
|
||||
- SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
- SECURE_SSL_REDIRECT = True
|
||||
+ SECURE_PROXY_SSL_HEADER = values.TupleValue(("HTTP_X_FORWARDED_PROTO", "https"),
|
||||
+ environ_name="SECURE_PROXY_SSL_HEADER")
|
||||
+ SECURE_HSTS_SECONDS = values.IntegerValue(
|
||||
+ 60, environ_name="SECURE_HSTS_SECONDS")
|
||||
+ SECURE_HSTS_PRELOAD = values.BooleanValue(
|
||||
+ True, environ_name="SECURE_HSTS_PRELOAD")
|
||||
+ SECURE_HSTS_INCLUDE_SUBDOMAINS = values.BooleanValue(
|
||||
+ True, environ_name="SECURE_HSTS_INCLUDE_SUBDOMAINS")
|
||||
+ SECURE_SSL_REDIRECT = values.BooleanValue(
|
||||
+ True, environ_name="SECURE_SSL_REDIRECT")
|
||||
SECURE_REDIRECT_EXEMPT = [
|
||||
"^__lbheartbeat__",
|
||||
"^__heartbeat__",
|
||||
]
|
||||
|
||||
# Modern browsers require to have the `secure` attribute on cookies with `Samesite=none`
|
||||
- CSRF_COOKIE_SECURE = True
|
||||
- SESSION_COOKIE_SECURE = True
|
||||
+ CSRF_COOKIE_SECURE = values.BooleanValue(True, environ_name="CSRF_COOKIE_SECURE")
|
||||
+ SESSION_COOKIE_SECURE = values.BooleanValue(True, environ_name="SESSION_COOKIE_SECURE")
|
||||
|
||||
# Privacy
|
||||
SECURE_REFERRER_POLICY = "same-origin"
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
lib,
|
||||
buildPythonPackage,
|
||||
fetchFromGitHub,
|
||||
setuptools,
|
||||
django,
|
||||
}:
|
||||
|
||||
buildPythonPackage (finalAttrs: {
|
||||
pname = "django-ltree";
|
||||
version = "0.6.0";
|
||||
pyproject = true;
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "mariocesar";
|
||||
repo = "django-ltree";
|
||||
tag = "v${finalAttrs.version}";
|
||||
hash = "sha256-XN2znH9bNU8jaY2HC8qmSR6VqShcEFGAtNtb/5aLgic=";
|
||||
};
|
||||
|
||||
build-system = [
|
||||
setuptools
|
||||
];
|
||||
|
||||
dependencies = [
|
||||
django
|
||||
];
|
||||
|
||||
pythonImportsCheck = [
|
||||
"django_ltree"
|
||||
];
|
||||
|
||||
meta = {
|
||||
description = "An ltree extension implementation to support hierarchical tree-like data using the native Postgres extension ltree in django models";
|
||||
homepage = "https://github.com/mariocesar/django-ltree";
|
||||
changelog = "https://github.com/mariocesar/django-ltree/releases/tag/${finalAttrs.src.tag}";
|
||||
license = lib.licenses.mit;
|
||||
maintainers = with lib.maintainers; [ soyouzpanda ];
|
||||
};
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
lib,
|
||||
buildPythonPackage,
|
||||
fetchFromGitHub,
|
||||
dj-database-url,
|
||||
django,
|
||||
django-test-migrations,
|
||||
djangorestframework,
|
||||
packaging,
|
||||
pytest-cov-stub,
|
||||
pytest-django,
|
||||
pytest-dotenv,
|
||||
pytestCheckHook,
|
||||
setuptools,
|
||||
setuptools-scm,
|
||||
}:
|
||||
|
||||
buildPythonPackage (finalAttrs: {
|
||||
pname = "djangorestframework-api-key";
|
||||
version = "3.1.0";
|
||||
pyproject = true;
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "florimondmanca";
|
||||
repo = "djangorestframework-api-key";
|
||||
tag = "v${finalAttrs.version}";
|
||||
hash = "sha256-TyYSO3OQslipl2T5BtsTABaTJD4HMCX61TOZXNR+lXE=";
|
||||
};
|
||||
|
||||
# Use python-dotenv instead of django-dotenv
|
||||
# as django-dotenv has not been maintained for
|
||||
# years.
|
||||
patchPhase = ''
|
||||
runHook prePatch
|
||||
|
||||
substituteInPlace tests/conftest.py test_project/manage.py \
|
||||
--replace-fail 'read_dotenv' 'load_dotenv'
|
||||
|
||||
runHook postPatch
|
||||
'';
|
||||
|
||||
build-system = [
|
||||
setuptools
|
||||
setuptools-scm
|
||||
];
|
||||
|
||||
dependencies = [
|
||||
django
|
||||
djangorestframework
|
||||
packaging
|
||||
];
|
||||
|
||||
nativeCheckInputs = [
|
||||
dj-database-url
|
||||
django-test-migrations
|
||||
pytest-cov-stub
|
||||
pytest-django
|
||||
pytest-dotenv
|
||||
pytestCheckHook
|
||||
];
|
||||
|
||||
pythonImportsCheck = [
|
||||
"rest_framework_api_key"
|
||||
];
|
||||
|
||||
meta = {
|
||||
description = "API key permissions for Django REST Framework";
|
||||
homepage = "https://github.com/florimondmanca/djangorestframework-api-key";
|
||||
changelog = "https://github.com/florimondmanca/djangorestframework-api-key/blob/${finalAttrs.src.tag}/CHANGELOG.md";
|
||||
license = lib.licenses.mit;
|
||||
maintainers = with lib.maintainers; [ soyouzpanda ];
|
||||
};
|
||||
})
|
||||
@@ -4282,6 +4282,8 @@ self: super: with self; {
|
||||
callPackage ../development/python-modules/django-login-required-middleware
|
||||
{ };
|
||||
|
||||
django-ltree = callPackage ../development/python-modules/django-ltree { };
|
||||
|
||||
django-mailbox = callPackage ../development/python-modules/django-mailbox { };
|
||||
|
||||
django-mailman3 = callPackage ../development/python-modules/django-mailman3 { };
|
||||
@@ -4507,6 +4509,10 @@ self: super: with self; {
|
||||
|
||||
djangorestframework = callPackage ../development/python-modules/djangorestframework { };
|
||||
|
||||
djangorestframework-api-key =
|
||||
callPackage ../development/python-modules/djangorestframework-api-key
|
||||
{ };
|
||||
|
||||
djangorestframework-camel-case =
|
||||
callPackage ../development/python-modules/djangorestframework-camel-case
|
||||
{ };
|
||||
|
||||
Reference in New Issue
Block a user