user avatars

This commit is contained in:
James Shiffer 2020-07-04 04:40:49 -07:00
parent e3f6fe0d2e
commit 060e7ae1e6
11 changed files with 187 additions and 10 deletions

View File

@ -9,7 +9,8 @@ const UserSchema = new Schema({
username: { type: String, required: true, index: { unique: true } },
password: { type: String, required: true },
realname: { type: String, required: true },
author: { type: Boolean, default: false }
author: { type: Boolean, default: false },
avatar: { type: String, default: 'default.jpg' }
});

View File

@ -6,14 +6,14 @@ export async function get(req, res, next) {
const { slug } = req.params;
const article = await Article.findOne({ slug }).populate({
path: 'author',
select: 'realname'
select: 'realname avatar'
}).populate({
path: 'category'
});
if (article) {
article.set({ views: article.views + 1 });
article.save();
await article.save();
res.writeHead(200, {
'Content-Type': 'application/json'
});

View File

@ -82,6 +82,10 @@
margin: 0 0 0.5em 0;
}
.content :global(img) {
max-width: 100%;
}
@media (min-width: 800px) {
.content {
width: 75vw;
@ -154,7 +158,7 @@
<div class="article-meta">
<h1 class="article-title">{article.title}</h1>
<blockquote>
<p>Author: <strong>{article.author.realname}</strong></p>
<p>Author: <img class="avatar" alt={article.author.realname} src={`/u/${article.author.avatar || 'default.jpg'}`}> <strong>{article.author.realname}</strong></p>
<p>Category: <strong><a href={`/c/${article.category.slug}`}>{article.category.name}</a></strong></p>
<p>Published: <strong>{new Date(article.created_at).toLocaleString()}</strong></p>
<p>Views: <strong>{article.views}</strong></p>
@ -168,6 +172,9 @@
<div class="comments">
{#each comments as comment}
<div class="comment">
{#if comment.author_user}
<img class="avatar" alt={comment.author_user.realname} src={`/u/${comment.author_user.avatar || 'default.jpg'}`}>
{/if}
<p class="comment-meta">
{#if comment.author_user}
<span class="comment-username">{comment.author_user.realname}</span> <span class="comment-verified">(verified)</span> - {new Date(comment.created_at).toLocaleString()}

View File

@ -4,7 +4,7 @@ export async function get(req, res) {
const { slug } = req.params;
const article = await Article.findOne({ slug }).populate({
path: 'comments.author_user',
select: 'realname'
select: 'realname avatar'
});
if (article) {
res.writeHead(200, {
@ -25,7 +25,7 @@ export async function post(req, res) {
const { slug } = req.params;
let article = await Article.findOne({ slug }).populate({
path: 'comments.author_user',
select: 'realname'
select: 'realname avatar'
});
if (article) {
@ -63,7 +63,7 @@ export async function post(req, res) {
} else {
article.comments.push({ author, content });
}
article.save();
await article.save();
res.writeHead(200, {
'Content-Type': 'application/json'
});

View File

@ -24,6 +24,8 @@
<p><a href="/cms/create">Publish a new article</a></p>
<p><strike><a href="/cms/update">Edit an existing article</a></strike> Coming soon!</p>
<p><a href="/cms/delete">Delete an article</a></p>
<h1>Account Settings</h1>
<p><a href="/me/avatar">Change your avatar</a></p>
{:else}
<p>Welcome to your account. Contact the webmaster if your account needs publisher privileges.</p>
{/if}

View File

@ -17,7 +17,7 @@
<div class="people">
<div>
<h3>Myles C. Linden</h3>
<p>Creative Operations Director</p>
<p>Director of Financial Growth and Prosperity</p>
<p>FemboyFinancial Holdings Co., Ltd. (USA LLC)</p>
<address>
1198 South 6th Street<br>

View File

@ -0,0 +1,70 @@
<script context="module">
export async function preload(page, session)
{
if (!session.user || !session.user.author) {
return this.redirect(302, '/cms');
}
}
</script>
<svelte:head>
<title>Change Avatar | HOWFEED.BIZ</title>
</svelte:head>
<script>
import { goto, stores } from '@sapper/app';
const { session } = stores();
let uploadForm;
async function upload()
{
let fd = new FormData(uploadForm);
const res = await fetch(`/me/avatar`, {
method: 'POST',
headers: {
'Accept': 'application/json'
},
body: fd
});
const json = await res.json();
if (res.status === 200) {
$session.user.avatar = json.filename;
} else {
alert(`Error ${res.status}: ${json.message}`);
}
}
async function del()
{
const res = await fetch(`/me/avatar`, {
method: 'DELETE',
headers: {
'Accept': 'application/json'
}
});
const json = await res.json();
if (res.status === 200) {
$session.user.avatar = json.filename;
} else {
alert(`Error ${res.status}: ${json.message}`);
}
}
</script>
<div class="content">
<a href="/cms">&lt; Back to Dashboard</a>
<h1>Change Avatar</h1>
<p>
<img class="avatar" alt={$session.user.realname} src={`/u/${$session.user.avatar || 'default.jpg'}`}>
← This is you, you ugly piece of shit. God, no wonder you're an incel.
</p>
<form bind:this={uploadForm} on:submit|preventDefault={upload}>
<p>
Change your photo so you at least have a chance:
<input type="file" name="upload">
<button type="submit">Upload</button>
</p>
</form>
<p>Or better yet, erase your wretched face from this site entirely: <button on:click={del}>Reset Avatar</button></p>
</div>

View File

@ -10,6 +10,7 @@ import { Strategy } from 'passport-local';
import sessionFileStore from 'session-file-store';
import { RateLimiterMemory } from 'rate-limiter-flexible';
import fileUpload from 'express-fileupload';
import fs from 'fs';
import helmet from 'helmet';
import crypto from 'crypto';
import Article from './models/article.js';
@ -411,6 +412,93 @@ express()
}
})
.post('/me/avatar',
async function(req, res, next) {
if (!req.user) {
res.writeHead(401, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
message: `You must be logged in to set an avatar.`
}));
return false;
}
try {
const { upload } = req.files;
if (!upload) {
res.writeHead(422, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
message: `You must supply a file.`
}));
return false;
}
if (!/^image\//.test(upload.mimetype)) {
res.writeHead(422, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
message: `Invalid MIME type for the uploaded image.`
}));
return false;
}
if (upload.truncated) {
res.writeHead(422, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
message: `Received truncated image file. Try again with a smaller file.`
}));
return false;
}
const ext = upload.name.match(/(\.[^.]+)$/)[0];
const filename = crypto.randomBytes(20).toString('hex') + ext;
const url = `/u/${filename}`;
await upload.mv('./static' + url);
const user = await User.findById(req.user._id);
req.user.avatar = user.avatar = filename;
await user.save();
res.writeHead(200, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({ filename }));
} catch (err) {
res.writeHead(500, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
message: `Failed to upload image: ${err}`
}));
}
}
)
.delete('/me/avatar',
async function(req, res, next) {
if (!req.user) {
res.writeHead(401, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
message: `You must be logged in to set an avatar.`
}));
return false;
}
const user = await User.findById(req.user._id);
const filename = 'default.jpg';
if (user.avatar !== filename) {
fs.unlinkSync(`./static/u/${user.avatar}`);
}
req.user.avatar = user.avatar = filename;
await user.save();
res.writeHead(200, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({ filename }));
}
)
.use(compression({ threshold: 0 }))
.use(sirv('./static', { dev }))
.use(sapper.middleware({

View File

@ -48,7 +48,7 @@ code {
font-size: 2rem !important;
}
figure.article-image img {
height: 12rem !important;
height: 24rem !important;
}
div.article-list > a {
margin: 2rem !important;
@ -83,7 +83,7 @@ figure.article-image {
margin: 0;
}
figure.article-image img {
height: 6rem;
height: 12rem;
width: auto;
object-fit: contain;
max-width: 100%;
@ -143,3 +143,9 @@ div.floaty {
padding-top: 5rem !important;
}
}
img.avatar {
height: 48px;
width: 48px;
object-fit: cover;
}

3
static/u/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*
!.gitignore
!default.jpg

BIN
static/u/default.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB