Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(user): add profile images #1041

Merged
merged 7 commits into from
Dec 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules/
junit.xml
coverage/
.seed-executed
tmp_upload/
32 changes: 32 additions & 0 deletions docker/core-static/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
worker_processes 4;
pid /run/nginx.pid;

events {
worker_connections 2048;
multi_accept on;
use epoll;
}

http {
server_tokens off;
sendfile off;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 15;
types_hash_max_size 2048;
client_max_body_size 20M;
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
gzip_disable "msie6";
include /etc/nginx/sites-available/*;
open_file_cache max=100;
charset UTF-8;

# setting real_ip from Docker network
set_real_ip_from 172.18.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
}
22 changes: 22 additions & 0 deletions docker/core-static/sites/default
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
server {
listen 80;
server_name oms-frontend;
root "/usr/app/media";

charset utf-8;

location /healthcheck {
alias /usr/app/status.json;
add_header "Content-Type" "application/json";
}

location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }

access_log /dev/stdout;
error_log stderr;

sendfile off;

client_max_body_size 100m;
}
3 changes: 3 additions & 0 deletions docker/core-static/status.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"success": true
LeonVreling marked this conversation as resolved.
Show resolved Hide resolved
}
24 changes: 24 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,30 @@ services:
- "traefik.auth.frontend.priority=120"
- "traefik.enable=true"

core-static:
restart: on-failure
image: aegee/nginx-static:latest
volumes:
- core-media:/usr/app/media:ro
- ./${PATH_CORE}/core-static/status.json:/usr/app/status.json:ro
- ./${PATH_CORE}/core-static/nginx.conf:/etc/nginx/nginx.conf:ro
- ./${PATH_CORE}/core-static/sites/default:/etc/nginx/sites-available/default:ro
- shared:/usr/app/shared:ro
expose:
- "80"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/healthcheck"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
labels:
- "traefik.backend=core-static"
- "traefik.port=80"
- "traefik.frontend.rule=PathPrefix:/media/core;PathPrefixStrip:/media/core"
- "traefik.frontend.priority=110"
- "traefik.enable=true"

volumes:
postgres-core:
driver: local
Expand Down
107 changes: 107 additions & 0 deletions lib/imageserv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
const path = require('path');
const util = require('util');
const fs = require('fs');
const multer = require('multer');
const readChunk = require('read-chunk');
const FileType = require('file-type');

const errors = require('./errors');
const log = require('./logger');
const config = require('../config');

const uploadFolderName = `${config.media_dir}/headimages`;
const allowedExtensions = ['.png', '.jpg', '.jpeg'];

const storage = multer.diskStorage({ // multers disk storage settings
destination(req, file, cb) {
cb(null, uploadFolderName);
},

// Filename is 4 character random string and the current datetime to avoid collisions
filename(req, file, cb) {
const prefix = Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 4);
const date = (new Date()).getTime();
const extension = path.extname(file.originalname);

cb(null, `${prefix}-${date}${extension}`);
},
});
const upload = multer({
storage,
fileFilter(req, file, cb) {
const extension = path.extname(file.originalname).toLowerCase();
if (!allowedExtensions.includes(extension)) {
const allowed = allowedExtensions.map((e) => `'${e}'`).join(', ');
return cb(new Error(`Allowed extensions: ${allowed}, but '${extension}' was passed.`));
}

return cb(null, true);
},
}).single('head_image');

const uploadAsync = util.promisify(upload);

exports.uploadImage = async (req, res) => {
const oldimg = req.user.image;

// If upload folder doesn't exists, create it.
if (!fs.existsSync(uploadFolderName)) {
await fs.promises.mkdir(uploadFolderName, { recursive: true });
}

try {
await uploadAsync(req, res);
} catch (err) {
log.error({ err }, 'Could not store image');
return errors.makeValidationError(res, err);
}

// If the head_image field is missing, do nothing.
if (!req.file) {
return errors.makeValidationError(res, 'No head_image is specified.');
}

// If the file's content is malformed, don't save it.
const buffer = readChunk.sync(req.file.path, 0, 4100);
const type = await FileType.fromBuffer(buffer);

const originalExtension = path.extname(req.file.originalname).toLowerCase();
const determinedExtension = (type && type.ext ? `.${type.ext}` : 'unknown');

if (originalExtension !== determinedExtension || !allowedExtensions.includes(determinedExtension)) {
return errors.makeValidationError(res, 'Malformed file content.');
}

await req.user.update({
image: req.file.filename
});

// Remove old file
if (oldimg) {
await fs.promises.unlink(path.join(uploadFolderName, oldimg));
}

return res.json({
success: true,
message: 'File uploaded successfully',
data: req.user.image,
});
};

exports.removeImage = async (req, res) => {
if (!req.user.image) {
return errors.makeValidationError(res, 'No image is specified for the user.');
}

await fs.promises.unlink(path.join(uploadFolderName, req.user.image));

await req.user.update({
image: null
});

return res.json({
success: true,
message: 'File removed successfully',
data: req.user.image
});
};
3 changes: 3 additions & 0 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const log = require('./logger');
const config = require('../config');
const Bugsnag = require('./bugsnag');
const cron = require('./cron');
const imageserv = require('./imageserv');

const middlewares = require('../middlewares/generic');
const fetch = require('../middlewares/fetch');
Expand Down Expand Up @@ -102,6 +103,8 @@ MemberRouter.post('/listserv', members.subscribeListserv);
MemberRouter.get('/', members.getUser);
MemberRouter.put('/', members.updateUser);
MemberRouter.delete('/', members.deleteUser);
MemberRouter.post('/upload', imageserv.uploadImage);
MemberRouter.delete('/image', imageserv.removeImage);

// Everything related to a specific body. Auth only (except for body details).
BodiesRouter.use(middlewares.maybeAuthorize, fetch.fetchBody);
Expand Down
11 changes: 11 additions & 0 deletions migrations/20241207102442-add-user-image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
up: (queryInterface, Sequelize) => queryInterface.addColumn(
'users',
'image',
{
type: Sequelize.STRING,
allowNull: true
},
),
down: (queryInterface) => queryInterface.removeColumn('users', 'image')
};
4 changes: 4 additions & 0 deletions models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ const User = sequelize.define('user', {
allowNull: true,
defaultValue: ''
},
image: {
type: Sequelize.STRING,
allowNull: true
},
about_me: {
type: Sequelize.TEXT,
allowNull: true,
Expand Down
Loading
Loading