mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-13 07:42:31 +02:00
Add user profile image
This commit is contained in:
Generated
+107
-24
@@ -12,6 +12,7 @@
|
|||||||
"@jellyfin/client-axios": "^10.7.8",
|
"@jellyfin/client-axios": "^10.7.8",
|
||||||
"@mantine/core": "^5.7.2",
|
"@mantine/core": "^5.7.2",
|
||||||
"@mantine/dates": "^5.7.2",
|
"@mantine/dates": "^5.7.2",
|
||||||
|
"@mantine/dropzone": "^5.7.2",
|
||||||
"@mantine/form": "^5.7.2",
|
"@mantine/form": "^5.7.2",
|
||||||
"@mantine/hooks": "^5.7.2",
|
"@mantine/hooks": "^5.7.2",
|
||||||
"@mantine/modals": "^5.7.2",
|
"@mantine/modals": "^5.7.2",
|
||||||
@@ -1680,6 +1681,21 @@
|
|||||||
"react": ">=16.8.0"
|
"react": ">=16.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mantine/dropzone": {
|
||||||
|
"version": "5.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-5.7.2.tgz",
|
||||||
|
"integrity": "sha512-sGl8WrBpCfXFz1nMTsPzlcZxVOkrNkDWeZ0wCf44/gwJ+AJBpnlCmOgpLwgul7qIfCjnGsJQWC9wZ7L7iglb5w==",
|
||||||
|
"dependencies": {
|
||||||
|
"@mantine/utils": "5.7.2",
|
||||||
|
"react-dropzone": "14.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@mantine/core": "5.7.2",
|
||||||
|
"@mantine/hooks": "5.7.2",
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@mantine/form": {
|
"node_modules/@mantine/form": {
|
||||||
"version": "5.7.2",
|
"version": "5.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@mantine/form/-/form-5.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mantine/form/-/form-5.7.2.tgz",
|
||||||
@@ -4266,6 +4282,14 @@
|
|||||||
"node": ">=10.12.0"
|
"node": ">=10.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/attr-accept": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "9.8.8",
|
"version": "9.8.8",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.8.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.8.tgz",
|
||||||
@@ -7655,6 +7679,18 @@
|
|||||||
"graceful-fs": "^4.1.6"
|
"graceful-fs": "^4.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/electron-publish/node_modules/mime": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"mime": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-publish/node_modules/universalify": {
|
"node_modules/electron-publish/node_modules/universalify": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
||||||
@@ -10193,6 +10229,17 @@
|
|||||||
"webpack": "^4.0.0 || ^5.0.0"
|
"webpack": "^4.0.0 || ^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/file-selector": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/filelist": {
|
"node_modules/filelist": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||||
@@ -14462,18 +14509,6 @@
|
|||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mime": {
|
|
||||||
"version": "2.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
|
|
||||||
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"mime": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mime-db": {
|
"node_modules/mime-db": {
|
||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
@@ -17290,6 +17325,22 @@
|
|||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-dropzone": {
|
||||||
|
"version": "14.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz",
|
||||||
|
"integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==",
|
||||||
|
"dependencies": {
|
||||||
|
"attr-accept": "^2.2.2",
|
||||||
|
"file-selector": "^0.6.0",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.13"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16.8 || 18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-error-boundary": {
|
"node_modules/react-error-boundary": {
|
||||||
"version": "3.1.4",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
|
||||||
@@ -21842,9 +21893,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.3.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
|
||||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
|
||||||
},
|
},
|
||||||
"node_modules/tsutils": {
|
"node_modules/tsutils": {
|
||||||
"version": "3.21.0",
|
"version": "3.21.0",
|
||||||
@@ -24817,6 +24868,15 @@
|
|||||||
"@mantine/utils": "5.7.2"
|
"@mantine/utils": "5.7.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@mantine/dropzone": {
|
||||||
|
"version": "5.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-5.7.2.tgz",
|
||||||
|
"integrity": "sha512-sGl8WrBpCfXFz1nMTsPzlcZxVOkrNkDWeZ0wCf44/gwJ+AJBpnlCmOgpLwgul7qIfCjnGsJQWC9wZ7L7iglb5w==",
|
||||||
|
"requires": {
|
||||||
|
"@mantine/utils": "5.7.2",
|
||||||
|
"react-dropzone": "14.2.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@mantine/form": {
|
"@mantine/form": {
|
||||||
"version": "5.7.2",
|
"version": "5.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@mantine/form/-/form-5.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mantine/form/-/form-5.7.2.tgz",
|
||||||
@@ -26875,6 +26935,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz",
|
||||||
"integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w=="
|
"integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w=="
|
||||||
},
|
},
|
||||||
|
"attr-accept": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg=="
|
||||||
|
},
|
||||||
"autoprefixer": {
|
"autoprefixer": {
|
||||||
"version": "9.8.8",
|
"version": "9.8.8",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.8.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.8.tgz",
|
||||||
@@ -29489,6 +29554,12 @@
|
|||||||
"universalify": "^2.0.0"
|
"universalify": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"mime": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"universalify": {
|
"universalify": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
||||||
@@ -31345,6 +31416,14 @@
|
|||||||
"schema-utils": "^3.0.0"
|
"schema-utils": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"file-selector": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"filelist": {
|
"filelist": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||||
@@ -34582,12 +34661,6 @@
|
|||||||
"picomatch": "^2.3.1"
|
"picomatch": "^2.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mime": {
|
|
||||||
"version": "2.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
|
|
||||||
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"mime-db": {
|
"mime-db": {
|
||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
@@ -36680,6 +36753,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-dropzone": {
|
||||||
|
"version": "14.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz",
|
||||||
|
"integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==",
|
||||||
|
"requires": {
|
||||||
|
"attr-accept": "^2.2.2",
|
||||||
|
"file-selector": "^0.6.0",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-error-boundary": {
|
"react-error-boundary": {
|
||||||
"version": "3.1.4",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
|
||||||
@@ -40225,9 +40308,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tslib": {
|
"tslib": {
|
||||||
"version": "2.3.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
|
||||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
|
||||||
},
|
},
|
||||||
"tsutils": {
|
"tsutils": {
|
||||||
"version": "3.21.0",
|
"version": "3.21.0",
|
||||||
|
|||||||
@@ -252,6 +252,7 @@
|
|||||||
"@jellyfin/client-axios": "^10.7.8",
|
"@jellyfin/client-axios": "^10.7.8",
|
||||||
"@mantine/core": "^5.7.2",
|
"@mantine/core": "^5.7.2",
|
||||||
"@mantine/dates": "^5.7.2",
|
"@mantine/dates": "^5.7.2",
|
||||||
|
"@mantine/dropzone": "^5.7.2",
|
||||||
"@mantine/form": "^5.7.2",
|
"@mantine/form": "^5.7.2",
|
||||||
"@mantine/hooks": "^5.7.2",
|
"@mantine/hooks": "^5.7.2",
|
||||||
"@mantine/modals": "^5.7.2",
|
"@mantine/modals": "^5.7.2",
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ const updateUser = async (
|
|||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
const { userId } = req.params;
|
const { userId } = req.params;
|
||||||
const user = await service.users.updateUser({ userId }, req.body);
|
|
||||||
|
const user = await service.users.updateUser(
|
||||||
|
{ userId },
|
||||||
|
{ ...req.body, image: req.file }
|
||||||
|
);
|
||||||
const success = ApiSuccess.ok({ data: toApiModel.users([user])[0] });
|
const success = ApiSuccess.ok({ data: toApiModel.users([user])[0] });
|
||||||
return res.status(success.statusCode).json(getSuccessResponse(success));
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
Artist,
|
Artist,
|
||||||
ArtistRating,
|
ArtistRating,
|
||||||
External,
|
External,
|
||||||
|
File,
|
||||||
|
FileType,
|
||||||
Genre,
|
Genre,
|
||||||
Image,
|
Image,
|
||||||
ImageType,
|
ImageType,
|
||||||
@@ -585,9 +587,21 @@ const relatedServerPermissions = (items: ServerPermission[]) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const relatedFile = (item: File) => {
|
||||||
|
return {
|
||||||
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||||
|
id: item.id,
|
||||||
|
name: item.fileName,
|
||||||
|
path: item.path,
|
||||||
|
type: item.type,
|
||||||
|
/* eslint-enable sort-keys-fix/sort-keys-fix */
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const users = (
|
const users = (
|
||||||
items: (User & {
|
items: (User & {
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
|
files?: File[];
|
||||||
refreshToken?: string;
|
refreshToken?: string;
|
||||||
serverFolderPermissions?: ServerFolderPermission[];
|
serverFolderPermissions?: ServerFolderPermission[];
|
||||||
serverPermissions?: ServerPermission[];
|
serverPermissions?: ServerPermission[];
|
||||||
@@ -595,11 +609,14 @@ const users = (
|
|||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
items.map((item) => {
|
items.map((item) => {
|
||||||
|
const avatar = item.files?.find((f) => f.type === FileType.USER);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||||
id: item.id,
|
id: item.id,
|
||||||
username: item.username,
|
username: item.username,
|
||||||
displayName: item.displayName,
|
displayName: item.displayName,
|
||||||
|
avatar: avatar ? relatedFile(avatar) : null,
|
||||||
accessToken: item.accessToken,
|
accessToken: item.accessToken,
|
||||||
refreshToken: item.refreshToken,
|
refreshToken: item.refreshToken,
|
||||||
enabled: item.enabled,
|
enabled: item.enabled,
|
||||||
|
|||||||
Generated
+1009
-31
File diff suppressed because it is too large
Load Diff
@@ -34,9 +34,11 @@
|
|||||||
"@types/express": "^4.17.14",
|
"@types/express": "^4.17.14",
|
||||||
"@types/lodash": "^4.14.186",
|
"@types/lodash": "^4.14.186",
|
||||||
"@types/md5": "^2.3.2",
|
"@types/md5": "^2.3.2",
|
||||||
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^18.8.4",
|
"@types/node": "^18.8.4",
|
||||||
"@types/passport-jwt": "^3.0.7",
|
"@types/passport-jwt": "^3.0.7",
|
||||||
"@types/passport-local": "^1.0.34",
|
"@types/passport-local": "^1.0.34",
|
||||||
|
"@types/sharp": "^0.31.0",
|
||||||
"@typescript-eslint/parser": "^5.40.0",
|
"@typescript-eslint/parser": "^5.40.0",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
@@ -69,10 +71,12 @@
|
|||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
"p-throttle": "^4.1.1",
|
"p-throttle": "^4.1.1",
|
||||||
"passport": "^0.4.1",
|
"passport": "^0.4.1",
|
||||||
"passport-jwt": "^4.0.0",
|
"passport-jwt": "^4.0.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
|
"sharp": "^0.31.2",
|
||||||
"socket.io": "^4.5.3",
|
"socket.io": "^4.5.3",
|
||||||
"zod": "^3.19.1"
|
"zod": "^3.19.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "FileType" AS ENUM ('ALBUM', 'SONG', 'AUDIO', 'USER');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "File" (
|
||||||
|
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"path" TEXT NOT NULL,
|
||||||
|
"originalName" TEXT NOT NULL,
|
||||||
|
"fileName" TEXT NOT NULL,
|
||||||
|
"size" INTEGER NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"type" "FileType" NOT NULL,
|
||||||
|
"userId" UUID,
|
||||||
|
|
||||||
|
CONSTRAINT "File_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "File_path_key" ON "File"("path");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "File_fileName_key" ON "File"("fileName");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "File_userId_type_key" ON "File"("userId", "type");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "File" ADD CONSTRAINT "File_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -48,6 +48,13 @@ enum TaskType {
|
|||||||
LASTFM
|
LASTFM
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum FileType {
|
||||||
|
ALBUM
|
||||||
|
SONG
|
||||||
|
AUDIO
|
||||||
|
USER
|
||||||
|
}
|
||||||
|
|
||||||
model RefreshToken {
|
model RefreshToken {
|
||||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
token String @unique
|
token String @unique
|
||||||
@@ -74,6 +81,7 @@ model User {
|
|||||||
albumRatings AlbumRating[]
|
albumRatings AlbumRating[]
|
||||||
songRatings SongRating[]
|
songRatings SongRating[]
|
||||||
refreshTokens RefreshToken[]
|
refreshTokens RefreshToken[]
|
||||||
|
files File[]
|
||||||
|
|
||||||
serverFolderPermissions ServerFolderPermission[]
|
serverFolderPermissions ServerFolderPermission[]
|
||||||
serverPermissions ServerPermission[]
|
serverPermissions ServerPermission[]
|
||||||
@@ -85,6 +93,22 @@ model User {
|
|||||||
tasks Task[]
|
tasks Task[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model File {
|
||||||
|
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
|
path String @unique
|
||||||
|
originalName String
|
||||||
|
fileName String @unique
|
||||||
|
size Int
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
type FileType
|
||||||
|
|
||||||
|
User User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String? @db.Uuid
|
||||||
|
|
||||||
|
@@unique(fields: [userId, type], name: "uniqueFileId")
|
||||||
|
}
|
||||||
|
|
||||||
model History {
|
model History {
|
||||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import express, { Router } from 'express';
|
import express, { Router } from 'express';
|
||||||
|
import multer from 'multer';
|
||||||
import { controller } from '@controllers/index';
|
import { controller } from '@controllers/index';
|
||||||
import { service } from '@services/index';
|
import { service } from '@services/index';
|
||||||
import { ApiError } from '@utils/index';
|
import { ApiError } from '@utils/index';
|
||||||
import { validation } from '@validations/index';
|
import { validation } from '@validations/index';
|
||||||
import { validateRequest } from '@validations/shared.validation';
|
import { validateRequest } from '@validations/shared.validation';
|
||||||
import { authenticateAdmin } from '../middleware/authenticate-admin';
|
import { authenticateAdmin } from '../middleware/authenticate-admin';
|
||||||
|
const storage = multer.memoryStorage();
|
||||||
|
const upload = multer({ storage: storage });
|
||||||
|
|
||||||
export const router: Router = express.Router({ mergeParams: true });
|
export const router: Router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
@@ -41,6 +44,7 @@ router
|
|||||||
.get(validateRequest(validation.users.detail), controller.users.getUserDetail)
|
.get(validateRequest(validation.users.detail), controller.users.getUserDetail)
|
||||||
.patch(
|
.patch(
|
||||||
validateRequest(validation.users.updateUser),
|
validateRequest(validation.users.updateUser),
|
||||||
|
upload.single('image'),
|
||||||
controller.users.updateUser
|
controller.users.updateUser
|
||||||
)
|
)
|
||||||
.delete(
|
.delete(
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ const PORT = 9321;
|
|||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const staticPath = path.join(__dirname, '../feishin-client/');
|
const staticPath = path.join(__dirname, '../feishin-client/');
|
||||||
|
const filesPath = path.join(__dirname, './files/');
|
||||||
|
|
||||||
app.use(express.static(staticPath));
|
app.use(express.static(staticPath));
|
||||||
|
app.use('/files', express.static(filesPath));
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
credentials: false,
|
credentials: false,
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import { FileType } from '@prisma/client';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
|
import md5 from 'md5';
|
||||||
|
import sharp from 'sharp';
|
||||||
import { prisma } from '@lib/prisma';
|
import { prisma } from '@lib/prisma';
|
||||||
import { AuthUser } from '@middleware/authenticate';
|
import { AuthUser } from '@middleware/authenticate';
|
||||||
import { randomString, ApiError } from '@utils/index';
|
import { randomString, ApiError } from '@utils/index';
|
||||||
|
import { SortOrder } from '../types/types';
|
||||||
|
|
||||||
const findById = async (user: AuthUser, options: { id: string }) => {
|
const findById = async (user: AuthUser, options: { id: string }) => {
|
||||||
const { id } = options;
|
const { id } = options;
|
||||||
@@ -11,7 +16,11 @@ const findById = async (user: AuthUser, options: { id: string }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uniqueUser = await prisma.user.findUnique({
|
const uniqueUser = await prisma.user.findUnique({
|
||||||
include: { serverFolderPermissions: true, serverPermissions: true },
|
include: {
|
||||||
|
files: true,
|
||||||
|
serverFolderPermissions: true,
|
||||||
|
serverPermissions: true,
|
||||||
|
},
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -23,20 +32,23 @@ const findById = async (user: AuthUser, options: { id: string }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const findMany = async () => {
|
const findMany = async () => {
|
||||||
const users = await prisma.user.findMany({});
|
const users = await prisma.user.findMany({
|
||||||
|
include: { files: true },
|
||||||
|
orderBy: [{ isAdmin: SortOrder.DESC }, { username: SortOrder.ASC }],
|
||||||
|
});
|
||||||
return users;
|
return users;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createUser = async (
|
const createUser = async (
|
||||||
user: AuthUser,
|
user: AuthUser,
|
||||||
options: {
|
data: {
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
password: string;
|
password: string;
|
||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const { password, username, displayName, isAdmin } = options;
|
const { password, username, displayName, isAdmin } = data;
|
||||||
|
|
||||||
if (isAdmin && !user.isSuperAdmin) {
|
if (isAdmin && !user.isSuperAdmin) {
|
||||||
throw ApiError.badRequest('You are not authorized to create an admin.');
|
throw ApiError.badRequest('You are not authorized to create an admin.');
|
||||||
@@ -91,18 +103,81 @@ const updateUser = async (
|
|||||||
options: { userId: string },
|
options: { userId: string },
|
||||||
data: {
|
data: {
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
|
image?: Express.Multer.File | null;
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
password?: string;
|
password?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const { userId } = options;
|
const { userId } = options;
|
||||||
const { username, password, isAdmin, displayName } = data;
|
const { username, password, isAdmin, displayName, image } = data;
|
||||||
|
|
||||||
const hashedPassword = password && (await bcrypt.hash(password, 12));
|
const hashedPassword = password && (await bcrypt.hash(password, 12));
|
||||||
|
|
||||||
|
let avatar: {
|
||||||
|
fileName: string;
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
type: FileType;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
|
if (image) {
|
||||||
|
const existingUser = await prisma.user.findUnique({
|
||||||
|
include: { files: true },
|
||||||
|
where: { id: userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingFile = existingUser?.files.find(
|
||||||
|
(file) => file.type === FileType.USER
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete the existing file
|
||||||
|
if (existingFile) {
|
||||||
|
await prisma.file.delete({ where: { id: existingFile.id } });
|
||||||
|
const filePath = `../files/${existingFile.fileName}`;
|
||||||
|
fs.unlink(filePath, (err) => {
|
||||||
|
if (err) console.log(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create optimized webp image and delete the original
|
||||||
|
const avatarFilename = `${md5(randomString(12))}.webp`;
|
||||||
|
const avatarPath = `files/${avatarFilename}`;
|
||||||
|
const newImage = await sharp(image.buffer)
|
||||||
|
.webp({ quality: 20 })
|
||||||
|
.toFile(avatarPath);
|
||||||
|
avatar = {
|
||||||
|
fileName: avatarFilename,
|
||||||
|
path: avatarPath,
|
||||||
|
size: newImage.size,
|
||||||
|
type: FileType.USER,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const user = await prisma.user.update({
|
const user = await prisma.user.update({
|
||||||
data: { displayName, isAdmin, password: hashedPassword, username },
|
data: {
|
||||||
|
displayName,
|
||||||
|
files:
|
||||||
|
image && avatar
|
||||||
|
? {
|
||||||
|
create: {
|
||||||
|
fileName: avatar.fileName,
|
||||||
|
originalName: image?.originalname!,
|
||||||
|
path: avatar.path,
|
||||||
|
size: avatar.size,
|
||||||
|
type: avatar.type,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
isAdmin,
|
||||||
|
password: hashedPassword,
|
||||||
|
username,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
files: true,
|
||||||
|
serverFolderPermissions: true,
|
||||||
|
serverPermissions: true,
|
||||||
|
},
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,16 @@ export type RelatedServerPermission = {
|
|||||||
type: ServerPermissionType;
|
type: ServerPermissionType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ServerFile = {
|
||||||
|
id: string;
|
||||||
|
mimetype: string;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
|
avatar: ServerFile | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { FileWithPath } from '@mantine/dropzone';
|
||||||
import { BaseResponse, NullResponse, User } from '@/renderer/api/types';
|
import { BaseResponse, NullResponse, User } from '@/renderer/api/types';
|
||||||
import { ax } from '@/renderer/lib/axios';
|
import { ax } from '@/renderer/lib/axios';
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ const getUserList = async (signal?: AbortSignal) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type CreateUserBody = {
|
export type CreateUserBody = {
|
||||||
|
displayName?: string;
|
||||||
|
image?: FileWithPath;
|
||||||
password: string;
|
password: string;
|
||||||
username: string;
|
username: string;
|
||||||
};
|
};
|
||||||
@@ -32,9 +35,25 @@ const deleteUser = async (query: { userId: string }) => {
|
|||||||
export type UpdateUserBody = Partial<CreateUserBody>;
|
export type UpdateUserBody = Partial<CreateUserBody>;
|
||||||
|
|
||||||
const updateUser = async (query: { userId: string }, body: UpdateUserBody) => {
|
const updateUser = async (query: { userId: string }, body: UpdateUserBody) => {
|
||||||
|
if (body.image) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('image', body.image);
|
||||||
|
if (body.username) formData.append('username', body.username);
|
||||||
|
if (body.displayName) formData.append('displayName', body.displayName);
|
||||||
|
|
||||||
|
const { data } = await ax.patch<UserDetailResponse>(
|
||||||
|
`/users/${query.userId}`,
|
||||||
|
formData,
|
||||||
|
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
const { data } = await ax.patch<UserDetailResponse>(
|
const { data } = await ax.patch<UserDetailResponse>(
|
||||||
`/users/${query.userId}`,
|
`/users/${query.userId}`,
|
||||||
body
|
body,
|
||||||
|
{}
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const AddUserForm = ({ onCancel }: AddUserFormProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleAddUser}>
|
<form onSubmit={handleAddUser}>
|
||||||
<Stack ref={focusTrapRef}>
|
<Stack ref={focusTrapRef} m={5}>
|
||||||
<TextInput
|
<TextInput
|
||||||
data-autofocus
|
data-autofocus
|
||||||
required
|
required
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Stack, Group } from '@mantine/core';
|
import { Stack, Group, Grid, Image } from '@mantine/core';
|
||||||
|
import { FileWithPath, IMAGE_MIME_TYPE } from '@mantine/dropzone';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { useClipboard, useFocusTrap } from '@mantine/hooks';
|
import { useClipboard, useFocusTrap } from '@mantine/hooks';
|
||||||
|
import { RiImage2Line } from 'react-icons/ri';
|
||||||
import { User } from '@/renderer/api/types';
|
import { User } from '@/renderer/api/types';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -8,6 +10,8 @@ import {
|
|||||||
TextInput,
|
TextInput,
|
||||||
Switch,
|
Switch,
|
||||||
toast,
|
toast,
|
||||||
|
Dropzone,
|
||||||
|
Text,
|
||||||
} from '@/renderer/components';
|
} from '@/renderer/components';
|
||||||
import { usePermissions } from '@/renderer/features/shared';
|
import { usePermissions } from '@/renderer/features/shared';
|
||||||
import { useUpdateUser } from '@/renderer/features/users/mutations/update-user';
|
import { useUpdateUser } from '@/renderer/features/users/mutations/update-user';
|
||||||
@@ -31,6 +35,7 @@ export const EditUserForm = ({
|
|||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
displayName: user?.displayName || '',
|
displayName: user?.displayName || '',
|
||||||
|
image: null as FileWithPath | null,
|
||||||
isAdmin: user?.isAdmin || false,
|
isAdmin: user?.isAdmin || false,
|
||||||
password: '',
|
password: '',
|
||||||
repeatPassword: '',
|
repeatPassword: '',
|
||||||
@@ -58,6 +63,7 @@ export const EditUserForm = ({
|
|||||||
const body = {
|
const body = {
|
||||||
...values,
|
...values,
|
||||||
displayName: values.displayName || undefined,
|
displayName: values.displayName || undefined,
|
||||||
|
image: values.image || undefined,
|
||||||
password: values.password || undefined,
|
password: values.password || undefined,
|
||||||
repeatPassword: values.repeatPassword || undefined,
|
repeatPassword: values.repeatPassword || undefined,
|
||||||
};
|
};
|
||||||
@@ -81,62 +87,126 @@ export const EditUserForm = ({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getPreview = () => {
|
||||||
|
if (form.values.image instanceof File) {
|
||||||
|
const imageUrl = URL.createObjectURL(form.values.image);
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
height={150}
|
||||||
|
imageProps={{ onLoad: () => URL.revokeObjectURL(imageUrl) }}
|
||||||
|
radius={100}
|
||||||
|
src={imageUrl}
|
||||||
|
sx={{ objectFit: 'contain' }}
|
||||||
|
width={150}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveImage = () => form.setFieldValue('image', null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleUpdateUser}>
|
<form onSubmit={handleUpdateUser}>
|
||||||
<Stack ref={focusTrapRef} spacing="xl">
|
<Stack ref={focusTrapRef} spacing="xs" sx={{ margin: '5px' }}>
|
||||||
<TextInput
|
<Grid>
|
||||||
data-autofocus
|
<Grid.Col span={4}>
|
||||||
label="Username"
|
<Stack spacing="xs" sx={{ height: '100%' }}>
|
||||||
{...form.getInputProps('username')}
|
<Dropzone
|
||||||
/>
|
accept={IMAGE_MIME_TYPE}
|
||||||
<TextInput
|
maxSize={3 * 1024 ** 2}
|
||||||
label="Display name"
|
multiple={false}
|
||||||
{...form.getInputProps('displayName')}
|
onDrop={(file) => form.setFieldValue('image', file[0])}
|
||||||
/>
|
onReject={(err) =>
|
||||||
<PasswordInput label="Password" {...form.getInputProps('password')} />
|
toast.error({
|
||||||
{repeatPassword && (
|
message: `${err[0].errors[0].message}`,
|
||||||
<PasswordInput
|
title: 'Invalid Image',
|
||||||
label="Repeat password"
|
})
|
||||||
{...form.getInputProps('repeatPassword')}
|
}
|
||||||
/>
|
>
|
||||||
)}
|
<Group>
|
||||||
|
<Dropzone.Idle>
|
||||||
<Group position="apart">
|
{form.values.image ? (
|
||||||
{permissions.isAdmin && !user?.isSuperAdmin ? (
|
<Group position="center">{getPreview()}</Group>
|
||||||
<Group>
|
) : (
|
||||||
Admin
|
<Group position="center">
|
||||||
<Switch
|
<RiImage2Line color="var(--primary-color)" size={30} />
|
||||||
{...form.getInputProps('isAdmin', { type: 'checkbox' })}
|
<Stack spacing="xs">
|
||||||
|
<Text>Profile image</Text>
|
||||||
|
<Text>Max size: 5MB</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</Dropzone.Idle>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
<Button compact variant="subtle" onClick={handleRemoveImage}>
|
||||||
|
Remove image
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<Stack>
|
||||||
|
<TextInput
|
||||||
|
data-autofocus
|
||||||
|
label="Username"
|
||||||
|
{...form.getInputProps('username')}
|
||||||
/>
|
/>
|
||||||
</Group>
|
<TextInput
|
||||||
) : (
|
label="Display Name"
|
||||||
<Group />
|
{...form.getInputProps('displayName')}
|
||||||
)}
|
/>
|
||||||
{!repeatPassword && (
|
<PasswordInput
|
||||||
<Button
|
label="Password"
|
||||||
compact
|
{...form.getInputProps('password')}
|
||||||
sx={{ height: '1.5rem' }}
|
/>
|
||||||
variant="subtle"
|
{repeatPassword && (
|
||||||
onClick={handleGeneratePassword}
|
<PasswordInput
|
||||||
>
|
label="Repeat Password"
|
||||||
Generate password
|
{...form.getInputProps('repeatPassword')}
|
||||||
</Button>
|
/>
|
||||||
)}
|
)}
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group mt={10} position="right">
|
<Group position="apart">
|
||||||
<Button variant="subtle" onClick={onCancel}>
|
{permissions.isAdmin && !user?.isSuperAdmin ? (
|
||||||
Cancel
|
<Group>
|
||||||
</Button>
|
Admin
|
||||||
<Button
|
<Switch
|
||||||
disabled={!isSubmitValid()}
|
{...form.getInputProps('isAdmin', { type: 'checkbox' })}
|
||||||
loading={updateUserMutation.isLoading}
|
/>
|
||||||
type="submit"
|
</Group>
|
||||||
variant="filled"
|
) : (
|
||||||
>
|
<Group />
|
||||||
Submit
|
)}
|
||||||
</Button>
|
{!repeatPassword && (
|
||||||
</Group>
|
<Button
|
||||||
|
compact
|
||||||
|
sx={{ height: '1.5rem' }}
|
||||||
|
variant="subtle"
|
||||||
|
onClick={handleGeneratePassword}
|
||||||
|
>
|
||||||
|
Generate password
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group mt={10} position="right">
|
||||||
|
<Button variant="subtle" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={!isSubmitValid()}
|
||||||
|
loading={updateUserMutation.isLoading}
|
||||||
|
type="submit"
|
||||||
|
variant="filled"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export const UserList = () => {
|
|||||||
},
|
},
|
||||||
modal: 'base',
|
modal: 'base',
|
||||||
overflow: 'inside',
|
overflow: 'inside',
|
||||||
|
size: 'lg',
|
||||||
title: `Edit User`,
|
title: `Edit User`,
|
||||||
transition: 'slide-down',
|
transition: 'slide-down',
|
||||||
});
|
});
|
||||||
@@ -110,7 +111,7 @@ export const UserList = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group>
|
<Group>
|
||||||
<Avatar radius="xl" />
|
<Avatar radius="xl" src={u.avatarUrl} />
|
||||||
<Stack spacing="xs">
|
<Stack spacing="xs">
|
||||||
<Text overflow="hidden">
|
<Text overflow="hidden">
|
||||||
{u.username}
|
{u.username}
|
||||||
|
|||||||
@@ -3,11 +3,23 @@ import { api } from '@/renderer/api';
|
|||||||
import { queryKeys } from '@/renderer/api/query-keys';
|
import { queryKeys } from '@/renderer/api/query-keys';
|
||||||
import { UserListResponse } from '@/renderer/api/users.api';
|
import { UserListResponse } from '@/renderer/api/users.api';
|
||||||
import { QueryOptions } from '@/renderer/lib/react-query';
|
import { QueryOptions } from '@/renderer/lib/react-query';
|
||||||
|
import { useAuthStore } from '@/renderer/store';
|
||||||
|
import { getFileUrl } from '@/renderer/utils';
|
||||||
|
|
||||||
export const useUserList = (options?: QueryOptions<UserListResponse>) => {
|
export const useUserList = (options?: QueryOptions<UserListResponse>) => {
|
||||||
|
const serverUrl = useAuthStore((state) => state.serverUrl);
|
||||||
|
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryFn: () => api.users.getUserList(),
|
queryFn: () => api.users.getUserList(),
|
||||||
queryKey: queryKeys.users.list(),
|
queryKey: queryKeys.users.list(),
|
||||||
|
select: (data) => {
|
||||||
|
const users = data.data.map((user) => ({
|
||||||
|
...user,
|
||||||
|
avatarUrl: getFileUrl(serverUrl, user?.avatar),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { ...data, data: users };
|
||||||
|
},
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { ServerFile } from '@/renderer/api/types';
|
||||||
|
|
||||||
|
export const getFileUrl = (serverUrl: string, file: ServerFile | null) => {
|
||||||
|
if (!file) return undefined;
|
||||||
|
return `${serverUrl}/${file.path}`;
|
||||||
|
};
|
||||||
@@ -6,3 +6,4 @@ export * from './server-folder-auth';
|
|||||||
export * from './set-local-storage-setttings';
|
export * from './set-local-storage-setttings';
|
||||||
export * from './constrain-sidebar-width';
|
export * from './constrain-sidebar-width';
|
||||||
export * from './title-case';
|
export * from './title-case';
|
||||||
|
export * from './get-file-url';
|
||||||
|
|||||||
Reference in New Issue
Block a user