diff --git a/migrations/0002_whole_vivisector.sql b/migrations/0002_whole_vivisector.sql new file mode 100644 index 0000000..7006cee --- /dev/null +++ b/migrations/0002_whole_vivisector.sql @@ -0,0 +1,3 @@ +ALTER TABLE `user_token` MODIFY COLUMN `type` enum('generic','activation','deactivation','password','login','gdpr','totp','public_key','invite','recovery') NOT NULL;--> statement-breakpoint +ALTER TABLE `privilege` ADD `automatic` tinyint DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE `user_token` ADD `metadata` text; \ No newline at end of file diff --git a/migrations/meta/0002_snapshot.json b/migrations/meta/0002_snapshot.json new file mode 100644 index 0000000..eaafeee --- /dev/null +++ b/migrations/meta/0002_snapshot.json @@ -0,0 +1,1064 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "31269eb0-9aaf-417a-983c-68912644e6dc", + "prevId": "fa80e0d2-53bc-4557-a754-bc9fd9cad9aa", + "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 + } + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "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": {} + }, + "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 + }, + "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" + ] + } + } + }, + "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": {} + }, + "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": {} + }, + "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": {} + }, + "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','access_token','refresh_token')", + "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 + }, + "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": {} + }, + "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": {} + }, + "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": {} + }, + "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" + ] + } + } + }, + "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": {} + }, + "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": {} + } + }, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index 0c5c85a..fefc4a9 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1717232859846, "tag": "0001_redundant_layla_miller", "breakpoints": true + }, + { + "idx": 2, + "version": "5", + "when": 1717344670405, + "tag": "0002_whole_vivisector", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/lib/i18n/en/admin.json b/src/lib/i18n/en/admin.json index b918084..d5908ae 100644 --- a/src/lib/i18n/en/admin.json +++ b/src/lib/i18n/en/admin.json @@ -105,6 +105,12 @@ "account": "Change user account settings", "openid": "Get an ID token JWT (OpenID Connect)" }, + "managers": { + "title": "Application members", + "hint": "These users can edit this application just like you can, except that they cannot regenerate the secret or delete the application. Please note that they must have an active account on {{siteName}}, but not necessarily with the provided address.", + "add": "Invite a new member", + "invite": "Invite" + }, "errors": { "noRedirect": "At least one Redirect URI is required for you to be able to use this application!", "forbidden": "This action is forbidden for this user.", @@ -116,6 +122,8 @@ "invalidUrl": "Invalid URL provided.", "invalidPrivilegeId": "Invalid privilege ID for deletion.", "invalidPrivilege": "Invalid privilege provided.", + "invalidEmail": "Invalid email address.", + "emailExists": "This email address is already added.", "noFile": "Please upload a file first." } } diff --git a/src/lib/server/api-utils.ts b/src/lib/server/api-utils.ts index 562da61..5a702e3 100644 --- a/src/lib/server/api-utils.ts +++ b/src/lib/server/api-utils.ts @@ -5,4 +5,13 @@ export class ApiUtils { headers: { 'Content-Type': 'application/json' } }); } + + static redirect(url: string, status = 302): Response { + return new Response(null, { + status, + headers: { + Location: url + } + }); + } } diff --git a/src/lib/server/drizzle/schema.ts b/src/lib/server/drizzle/schema.ts index 70bd309..6cd42ea 100644 --- a/src/lib/server/drizzle/schema.ts +++ b/src/lib/server/drizzle/schema.ts @@ -145,7 +145,8 @@ export type NewOAuth2Token = typeof oauth2Token.$inferInsert; export const privilege = mysqlTable('privilege', { id: int('id').autoincrement().notNull(), name: text('name').notNull(), - clientId: int('clientId').references(() => oauth2Client.id, { onDelete: 'cascade' }) + clientId: int('clientId').references(() => oauth2Client.id, { onDelete: 'cascade' }), + automatic: tinyint('automatic').default(0).notNull() }); export type Privilege = typeof privilege.$inferSelect; @@ -235,11 +236,13 @@ export const userToken = mysqlTable('user_token', { 'gdpr', 'totp', 'public_key', + 'invite', 'recovery' ]).notNull(), expires_at: timestamp('expires_at', { mode: 'date' }), userId: int('userId').references(() => user.id, { onDelete: 'cascade' }), nonce: text('nonce'), + metadata: text('metadata'), created_at: datetime('created_at', { mode: 'date', fsp: 6 }) .default(sql`current_timestamp(6)`) .notNull() diff --git a/src/lib/server/email/templates/index.ts b/src/lib/server/email/templates/index.ts index 33f22bd..1e513c5 100644 --- a/src/lib/server/email/templates/index.ts +++ b/src/lib/server/email/templates/index.ts @@ -1,3 +1,4 @@ export * from './forgot-password.email'; export * from './invitation.email'; +export * from './oauth2-invitation.email'; export * from './registration.email'; diff --git a/src/lib/server/email/templates/oauth2-invitation.email.ts b/src/lib/server/email/templates/oauth2-invitation.email.ts new file mode 100644 index 0000000..76339c7 --- /dev/null +++ b/src/lib/server/email/templates/oauth2-invitation.email.ts @@ -0,0 +1,31 @@ +import { PUBLIC_SITE_NAME } from '$env/static/public'; +import type { EmailTemplate } from '../template.interface'; + +export const OAuth2InvitationEmail = ( + inviter: string, + clientName: string, + url: string +): EmailTemplate => ({ + text: ` +${PUBLIC_SITE_NAME} + +${inviter} has invited you to edit the "${clientName}" application on ${PUBLIC_SITE_NAME}. + +Please click on the following link to accept the invitation. + +Accept invitation: ${url} + +This email was sent to you because someone invited you to contribute to an application on ${PUBLIC_SITE_NAME}. If you believe that this was sent in error, you may safely ignore this email. + `, + html: /* html */ ` +
${inviter} has invited you to edit the "${clientName}" application on ${PUBLIC_SITE_NAME}. + +
Please click on the following link to accept the invitation ${PUBLIC_SITE_NAME}.
+ +Accept invitation: ${url}
+ +This email was sent to you because someone invited you to contribute to an application on ${PUBLIC_SITE_NAME}. If you believe that this was sent in error, you may safely ignore this email.
+ ` +}); diff --git a/src/lib/server/oauth2/model/client.ts b/src/lib/server/oauth2/model/client.ts index 2683526..70c0776 100644 --- a/src/lib/server/oauth2/model/client.ts +++ b/src/lib/server/oauth2/model/client.ts @@ -1,3 +1,4 @@ +import { PUBLIC_URL, PUBLIC_SITE_NAME } from '$env/static/public'; import { CryptoUtils } from '$lib/server/crypto-utils'; import { db, @@ -12,7 +13,9 @@ import { type OAuth2ClientUrl, type User } from '$lib/server/drizzle'; +import { Emails, OAuth2InvitationEmail } from '$lib/server/email'; import { Uploads } from '$lib/server/upload'; +import { UserTokens } from '$lib/server/users'; import type { PaginationMeta } from '$lib/types'; import { and, count, eq, like, or, sql } from 'drizzle-orm'; @@ -38,6 +41,7 @@ export interface OAuth2AuthorizedUser { } export interface OAuth2ManagerUser { + id: number; email: string; } @@ -369,6 +373,57 @@ export class OAuth2Clients { await db.update(oauth2Client).set(body).where(eq(oauth2Client.id, client.id)); } + static async getManagers(client: OAuth2Client) { + return await db + .select({ id: oauth2ClientManager.id, email: user.email }) + .from(oauth2ClientManager) + .innerJoin(user, eq(user.id, oauth2ClientManager.userId)) + .where(eq(oauth2ClientManager.clientId, client.id)); + } + + static async addManager(client: OAuth2Client, actor: User, subject: User) { + await db.insert(oauth2ClientManager).values({ + clientId: client.id, + userId: subject.id, + issuerId: actor.id + }); + } + + static async removeManager(client: OAuth2Client, managerId: number) { + await db + .delete(oauth2ClientManager) + .where( + and(eq(oauth2ClientManager.clientId, client.id), eq(oauth2ClientManager.id, managerId)) + ); + } + + static async sendManagerInvitationEmail(client: OAuth2Client, actor: User, email: string) { + const token = await UserTokens.create( + 'invite', + new Date(Date.now() + 3600 * 1000), + actor.id, + undefined, + `clientmanager=${client.client_id}` + ); + const params = new URLSearchParams({ token: token.token }); + const content = OAuth2InvitationEmail( + actor.display_name, + client.title, + `${PUBLIC_URL}/ssoadmin/oauth2/invite?${params.toString()}` + ); + + // TODO: logging + try { + await Emails.getSender().sendTemplate( + email, + `You have been invited to manage "${client.title}" on ${PUBLIC_SITE_NAME}`, + content + ); + } catch { + await UserTokens.remove(token); + } + } + static async authorizeClientInfo(client: OAuth2Client, scope: string[]) { const links = await OAuth2Clients.getClientUrls(client); const filteredLinks = links diff --git a/src/lib/server/users/index.ts b/src/lib/server/users/index.ts index fd03e7b..4d53d4b 100644 --- a/src/lib/server/users/index.ts +++ b/src/lib/server/users/index.ts @@ -194,10 +194,11 @@ export class Users { static async sendInvitationEmail(email: string) { const token = await UserTokens.create( - 'login', + 'invite', new Date(Date.now() + 3600 * 1000), undefined, - email + undefined, + `register=${email}` ); const params = new URLSearchParams({ token: token.token }); const content = InvitationEmail(`${PUBLIC_URL}/register?${params.toString()}`); diff --git a/src/lib/server/users/tokens.ts b/src/lib/server/users/tokens.ts index 4bc4fed..8376612 100644 --- a/src/lib/server/users/tokens.ts +++ b/src/lib/server/users/tokens.ts @@ -7,7 +7,8 @@ export class UserTokens { type: (typeof userToken.$inferInsert)['type'], expires: Date, userId?: number, - nonce?: string + nonce?: string, + metadata?: string ) { const token = CryptoUtils.generateString(64); const obj ={PUBLIC_URL}/oauth2/authorize
- {PUBLIC_URL}/oauth2/token
- {PUBLIC_URL}/oauth2/introspect
- {PUBLIC_URL}/api/user
- {PUBLIC_URL}/.well-known/openid-configuration
- {$t('admin.oauth2.managers.hint', { siteName: PUBLIC_SITE_NAME })}
+ +{PUBLIC_URL}/oauth2/authorize
+ {PUBLIC_URL}/oauth2/token
+ {PUBLIC_URL}/oauth2/introspect
+ {PUBLIC_URL}/api/user
+ {PUBLIC_URL}/.well-known/openid-configuration
+ {$t('admin.oauth2.authorizationsHint')}
diff --git a/src/routes/ssoadmin/oauth2/invite/+server.ts b/src/routes/ssoadmin/oauth2/invite/+server.ts new file mode 100644 index 0000000..90d8740 --- /dev/null +++ b/src/routes/ssoadmin/oauth2/invite/+server.ts @@ -0,0 +1,39 @@ +import { ApiUtils } from '$lib/server/api-utils.js'; +import { OAuth2Clients } from '$lib/server/oauth2/index.js'; +import { UserTokens, Users } from '$lib/server/users'; + +export const GET = async ({ locals, url }) => { + const userInfo = locals.session.data?.user; + const currentUser = await Users.getBySession(userInfo); + if (!userInfo || !currentUser) { + await locals.session.destroy(); + return ApiUtils.redirect(`/login?redirectTo=${encodeURIComponent(url.pathname)}`); + } + + const token = url.searchParams.get('token'); + if (!token) { + return ApiUtils.redirect('/'); + } + + const fetch = await UserTokens.getByToken(token, 'invite'); + if (!fetch?.userId || !fetch.metadata?.startsWith('clientmanager=')) { + return ApiUtils.redirect('/'); + } + + const inviter = await Users.getById(fetch.userId); + if (!inviter) { + return ApiUtils.redirect('/'); + } + + const [, clientId] = fetch.metadata.split('='); + const client = await OAuth2Clients.fetchById(clientId); + if (!client) { + return ApiUtils.redirect('/'); + } + + await OAuth2Clients.addManager(client, inviter, currentUser); + await UserTokens.remove(fetch); + + console.log('?'); + return ApiUtils.redirect(`/ssoadmin/oauth2/${client.client_id}`); +};