feat: add user avatar (#2920)

* feat: add user avatar

* update: @nest/platform-express from 8.0.0 to 8.4.4

* add avatar_id in login response

* add user avatar upload in frontend

* align cross divider with layout icons'

* generate nest model - extensions

* cleanup

* fix tests

* reduce the avatar size on homepage

* fix review comments

* import Express

* add blob to csp
This commit is contained in:
Gandharv 2022-06-02 12:19:49 +05:30 committed by GitHub
parent ed43ca844a
commit 5dbe795d73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 1493 additions and 387 deletions

View file

@ -1175,6 +1175,7 @@ class Editor extends React.Component {
updatePresence={this.props.updatePresence}
editingVersionId={this.state?.editingVersion?.id}
self={this.props.self}
othersOnSameVersion={this.props.othersOnSameVersion}
/>
)}
{editingVersion && (

View file

@ -1,13 +1,9 @@
/* eslint-disable import/no-unresolved */
import React from 'react';
import Avatar from '@/_ui/Avatar';
import { useOthers } from 'y-presence';
const MAX_DISPLAY_USERS = 3;
const RealtimeAvatars = ({ self, updatePresence, editingVersionId }) => {
const others = useOthers();
const othersOnSameVersion = others.filter((other) => other?.presence?.editingVersionId === editingVersionId);
const RealtimeAvatars = ({ self, othersOnSameVersion, updatePresence, editingVersionId }) => {
React.useEffect(() => {
updatePresence({ editingVersionId });
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -26,6 +22,7 @@ const RealtimeAvatars = ({ self, updatePresence, editingVersionId }) => {
borderColor={self?.presence?.color}
title={getAvatarTitle(self?.presence)}
text={getAvatarText(self?.presence)}
image={self?.presence?.image}
/>
)}
{othersOnSameVersion.slice(0, MAX_DISPLAY_USERS).map(({ id, presence }) => {
@ -35,6 +32,7 @@ const RealtimeAvatars = ({ self, updatePresence, editingVersionId }) => {
borderColor={presence.color}
title={getAvatarTitle(presence)}
text={getAvatarText(presence)}
image={presence?.image}
/>
);
})}

View file

@ -4,10 +4,10 @@ import { useOthers, useSelf } from 'y-presence';
import { xorWith, isEqual } from 'lodash';
import { Editor } from '@/Editor';
import { USER_COLORS } from '@/_helpers/constants';
import { userService } from '@/_services';
const RealtimeCursors = (props) => {
const currentUser = JSON.parse(localStorage.getItem('currentUser'));
const others = useOthers();
const unavailableColors = others.map((other) => other?.presence?.color);
@ -23,6 +23,19 @@ const RealtimeCursors = (props) => {
color: availableColors[Math.floor(Math.random() * availableColors.length)],
});
React.useEffect(() => {
async function fetchAvatar() {
const blob = await userService.getAvatar(currentUser.avatar_id);
const fileReader = new FileReader();
fileReader.onload = (e) => {
updatePresence({ image: e.target.result });
};
fileReader.readAsDataURL(blob);
}
if (currentUser.avatar_id) fetchAvatar();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentUser.avatar_id]);
const othersOnSameVersion = others.filter(
(other) => other?.presence?.editingVersionId === self?.presence.editingVersionId
);

View file

@ -6,11 +6,13 @@ import { toast } from 'react-hot-toast';
function SettingsPage(props) {
const [firstName, setFirstName] = React.useState(authenticationService.currentUserValue.first_name);
const email = authenticationService.currentUserValue.email;
const token = authenticationService.currentUserValue.auth_token;
const [lastName, setLastName] = React.useState(authenticationService.currentUserValue.last_name);
const [currentpassword, setCurrentPassword] = React.useState('');
const [newPassword, setNewPassword] = React.useState('');
const [updateInProgress, setUpdateInProgress] = React.useState(false);
const [passwordChangeInProgress, setPasswordChangeInProgress] = React.useState(false);
const [selectedFile, setSelectedFile] = React.useState(null);
const updateDetails = async () => {
if (!firstName || !lastName) {
@ -20,12 +22,25 @@ function SettingsPage(props) {
return;
}
setUpdateInProgress(true);
const updatedDetails = await userService.updateCurrentUser(firstName, lastName);
authenticationService.updateCurrentUserDetails(updatedDetails);
toast.success('Details updated!', {
duration: 3000,
});
setUpdateInProgress(false);
try {
const updatedDetails = await userService.updateCurrentUser(firstName, lastName);
authenticationService.updateCurrentUserDetails(updatedDetails);
if (selectedFile) {
const formData = new FormData();
formData.append('file', selectedFile);
const avatarData = await userService.updateAvatar(formData, token);
authenticationService.updateCurrentUserDetails({ avatar_id: avatarData.id });
}
toast.success('Details updated!', {
duration: 3000,
});
setUpdateInProgress(false);
} catch (error) {
toast.error('Something went wrong');
setUpdateInProgress(false);
}
};
const changePassword = async () => {
@ -111,26 +126,44 @@ function SettingsPage(props) {
/>
</div>
</div>
<div className="row">
<div className="col-6">
<div className="mb-3">
<label className="form-label" data-cy="email-label">
Email{' '}
</label>
<input
type="text"
className="form-control"
name="email"
value={email}
readOnly
disabled
data-cy="email-input"
/>
</div>
</div>
<div className="row">
<div className="col">
<div className="mb-3">
<label className="form-label" data-cy="email-label">
Email{' '}
</label>
<input
type="text"
className="form-control"
name="email"
value={email}
readOnly
disabled
data-cy="email-input"
/>
</div>
</div>
<div className="col">
<div className="mb-3">
<div className="form-label">Avatar</div>
<input
onChange={(e) => {
const file = e.target.files[0];
if (Math.round(file.size / 1024) > 2048) {
toast.error('File size cannot exceed more than 2MB');
e.target.value = null;
} else {
setSelectedFile(file);
}
}}
accept="image/*"
type="file"
className="form-control"
/>
</div>
</div>
</div>
<a
href="#"
className={'btn btn-primary' + (updateInProgress ? ' btn-loading' : '')}

View file

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { authenticationService } from '@/_services';
import { authenticationService, userService } from '@/_services';
import { history } from '@/_helpers';
import { DarkModeToggle } from './DarkModeToggle';
@ -10,12 +10,26 @@ import { Organization } from './Organization';
export const Header = function Header({ switchDarkMode, darkMode }) {
// eslint-disable-next-line no-unused-vars
const [pathName, setPathName] = useState(document.location.pathname);
const [avatar, setAvatar] = useState();
const { first_name, last_name, avatar_id, admin } = authenticationService.currentUserValue;
useEffect(() => {
setPathName(document.location.pathname);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [document.location.pathname]);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
async function fetchAvatar() {
const blob = await userService.getAvatar(avatar_id);
setAvatar(URL.createObjectURL(blob));
}
if (avatar_id) fetchAvatar();
() => avatar && URL.revokeObjectURL(avatar);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [avatar_id]);
function logout() {
authenticationService.logout();
history.push('/login');
@ -25,8 +39,6 @@ export const Header = function Header({ switchDarkMode, darkMode }) {
history.push('/settings');
}
const { first_name, last_name, admin } = authenticationService.currentUserValue;
return (
<header className="navbar tabbed-navbar navbar-expand-md navbar-light d-print-none">
<div className="container-xl">
@ -55,10 +67,19 @@ export const Header = function Header({ switchDarkMode, darkMode }) {
data-testid="userAvatarHeader"
>
<div className="d-xl-block" data-cy="user-menu">
<span className="avatar bg-secondary-lt">
{first_name ? first_name[0] : ''}
{last_name ? last_name[0] : ''}
</span>
{avatar_id ? (
<span
className="avatar avatar-sm"
style={{
backgroundImage: `url(${avatar})`,
}}
/>
) : (
<span className="avatar bg-secondary-lt">
{first_name ? first_name[0] : ''}
{last_name ? last_name[0] : ''}
</span>
)}
</div>
</a>
<div className="dropdown-menu dropdown-menu-end dropdown-menu-arrow end-0" data-cy="dropdown-menu">

View file

@ -9,6 +9,8 @@ export const userService = {
updateCurrentUser,
changePassword,
acceptInvite,
getAvatar,
updateAvatar,
};
function getAll() {
@ -16,6 +18,24 @@ function getAll() {
return fetch(`${config.apiUrl}/users`, requestOptions).then(handleResponse);
}
function getAvatar(id) {
const requestOptions = { method: 'GET', headers: authHeader() };
return fetch(`${config.apiUrl}/files/${id}`, requestOptions)
.then((response) => response.blob())
.then((blob) => blob);
}
function updateAvatar(formData, token) {
const requestOptions = {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
body: formData,
};
return fetch(`${config.apiUrl}/users/avatar`, requestOptions).then(handleResponse);
}
function createUser(first_name, last_name, email, role) {
const body = {
first_name,

View file

@ -3932,7 +3932,7 @@ input[type="text"] {
.close-icon {
position: fixed;
top: 84px;
right: 0;
right: 3px;
width: 60px;
height: 22;
border-bottom: 1px solid #e7eaef;

View file

@ -1,13 +1,13 @@
import React from 'react';
const Avatar = ({ text, title = '', borderColor = '' }) => {
const Avatar = ({ text, image, title = '', borderColor = '' }) => {
return (
<span
data-tip={title}
style={{ border: `1.5px solid ${borderColor}` }}
style={{ border: `1.5px solid ${borderColor}`, ...(image ? { backgroundImage: `url(${image})` } : {}) }}
className="avatar avatar-sm avatar-rounded animation-fade"
>
{text}
{!image && text}
</span>
);
};

471
package-lock.json generated
View file

@ -1,11 +1,12 @@
{
"name": "tooljet",
"name": "ToolJet",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"dependencies": {
"@nestjs/cli": "^8.1.0",
"@nestjs/mapped-types": "*",
"cypress": "^8.3.1",
"ts-node": "^10.1.0"
},
@ -78,6 +79,17 @@
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/@angular-devkit/core/node_modules/rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"dependencies": {
"tslib": "^1.9.0"
},
"engines": {
"npm": ">=2.0.0"
}
},
"node_modules/@angular-devkit/core/node_modules/source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
@ -86,6 +98,11 @@
"node": ">= 8"
}
},
"node_modules/@angular-devkit/core/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@angular-devkit/schematics": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-12.1.3.tgz",
@ -146,6 +163,38 @@
"node": ">=8.0.0"
}
},
"node_modules/@angular-devkit/schematics-cli/node_modules/rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"dependencies": {
"tslib": "^1.9.0"
},
"engines": {
"npm": ">=2.0.0"
}
},
"node_modules/@angular-devkit/schematics-cli/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@angular-devkit/schematics/node_modules/rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"dependencies": {
"tslib": "^1.9.0"
},
"engines": {
"npm": ">=2.0.0"
}
},
"node_modules/@angular-devkit/schematics/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@babel/code-frame": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz",
@ -932,6 +981,59 @@
"node": ">=10.13.0"
}
},
"node_modules/@nestjs/common": {
"version": "8.4.5",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-8.4.5.tgz",
"integrity": "sha512-DL30hLtcmosOWGRFrU1YYB59k+7FGX82sbq2QiXLsEXuSig8ZzFm8LR+tD8CX+aKabU9t1GGc18HLjFud/3sww==",
"peer": true,
"dependencies": {
"axios": "0.27.2",
"iterare": "1.2.1",
"tslib": "2.4.0",
"uuid": "8.3.2"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/nest"
},
"peerDependencies": {
"cache-manager": "*",
"class-transformer": "*",
"class-validator": "*",
"reflect-metadata": "^0.1.12",
"rxjs": "^7.1.0"
},
"peerDependenciesMeta": {
"cache-manager": {
"optional": true
},
"class-transformer": {
"optional": true
},
"class-validator": {
"optional": true
}
}
},
"node_modules/@nestjs/mapped-types": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.0.1.tgz",
"integrity": "sha512-NFvofzSinp00j5rzUd4tf+xi9od6383iY0JP7o0Bnu1fuItAUkWBgc4EKuIQ3D+c2QI3i9pG1kDWAeY27EMGtg==",
"peerDependencies": {
"@nestjs/common": "^7.0.8 || ^8.0.0",
"class-transformer": "^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0",
"class-validator": "^0.11.1 || ^0.12.0 || ^0.13.0",
"reflect-metadata": "^0.1.12"
},
"peerDependenciesMeta": {
"class-transformer": {
"optional": true
},
"class-validator": {
"optional": true
}
}
},
"node_modules/@nestjs/schematics": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-8.0.2.tgz",
@ -1038,6 +1140,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@nestjs/schematics/node_modules/rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"dependencies": {
"tslib": "^1.9.0"
},
"engines": {
"npm": ">=2.0.0"
}
},
"node_modules/@nestjs/schematics/node_modules/source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
@ -1046,6 +1159,11 @@
"node": ">= 8"
}
},
"node_modules/@nestjs/schematics/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@tsconfig/node10": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
@ -1530,6 +1648,30 @@
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA=="
},
"node_modules/axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"peer": true,
"dependencies": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
}
},
"node_modules/axios/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"peer": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/babel-loader": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.2.tgz",
@ -1779,12 +1921,6 @@
"tslib": "^2.0.3"
}
},
"node_modules/camel-case/node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
},
"node_modules/caniuse-lite": {
"version": "1.0.30001234",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001234.tgz",
@ -2431,12 +2567,6 @@
"tslib": "^2.0.3"
}
},
"node_modules/dot-case/node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
},
"node_modules/ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@ -2960,6 +3090,26 @@
"integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==",
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.0.tgz",
"integrity": "sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"peer": true,
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@ -3561,6 +3711,22 @@
"node": ">=8.0.0"
}
},
"node_modules/inquirer/node_modules/rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"dependencies": {
"tslib": "^1.9.0"
},
"engines": {
"npm": ">=2.0.0"
}
},
"node_modules/inquirer/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/interpret": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
@ -3738,6 +3904,15 @@
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
},
"node_modules/iterare": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz",
"integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==",
"peer": true,
"engines": {
"node": ">=6"
}
},
"node_modules/jest-worker": {
"version": "27.0.6",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.0.6.tgz",
@ -4040,6 +4215,22 @@
"enquirer": ">= 2.3.0 < 3"
}
},
"node_modules/listr2/node_modules/rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"dependencies": {
"tslib": "^1.9.0"
},
"engines": {
"npm": ">=2.0.0"
}
},
"node_modules/listr2/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/loader-runner": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz",
@ -4177,12 +4368,6 @@
"tslib": "^2.0.3"
}
},
"node_modules/lower-case/node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@ -4321,12 +4506,6 @@
"tslib": "^2.0.3"
}
},
"node_modules/no-case/node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
},
"node_modules/node-emoji": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz",
@ -4542,12 +4721,6 @@
"tslib": "^2.0.3"
}
},
"node_modules/param-case/node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -4586,12 +4759,6 @@
"tslib": "^2.0.3"
}
},
"node_modules/pascal-case/node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
},
"node_modules/path": {
"version": "0.12.7",
"resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
@ -4828,6 +4995,12 @@
"node": ">= 0.10"
}
},
"node_modules/reflect-metadata": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==",
"peer": true
},
"node_modules/regexpp": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz",
@ -4975,14 +5148,12 @@
}
},
"node_modules/rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz",
"integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==",
"peer": true,
"dependencies": {
"tslib": "^1.9.0"
},
"engines": {
"npm": ">=2.0.0"
"tslib": "^2.1.0"
}
},
"node_modules/safe-buffer": {
@ -5581,9 +5752,9 @@
}
},
"node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
@ -6100,10 +6271,23 @@
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"requires": {
"tslib": "^1.9.0"
}
},
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
@ -6115,6 +6299,21 @@
"@angular-devkit/core": "12.1.3",
"ora": "5.4.1",
"rxjs": "6.6.7"
},
"dependencies": {
"rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"requires": {
"tslib": "^1.9.0"
}
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"@angular-devkit/schematics-cli": {
@ -6150,6 +6349,19 @@
"strip-ansi": "^6.0.0",
"through": "^2.3.6"
}
},
"rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"requires": {
"tslib": "^1.9.0"
}
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
@ -6817,6 +7029,24 @@
}
}
},
"@nestjs/common": {
"version": "8.4.5",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-8.4.5.tgz",
"integrity": "sha512-DL30hLtcmosOWGRFrU1YYB59k+7FGX82sbq2QiXLsEXuSig8ZzFm8LR+tD8CX+aKabU9t1GGc18HLjFud/3sww==",
"peer": true,
"requires": {
"axios": "0.27.2",
"iterare": "1.2.1",
"tslib": "2.4.0",
"uuid": "8.3.2"
}
},
"@nestjs/mapped-types": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.0.1.tgz",
"integrity": "sha512-NFvofzSinp00j5rzUd4tf+xi9od6383iY0JP7o0Bnu1fuItAUkWBgc4EKuIQ3D+c2QI3i9pG1kDWAeY27EMGtg==",
"requires": {}
},
"@nestjs/schematics": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-8.0.2.tgz",
@ -6892,10 +7122,23 @@
"wcwidth": "^1.0.1"
}
},
"rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"requires": {
"tslib": "^1.9.0"
}
},
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
@ -7301,6 +7544,29 @@
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA=="
},
"axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"peer": true,
"requires": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
},
"dependencies": {
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"peer": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
},
"babel-loader": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.2.tgz",
@ -7470,14 +7736,6 @@
"requires": {
"pascal-case": "^3.1.2",
"tslib": "^2.0.3"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
}
}
},
"caniuse-lite": {
@ -7964,14 +8222,6 @@
"requires": {
"no-case": "^3.0.4",
"tslib": "^2.0.3"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
}
}
},
"ecc-jsbn": {
@ -8382,6 +8632,12 @@
"integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==",
"dev": true
},
"follow-redirects": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.0.tgz",
"integrity": "sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==",
"peer": true
},
"forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@ -8802,6 +9058,21 @@
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0",
"through": "^2.3.6"
},
"dependencies": {
"rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"requires": {
"tslib": "^1.9.0"
}
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"interpret": {
@ -8924,6 +9195,12 @@
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
},
"iterare": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz",
"integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==",
"peer": true
},
"jest-worker": {
"version": "27.0.6",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.0.6.tgz",
@ -9154,6 +9431,21 @@
"rxjs": "^6.6.7",
"through": "^2.3.8",
"wrap-ansi": "^7.0.0"
},
"dependencies": {
"rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"requires": {
"tslib": "^1.9.0"
}
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"loader-runner": {
@ -9263,14 +9555,6 @@
"dev": true,
"requires": {
"tslib": "^2.0.3"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
}
}
},
"lru-cache": {
@ -9382,14 +9666,6 @@
"requires": {
"lower-case": "^2.0.2",
"tslib": "^2.0.3"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
}
}
},
"node-emoji": {
@ -9547,14 +9823,6 @@
"requires": {
"dot-case": "^3.0.4",
"tslib": "^2.0.3"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
}
}
},
"parent-module": {
@ -9584,14 +9852,6 @@
"requires": {
"no-case": "^3.0.4",
"tslib": "^2.0.3"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
}
}
},
"path": {
@ -9777,6 +10037,12 @@
"resolve": "^1.1.6"
}
},
"reflect-metadata": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==",
"peer": true
},
"regexpp": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz",
@ -9886,11 +10152,12 @@
"integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ=="
},
"rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz",
"integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==",
"peer": true,
"requires": {
"tslib": "^1.9.0"
"tslib": "^2.1.0"
}
},
"safe-buffer": {
@ -10316,9 +10583,9 @@
}
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"tunnel-agent": {
"version": "0.6.0",

View file

@ -33,6 +33,7 @@
},
"dependencies": {
"@nestjs/cli": "^8.1.0",
"@nestjs/mapped-types": "*",
"cypress": "^8.3.1",
"ts-node": "^10.1.0"
},

