mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-24 05:05:20 -04:00
Merge branch 'backup' into 'main'
(feature)(profile) cards backup See merge request PronounsPage/PronounsPage!375
This commit is contained in:
commit
84a798e497
@ -173,7 +173,9 @@
|
||||
|
||||
<CircleMentions/>
|
||||
|
||||
<section>
|
||||
<CardsBackup v-if="!$user().bannedReason"/>
|
||||
|
||||
<section class="mt-5">
|
||||
<a href="#" class="badge bg-light text-dark border" @click.prevent="logout">
|
||||
<Icon v="sign-out"/>
|
||||
<T>user.logout</T>
|
||||
@ -406,7 +408,7 @@
|
||||
|
||||
this.$setToken(response.token);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
60
components/CardsBackup.vue
Normal file
60
components/CardsBackup.vue
Normal file
@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<section>
|
||||
<h3 class="h4"><T>profile.backup.header</T></h3>
|
||||
<ul class="list-inline">
|
||||
<li class="list-inline-item">
|
||||
<a :href="exportLink"
|
||||
:class="['btn btn-outline-primary', exportStarted ? 'disabled' : '']" :aria-disabled="exportStarted"
|
||||
@click="startExport">
|
||||
<Icon v="cloud-download"/>
|
||||
<T>profile.backup.export.action</T>
|
||||
</a>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<FileUploader :url="importLink" mime="application/gzip"
|
||||
classes="btn btn-outline-primary"
|
||||
@uploaded="importDone">
|
||||
<Icon v="cloud-upload"/>
|
||||
<T>profile.backup.import.action</T>
|
||||
</FileUploader>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="alert alert-success" v-if="exportStarted">
|
||||
<Icon v="check-circle"/>
|
||||
<T>profile.backup.export.success</T>
|
||||
</div>
|
||||
<div class="alert alert-success" v-if="importSuccessful">
|
||||
<Icon v="check-circle"/>
|
||||
<T>profile.backup.import.success</T>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
exportStarted: false,
|
||||
importSuccessful: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
exportLink() {
|
||||
return `${process.env.BASE_URL}/api/profile/export`;
|
||||
},
|
||||
importLink() {
|
||||
return `${process.env.BASE_URL}/api/profile/import`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
startExport() {
|
||||
this.exportStarted = true;
|
||||
setTimeout(() => this.exportStarted = false, 5000);
|
||||
},
|
||||
importDone() {
|
||||
this.importSuccessful = true;
|
||||
setTimeout(() => window.location.reload(), 3000);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
98
components/FileUploader.vue
Normal file
98
components/FileUploader.vue
Normal file
@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div
|
||||
:class="['uploader-container', form ? 'form-control p-2' : classes, drag ? 'drag' : '']"
|
||||
@dragover="drag=true" @dragleave="drag=false"
|
||||
>
|
||||
<input type="file"
|
||||
:name="name + (multiple ? '[]' : '')"
|
||||
:multiple="multiple"
|
||||
:disabled="uploading"
|
||||
@change="filesChange($event.target.name, $event.target.files)"
|
||||
:accept="mime"/>
|
||||
<p v-if="errorMessage" class="text-danger mb-0">
|
||||
<Icon v="exclamation-circle"/>
|
||||
<T>{{errorMessage}}</T>
|
||||
</p>
|
||||
<p v-else-if="uploading" class="mb-0">
|
||||
<Spinner/>
|
||||
</p>
|
||||
<p v-else class="mb-0">
|
||||
<slot>
|
||||
<Icon v="upload"/>
|
||||
<T>images.upload.instructionShort</T>
|
||||
</slot>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
url: {required: true},
|
||||
multiple: {type: Boolean},
|
||||
mime: {'default': '*/*'},
|
||||
name: {'default': 'files'},
|
||||
form: {type: Boolean},
|
||||
classes: {'default': 'btn btn-outline-primary btn-sm'},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
uploading: false,
|
||||
drag: false,
|
||||
errorMessage: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async filesChange(fieldName, fileList) {
|
||||
if (!fileList.length) {
|
||||
return;
|
||||
}
|
||||
this.drag = false;
|
||||
const formData = new FormData();
|
||||
for (let file of fileList) {
|
||||
formData.append(fieldName, file, file.name);
|
||||
}
|
||||
await this.save(formData);
|
||||
},
|
||||
async save(formData) {
|
||||
this.uploading = true;
|
||||
this.errorMessage = '';
|
||||
try {
|
||||
const ids = await this.$axios.$post(this.url, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
this.$emit('uploaded', ids);
|
||||
} catch (e) {
|
||||
this.errorMessage = e?.response?.data?.error || 'error.invalidImage';
|
||||
}
|
||||
this.uploading = false;
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/style";
|
||||
|
||||
.uploader-container {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
&.form-control {
|
||||
&:hover, &.drag {
|
||||
background: lighten($primary, 50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
@ -1,26 +1,10 @@
|
||||
<template>
|
||||
<div
|
||||
:class="['uploader-container', form ? 'form-control p-2' : 'btn btn-outline-primary btn-sm', drag ? 'drag' : '']"
|
||||
@dragover="drag=true" @dragleave="drag=false"
|
||||
<FileUploader :url="'/images/upload?sizes=' + sizes" :multiple="multiple" mime="image/*" :name="name" :form="form"
|
||||
@uploaded="uploaded"
|
||||
>
|
||||
<input type="file"
|
||||
:name="name + (multiple ? '[]' : '')"
|
||||
:multiple="multiple"
|
||||
:disabled="uploading"
|
||||
@change="filesChange($event.target.name, $event.target.files)"
|
||||
accept="image/*">
|
||||
<p v-if="errorMessage" class="text-danger mb-0">
|
||||
<Icon v="exclamation-circle"/>
|
||||
<T>{{errorMessage}}</T>
|
||||
</p>
|
||||
<p v-else-if="uploading" class="mb-0">
|
||||
<Spinner/>
|
||||
</p>
|
||||
<p v-else class="mb-0">
|
||||
<Icon v="upload"/>
|
||||
<T>images.upload.instruction{{small ? 'Short' : ''}}</T>
|
||||
</p>
|
||||
</div>
|
||||
<Icon v="upload"/>
|
||||
<T>images.upload.instruction{{small ? 'Short' : ''}}</T>
|
||||
</FileUploader>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -32,64 +16,10 @@
|
||||
small: {type: Boolean},
|
||||
sizes: {'default': 'all'},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
uploading: false,
|
||||
drag: false,
|
||||
errorMessage: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async filesChange(fieldName, fileList) {
|
||||
if (!fileList.length) {
|
||||
return;
|
||||
}
|
||||
this.drag = false;
|
||||
const formData = new FormData();
|
||||
for (let file of fileList) {
|
||||
formData.append(fieldName, file, file.name);
|
||||
}
|
||||
await this.save(formData);
|
||||
},
|
||||
async save(formData) {
|
||||
this.uploading = true;
|
||||
this.errorMessage = '';
|
||||
try {
|
||||
const ids = await this.$axios.$post('/images/upload?sizes=' + this.sizes, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
this.$emit('uploaded', ids);
|
||||
} catch (e) {
|
||||
this.errorMessage = e?.response?.data?.error || 'error.invalidImage';
|
||||
}
|
||||
this.uploading = false;
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/style";
|
||||
|
||||
.uploader-container {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
&.form-control {
|
||||
&:hover, &.drag {
|
||||
background: lighten($primary, 50%);
|
||||
uploaded(ids) {
|
||||
this.$emit('uploaded', ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</script>
|
||||
|
@ -797,6 +797,16 @@ profile:
|
||||
local: 'Link to this language version'
|
||||
atAlternative: 'Use the /u/ notation'
|
||||
pronouns: 'Include pronouns'
|
||||
backup:
|
||||
header: 'Cards backup'
|
||||
export:
|
||||
action: 'Generate backup'
|
||||
success: 'A backup file is being generated, download should begin shortly'
|
||||
import:
|
||||
action: 'Restore backup'
|
||||
success: 'Your backup was successfully restored! The page will be refreshed now.'
|
||||
error:
|
||||
signature: 'Invalid signature, can''t verify file integrity'
|
||||
|
||||
share: 'Share'
|
||||
|
||||
|
@ -966,6 +966,16 @@ profile:
|
||||
local: 'Link to this language version'
|
||||
atAlternative: 'Use the /u/ notation'
|
||||
pronouns: 'Include pronouns'
|
||||
backup:
|
||||
header: 'Cards backup'
|
||||
export:
|
||||
action: 'Generate backup'
|
||||
success: 'A backup file is being generated, download should begin shortly'
|
||||
import:
|
||||
action: 'Restore backup'
|
||||
success: 'Your backup was successfully restored! The page will be refreshed now.'
|
||||
error:
|
||||
signature: 'Invalid signature, can''t verify file integrity'
|
||||
|
||||
share: 'Share'
|
||||
|
||||
|
@ -130,4 +130,10 @@ module.exports = [
|
||||
'calendar.onlyFirstDays',
|
||||
'calendar.start',
|
||||
'calendar.events.nonmonogamy_visibility_day',
|
||||
'profile.backup.header',
|
||||
'profile.backup.export.action',
|
||||
'profile.backup.export.success',
|
||||
'profile.backup.import.action',
|
||||
'profile.backup.import.success',
|
||||
'profile.backup.error.signature',
|
||||
];
|
||||
|
@ -1513,6 +1513,16 @@ profile:
|
||||
local: 'Podlinkuj do tej wersji językowej'
|
||||
atAlternative: 'Użyj notacji z /u/'
|
||||
pronouns: 'Zamieść zaimki bezpośrednio w linku'
|
||||
backup:
|
||||
header: 'Kopia zapasowa wizytówek'
|
||||
export:
|
||||
action: 'Ściągnij kopię'
|
||||
success: 'Plik kopii zapasowej jest generowany, zaraz zacznie się ściąganie pliku'
|
||||
import:
|
||||
action: 'Przywróć kopię'
|
||||
success: 'Kopia zapasowa została przywrócona! Strona zostanie teraz odświeżona.'
|
||||
error:
|
||||
signature: 'Nieprawidłowa sygnatura pliku, nie możemy potwierdzić jego autentyczności'
|
||||
|
||||
census:
|
||||
header: 'Spis'
|
||||
|
@ -13,6 +13,11 @@ import { colours, styles } from '../../src/styling';
|
||||
import {normaliseUrl} from "../../src/links";
|
||||
import allLocales from '../../locale/locales';
|
||||
import auditLog from '../audit';
|
||||
import crypto from '../../src/crypto';
|
||||
import awsConfig from '../aws';
|
||||
import S3 from 'aws-sdk/clients/s3';
|
||||
import zlib from 'zlib';
|
||||
import multer from "multer";
|
||||
|
||||
const escapeRegExp = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // https://stackoverflow.com/a/6969486/3297012
|
||||
const normalise = s => decodeURIComponent(s.trim().toLowerCase());
|
||||
@ -522,6 +527,63 @@ const cleanupOpinions = (opinions) => {
|
||||
return cleanOpinions;
|
||||
}
|
||||
|
||||
const saveProfile = async (req, locale, {
|
||||
opinions, names, pronouns, description, birthday, timezone, links, flags, customFlags, words, sensitive,
|
||||
teamName, footerName, footerAreas, credentials, credentialsLevel, credentialsName,
|
||||
}) => {
|
||||
// TODO just make it a transaction...
|
||||
const ids = (await req.db.all(SQL`SELECT * FROM profiles WHERE userId = ${req.user.id} AND locale = ${locale}`)).map(row => row.id);
|
||||
let profileId;
|
||||
if (ids.length) {
|
||||
profileId = ids[0];
|
||||
await req.db.get(SQL`UPDATE profiles
|
||||
SET
|
||||
opinions = ${JSON.stringify(opinions)},
|
||||
names = ${JSON.stringify(names)},
|
||||
pronouns = ${JSON.stringify(pronouns)},
|
||||
description = ${description},
|
||||
birthday = ${birthday},
|
||||
timezone = ${JSON.stringify(timezone)},
|
||||
links = ${JSON.stringify(links)},
|
||||
flags = ${JSON.stringify(flags)},
|
||||
customFlags = ${JSON.stringify(customFlags)},
|
||||
words = ${JSON.stringify(words)},
|
||||
sensitive = ${JSON.stringify(sensitive)},
|
||||
teamName = ${req.isGranted() ? teamName || null : ''},
|
||||
footerName = ${req.isGranted() ? footerName || null : ''},
|
||||
footerAreas = ${req.isGranted() ? footerAreas || null : ''},
|
||||
credentials = ${req.isGranted() ? credentials || null : null},
|
||||
credentialsLevel = ${req.isGranted() ? credentialsLevel || null : null},
|
||||
credentialsName = ${req.isGranted() ? credentialsName || null : null},
|
||||
card = NULL,
|
||||
cardDark = NULL
|
||||
WHERE id = ${profileId}
|
||||
`);
|
||||
} else {
|
||||
profileId = ulid();
|
||||
await req.db.get(SQL`INSERT INTO profiles (id, userId, locale, opinions, names, pronouns, description, birthday, timezone, links, flags, customFlags, words, sensitive, active, teamName, footerName, footerAreas)
|
||||
VALUES (${profileId}, ${req.user.id}, ${locale},
|
||||
${JSON.stringify(opinions)},
|
||||
${JSON.stringify(names)},
|
||||
${JSON.stringify(pronouns)},
|
||||
${description},
|
||||
${birthday},
|
||||
${JSON.stringify(timezone)},
|
||||
${JSON.stringify(links)},
|
||||
${JSON.stringify(flags)},
|
||||
${JSON.stringify(customFlags)},
|
||||
${JSON.stringify(words)},
|
||||
${JSON.stringify(sensitive)},
|
||||
1,
|
||||
${req.isGranted() ? teamName || null : ''},
|
||||
${req.isGranted() ? footerName || null : ''},
|
||||
${req.isGranted() ? footerAreas || null : ''}
|
||||
)`);
|
||||
}
|
||||
|
||||
return profileId;
|
||||
}
|
||||
|
||||
router.post('/profile/save', handleErrorAsync(async (req, res) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({error: 'Unauthorised'});
|
||||
@ -578,55 +640,17 @@ router.post('/profile/save', handleErrorAsync(async (req, res) => {
|
||||
loc: req.body.timezone.loc,
|
||||
} : null;
|
||||
|
||||
// TODO just make it a transaction...
|
||||
const ids = (await req.db.all(SQL`SELECT * FROM profiles WHERE userId = ${req.user.id} AND locale = ${global.config.locale}`)).map(row => row.id);
|
||||
let profileId;
|
||||
if (ids.length) {
|
||||
profileId = ids[0];
|
||||
await req.db.get(SQL`UPDATE profiles
|
||||
SET
|
||||
opinions = ${JSON.stringify(opinions)},
|
||||
names = ${JSON.stringify(names)},
|
||||
pronouns = ${JSON.stringify(pronouns)},
|
||||
description = ${description},
|
||||
birthday = ${birthday},
|
||||
timezone = ${JSON.stringify(timezone)},
|
||||
links = ${JSON.stringify(links)},
|
||||
flags = ${JSON.stringify(req.body.flags)},
|
||||
customFlags = ${JSON.stringify(customFlags)},
|
||||
words = ${JSON.stringify(words)},
|
||||
sensitive = ${JSON.stringify(sensitive)},
|
||||
teamName = ${req.isGranted() ? req.body.teamName || null : ''},
|
||||
footerName = ${req.isGranted() ? req.body.footerName || null : ''},
|
||||
footerAreas = ${req.isGranted() ? req.body.footerAreas.join(',') || null : ''},
|
||||
credentials = ${req.isGranted() ? req.body.credentials.join('|') || null : null},
|
||||
credentialsLevel = ${req.isGranted() ? req.body.credentialsLevel || null : null},
|
||||
credentialsName = ${req.isGranted() ? req.body.credentialsName || null : null},
|
||||
card = NULL,
|
||||
cardDark = NULL
|
||||
WHERE id = ${profileId}
|
||||
`);
|
||||
} else {
|
||||
profileId = ulid();
|
||||
await req.db.get(SQL`INSERT INTO profiles (id, userId, locale, opinions, names, pronouns, description, birthday, timezone, links, flags, customFlags, words, sensitive, active, teamName, footerName, footerAreas)
|
||||
VALUES (${profileId}, ${req.user.id}, ${global.config.locale},
|
||||
${JSON.stringify(opinions)},
|
||||
${JSON.stringify(names)},
|
||||
${JSON.stringify(pronouns)},
|
||||
${description},
|
||||
${birthday},
|
||||
${JSON.stringify(timezone)},
|
||||
${JSON.stringify(links)},
|
||||
${JSON.stringify(req.body.flags)},
|
||||
${JSON.stringify(customFlags)},
|
||||
${JSON.stringify(words)},
|
||||
${JSON.stringify(sensitive)},
|
||||
1,
|
||||
${req.isGranted() ? req.body.teamName || null : ''},
|
||||
${req.isGranted() ? req.body.footerName || null : ''},
|
||||
${req.isGranted() ? req.body.footerAreas.join(',') || null : ''}
|
||||
)`);
|
||||
}
|
||||
const profileId = await saveProfile(req, global.config.locale, {
|
||||
opinions, names, pronouns, description, birthday, timezone, links,
|
||||
flags: req.body.flags,
|
||||
customFlags, words, sensitive,
|
||||
teamName: req.body.teamName,
|
||||
footerName: req.body.footerName,
|
||||
footerAreas: (req.body.footerAreas || []).join(','),
|
||||
credentials: (req.body.credentials || []).join('|'),
|
||||
credentialsLevel: req.body.credentialsLevel,
|
||||
credentialsName: req.body.credentialsName,
|
||||
});
|
||||
|
||||
await req.db.get(SQL`DELETE FROM user_connections WHERE from_profileId = ${profileId}`);
|
||||
const usernameIdMap = await usernamesToIds(req.db, req.body.circle.map(r => r.username));
|
||||
@ -832,4 +856,124 @@ router.post('/profile/remove-self-circle/:username', handleErrorAsync(async (req
|
||||
return res.json('OK');
|
||||
}));
|
||||
|
||||
router.get('/profile/export', handleErrorAsync(async (req, res) => {
|
||||
if (!req.user || req.user.bannedReason) {
|
||||
return res.status(401).json({error: 'Unauthorised'});
|
||||
}
|
||||
|
||||
const profiles = {};
|
||||
const customFlagIds = new Set();
|
||||
for (let profile of await req.db.all(SQL`SELECT * FROM profiles WHERE userId = ${req.user.id}`)) {
|
||||
const exportProfile = {
|
||||
names: JSON.parse(profile['names']),
|
||||
pronouns: JSON.parse(profile['pronouns']),
|
||||
description: profile['description'],
|
||||
birthday: profile['birthday'],
|
||||
links: JSON.parse(profile['links']),
|
||||
flags: JSON.parse(profile['flags']),
|
||||
words: JSON.parse(profile['words']),
|
||||
customFlags: JSON.parse(profile['customFlags']),
|
||||
opinions: JSON.parse(profile['opinions']),
|
||||
timezone: JSON.parse(profile['timezone']),
|
||||
sensitive: JSON.parse(profile['sensitive']),
|
||||
teamName: profile['teamName'],
|
||||
footerName: profile['footerName'],
|
||||
footerAreas: profile['footerAreas'],
|
||||
credentials: profile['credentials'],
|
||||
credentialsLevel: profile['credentialsLevel'],
|
||||
credentialsName: profile['credentialsName'],
|
||||
};
|
||||
|
||||
for (let customFlag of exportProfile.customFlags) {
|
||||
customFlagIds.add(customFlag['value']);
|
||||
}
|
||||
|
||||
profiles[profile['locale']] = exportProfile;
|
||||
}
|
||||
|
||||
const s3 = new S3(awsConfig);
|
||||
const images = {};
|
||||
for (let id of customFlagIds) {
|
||||
try {
|
||||
const data = await s3.getObject({
|
||||
Key: `images/${id}-flag.png`,
|
||||
}).promise();
|
||||
images[id] = {
|
||||
flag: data.Body.toString('base64'),
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching object from S3:', error);
|
||||
}
|
||||
}
|
||||
|
||||
await auditLog(req, 'profile/exported', {
|
||||
profiles,
|
||||
});
|
||||
|
||||
const payload = Buffer.from(JSON.stringify({
|
||||
version: 1,
|
||||
profiles,
|
||||
images,
|
||||
})).toString('base64');
|
||||
|
||||
const signature = crypto.sign(payload);
|
||||
|
||||
res.setHeader('Content-disposition', `attachment; filename=pronounspage-${req.user.username}-${+new Date}.card.gz`);
|
||||
res.setHeader('Content-type', 'application/gzip');
|
||||
res.end(
|
||||
zlib.gzipSync(
|
||||
payload + '\n' +
|
||||
signature
|
||||
)
|
||||
);
|
||||
}));
|
||||
|
||||
router.post('/profile/import', multer({limits: {fileSize: 10 * 1024 * 1024}}).any('files[]', 1), handleErrorAsync(async (req, res) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({error: 'Unauthorised'});
|
||||
}
|
||||
|
||||
if (req.files.length !== 1) {
|
||||
return res.status(401).json({error: 'One file expected'});
|
||||
}
|
||||
|
||||
const contentParts = zlib.gunzipSync(req.files[0].buffer).toString('utf-8').split('\n');
|
||||
if (contentParts.length !== 2) {
|
||||
return res.status(401).json({error: 'profile.backup.error.signature'});
|
||||
}
|
||||
|
||||
const [payload, signature] = contentParts;
|
||||
|
||||
if (!crypto.validate(payload, signature)) {
|
||||
return res.status(400).json({error: 'profile.backup.error.signature'});
|
||||
}
|
||||
|
||||
const {version, profiles, images} = JSON.parse(Buffer.from(payload, 'base64').toString('utf-8'));
|
||||
|
||||
const s3 = new S3(awsConfig);
|
||||
for (let [id, sizes] of Object.entries(images)) {
|
||||
for (let [size, content] of Object.entries(sizes)) {
|
||||
try {
|
||||
const data = await s3.headObject({
|
||||
Key: `images/${id}-${size}.png`,
|
||||
}).promise();
|
||||
continue;
|
||||
} catch (error) {}
|
||||
|
||||
await s3.putObject({
|
||||
Key: `images/${id}-${size}.png`,
|
||||
Body: Buffer.from(content, 'base64'),
|
||||
ContentType: 'image/png',
|
||||
ACL: 'public-read',
|
||||
}).promise();
|
||||
}
|
||||
}
|
||||
|
||||
for (let [locale, profile] of Object.entries(profiles)) {
|
||||
await saveProfile(req, locale, profile);
|
||||
}
|
||||
|
||||
return res.json('OK');
|
||||
}));
|
||||
|
||||
export default router;
|
||||
|
23
src/crypto.js
Normal file
23
src/crypto.js
Normal file
@ -0,0 +1,23 @@
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs'
|
||||
|
||||
class Crypto {
|
||||
constructor(privateKey, publicKey) {
|
||||
this.privateKey = crypto.createPrivateKey(fs.readFileSync(privateKey));
|
||||
this.publicKey = crypto.createPublicKey(fs.readFileSync(publicKey));
|
||||
}
|
||||
|
||||
sign(payload) {
|
||||
const sign = crypto.createSign('SHA256');
|
||||
sign.update(payload);
|
||||
return sign.sign(this.privateKey, 'hex');
|
||||
}
|
||||
|
||||
validate(payload, signature) {
|
||||
const verify = crypto.createVerify('SHA256');
|
||||
verify.update(payload);
|
||||
return verify.verify(this.publicKey, signature, 'hex');
|
||||
}
|
||||
}
|
||||
|
||||
export default new Crypto(__dirname + '/../keys/private.pem', __dirname + '/../keys/public.pem');
|
Loading…
x
Reference in New Issue
Block a user