diff --git a/migrations/0008_faithful_golden_guardian.sql b/migrations/0008_faithful_golden_guardian.sql new file mode 100644 index 0000000..c8d5248 --- /dev/null +++ b/migrations/0008_faithful_golden_guardian.sql @@ -0,0 +1,34 @@ +CREATE TABLE `ip_address` ( + `id` int unsigned AUTO_INCREMENT NOT NULL, + `ip_address` int unsigned NOT NULL, + `flags` tinyint unsigned NOT NULL, + `listId` int unsigned, + `created_at` datetime(6) NOT NULL DEFAULT current_timestamp(6), + `updated_at` datetime(6) NOT NULL DEFAULT current_timestamp(6), + CONSTRAINT `ip_address_id` PRIMARY KEY(`id`), + CONSTRAINT `ip_address_idx` UNIQUE(`ip_address`) +); +--> statement-breakpoint +CREATE TABLE `ip_list` ( + `id` int unsigned AUTO_INCREMENT NOT NULL, + `name` text NOT NULL, + `url` text, + `default_flags` tinyint unsigned NOT NULL, + `created_at` datetime(6) NOT NULL DEFAULT current_timestamp(6), + `updated_at` datetime(6) NOT NULL DEFAULT current_timestamp(6), + CONSTRAINT `ip_list_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `ip_address_user` ( + `id` int unsigned AUTO_INCREMENT NOT NULL, + `userId` int, + `ipAddressId` int unsigned, + CONSTRAINT `ip_address_user_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +ALTER TABLE `audit_log` ADD `ipAddressId` int unsigned;--> statement-breakpoint +ALTER TABLE `ip_address` ADD CONSTRAINT `ip_address_listId_ip_list_id_fk` FOREIGN KEY (`listId`) REFERENCES `ip_list`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ip_address_user` ADD CONSTRAINT `ip_address_user_userId_user_id_fk` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `ip_address_user` ADD CONSTRAINT `ip_address_user_ipAddressId_ip_address_id_fk` FOREIGN KEY (`ipAddressId`) REFERENCES `ip_address`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX `ip_flags_idx` ON `ip_address` (`flags`);--> statement-breakpoint +ALTER TABLE `audit_log` ADD CONSTRAINT `audit_log_ipAddressId_ip_address_id_fk` FOREIGN KEY (`ipAddressId`) REFERENCES `ip_address`(`id`) ON DELETE set null ON UPDATE no action; \ No newline at end of file diff --git a/migrations/0009_broad_kylun.sql b/migrations/0009_broad_kylun.sql new file mode 100644 index 0000000..84365ee --- /dev/null +++ b/migrations/0009_broad_kylun.sql @@ -0,0 +1 @@ +ALTER TABLE `ip_address` MODIFY COLUMN `ip_address` varbinary(16) NOT NULL; \ No newline at end of file diff --git a/migrations/meta/0008_snapshot.json b/migrations/meta/0008_snapshot.json new file mode 100644 index 0000000..da56293 --- /dev/null +++ b/migrations/meta/0008_snapshot.json @@ -0,0 +1,1422 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "25719548-9213-4eec-ace3-2241fda14d8d", + "prevId": "ae1f8d60-30eb-44f7-9f01-8481d6d40644", + "tables": { + "audit_log": { + "name": "audit_log", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_ip": { + "name": "actor_ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_ua": { + "name": "actor_ua", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "flagged": { + "name": "flagged", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "actorId": { + "name": "actorId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ipAddressId": { + "name": "ipAddressId", + "type": "int unsigned", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "audit_log_actorId_user_id_fk": { + "name": "audit_log_actorId_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": [ + "actorId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_ipAddressId_ip_address_id_fk": { + "name": "audit_log_ipAddressId_ip_address_id_fk", + "tableFrom": "audit_log", + "tableTo": "ip_address", + "columnsFrom": [ + "ipAddressId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "document": { + "name": "document", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "authorId": { + "name": "authorId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": {}, + "foreignKeys": { + "document_authorId_user_id_fk": { + "name": "document_authorId_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": [ + "authorId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ip_address": { + "name": "ip_address", + "columns": { + "id": { + "name": "id", + "type": "int unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "ip_address": { + "name": "ip_address", + "type": "int unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "flags": { + "name": "flags", + "type": "tinyint unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "int unsigned", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": { + "ip_address_idx": { + "name": "ip_address_idx", + "columns": [ + "ip_address" + ], + "isUnique": true + }, + "ip_flags_idx": { + "name": "ip_flags_idx", + "columns": [ + "flags" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ip_address_listId_ip_list_id_fk": { + "name": "ip_address_listId_ip_list_id_fk", + "tableFrom": "ip_address", + "tableTo": "ip_list", + "columnsFrom": [ + "listId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ip_address_id": { + "name": "ip_address_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ip_list": { + "name": "ip_list", + "columns": { + "id": { + "name": "id", + "type": "int unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_flags": { + "name": "default_flags", + "type": "tinyint unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "ip_list_id": { + "name": "ip_list_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ip_address_user": { + "name": "ip_address_user", + "columns": { + "id": { + "name": "id", + "type": "int unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ipAddressId": { + "name": "ipAddressId", + "type": "int unsigned", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "ip_address_user_userId_user_id_fk": { + "name": "ip_address_user_userId_user_id_fk", + "tableFrom": "ip_address_user", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ip_address_user_ipAddressId_ip_address_id_fk": { + "name": "ip_address_user_ipAddressId_ip_address_id_fk", + "tableFrom": "ip_address_user", + "tableTo": "ip_address", + "columnsFrom": [ + "ipAddressId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ip_address_user_id": { + "name": "ip_address_user_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "jwks": { + "name": "jwks", + "columns": { + "uuid": { + "name": "uuid", + "type": "varchar(36)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fingerprint": { + "name": "fingerprint", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current": { + "name": "current", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rotate_at": { + "name": "rotate_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "jwks_uuid": { + "name": "jwks_uuid", + "columns": [ + "uuid" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "o_auth2_client": { + "name": "o_auth2_client", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "client_id": { + "name": "client_id", + "type": "varchar(36)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "grants": { + "name": "grants", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('authorization_code')" + }, + "activated": { + "name": "activated", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "verified": { + "name": "verified", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "confidential": { + "name": "confidential", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "enforce_par": { + "name": "enforce_par", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "jwks": { + "name": "jwks", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pictureId": { + "name": "pictureId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ownerId": { + "name": "ownerId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": {}, + "foreignKeys": { + "o_auth2_client_pictureId_upload_id_fk": { + "name": "o_auth2_client_pictureId_upload_id_fk", + "tableFrom": "o_auth2_client", + "tableTo": "upload", + "columnsFrom": [ + "pictureId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "o_auth2_client_ownerId_user_id_fk": { + "name": "o_auth2_client_ownerId_user_id_fk", + "tableFrom": "o_auth2_client", + "tableTo": "user", + "columnsFrom": [ + "ownerId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "IDX_e9d16c213910ad57bd05e97b42": { + "name": "IDX_e9d16c213910ad57bd05e97b42", + "columns": [ + "client_id" + ] + } + }, + "checkConstraint": {} + }, + "o_auth2_client_authorization": { + "name": "o_auth2_client_authorization", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "clientId": { + "name": "clientId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "current": { + "name": "current", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "o_auth2_client_authorization_clientId_o_auth2_client_id_fk": { + "name": "o_auth2_client_authorization_clientId_o_auth2_client_id_fk", + "tableFrom": "o_auth2_client_authorization", + "tableTo": "o_auth2_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "o_auth2_client_authorization_userId_user_id_fk": { + "name": "o_auth2_client_authorization_userId_user_id_fk", + "tableFrom": "o_auth2_client_authorization", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "o_auth2_client_manager": { + "name": "o_auth2_client_manager", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "clientId": { + "name": "clientId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "issuerId": { + "name": "issuerId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": {}, + "foreignKeys": { + "o_auth2_client_manager_clientId_o_auth2_client_id_fk": { + "name": "o_auth2_client_manager_clientId_o_auth2_client_id_fk", + "tableFrom": "o_auth2_client_manager", + "tableTo": "o_auth2_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "o_auth2_client_manager_userId_user_id_fk": { + "name": "o_auth2_client_manager_userId_user_id_fk", + "tableFrom": "o_auth2_client_manager", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "o_auth2_client_manager_issuerId_user_id_fk": { + "name": "o_auth2_client_manager_issuerId_user_id_fk", + "tableFrom": "o_auth2_client_manager", + "tableTo": "user", + "columnsFrom": [ + "issuerId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "o_auth2_client_url": { + "name": "o_auth2_client_url", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "url": { + "name": "url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "enum('redirect_uri','terms','privacy','website')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "clientId": { + "name": "clientId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "o_auth2_client_url_clientId_o_auth2_client_id_fk": { + "name": "o_auth2_client_url_clientId_o_auth2_client_id_fk", + "tableFrom": "o_auth2_client_url", + "tableTo": "o_auth2_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "o_auth2_token": { + "name": "o_auth2_token", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "enum('code','device_code','access_token','refresh_token','par')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp()" + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "clientId": { + "name": "clientId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "grants": { + "name": "grants", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "pcke": { + "name": "pcke", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "o_auth2_token_userId_user_id_fk": { + "name": "o_auth2_token_userId_user_id_fk", + "tableFrom": "o_auth2_token", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "o_auth2_token_clientId_o_auth2_client_id_fk": { + "name": "o_auth2_token_clientId_o_auth2_client_id_fk", + "tableFrom": "o_auth2_token", + "tableTo": "o_auth2_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "privilege": { + "name": "privilege", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "clientId": { + "name": "clientId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "automatic": { + "name": "automatic", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "privilege_clientId_o_auth2_client_id_fk": { + "name": "privilege_clientId_o_auth2_client_id_fk", + "tableFrom": "privilege", + "tableTo": "o_auth2_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "upload": { + "name": "upload", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "original_name": { + "name": "original_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mimetype": { + "name": "mimetype", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file": { + "name": "file", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uploaderId": { + "name": "uploaderId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": {}, + "foreignKeys": { + "upload_uploaderId_user_id_fk": { + "name": "upload_uploaderId_user_id_fk", + "tableFrom": "upload", + "tableTo": "user", + "columnsFrom": [ + "uploaderId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "uuid": { + "name": "uuid", + "type": "varchar(36)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "varchar(26)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "activated": { + "name": "activated", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "activity_at": { + "name": "activity_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp()" + }, + "pictureId": { + "name": "pictureId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_pictureId_upload_id_fk": { + "name": "user_pictureId_upload_id_fk", + "tableFrom": "user", + "tableTo": "upload", + "columnsFrom": [ + "pictureId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "IDX_a95e949168be7b7ece1a2382fe": { + "name": "IDX_a95e949168be7b7ece1a2382fe", + "columns": [ + "uuid" + ] + }, + "IDX_78a916df40e02a9deb1c4b75ed": { + "name": "IDX_78a916df40e02a9deb1c4b75ed", + "columns": [ + "username" + ] + }, + "IDX_e12875dfb3b1d92d7d7c5377e2": { + "name": "IDX_e12875dfb3b1d92d7d7c5377e2", + "columns": [ + "email" + ] + } + }, + "checkConstraint": {} + }, + "user_privileges_privilege": { + "name": "user_privileges_privilege", + "columns": { + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "privilegeId": { + "name": "privilegeId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "IDX_0664a7ff494a1859a09014c0f1": { + "name": "IDX_0664a7ff494a1859a09014c0f1", + "columns": [ + "userId" + ], + "isUnique": false + }, + "IDX_e71171f4ed20bc8564a1819d0b": { + "name": "IDX_e71171f4ed20bc8564a1819d0b", + "columns": [ + "privilegeId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_privileges_privilege_userId_user_id_fk": { + "name": "user_privileges_privilege_userId_user_id_fk", + "tableFrom": "user_privileges_privilege", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "user_privileges_privilege_privilegeId_privilege_id_fk": { + "name": "user_privileges_privilege_privilegeId_privilege_id_fk", + "tableFrom": "user_privileges_privilege", + "tableTo": "privilege", + "columnsFrom": [ + "privilegeId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_token": { + "name": "user_token", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "enum('generic','activation','deactivation','password','login','gdpr','totp','public_key','invite','recovery')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_token_userId_user_id_fk": { + "name": "user_token_userId_user_id_fk", + "tableFrom": "user_token", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/migrations/meta/0009_snapshot.json b/migrations/meta/0009_snapshot.json new file mode 100644 index 0000000..56b8188 --- /dev/null +++ b/migrations/meta/0009_snapshot.json @@ -0,0 +1,1422 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "f06be988-3520-4b19-87e4-34310bceb540", + "prevId": "25719548-9213-4eec-ace3-2241fda14d8d", + "tables": { + "audit_log": { + "name": "audit_log", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_ip": { + "name": "actor_ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_ua": { + "name": "actor_ua", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "flagged": { + "name": "flagged", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "actorId": { + "name": "actorId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ipAddressId": { + "name": "ipAddressId", + "type": "int unsigned", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "audit_log_actorId_user_id_fk": { + "name": "audit_log_actorId_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": [ + "actorId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_ipAddressId_ip_address_id_fk": { + "name": "audit_log_ipAddressId_ip_address_id_fk", + "tableFrom": "audit_log", + "tableTo": "ip_address", + "columnsFrom": [ + "ipAddressId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "document": { + "name": "document", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "authorId": { + "name": "authorId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": {}, + "foreignKeys": { + "document_authorId_user_id_fk": { + "name": "document_authorId_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": [ + "authorId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ip_address": { + "name": "ip_address", + "columns": { + "id": { + "name": "id", + "type": "int unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "ip_address": { + "name": "ip_address", + "type": "varbinary(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "flags": { + "name": "flags", + "type": "tinyint unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "int unsigned", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": { + "ip_address_idx": { + "name": "ip_address_idx", + "columns": [ + "ip_address" + ], + "isUnique": true + }, + "ip_flags_idx": { + "name": "ip_flags_idx", + "columns": [ + "flags" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ip_address_listId_ip_list_id_fk": { + "name": "ip_address_listId_ip_list_id_fk", + "tableFrom": "ip_address", + "tableTo": "ip_list", + "columnsFrom": [ + "listId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ip_address_id": { + "name": "ip_address_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ip_list": { + "name": "ip_list", + "columns": { + "id": { + "name": "id", + "type": "int unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_flags": { + "name": "default_flags", + "type": "tinyint unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "ip_list_id": { + "name": "ip_list_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ip_address_user": { + "name": "ip_address_user", + "columns": { + "id": { + "name": "id", + "type": "int unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ipAddressId": { + "name": "ipAddressId", + "type": "int unsigned", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "ip_address_user_userId_user_id_fk": { + "name": "ip_address_user_userId_user_id_fk", + "tableFrom": "ip_address_user", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ip_address_user_ipAddressId_ip_address_id_fk": { + "name": "ip_address_user_ipAddressId_ip_address_id_fk", + "tableFrom": "ip_address_user", + "tableTo": "ip_address", + "columnsFrom": [ + "ipAddressId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ip_address_user_id": { + "name": "ip_address_user_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "jwks": { + "name": "jwks", + "columns": { + "uuid": { + "name": "uuid", + "type": "varchar(36)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fingerprint": { + "name": "fingerprint", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current": { + "name": "current", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rotate_at": { + "name": "rotate_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "jwks_uuid": { + "name": "jwks_uuid", + "columns": [ + "uuid" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "o_auth2_client": { + "name": "o_auth2_client", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "client_id": { + "name": "client_id", + "type": "varchar(36)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "grants": { + "name": "grants", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('authorization_code')" + }, + "activated": { + "name": "activated", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "verified": { + "name": "verified", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "confidential": { + "name": "confidential", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "enforce_par": { + "name": "enforce_par", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "jwks": { + "name": "jwks", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pictureId": { + "name": "pictureId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ownerId": { + "name": "ownerId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": {}, + "foreignKeys": { + "o_auth2_client_pictureId_upload_id_fk": { + "name": "o_auth2_client_pictureId_upload_id_fk", + "tableFrom": "o_auth2_client", + "tableTo": "upload", + "columnsFrom": [ + "pictureId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "o_auth2_client_ownerId_user_id_fk": { + "name": "o_auth2_client_ownerId_user_id_fk", + "tableFrom": "o_auth2_client", + "tableTo": "user", + "columnsFrom": [ + "ownerId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "IDX_e9d16c213910ad57bd05e97b42": { + "name": "IDX_e9d16c213910ad57bd05e97b42", + "columns": [ + "client_id" + ] + } + }, + "checkConstraint": {} + }, + "o_auth2_client_authorization": { + "name": "o_auth2_client_authorization", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "clientId": { + "name": "clientId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "current": { + "name": "current", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "o_auth2_client_authorization_clientId_o_auth2_client_id_fk": { + "name": "o_auth2_client_authorization_clientId_o_auth2_client_id_fk", + "tableFrom": "o_auth2_client_authorization", + "tableTo": "o_auth2_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "o_auth2_client_authorization_userId_user_id_fk": { + "name": "o_auth2_client_authorization_userId_user_id_fk", + "tableFrom": "o_auth2_client_authorization", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "o_auth2_client_manager": { + "name": "o_auth2_client_manager", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "clientId": { + "name": "clientId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "issuerId": { + "name": "issuerId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": {}, + "foreignKeys": { + "o_auth2_client_manager_clientId_o_auth2_client_id_fk": { + "name": "o_auth2_client_manager_clientId_o_auth2_client_id_fk", + "tableFrom": "o_auth2_client_manager", + "tableTo": "o_auth2_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "o_auth2_client_manager_userId_user_id_fk": { + "name": "o_auth2_client_manager_userId_user_id_fk", + "tableFrom": "o_auth2_client_manager", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "o_auth2_client_manager_issuerId_user_id_fk": { + "name": "o_auth2_client_manager_issuerId_user_id_fk", + "tableFrom": "o_auth2_client_manager", + "tableTo": "user", + "columnsFrom": [ + "issuerId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "o_auth2_client_url": { + "name": "o_auth2_client_url", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "url": { + "name": "url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "enum('redirect_uri','terms','privacy','website')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "clientId": { + "name": "clientId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "o_auth2_client_url_clientId_o_auth2_client_id_fk": { + "name": "o_auth2_client_url_clientId_o_auth2_client_id_fk", + "tableFrom": "o_auth2_client_url", + "tableTo": "o_auth2_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "o_auth2_token": { + "name": "o_auth2_token", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "enum('code','device_code','access_token','refresh_token','par')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp()" + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "clientId": { + "name": "clientId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "grants": { + "name": "grants", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "pcke": { + "name": "pcke", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "o_auth2_token_userId_user_id_fk": { + "name": "o_auth2_token_userId_user_id_fk", + "tableFrom": "o_auth2_token", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "o_auth2_token_clientId_o_auth2_client_id_fk": { + "name": "o_auth2_token_clientId_o_auth2_client_id_fk", + "tableFrom": "o_auth2_token", + "tableTo": "o_auth2_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "privilege": { + "name": "privilege", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "clientId": { + "name": "clientId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "automatic": { + "name": "automatic", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "privilege_clientId_o_auth2_client_id_fk": { + "name": "privilege_clientId_o_auth2_client_id_fk", + "tableFrom": "privilege", + "tableTo": "o_auth2_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "upload": { + "name": "upload", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "original_name": { + "name": "original_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mimetype": { + "name": "mimetype", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file": { + "name": "file", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uploaderId": { + "name": "uploaderId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": {}, + "foreignKeys": { + "upload_uploaderId_user_id_fk": { + "name": "upload_uploaderId_user_id_fk", + "tableFrom": "upload", + "tableTo": "user", + "columnsFrom": [ + "uploaderId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "uuid": { + "name": "uuid", + "type": "varchar(36)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "varchar(26)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "activated": { + "name": "activated", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "activity_at": { + "name": "activity_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp()" + }, + "pictureId": { + "name": "pictureId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_pictureId_upload_id_fk": { + "name": "user_pictureId_upload_id_fk", + "tableFrom": "user", + "tableTo": "upload", + "columnsFrom": [ + "pictureId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "IDX_a95e949168be7b7ece1a2382fe": { + "name": "IDX_a95e949168be7b7ece1a2382fe", + "columns": [ + "uuid" + ] + }, + "IDX_78a916df40e02a9deb1c4b75ed": { + "name": "IDX_78a916df40e02a9deb1c4b75ed", + "columns": [ + "username" + ] + }, + "IDX_e12875dfb3b1d92d7d7c5377e2": { + "name": "IDX_e12875dfb3b1d92d7d7c5377e2", + "columns": [ + "email" + ] + } + }, + "checkConstraint": {} + }, + "user_privileges_privilege": { + "name": "user_privileges_privilege", + "columns": { + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "privilegeId": { + "name": "privilegeId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "IDX_0664a7ff494a1859a09014c0f1": { + "name": "IDX_0664a7ff494a1859a09014c0f1", + "columns": [ + "userId" + ], + "isUnique": false + }, + "IDX_e71171f4ed20bc8564a1819d0b": { + "name": "IDX_e71171f4ed20bc8564a1819d0b", + "columns": [ + "privilegeId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_privileges_privilege_userId_user_id_fk": { + "name": "user_privileges_privilege_userId_user_id_fk", + "tableFrom": "user_privileges_privilege", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "user_privileges_privilege_privilegeId_privilege_id_fk": { + "name": "user_privileges_privilege_privilegeId_privilege_id_fk", + "tableFrom": "user_privileges_privilege", + "tableTo": "privilege", + "columnsFrom": [ + "privilegeId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_token": { + "name": "user_token", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "enum('generic','activation','deactivation','password','login','gdpr','totp','public_key','invite','recovery')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_token_userId_user_id_fk": { + "name": "user_token_userId_user_id_fk", + "tableFrom": "user_token", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index e6f76d6..7af575f 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -57,6 +57,20 @@ "when": 1739896136734, "tag": "0007_slim_dexter_bennett", "breakpoints": true + }, + { + "idx": 8, + "version": "5", + "when": 1740393886197, + "tag": "0008_faithful_golden_guardian", + "breakpoints": true + }, + { + "idx": 9, + "version": "5", + "when": 1740394195704, + "tag": "0009_broad_kylun", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/lib/components/admin/AdminAuditCard.svelte b/src/lib/components/admin/AdminAuditCard.svelte index cce0440..6d408c6 100644 --- a/src/lib/components/admin/AdminAuditCard.svelte +++ b/src/lib/components/admin/AdminAuditCard.svelte @@ -7,9 +7,11 @@ interface Props { audit: PageData['list'][0]; + ipLink?: boolean; + userLink?: boolean; } - let { audit }: Props = $props(); + let { audit, ipLink, userLink }: Props = $props(); let expanded = $state(false); @@ -26,11 +28,21 @@
{audit.action}
{#if audit.user}
{$t('admin.audit.user')}
-
{audit.user.uuid} ({audit.user.name})
+
+ {audit.user.uuid} ({audit.user.name}) + {#if userLink} + ({$t('admin.audit.userLookup')}) + {/if} +
{/if} {#if audit.ip}
{$t('admin.audit.ip')}
-
{audit.ip}
+
+ {audit.ip} + {#if ipLink} + ({$t('admin.audit.ipLookup')}) + {/if} +
{/if} {#if audit.ua}
{$t('admin.audit.ua')}
diff --git a/src/lib/components/admin/AdminIpTable.svelte b/src/lib/components/admin/AdminIpTable.svelte new file mode 100644 index 0000000..94e9b9b --- /dev/null +++ b/src/lib/components/admin/AdminIpTable.svelte @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + {#each list as address} + + + + + + + + + + + {#if address.users?.length} + + + + {/if} + {/each} + +
{$t('admin.ip.column.ip')}{$t('admin.ip.column.flags')}{$t('admin.ip.column.created_at')}{$t('admin.ip.column.updated_at')}{$t('admin.ip.column.user_count')}{$t('admin.ip.column.list')}
{address.ip_address}{unpackFlags(address.flags)}{address.user_count}{address.list?.name || '-'} + {#if hasAuditPermission} + {$t('admin.audit.title')} + {/if} +
+ {$t('admin.ip.users')}: + {#each address.users as user} + {user.display_name} + {/each} +
+ + diff --git a/src/lib/components/admin/AdminSidebar.svelte b/src/lib/components/admin/AdminSidebar.svelte index 943f8bc..36d168b 100644 --- a/src/lib/components/admin/AdminSidebar.svelte +++ b/src/lib/components/admin/AdminSidebar.svelte @@ -27,6 +27,11 @@ href: '/ssoadmin/audit', title: $t('admin.menu.audit'), privileges: ['admin:audit'] + }, + { + href: '/ssoadmin/addresses', + title: $t('admin.menu.ip'), + privileges: ['admin:ip'] } ]; diff --git a/src/lib/i18n/en/admin.json b/src/lib/i18n/en/admin.json index bfc5250..a7f02ac 100644 --- a/src/lib/i18n/en/admin.json +++ b/src/lib/i18n/en/admin.json @@ -6,7 +6,8 @@ "close": "Close menu", "users": "Users", "oauth2": "OAuth 2.0 applications", - "audit": "Audit logs" + "audit": "Audit logs", + "ip": "IP addresses" }, "users": { "title": "Users", @@ -144,12 +145,27 @@ "invalidJwks": "Invalid JSON Web Keys provided, please check your input and try again." } }, + "ip": { + "title": "IP Addresses", + "user": "Associated user", + "users": "Associated users", + "column": { + "ip": "IP address", + "flags": "Flags", + "created_at": "First seen at", + "updated_at": "Last seen at", + "user_count": "User count", + "list": "List" + } + }, "audit": { "title": "Audit logs", "action": "Action", "comment": "Comment", "user": "Actor", "ip": "IP address", + "userLookup": "View user info", + "ipLookup": "View IP info", "ua": "User agent", "flagged": "Flagged", "createdAt": "Created at" diff --git a/src/lib/server/audit/audit.ts b/src/lib/server/audit/audit.ts index e62564b..9ca66fc 100644 --- a/src/lib/server/audit/audit.ts +++ b/src/lib/server/audit/audit.ts @@ -1,5 +1,13 @@ import { SQL, count, desc, eq, inArray, or, sql } from 'drizzle-orm'; -import { DB, auditLog, user, type AuditLog, type NewAuditLog, type User } from '../drizzle'; +import { + DB, + auditLog, + ipAddress, + user, + type AuditLog, + type NewAuditLog, + type User +} from '../drizzle'; import { AuditAction, type AuditListItem, @@ -12,6 +20,7 @@ import { AdminNotificationEmail, Emails } from '../email'; import { env } from '$env/dynamic/private'; import { env as publicEnv } from '$env/dynamic/public'; import { CacheBackend } from '../cache-backend'; +import { IPAddresses, IPFlag } from '../ip'; const FLAG_EMAIL_COOLDOWN = 1 * 60 * 1000; const FLAG_TRESHOLD_COOLDOWN = 30 * 60 * 1000; @@ -34,7 +43,6 @@ export class Audit { action, content: comment, actorId: user?.id, - actor_ip: ip, actor_ua: userAgent, flagged: Number(flagged) }; @@ -47,6 +55,25 @@ export class Audit { Audit.auditFlagTrigger(); } + if (ip) { + const ipFlags: IPFlag[] = []; + + if (flagged) { + ipFlags.push(IPFlag.FLAGGED); + } + + if (action === AuditAction.LOGIN) { + ipFlags.push(IPFlag.USER_IP); + } + + newAuditLog.ipAddressId = await IPAddresses.storeOrUpdateIpAddress( + ip, + ipFlags, + undefined, + user?.id + ); + } + await DB.drizzle.insert(auditLog).values(newAuditLog); } @@ -67,6 +94,7 @@ export class Audit { .select({ rowCount: count(auditLog.id).mapWith(Number) }) .from(auditLog) .leftJoin(user, eq(user.id, auditLog.actorId)) + .leftJoin(ipAddress, eq(ipAddress.id, auditLog.ipAddressId)) .where(Audit.getAuditWhere(search)); return rowCount; } @@ -84,6 +112,7 @@ export class Audit { .select({ id: auditLog.id }) .from(auditLog) .leftJoin(user, eq(user.id, auditLog.actorId)) + .leftJoin(ipAddress, eq(ipAddress.id, auditLog.ipAddressId)) .limit(limit) .offset(search.offset || 0) .where(Audit.getAuditWhere(search)) @@ -93,10 +122,12 @@ export class Audit { const junkList = await DB.drizzle .select({ audit_log: auditLog, + ip_address: sql`INET6_NTOA(${ipAddress.ip_address})`, user: user }) .from(auditSubquery) .innerJoin(auditLog, eq(auditLog.id, auditSubquery.id)) + .leftJoin(ipAddress, eq(ipAddress.id, auditLog.ipAddressId)) .leftJoin(user, eq(user.id, auditLog.actorId)); const list = Audit.mapAuditRows(junkList); @@ -124,6 +155,7 @@ export class Audit { if (search.ip) { selectList.push(sql`lower(${auditLog.actor_ip}) LIKE ${`%${search.ip.toLowerCase()}%`}`); + selectList.push(sql`${ipAddress.ip_address} = INET6_ATON(${search.ip})`); } if (search.ua) { @@ -144,6 +176,7 @@ export class Audit { private static mapAuditRows( rows: { audit_log: AuditLog; + ip_address?: string; user?: User | null; }[] ) { @@ -153,7 +186,7 @@ export class Audit { existingEntry = { id: entry.audit_log.id, action: entry.audit_log.action as AuditAction, - ip: entry.audit_log.actor_ip || undefined, + ip: entry.ip_address || entry.audit_log.actor_ip || undefined, ua: entry.audit_log.actor_ua || undefined, content: entry.audit_log.content || undefined, flagged: Boolean(entry.audit_log.flagged), @@ -169,6 +202,10 @@ export class Audit { }; } + if (entry.ip_address) { + existingEntry.ip = entry.ip_address; + } + return accum; }, []); } diff --git a/src/lib/server/drizzle/schema.ts b/src/lib/server/drizzle/schema.ts index 448108f..af666a8 100644 --- a/src/lib/server/drizzle/schema.ts +++ b/src/lib/server/drizzle/schema.ts @@ -11,10 +11,59 @@ import { mysqlEnum, index, type AnyMySqlColumn, - json + json, + uniqueIndex, + varbinary } from 'drizzle-orm/mysql-core'; import type { JWK } from 'jose'; +export const ipList = mysqlTable('ip_list', { + id: int('id', { unsigned: true }).autoincrement().primaryKey(), + name: text('name').notNull(), + url: text('url'), + default_flags: tinyint('default_flags', { unsigned: true }).notNull(), + created_at: datetime('created_at', { mode: 'date', fsp: 6 }) + .default(sql`current_timestamp(6)`) + .notNull(), + updated_at: datetime('updated_at', { mode: 'date', fsp: 6 }) + .default(sql`current_timestamp(6)`) + .notNull() +}); + +export type IPList = typeof ipList.$inferSelect; +export type NewIPList = typeof ipList.$inferInsert; + +export const ipAddress = mysqlTable( + 'ip_address', + { + id: int('id', { unsigned: true }).autoincrement().primaryKey(), + ip_address: varbinary('ip_address', { length: 16 }).notNull(), + flags: tinyint('flags', { unsigned: true }).notNull(), + listId: int('listId', { unsigned: true }).references(() => ipList.id, { onDelete: 'cascade' }), + created_at: datetime('created_at', { mode: 'date', fsp: 6 }) + .default(sql`current_timestamp(6)`) + .notNull(), + updated_at: datetime('updated_at', { mode: 'date', fsp: 6 }) + .default(sql`current_timestamp(6)`) + .notNull() + }, + (table) => [ + uniqueIndex('ip_address_idx').on(table.ip_address), + index('ip_flags_idx').on(table.flags) + ] +); + +export type IPAddress = typeof ipAddress.$inferSelect; +export type NewIPAddress = typeof ipAddress.$inferInsert; + +export const ipUser = mysqlTable('ip_address_user', { + id: int('id', { unsigned: true }).autoincrement().primaryKey(), + userId: int('userId').references(() => user.id, { onDelete: 'cascade' }), + ipAddressId: int('ipAddressId', { unsigned: true }).references(() => ipAddress.id, { + onDelete: 'cascade' + }) +}); + export const jwks = mysqlTable('jwks', { uuid: varchar('uuid', { length: 36 }).primaryKey(), fingerprint: varchar('fingerprint', { length: 64 }).notNull(), @@ -39,7 +88,10 @@ export const auditLog = mysqlTable('audit_log', { created_at: datetime('created_at', { mode: 'date', fsp: 6 }) .default(sql`current_timestamp(6)`) .notNull(), - actorId: int('actorId').references(() => user.id, { onDelete: 'set null' }) + actorId: int('actorId').references(() => user.id, { onDelete: 'set null' }), + ipAddressId: int('ipAddressId', { unsigned: true }).references(() => ipAddress.id, { + onDelete: 'set null' + }) }); export type AuditLog = typeof auditLog.$inferSelect; @@ -286,6 +338,10 @@ export const auditLogRelations = relations(auditLog, ({ one }) => ({ user: one(user, { fields: [auditLog.actorId], references: [user.id] + }), + ip_address: one(ipAddress, { + fields: [auditLog.ipAddressId], + references: [ipAddress.id] }) })); @@ -413,3 +469,21 @@ export const userTokenRelations = relations(userToken, ({ one }) => ({ references: [user.id] }) })); + +export const ipAddressRelations = relations(ipAddress, ({ one }) => ({ + list: one(ipList, { + fields: [ipAddress.listId], + references: [ipList.id] + }) +})); + +export const ipAddressUserRelations = relations(ipUser, ({ one }) => ({ + user: one(user, { + fields: [ipUser.userId], + references: [user.id] + }), + ipAddress: one(ipAddress, { + fields: [ipUser.ipAddressId], + references: [ipAddress.id] + }) +})); diff --git a/src/lib/server/drizzle/seeds/privileges.ts b/src/lib/server/drizzle/seeds/privileges.ts index 6a1aeeb..4709922 100644 --- a/src/lib/server/drizzle/seeds/privileges.ts +++ b/src/lib/server/drizzle/seeds/privileges.ts @@ -11,6 +11,7 @@ const privileges = [ 'admin:oauth2', 'admin:audit', 'admin:document', + 'admin:ip', 'self:oauth2', 'self:oauth2:implicit', 'self:oauth2:create' diff --git a/src/lib/server/ip/admin.ts b/src/lib/server/ip/admin.ts new file mode 100644 index 0000000..e783601 --- /dev/null +++ b/src/lib/server/ip/admin.ts @@ -0,0 +1,103 @@ +import { and, count, countDistinct, desc, eq, sql, type SQL } from 'drizzle-orm'; +import type { IPAddressListEntry, IPAddressQuery } from './types'; +import { DB, ipAddress, ipList, ipUser, user } from '../drizzle'; +import type { PaginationMeta, Paginated } from '$lib/types'; + +export class IPAddressesAdmin { + static async filterSearchIpAddresses( + search: IPAddressQuery & { + limit?: number; + offset?: number; + } + ) { + const limit = search.limit || 20; + const rowCount = await IPAddressesAdmin.ipCount(search); + + const ipAddressSubquery = DB.drizzle + .select({ id: ipAddress.id }) + .from(ipAddress) + .leftJoin(ipUser, eq(ipAddress.id, ipUser.ipAddressId)) + .leftJoin(user, eq(user.id, ipUser.userId)) + .limit(limit) + .offset(search.offset || 0) + .where(IPAddressesAdmin.getIpAddressWhere(search)) + .orderBy(desc(ipAddress.updated_at)) + .as('ipAddressSubquery'); + + const list: IPAddressListEntry[] = await DB.drizzle + .select({ + id: ipAddress.id, + ip_address: sql`INET6_NTOA(${ipAddress.ip_address})`, + flags: ipAddress.flags, + created_at: ipAddress.created_at, + updated_at: ipAddress.updated_at, + user_count: countDistinct(user.id), + list: { + id: ipList.id, + name: ipList.name + } + }) + .from(ipAddressSubquery) + .innerJoin(ipAddress, eq(ipAddress.id, ipAddressSubquery.id)) + .leftJoin(ipList, eq(ipAddress.listId, ipList.id)) + .leftJoin(ipUser, eq(ipAddress.id, ipUser.ipAddressId)) + .leftJoin(user, eq(user.id, ipUser.userId)) + .orderBy(desc(ipAddress.updated_at)) + .groupBy(ipAddress.id); + + const meta: PaginationMeta = { + rowCount, + pageSize: limit, + pageCount: Math.ceil(rowCount / limit) + }; + + if (search.includeUsers) { + await Promise.all( + list.map(async (entry) => { + entry.users = await DB.drizzle + .select({ + uuid: user.uuid, + display_name: user.display_name + }) + .from(user) + .innerJoin(ipUser, eq(user.id, ipUser.userId)) + .where(eq(ipUser.ipAddressId, entry.id)); + }) + ); + } + + return { list, meta } satisfies Paginated; + } + + static async ipCount(search: IPAddressQuery) { + const [{ rowCount }] = await DB.drizzle + .select({ rowCount: count(ipAddress.id).mapWith(Number) }) + .from(ipAddress) + .leftJoin(ipUser, eq(ipUser.ipAddressId, ipAddress.id)) + .leftJoin(user, eq(user.id, ipUser.userId)) + .where(IPAddressesAdmin.getIpAddressWhere(search)); + return rowCount; + } + + static getIpAddressWhere(search: IPAddressQuery) { + const selectList: SQL[] = []; + + if (search.ip) { + selectList.push(sql`${ipAddress.ip_address} = INET6_ATON(${search.ip})`); + } + + if (search.flags) { + selectList.push(sql`${ipAddress.flags} & ${Number(search.flags)}`); + } + + if (search.user) { + selectList.push(eq(user.uuid, search.user)); + } + + if (search.listId) { + selectList.push(eq(ipAddress.listId, Number(search.listId))); + } + + return and(...selectList); + } +} diff --git a/src/lib/server/ip/index.ts b/src/lib/server/ip/index.ts new file mode 100644 index 0000000..66fcc78 --- /dev/null +++ b/src/lib/server/ip/index.ts @@ -0,0 +1,114 @@ +import { and, eq, sql } from 'drizzle-orm'; +import { DB, ipAddress, ipList, ipUser } from '../drizzle'; +import { CacheBackend } from '../cache-backend'; +import { IPFlag } from './types'; +import { ensureArray } from '$lib/utils'; + +export class IPAddresses { + static flagsList = Object.values(IPFlag).filter((entry) => isNaN(Number(entry))) as string[]; + + static async getIpAddressesByFlag(flag: IPFlag | IPFlag[]) { + const flags = ensureArray(flag); + const combinedFlag = flags.reduce((num, flag) => num | (1 << flag), 0); + const ipAddresses = await DB.drizzle + .select({ ip_address: sql`INET6_NTOA(${ipAddress.ip_address})` }) + .from(ipAddress) + .where(sql`${ipAddress.flags} & ${combinedFlag}`); + return ipAddresses.map(({ ip_address }) => ip_address); + } + + static async getIpAddressesByUserId(userId: number) { + return DB.drizzle + .select({ + id: ipAddress.id, + ip_address: sql`INET6_NTOA(${ipAddress.ip_address})`, + flags: ipAddress.flags, + updated_at: ipAddress.updated_at, + list: { + id: ipList.id, + name: ipList.name + } + }) + .from(ipAddress) + .leftJoin(ipUser, eq(ipAddress.id, ipUser.ipAddressId)) + .leftJoin(ipList, eq(ipList.id, ipAddress.listId)) + .where(eq(ipUser.userId, userId)); + } + + static async getCachedIpAddressesByFlag(flag: IPFlag) { + const existing = await CacheBackend.get(`ipAddressList${flag}`); + if (!existing) { + const result = await IPAddresses.getIpAddressesByFlag(flag); + await CacheBackend.set(`ipAddressList${flag}`, result, 60 * 60 * 1000); // 1 hour cache + return result; + } + return existing; + } + + static async isIpFlagged(address: string, flag: IPFlag) { + const [entry] = await DB.drizzle + .select() + .from(ipAddress) + .where( + and( + sql`${ipAddress.ip_address} = INET6_ATON(${address})`, + sql`${ipAddress.flags} & ${1 << flag}` + ) + ); + return !!entry; + } + + static async storeOrUpdateIpAddress( + address: string, + flags: IPFlag[], + listId?: number, + userId?: number + ) { + let updateId: number; + const combinedFlag = flags.reduce((num, flag) => num | (1 << flag), 0); + const [existingIp] = await DB.drizzle + .select() + .from(ipAddress) + .where(sql`${ipAddress.ip_address} = INET6_ATON(${address})`); + + if (existingIp) { + updateId = existingIp.id; + existingIp.flags = existingIp.flags | combinedFlag; + existingIp.updated_at = new Date(); + if (listId) { + existingIp.listId = listId || null; + } + await DB.drizzle.update(ipAddress).set(existingIp).where(eq(ipAddress.id, existingIp.id)); + } else { + const [result] = await DB.drizzle + .insert(ipAddress) + .values({ + ip_address: sql`INET6_ATON(${address})`, + flags: combinedFlag, + listId: listId || null + }) + .$returningId(); + updateId = result.id; + } + + if (userId) { + const [relationExists] = await DB.drizzle + .select() + .from(ipUser) + .where(and(eq(ipUser.userId, userId), eq(ipUser.ipAddressId, updateId))); + + if (relationExists) { + return updateId; + } + + await DB.drizzle.insert(ipUser).values({ + ipAddressId: updateId, + userId + }); + } + + return updateId; + } +} + +export { IPFlag }; diff --git a/src/lib/server/ip/types.ts b/src/lib/server/ip/types.ts new file mode 100644 index 0000000..787cfa7 --- /dev/null +++ b/src/lib/server/ip/types.ts @@ -0,0 +1,32 @@ +export enum IPFlag { + FLAGGED = 0, + USER_IP, + RESTRICT_UPLOAD, + RESTRICT_REGISTRATION, + REJECT +} + +export interface IPAddressQuery { + ip?: string; + flags?: number; + user?: string; + listId?: number; + includeUsers?: boolean; +} + +export interface IPAddressListEntry { + id: number; + ip_address: string; + flags: number; + created_at: Date; + updated_at: Date; + list: { + id: number; + name: string; + } | null; + user_count: number; + users?: { + uuid: string; + display_name: string; + }[]; +} diff --git a/src/routes/account/+page.server.ts b/src/routes/account/+page.server.ts index e08e39b..9c89f85 100644 --- a/src/routes/account/+page.server.ts +++ b/src/routes/account/+page.server.ts @@ -2,11 +2,12 @@ import { Audit } from '$lib/server/audit/audit.js'; import { AuditAction } from '$lib/server/audit/types.js'; import { Challenge } from '$lib/server/challenge.js'; import type { User } from '$lib/server/drizzle'; +import { IPAddresses, IPFlag } from '$lib/server/ip'; import { Uploads } from '$lib/server/upload.js'; import { Users, type UserSession } from '$lib/server/users/index.js'; import { TimeOTP } from '$lib/server/users/totp.js'; import { passwordRegex } from '$lib/validators.js'; -import { fail, redirect } from '@sveltejs/kit'; +import { error, fail, redirect } from '@sveltejs/kit'; interface AccountUpdate { displayName: string; @@ -166,6 +167,11 @@ export const actions = { return redirect(303, '/login'); } + // IP banned from uploading files + if (await IPAddresses.isIpFlagged(getClientAddress(), IPFlag.RESTRICT_UPLOAD)) { + throw error(403); + } + const formData = Object.fromEntries(await request.formData()); if (!(formData.file as File)?.name || (formData.file as File).name === 'undefined') { return fail(400, { diff --git a/src/routes/api/server/reject.txt/+server.ts b/src/routes/api/server/reject.txt/+server.ts new file mode 100644 index 0000000..a594eac --- /dev/null +++ b/src/routes/api/server/reject.txt/+server.ts @@ -0,0 +1,15 @@ +import { IPAddresses, IPFlag } from '$lib/server/ip'; + +export const GET = () => + IPAddresses.getCachedIpAddressesByFlag(IPFlag.REJECT) + .then( + (list) => + new Response(list.join('\n'), { status: 200, headers: { 'Content-Type': 'text/plain' } }) + ) + .catch( + () => + new Response('Internal Server Error', { + status: 500, + headers: { 'Content-Type': 'text/plain' } + }) + ); diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts index ebe4e9e..65a8bf6 100644 --- a/src/routes/login/+page.server.ts +++ b/src/routes/login/+page.server.ts @@ -19,7 +19,7 @@ const rainbowTableLimiter = new RateLimiter({ }); const limiter = new RateLimiter({ - IP: [6, 'm'] + IP: [8, 'm'] }); export const actions = { diff --git a/src/routes/login/password/+page.server.ts b/src/routes/login/password/+page.server.ts index b1e9584..e3d5e8c 100644 --- a/src/routes/login/password/+page.server.ts +++ b/src/routes/login/password/+page.server.ts @@ -1,16 +1,12 @@ import { Audit, AuditAction } from '$lib/server/audit'; import { Changesets } from '$lib/server/changesets.js'; +import { IPAddresses, IPFlag } from '$lib/server/ip'; import { Users } from '$lib/server/users/index.js'; import { UserTokens } from '$lib/server/users/tokens.js'; import { emailRegex, passwordRegex } from '$lib/validators.js'; import { error, redirect } from '@sveltejs/kit'; import { RateLimiter } from 'sveltekit-rate-limiter/server'; -interface PasswordRequest { - newPassword: string; - repeatPassword: string; -} - // Sending an email asynchronously has a similar amount of delay, // so lets fake it. TODO: offload email sending somewhere else. const failDelay = () => @@ -28,6 +24,11 @@ export const actions = { throw error(429); } + // IP banned + if (await IPAddresses.isIpFlagged(event.getClientAddress(), IPFlag.RESTRICT_REGISTRATION)) { + throw error(403); + } + if (locals.session.data?.user) { return redirect(303, '/'); } diff --git a/src/routes/register/+page.server.ts b/src/routes/register/+page.server.ts index 17eb7e2..0b0589b 100644 --- a/src/routes/register/+page.server.ts +++ b/src/routes/register/+page.server.ts @@ -2,6 +2,7 @@ import { env } from '$env/dynamic/private'; import { AuditAction } from '$lib/server/audit'; import { Audit } from '$lib/server/audit/audit.js'; import { Changesets } from '$lib/server/changesets.js'; +import { IPAddresses, IPFlag } from '$lib/server/ip/index.js'; import { Users } from '$lib/server/users/index.js'; import { emailRegex, passwordRegex, usernameRegex } from '$lib/validators.js'; import { error, fail, redirect } from '@sveltejs/kit'; @@ -30,7 +31,16 @@ const limiter = new RateLimiter({ export const actions = { default: async (event) => { const { request, locals } = event; - if (await limiter.isLimited(event)) throw error(429); + + // Rate limited + if (await limiter.isLimited(event)) { + throw error(429); + } + + // IP banned + if (await IPAddresses.isIpFlagged(event.getClientAddress(), IPFlag.RESTRICT_REGISTRATION)) { + throw error(403); + } // Logged in users cannot make more accounts if (locals.session.data?.user || env.REGISTRATIONS === 'false') { diff --git a/src/routes/ssoadmin/addresses/+page.server.ts b/src/routes/ssoadmin/addresses/+page.server.ts new file mode 100644 index 0000000..acf5c11 --- /dev/null +++ b/src/routes/ssoadmin/addresses/+page.server.ts @@ -0,0 +1,39 @@ +import { AdminUtils } from '$lib/server/admin-utils'; +import { Changesets } from '$lib/server/changesets'; +import { IPAddressesAdmin } from '$lib/server/ip/admin'; +import { IPAddresses } from '$lib/server/ip/index.js'; +import { hasPrivileges } from '$lib/utils.js'; + +const PAGE_SIZE = 100; + +export const load = async ({ parent, url }) => { + const { user } = await parent(); + AdminUtils.checkPrivileges(user, ['admin:ip']); + + const { + page, + pageSize, + ip, + flags, + user: listUser, + listId + } = Changesets.only(['page', 'pageSize', 'user', 'flags', 'ip', 'listId'], url.searchParams); + + const limit = Number(pageSize) || PAGE_SIZE; + const offset = ((Number(page) || 1) - 1) * limit; + + const data = await IPAddressesAdmin.filterSearchIpAddresses({ + limit, + offset, + ip, + includeUsers: !!ip && hasPrivileges(user.privileges, ['admin:user']), + user: listUser, + flags: flags ? Number(flags) : undefined, + listId: listId ? Number(listId) : undefined + }); + + return { + ...data, + flags: IPAddresses.flagsList + }; +}; diff --git a/src/routes/ssoadmin/addresses/+page.svelte b/src/routes/ssoadmin/addresses/+page.svelte new file mode 100644 index 0000000..6266124 --- /dev/null +++ b/src/routes/ssoadmin/addresses/+page.svelte @@ -0,0 +1,93 @@ + + + + {$t('admin.ip.title')} - {env.PUBLIC_SITE_NAME} {$t('admin.title')} + + +

{$t('admin.ip.title')} ({data.meta.rowCount})

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + {$t('common.clear')} + + + +
+ + + + + + +
diff --git a/src/routes/ssoadmin/audit/+page.svelte b/src/routes/ssoadmin/audit/+page.svelte index 52d7629..c69d323 100644 --- a/src/routes/ssoadmin/audit/+page.svelte +++ b/src/routes/ssoadmin/audit/+page.svelte @@ -9,12 +9,17 @@ import SplitView from '$lib/components/container/SplitView.svelte'; import Button from '$lib/components/Button.svelte'; import AdminAuditCard from '$lib/components/admin/AdminAuditCard.svelte'; + import { hasPrivileges } from '$lib/utils'; + import FormActions from '$lib/components/form/FormActions.svelte'; interface Props { data: PageData; } let { data }: Props = $props(); + + const hasIpPermission = $derived(hasPrivileges(data.user.privileges, ['admin:ip'])); + const hasUserPermission = $derived(hasPrivileges(data.user.privileges, ['admin:user'])); @@ -70,16 +75,17 @@ /> -
+ + {$t('common.clear')} -
+
{#each data.list as audit} - + {/each}
diff --git a/src/routes/ssoadmin/oauth2/[uuid]/+page.server.ts b/src/routes/ssoadmin/oauth2/[uuid]/+page.server.ts index b7379d0..2c4c89e 100644 --- a/src/routes/ssoadmin/oauth2/[uuid]/+page.server.ts +++ b/src/routes/ssoadmin/oauth2/[uuid]/+page.server.ts @@ -4,6 +4,7 @@ import { Audit, AuditAction } from '$lib/server/audit'; import { Changesets } from '$lib/server/changesets.js'; import { CryptoUtils } from '$lib/server/crypto-utils.js'; import type { OAuth2Client, User } from '$lib/server/drizzle'; +import { IPAddresses, IPFlag } from '$lib/server/ip'; import { OAuth2ClientURLType, OAuth2Clients, @@ -359,6 +360,11 @@ export const actions = { avatar: async ({ request, locals, params: { uuid }, getClientAddress }) => { const { currentUser, details } = await getActionData(locals, uuid); + // IP banned from uploading files + if (await IPAddresses.isIpFlagged(getClientAddress(), IPFlag.RESTRICT_UPLOAD)) { + throw error(403); + } + const formData = Object.fromEntries(await request.formData()); if (!(formData.file as File)?.name || (formData.file as File).name === 'undefined') { return fail(400, { diff --git a/src/routes/ssoadmin/users/[uuid]/+page.svelte b/src/routes/ssoadmin/users/[uuid]/+page.svelte index 5ac2192..36f923f 100644 --- a/src/routes/ssoadmin/users/[uuid]/+page.svelte +++ b/src/routes/ssoadmin/users/[uuid]/+page.svelte @@ -1,4 +1,14 @@