View file

@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddAvatarToUser1651048832555 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
'users',
new TableColumn({
name: 'avatar_id',
type: 'uuid',
isNullable: true,
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('users', 'avatar_id');
}
}

View file

@ -0,0 +1,31 @@
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
export class CreateFiles1651056032050 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'files',
columns: [
{
name: 'id',
type: 'uuid',
isGenerated: true,
default: 'gen_random_uuid()',
isPrimary: true,
},
{
name: 'data',
type: 'bytea',
},
{
name: 'filename',
type: 'varchar',
},
],
}),
true
);
}
public async down(queryRunner: QueryRunner): Promise<void> {}
}

883
server/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -39,7 +39,7 @@
"@nestjs/jwt": "^8.0.0",
"@nestjs/mapped-types": "^1.0.1",
"@nestjs/passport": "^8.2.1",
"@nestjs/platform-express": "^8.0.0",
"@nestjs/platform-express": "^8.4.4",
"@nestjs/platform-ws": "^8.0.10",
"@nestjs/serve-static": "^2.2.2",
"@nestjs/typeorm": "^8.0.0",
@ -67,8 +67,8 @@
"passport": "^0.4.1",
"passport-jwt": "^4.0.0",
"pg": "^8.7.1",
"preview-email": "^3.0.4",
"pino-pretty": "^6.0.0",
"preview-email": "^3.0.4",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
@ -91,6 +91,7 @@
"@types/got": "^9.6.12",
"@types/humps": "^2.0.1",
"@types/jest": "^26.0.24",
"@types/multer": "^1.4.7",
"@types/node": "^16.0.0",
"@types/nodemailer": "^6.4.4",
"@types/passport-jwt": "^3.0.6",

