Upload First Image with Multer

This commit is contained in:
ADAMJR 2021-11-03 00:22:38 +00:00
parent 6f7c22f618
commit 7a1027932d
8 changed files with 1509 additions and 40 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

1428
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -25,9 +25,12 @@
"faker": "^5.4.0",
"got": "^11.7.0",
"helmet": "^4.4.1",
"image-hash": "^4.0.1",
"imghash": "^0.0.9",
"jsonwebtoken": "^8.5.1",
"mongoose": "^5.10.7",
"mongoose-unique-validator": "^2.0.3",
"multer": "^1.4.3",
"node-fetch": "^2.6.1",
"nodemailer": "^6.5.0",
"nodemailer-pug-engine": "^2.0.0",
@ -59,6 +62,7 @@
"@types/jsonwebtoken": "^8.5.0",
"@types/mocha": "^8.2.3",
"@types/mongoose": "^5.7.36",
"@types/multer": "^1.4.7",
"@types/node": "^14.11.2",
"@types/node-fetch": "^2.5.7",
"@types/nodemailer": "^6.4.1",

View File

@ -2,9 +2,6 @@ import { Document, model, Schema } from 'mongoose';
import patterns from '../../types/patterns';
import { createdAtToDate, useId } from '../../utils/utils';
import { generateSnowflake } from '../snowflake-entity';
import AES from 'crypto-js/aes';
import { readFileSync } from 'fs';
import { resolve } from 'path';
export interface MessageDocument extends Document, Entity.Message {
_id: string | never;
@ -17,6 +14,9 @@ export const Message = model<MessageDocument>('message', new Schema({
type: String,
default: generateSnowflake,
},
attachments: {
type: [Object],
},
authorId: {
type: String,
required: [true, 'Author ID is required'],

View File

@ -5,17 +5,45 @@ import passport from 'passport';
import cors from 'cors';
import { User } from '../../data/models/user';
import rateLimiter from '../modules/rate-limiter';
import multer from 'multer';
import { generateSnowflake } from '../../data/snowflake-entity';
import { imageHash } from 'image-hash';
import path, { extname, resolve } from 'path';
export default (app: Application) => {
function setupMulter(app: Application) {
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, resolve('./assets/upload')),
filename: (req, file, cb) => {
// imageHash({ ext: file.mimetype, data: file.buffer }, 8, true, (error, data) => {
// if (error) return log.error(error);
// log.debug(data);
// });
console.log(file);
cb(null, Date.now() + extname(file.originalname));
},
});
const upload = multer({ storage });
// TODO: validate is logged in, etc.
app.post('/v2/upload', upload.single('file'), (req, res) => {
res.status(201).json({ message: 'Files uploaded' });
});
}
function setupPassport(app: Application) {
passport.use(new LocalStrategy(
{ usernameField: 'email' },
(User as any).authenticate(),
));
passport.serializeUser((User as any).serializeUser());
passport.deserializeUser((User as any).deserializeUser());
}
export default (app: Application) => {
app.use(cors());
app.use(bodyParser.json());
app.use(passport.initialize());
app.use(rateLimiter);
setupPassport(app);
setupMulter(app);
}

View File

@ -1,10 +1,12 @@
import { faUpload } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { useDispatch, useSelector, useStore } from 'react-redux';
import { Link } from 'react-router-dom';
import TextareaAutosize from 'react-textarea-autosize';
import usePerms from '../../hooks/use-perms';
import { createMessage, updateMessage } from '../../store/messages';
import { createMessage, updateMessage, uploadFileAsMessage } from '../../store/messages';
import { getTypersInChannel, startTyping } from '../../store/typing';
import { actions as ui } from '../../store/ui';
import { getUser } from '../../store/users';
@ -64,8 +66,7 @@ const MessageBox: React.FunctionComponent<MessageBoxProps> = (props) => {
const handleEscape = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key !== 'Escape') return;
if (props.editingMessageId)
esc();
if (props.editingMessageId) esc();
}
const user = (userId: string) => getUser(userId)(store.getState());
@ -85,23 +86,49 @@ const MessageBox: React.FunctionComponent<MessageBoxProps> = (props) => {
if (!canSend) return `Insufficient perms.`;
if (!props.editingMessageId) return `Message #${channel.name}`;
}
const MessageBoxLeftSide = () => {
const onChange: any = (e: Event) => {
const input = e.target as HTMLInputElement;
console.log(input.files);
dispatch(uploadFileAsMessage(input.files![0]));
}
return (!props.editingMessageId) ? (
<div className="px-4">
<div className="relative">
{/* TODO: add multiple file support */}
<input
className="absolute opacity-0 w-full"
type="file"
name="file"
accept="image/*"
onChange={onChange} />
<FontAwesomeIcon icon={faUpload} />
</div>
</div>
) : null;
}
return (
<div className={`${props.editingMessageId ? 'mt-2' : 'px-4'}`}>
<TextareaAutosize
id="messageBox"
onChange={e => setContent(e.target.value)}
onKeyDown={onKeyDown}
value={content}
rows={1}
placeholder={getPlaceholder()}
className={classNames(
'resize-none normal appearance-none rounded-lg leading-tight',
'focus:outline-none w-full right-5 left-5 max-h-96 py-3 px-4',
{ 'cursor-not-allowed': !canSend },
)}
disabled={!canSend}
autoFocus />
<div className="rounded-lg bg-bg-secondary flex items-center">
<MessageBoxLeftSide />
<TextareaAutosize
id="messageBox"
onChange={e => setContent(e.target.value)}
onKeyDown={onKeyDown}
value={content}
rows={1}
className={classNames(
'resize-none normal rounded-lg appearance-none leading-tight',
'focus:outline-none w-full right-5 left-5 max-h-96 py-3 px-4',
{ 'cursor-not-allowed': !canSend },
)}
placeholder={getPlaceholder()}
disabled={!canSend}
autoFocus />
</div>
{(props.editingMessageId)
? <span className="text-xs py-2">
escape to <Link to="#" onClick={esc}>cancel</Link>

View File

@ -55,6 +55,19 @@ export const createMessage = (channelId: string, payload: Partial<Entity.Message
}));
}
// each file is uploaded individually as a separate API call
export const uploadFileAsMessage = (file: File) => (dispatch) => {
const formData = new FormData();
formData.append('file', file, file.name);
dispatch(api.restCallBegan({
method: 'post',
url: '/upload',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' },
}));
}
export const updateMessage = (id: string, payload: Partial<Entity.Message>) => (dispatch) => {
dispatch(api.wsCallBegan({
event: 'MESSAGE_UPDATE',

7
types/entity.d.ts vendored
View File

@ -43,6 +43,7 @@ declare namespace Entity {
}
export interface Message {
id: string;
attachments: MessageTypes.Attachment[];
authorId: string;
channelId: string;
content: string;
@ -110,6 +111,12 @@ declare namespace InviteTypes {
}
declare namespace MessageTypes {
export interface Attachment {
id: string;
name: string;
size: number;
url: string;
}
export interface Embed {
description: string;
imageURL: string;