View file

@ -18,6 +18,7 @@ import { MetaModule } from './modules/meta/meta.module';
import { AppController } from './controllers/app.controller';
import { AuthModule } from './modules/auth/auth.module';
import { UsersModule } from './modules/users/users.module';
import { FilesModule } from './modules/files/files.module';
import { AppConfigModule } from './modules/app_config/app_config.module';
import { AppsModule } from './modules/apps/apps.module';
import { FoldersModule } from './modules/folders/folders.module';
@ -77,6 +78,7 @@ const imports = [
MetaModule,
LibraryAppModule,
GroupPermissionsModule,
FilesModule,
EventsModule,
];

View file

@ -0,0 +1,36 @@
import {
Controller,
Get,
Param,
UseInterceptors,
ClassSerializerInterceptor,
Res,
StreamableFile,
UseGuards,
} from '@nestjs/common';
import { Readable } from 'stream';
import { Response } from 'express';
import { FilesService } from '../services/files.service';
import { JwtAuthGuard } from 'src/modules/auth/jwt-auth.guard';
@Controller('files')
@UseInterceptors(ClassSerializerInterceptor)
export class FilesController {
constructor(private readonly filesService: FilesService) {}
@Get(':id')
@UseGuards(JwtAuthGuard)
async show(@Param('id') id: string, @Res({ passthrough: true }) response: Response) {
const file = await this.filesService.findOne(id);
const stream = Readable.from(file.data);
response.set({
'Content-Disposition': `inline; filename="${file.filename}"`,
'Content-Type': 'image',
});
// https://docs.nestjs.com/techniques/streaming-files
return new StreamableFile(stream);
}
}

View file

@ -1,4 +1,6 @@
import { Body, Controller, Post, Patch, UseGuards } from '@nestjs/common';
import { Body, Controller, Post, Patch, UseGuards, UseInterceptors, Req, UploadedFile } from '@nestjs/common';
import { Express } from 'express';
import { FileInterceptor } from '@nestjs/platform-express';
import { JwtAuthGuard } from 'src/modules/auth/jwt-auth.guard';
import { PasswordRevalidateGuard } from 'src/modules/auth/password-revalidate.guard';
import { UsersService } from 'src/services/users.service';
@ -37,6 +39,13 @@ export class UsersController {
};
}
@Post('avatar')
@UseGuards(JwtAuthGuard)
@UseInterceptors(FileInterceptor('file'))
async addAvatar(@Req() req, @UploadedFile() file: Express.Multer.File) {
return this.usersService.addAvatar(req.user.id, file.buffer, file.originalname);
}
@UseGuards(JwtAuthGuard, PasswordRevalidateGuard)
@Patch('change_password')
async changePassword(@User() user, @Body('newPassword') newPassword) {

View file

@ -0,0 +1,9 @@
import { IsNotEmpty } from 'class-validator';
export class CreateFileDto {
@IsNotEmpty()
data: Uint8Array | Buffer | string;
@IsNotEmpty()
filename: string;
}

View file

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateFileDto } from './create-file.dto';
export class UpdateFileDto extends PartialType(CreateFileDto) {}

View file

@ -0,0 +1,15 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity({ name: 'files' })
export class File {
@PrimaryGeneratedColumn()
public id: string;
@Column()
filename: string;
@Column({
type: 'bytea',
})
data: Uint8Array | Buffer | string;
}

View file

@ -10,12 +10,17 @@ import {
BaseEntity,
ManyToMany,
JoinTable,
OneToOne,
JoinColumn,
ManyToOne,
} from 'typeorm';
import { App } from './app.entity';
import { GroupPermission } from './group_permission.entity';
const bcrypt = require('bcrypt');
import { OrganizationUser } from './organization_user.entity';
import { UserGroupPermission } from './user_group_permission.entity';
import { File } from './file.entity';
import { Organization } from './organization.entity';
@Entity({ name: 'users' })
export class User extends BaseEntity {
@ -39,6 +44,9 @@ export class User extends BaseEntity {
@Column()
email: string;
@Column({ name: 'avatar_id', nullable: true, default: null })
avatarId?: string;
@Column({ name: 'invitation_token' })
invitationToken: string;
@ -63,6 +71,16 @@ export class User extends BaseEntity {
@OneToMany(() => OrganizationUser, (organizationUser) => organizationUser.user, { eager: true })
organizationUsers: OrganizationUser[];
@ManyToOne(() => Organization, (organization) => organization.id)
@JoinColumn({ name: 'organization_id' })
organization: Organization;
@JoinColumn({ name: 'avatar_id' })
@OneToOne(() => File, {
nullable: true,
})
avatar?: File;
@ManyToMany(() => GroupPermission)
@JoinTable({
name: 'user_group_permissions',

View file

@ -33,7 +33,7 @@ async function bootstrap() {
useDefaults: true,
directives: {
upgradeInsecureRequests: null,
'img-src': ['*', 'data:'],
'img-src': ['*', 'data:', 'blob:'],
'script-src': [
'maps.googleapis.com',
'apis.google.com',

View file

@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { App } from '../../entities/app.entity';
import { File } from '../../entities/file.entity';
import { AppsController } from '../../controllers/apps.controller';
import { AppsService } from '../../services/apps.service';
import { AppVersion } from '../../../src/entities/app_version.entity';
@ -13,6 +14,7 @@ import { OrganizationUser } from 'src/entities/organization_user.entity';
import { UsersService } from '@services/users.service';
import { User } from 'src/entities/user.entity';
import { Organization } from 'src/entities/organization.entity';
import { FilesService } from '@services/files.service';
import { FoldersService } from '@services/folders.service';
import { Folder } from 'src/entities/folder.entity';
import { FolderApp } from 'src/entities/folder_app.entity';
@ -43,6 +45,7 @@ import { Credential } from 'src/entities/credential.entity';
AppGroupPermission,
UserGroupPermission,
Credential,
File,
]),
CaslModule,
],
@ -55,6 +58,7 @@ import { Credential } from 'src/entities/credential.entity';
DataSourcesService,
CredentialsService,
EncryptionService,
FilesService,
],
controllers: [AppsController, AppUsersController],
})

View file

@ -17,6 +17,8 @@ import { OauthService, GoogleOAuthService, GitOAuthService } from '@ee/services/
import { OauthController } from '@ee/controllers/oauth.controller';
import { GroupPermission } from 'src/entities/group_permission.entity';
import { App } from 'src/entities/app.entity';
import { File } from 'src/entities/file.entity';
import { FilesService } from '@services/files.service';
import { SSOConfigs } from 'src/entities/sso_config.entity';
import { GroupPermissionsService } from '@services/group_permissions.service';
import { AppGroupPermission } from 'src/entities/app_group_permission.entity';
@ -29,6 +31,7 @@ import { EncryptionService } from '@services/encryption.service';
PassportModule,
TypeOrmModule.forFeature([
User,
File,
Organization,
OrganizationUser,
GroupPermission,
@ -59,6 +62,7 @@ import { EncryptionService } from '@services/encryption.service';
OauthService,
GoogleOAuthService,
GitOAuthService,
FilesService,
GroupPermissionsService,
EncryptionService,
],

View file

@ -4,6 +4,7 @@ import { EmailService } from '@services/email.service';
import { OrganizationUsersService } from '@services/organization_users.service';
import { UsersService } from '@services/users.service';
import { App } from 'src/entities/app.entity';
import { File } from 'src/entities/file.entity';
import { Organization } from 'src/entities/organization.entity';
import { OrganizationUser } from 'src/entities/organization_user.entity';
import { User } from 'src/entities/user.entity';
@ -12,12 +13,14 @@ import { ThreadsAbilityFactory } from './abilities/threads-ability.factory';
import { CommentsAbilityFactory } from './abilities/comments-ability.factory';
import { CaslAbilityFactory } from './casl-ability.factory';
import { FoldersAbilityFactory } from './abilities/folders-ability.factory';
import { FilesService } from '@services/files.service';
@Module({
imports: [TypeOrmModule.forFeature([User, Organization, OrganizationUser, App])],
imports: [TypeOrmModule.forFeature([User, File, Organization, OrganizationUser, App])],
providers: [
CaslAbilityFactory,
OrganizationUsersService,
FilesService,
UsersService,
EmailService,
AppsAbilityFactory,

View file

@ -8,6 +8,7 @@ import { EncryptionService } from '../../../src/services/encryption.service';
import { Credential } from '../../../src/entities/credential.entity';
import { DataSourcesService } from '../../../src/services/data_sources.service';
import { DataSource } from '../../../src/entities/data_source.entity';
import { File } from 'src/entities/file.entity';
import { CaslModule } from '../casl/casl.module';
import { AppsService } from '@services/apps.service';
import { App } from 'src/entities/app.entity';
@ -21,11 +22,13 @@ import { User } from 'src/entities/user.entity';
import { OrganizationUser } from 'src/entities/organization_user.entity';
import { Organization } from 'src/entities/organization.entity';
import { AppImportExportService } from '@services/app_import_export.service';
import { FilesService } from '@services/files.service';
@Module({
imports: [
TypeOrmModule.forFeature([
App,
File,
AppVersion,
AppUser,
DataQuery,
@ -48,6 +51,7 @@ import { AppImportExportService } from '@services/app_import_export.service';
AppsService,
UsersService,
AppImportExportService,
FilesService,
],
controllers: [DataQueriesController],
})

View file

@ -8,6 +8,7 @@ import { Credential } from '../../../src/entities/credential.entity';
import { EncryptionService } from '../../../src/services/encryption.service';
import { AppsService } from '@services/apps.service';
import { App } from 'src/entities/app.entity';
import { File } from 'src/entities/file.entity';
import { AppVersion } from 'src/entities/app_version.entity';
import { AppUser } from 'src/entities/app_user.entity';
import { CaslModule } from '../casl/casl.module';
@ -21,6 +22,7 @@ import { User } from 'src/entities/user.entity';
import { OrganizationUser } from 'src/entities/organization_user.entity';
import { Organization } from 'src/entities/organization.entity';
import { AppImportExportService } from '@services/app_import_export.service';
import { FilesService } from '@services/files.service';
@Module({
imports: [
@ -29,6 +31,7 @@ import { AppImportExportService } from '@services/app_import_export.service';
DataQuery,
Credential,
App,
File,
AppVersion,
AppUser,
FolderApp,
@ -48,6 +51,7 @@ import { AppImportExportService } from '@services/app_import_export.service';
DataQueriesService,
UsersService,
AppImportExportService,
FilesService,
],
controllers: [DataSourcesController],
})

View file

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { File } from 'src/entities/file.entity';
import { FilesController } from '../../controllers/files.controller';
import { FilesService } from '../../services/files.service';
@Module({
imports: [TypeOrmModule.forFeature([File])],
controllers: [FilesController],
providers: [FilesService],
})
export class FilesModule {}

View file

@ -5,7 +5,9 @@ import { Folder } from '../../entities/folder.entity';
import { FoldersController } from '../../controllers/folders.controller';
import { FoldersService } from '../../services/folders.service';
import { App } from 'src/entities/app.entity';
import { File } from 'src/entities/file.entity';
import { UsersService } from '@services/users.service';
import { FilesService } from '@services/files.service';
import { User } from 'src/entities/user.entity';
import { OrganizationUser } from 'src/entities/organization_user.entity';
import { Organization } from 'src/entities/organization.entity';
@ -13,7 +15,7 @@ import { CaslModule } from '../casl/casl.module';
@Module({
controllers: [FoldersController],
imports: [TypeOrmModule.forFeature([App, Folder, FolderApp, User, OrganizationUser, Organization]), CaslModule],
providers: [FoldersService, UsersService],
imports: [TypeOrmModule.forFeature([App, File, Folder, FolderApp, User, OrganizationUser, Organization]), CaslModule],
providers: [FilesService, FoldersService, UsersService],
})
export class FoldersModule {}

View file

@ -11,6 +11,8 @@ import { User } from 'src/entities/user.entity';
import { OrganizationUser } from 'src/entities/organization_user.entity';
import { Organization } from 'src/entities/organization.entity';
import { App } from 'src/entities/app.entity';
import { File } from 'src/entities/file.entity';
import { FilesService } from '@services/files.service';
@Module({
controllers: [GroupPermissionsController],
@ -23,9 +25,10 @@ import { App } from 'src/entities/app.entity';
OrganizationUser,
Organization,
App,
File,
]),
CaslModule,
],
providers: [GroupPermissionsService, UsersService],
providers: [GroupPermissionsService, FilesService, UsersService],
})
export class GroupPermissionsModule {}

View file

@ -10,8 +10,10 @@ import { OrganizationUsersController } from '@controllers/organization_users.con
import { UsersService } from 'src/services/users.service';
import { CaslModule } from '../casl/casl.module';
import { EmailService } from '@services/email.service';
import { FilesService } from '@services/files.service';
import { GroupPermission } from 'src/entities/group_permission.entity';
import { App } from 'src/entities/app.entity';
import { File } from 'src/entities/file.entity';
import { SSOConfigs } from 'src/entities/sso_config.entity';
import { AuthService } from '@services/auth.service';
import { JwtModule } from '@nestjs/jwt';
@ -27,6 +29,7 @@ import { EncryptionService } from '@services/encryption.service';
Organization,
OrganizationUser,
User,
File,
GroupPermission,
App,
SSOConfigs,
@ -51,6 +54,7 @@ import { EncryptionService } from '@services/encryption.service';
OrganizationUsersService,
UsersService,
EmailService,
FilesService,
AuthService,
GroupPermissionsService,
EncryptionService,

View file

@ -4,13 +4,15 @@ import { UsersService } from '../../services/users.service';
import { OrganizationUser } from '../../entities/organization_user.entity';
import { Organization } from '../../entities/organization.entity';
import { User } from '../../entities/user.entity';
import { File } from '../../entities/file.entity';
import { UsersController } from 'src/controllers/users.controller';
import { OrganizationsModule } from '../organizations/organizations.module';
import { App } from 'src/entities/app.entity';
import { FilesService } from '@services/files.service';
@Module({
imports: [OrganizationsModule, TypeOrmModule.forFeature([User, Organization, OrganizationUser, App])],
providers: [UsersService],
imports: [OrganizationsModule, TypeOrmModule.forFeature([User, File, Organization, OrganizationUser, App])],
providers: [UsersService, FilesService],
controllers: [UsersController],
})
export class UsersModule {}

View file

@ -107,6 +107,7 @@ export class AuthService {
email: user.email,
first_name: user.firstName,
last_name: user.lastName,
avatar_id: user.avatarId,
organizationId: user.organizationId,
organization: organization.name,
admin: await this.usersService.hasGroup(user, 'admin'),

View file

@ -0,0 +1,51 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { QueryRunner, Repository } from 'typeorm';
import { CreateFileDto } from '../dto/create-file.dto';
import { UpdateFileDto } from '../dto/update-file.dto';
import { File } from '../entities/file.entity';
@Injectable()
export class FilesService {
constructor(
@InjectRepository(File)
private fileRepository: Repository<File>
) {}
async create(createFileDto: CreateFileDto, queryRunner: QueryRunner) {
const newFile = queryRunner.manager.create(File, {
filename: createFileDto.filename,
data: createFileDto.data,
});
try {
await queryRunner.manager.save(File, newFile);
} catch (error) {
console.log(error);
}
return newFile;
}
findAll() {
return `This action returns all files`;
}
async findOne(id: string) {
const file = await this.fileRepository.findOne(id);
if (!file) {
throw new NotFoundException();
}
return file;
}
update(id: string, updateFileDto: UpdateFileDto) {
return `This action updates a #${id} file`;
}
async remove(id: string, queryRunner: QueryRunner) {
const deleteResponse = await queryRunner.manager.delete(File, id);
if (!deleteResponse?.affected) {
throw new NotFoundException();
}
return deleteResponse;
}
}

View file

@ -1,9 +1,10 @@
import { Injectable } from '@nestjs/common';
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from '../entities/user.entity';
import { FilesService } from '../services/files.service';
import { Organization } from 'src/entities/organization.entity';
import { App } from 'src/entities/app.entity';
import { createQueryBuilder, EntityManager, getManager, getRepository, In, Repository } from 'typeorm';
import { Connection, createQueryBuilder, EntityManager, getManager, getRepository, In, Repository } from 'typeorm';
import { OrganizationUser } from '../entities/organization_user.entity';
import { AppGroupPermission } from 'src/entities/app_group_permission.entity';
import { UserGroupPermission } from 'src/entities/user_group_permission.entity';
@ -11,12 +12,15 @@ import { GroupPermission } from 'src/entities/group_permission.entity';
import { BadRequestException } from '@nestjs/common';
import { cleanObject } from 'src/helpers/utils.helper';
import { CreateUserDto } from '@dto/user.dto';
import { CreateFileDto } from '@dto/create-file.dto';
const uuid = require('uuid');
const bcrypt = require('bcrypt');
@Injectable()
export class UsersService {
constructor(
private readonly filesService: FilesService,
private connection: Connection,
@InjectRepository(User)
private usersRepository: Repository<User>,
@InjectRepository(OrganizationUser)
@ -422,6 +426,39 @@ export class UsersService {
return !!app && app.organizationId === user.organizationId;
}
async addAvatar(userId: number, imageBuffer: Buffer, filename: string) {
const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const user = await queryRunner.manager.findOne(User, userId);
const currentAvatarId = user.avatarId;
const createFileDto = new CreateFileDto();
createFileDto.filename = filename;
createFileDto.data = imageBuffer;
const avatar = await this.filesService.create(createFileDto, queryRunner);
await queryRunner.manager.update(User, userId, {
avatarId: avatar.id,
});
if (currentAvatarId) {
await this.filesService.remove(currentAvatarId, queryRunner);
}
await queryRunner.commitTransaction();
return avatar;
} catch (error) {
await queryRunner.rollbackTransaction();
throw new InternalServerErrorException(error);
} finally {
await queryRunner.release();
}
}
canAnyGroupPerformAction(action: string, permissions: AppGroupPermission[] | GroupPermission[]): boolean {
return permissions.some((p) => p[action]);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -0,0 +1,33 @@
import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
import { authHeaderForUser, createFile, clearDB, createUser, createNestAppInstance } from '../test.helper';
describe('files controller', () => {
let app: INestApplication;
beforeEach(async () => {
await clearDB();
});
beforeAll(async () => {
app = await createNestAppInstance();
});
it('should not allow un-authenticated users to fetch a file', async () => {
await request(app.getHttpServer()).get('/api/files/2540333b-f6fe-42b7-857c-736f24f9b644').expect(401);
});
it('should allow only authenticated users to fetch a file', async () => {
const userData = await createUser(app, { email: 'admin@tooljet.io' });
const { user } = userData;
const file = await createFile(app);
const response = await request(app.getHttpServer())
.get(`/api/files/${file.id}`)
.set('Authorization', authHeaderForUser(user));
expect(response.statusCode).toBe(200);
});
});

View file

@ -5,6 +5,7 @@ import { getManager } from 'typeorm';
import { User } from 'src/entities/user.entity';
import { v4 as uuidv4 } from 'uuid';
import { OrganizationUser } from 'src/entities/organization_user.entity';
const path = require('path');
describe('users controller', () => {
let app: INestApplication;
@ -346,6 +347,22 @@ describe('users controller', () => {
});
});
describe('POST /api/users/avatar', () => {
it('should allow users to add a avatar', async () => {
const userData = await createUser(app, { email: 'admin@tooljet.io' });
const { user } = userData;
const filePath = path.join(__dirname, '../__mocks__/avatar.png');
const response = await request(app.getHttpServer())
.post('/api/users/avatar')
.set('Authorization', authHeaderForUser(user))
.attach('file', filePath);
expect(response.statusCode).toBe(201);
});
});
afterAll(async () => {
await app.close();
});

View file

@ -6,6 +6,7 @@ import { OrganizationUser } from 'src/entities/organization_user.entity';
import { Organization } from 'src/entities/organization.entity';
import { User } from 'src/entities/user.entity';
import { App } from 'src/entities/app.entity';
import { File } from 'src/entities/file.entity';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { AppModule } from 'src/app.module';
@ -25,6 +26,7 @@ import { AppsModule } from 'src/modules/apps/apps.module';
import { LibraryAppCreationService } from '@services/library_app_creation.service';
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { v4 as uuidv4 } from 'uuid';
import { CreateFileDto } from '@dto/create-file.dto';
export async function createNestAppInstance(): Promise<INestApplication> {
let app: INestApplication;
@ -443,6 +445,15 @@ export async function createDataQuery(nestApp, { application, kind, dataSource,
);
}
export async function createFile(nestApp: any) {
let fileRepository: Repository<File>;
fileRepository = nestApp.get('FileRepository');
const createFileDto = new CreateFileDto();
createFileDto.filename = 'testfile';
createFileDto.data = Buffer.from([1, 2, 3, 4]);
return await fileRepository.save(fileRepository.create(createFileDto));
}
export async function createThread(_nestApp, { appId, x, y, userId, organizationId, appVersionsId }: any) {
const threadRepository = new ThreadRepository();