Browse Source

added changing site icon and small touchups

- added the ability to change a site icon
- small UI touchups for core picker page when no Cores are connected
develop
Andrew Woodlee 1 year ago
parent
commit
81614236ff
  1. 1
      .gitignore
  2. 2
      app/controllers/admin.js
  3. 6
      app/controllers/admin/otp.js
  4. 38
      app/controllers/admin/settings.js
  5. 114
      app/services/image.js
  6. 2
      app/services/otp-auth.js
  7. 51
      app/views/admin/components/file-upload-image.pug
  8. 6
      app/views/admin/components/menu.pug
  9. 28
      app/views/admin/settings/images.pug
  10. 2
      app/views/otp/welcome.pug
  11. 48
      app/views/welcome/core-home.pug
  12. 37
      client/js/site-admin-app.js

1
.gitignore

@ -6,3 +6,4 @@ ssl/*key
data/minio
node_modules
dist
start-local-*

2
app/controllers/admin.js

@ -84,6 +84,8 @@ class AdminController extends SiteController {
constellation: await coreNodeService.getConstellationStats(),
};
res.locals.pageTitle = `Admin Dashbord for ${this.dtp.config.site.name}`;
res.render('admin/index');
}
}

6
app/controllers/admin/otp.js

@ -26,13 +26,9 @@ class OtpAdminController extends SiteController {
});
// router.param('otp', this.populateOtp.bind(this));
router.get('/', this.getIndex.bind(this));
// router.delete('/:postId', this.deletePost.bind(this));
return router;
}

38
app/controllers/admin/settings.js

@ -16,6 +16,13 @@ class SettingsController extends SiteController {
async start ( ) {
const router = express.Router();
const imageUpload = this.createMulter('uploads', {
limits: {
fileSize: 1024 * 1000 * 12,
},
});
router.use(async (req, res, next) => {
res.locals.currentView = 'admin';
res.locals.adminView = 'settings';
@ -23,9 +30,13 @@ class SettingsController extends SiteController {
});
router.post('/', this.postUpdateSettings.bind(this));
router.post('/images/updateSiteIcon', imageUpload.single('imageFile'), this.postUpdateSiteIcon.bind(this));
router.get('/', this.getSettingsView.bind(this));
router.get('/images', this.getImageSettings.bind(this));
return router;
}
@ -47,6 +58,33 @@ class SettingsController extends SiteController {
return next(error);
}
}
async getImageSettings (req, res, next) {
const { image: imageService } = this.dtp.services;
res.locals.adminView = 'image-settings';
res.locals.pageTitle = `Image settings for ${this.dtp.config.site.name}`;
try {
res.locals.siteIcon = await imageService.getSiteIconInfo();
res.render('admin/settings/images');
} catch (error) {
return next(error);
}
}
async postUpdateSiteIcon (req, res, next) {
const { image: imageService } = this.dtp.services;
try {
res.locals.image = await imageService.updateSiteIcon(req.body, req.file);
res.status(200).json({
success: true,
imageId: res.locals.image.toString(),
});
} catch (error) {
this.log.error('failed to create image', { error });
return next(error);
}
}
}
module.exports = {

114
app/services/image.js

@ -16,7 +16,7 @@ const { SiteService, SiteAsync } = require('../../lib/site-lib');
class ImageService extends SiteService {
constructor (dtp) {
constructor(dtp) {
super(dtp, module.exports);
this.populateImage = [
{
@ -26,20 +26,20 @@ class ImageService extends SiteService {
];
}
async start ( ) {
async start() {
await super.start();
await fs.promises.mkdir(process.env.DTP_IMAGE_WORK_PATH, { recursive: true });
}
async create (owner, imageDefinition, file) {
async create(owner, imageDefinition, file) {
const NOW = new Date();
const { minio: minioService } = this.dtp.services;
try {
this.log.debug('processing uploaded image', { imageDefinition, file });
const sharpImage = await sharp(file.path);
const sharpImage = sharp(file.path);
const metadata = await sharpImage.metadata();
// create an Image model instance, but leave it here in application memory.
// we don't persist it to the db until MinIO accepts the binary data.
const image = new SiteImage();
@ -49,12 +49,12 @@ class ImageService extends SiteService {
image.size = file.size;
image.file.bucket = process.env.MINIO_IMAGE_BUCKET;
image.metadata = this.makeImageMetadata(metadata);
const imageId = image._id.toString();
const ownerId = owner._id.toString();
const fileKey = `/${ownerId.slice(0, 3)}/${ownerId}/${imageId.slice(0, 3)}/${imageId}`;
image.file.key = fileKey;
// upload the image file to MinIO
const response = await minioService.uploadFile({
bucket: image.file.bucket,
@ -65,13 +65,13 @@ class ImageService extends SiteService {
'Content-Length': file.size,
},
});
// store the eTag from MinIO in the Image model
image.file.etag = response.etag;
// save the Image model to the db
await image.save();
this.log.info('processed uploaded image', { ownerId, imageId, fileKey });
return image.toObject();
} catch (error) {
@ -83,14 +83,14 @@ class ImageService extends SiteService {
}
}
async getImageById (imageId) {
async getImageById(imageId) {
const image = await SiteImage
.findById(imageId)
.populate(this.populateImage);
return image;
}
async getRecentImagesForOwner (owner) {
async getRecentImagesForOwner(owner) {
const images = await SiteImage
.find({ owner: owner._id })
.sort({ created: -1 })
@ -100,7 +100,7 @@ class ImageService extends SiteService {
return images;
}
async deleteImage (image) {
async deleteImage(image) {
const { minio: minioService } = this.dtp.services;
this.log.debug('removing image from storage', { bucket: image.file.bucket, key: image.file.key });
@ -110,13 +110,13 @@ class ImageService extends SiteService {
await SiteImage.deleteOne({ _id: image._id });
}
async processImageFile (owner, file, outputs, options) {
async processImageFile(owner, file, outputs, options) {
this.log.debug('processing image file', { owner, file, outputs });
const sharpImage = sharp(file.path);
return this.processImage(owner, sharpImage, outputs, options);
}
async processImage (owner, sharpImage, outputs, options) {
async processImage(owner, sharpImage, outputs, options) {
const NOW = new Date();
const service = this;
const { minio: minioService } = this.dtp.services;
@ -128,7 +128,7 @@ class ImageService extends SiteService {
const imageWorkPath = process.env.DTP_IMAGE_WORK_PATH || '/tmp';
const metadata = await sharpImage.metadata();
async function processOutputImage (output) {
async function processOutputImage(output) {
const outputMetadata = service.makeImageMetadata(metadata);
outputMetadata.width = output.width;
outputMetadata.height = output.height;
@ -149,7 +149,7 @@ class ImageService extends SiteService {
height: output.height,
options: output.resizeOptions,
})
;
;
chain = chain[output.format](output.formatParameters);
output.filePath = path.join(imageWorkPath, `${image._id}.${output.width}x${output.height}.${output.format}`);
@ -165,11 +165,11 @@ class ImageService extends SiteService {
const imageId = image._id.toString();
const ownerId = owner._id.toString();
const fileKey = `/${ownerId.slice(0, 3)}/${ownerId}/images/${imageId.slice(0, 3)}/${imageId}.${output.format}`;
image.file.bucket = process.env.MINIO_IMAGE_BUCKET;
image.file.key = fileKey;
image.size = output.stat.size;
// upload the image file to MinIO
const response = await minioService.uploadFile({
bucket: image.file.bucket,
@ -180,13 +180,13 @@ class ImageService extends SiteService {
'Content-Length': output.stat.size,
},
});
// store the eTag from MinIO in the Image model
image.file.etag = response.etag;
// save the Image model to the db
await image.save();
service.log.info('processed uploaded image', { ownerId, imageId, fileKey });
if (options.removeWorkFiles) {
@ -216,7 +216,73 @@ class ImageService extends SiteService {
await SiteAsync.each(outputs, processOutputImage, 4);
}
makeImageMetadata (metadata) {
async getSiteIconInfo() {
const siteDomain = this.dtp.config.site.domainKey;
const siteImagesDir = path.join(this.dtp.config.root, 'client', 'img');
const siteIconDir = path.join(siteImagesDir, 'icon', siteDomain);
let icon;
try {
await fs.promises.access(siteIconDir);
const iconMetadata = await sharp(path.join(siteIconDir, 'icon-512x512.png')).metadata();
icon = {
metadata: iconMetadata,
path: `/img/icon/${siteDomain}/icon-512x512.png`,
};
} catch (error) {
icon = null;
}
return icon;
}
async updateSiteIcon(imageDefinition, file) {
this.log.debug('updating site icon', { imageDefinition, file });
try {
const siteDomain = this.dtp.config.site.domainKey;
const siteImagesDir = path.join(this.dtp.config.root, 'client', 'img');
const siteIconDir = path.join(siteImagesDir, 'icon', siteDomain);
const sourceIconFilePath = file.path;
const sizes = [16, 32, 36, 48, 57, 60, 70, 72, 76, 96, 114, 120, 144, 150, 152, 180, 192, 256, 310, 384, 512];
await fs.promises.mkdir(siteIconDir, { force: true, recursive: true });
for (var size of sizes) {
await sharp(sourceIconFilePath).resize({
fit: sharp.fit.contain,
width: size,
height: size,
}).png()
.toFile(path.join(siteIconDir, `icon-${size}x${size}.png`));
}
await fs.promises.cp(sourceIconFilePath, path.join(siteIconDir, `${siteDomain}.png`));
await fs.promises.cp(sourceIconFilePath, path.join(siteImagesDir, 'social-cards', `${siteDomain}.png`));
return path.join(siteIconDir, 'icon-512x512.png');
} catch (error) {
this.log.error('failed to update site icon', { error });
throw error;
} finally {
this.log.info('removing uploaded image from local file system', { file: file.path });
await fs.promises.rm(file.path);
}
}
makeImageMetadata(metadata) {
return {
format: metadata.format,
size: metadata.size,

2
app/services/otp-auth.js

@ -219,7 +219,7 @@ class OtpAuthService extends SiteService {
async destroyOtpSession (req, serviceName) {
delete req.session.otp[serviceName];
await this.saveSession(req)
await this.saveSession(req);
}
async removeForUser (user, serviceName) {

51
app/views/admin/components/file-upload-image.pug

@ -0,0 +1,51 @@
mixin renderFileUploadImage (actionUrl, containerId, imageId, imageClass, defaultImage, currentImage, cropperOptions)
div(id= containerId).dtp-file-upload
form(method="POST", action= actionUrl, enctype="multipart/form-data", onsubmit= "return dtp.app.submitImageForm(event);").uk-form
.uk-margin
.uk-card.uk-card-default.uk-card-small
.uk-card-body
div(uk-grid).uk-flex-middle.uk-flex-center
div(class="uk-width-1-1 uk-width-auto@m")
.upload-image-container.size-512
if !!currentImage
img(id= imageId, src= currentImage.path, class= imageClass).sb-large
else
img(id= imageId, src= defaultImage, class= imageClass)
div(class="uk-width-1-1 uk-width-auto@m")
.uk-text-small.uk-margin
#file-select
.uk-margin(class="uk-text-center uk-text-left@m")
span.uk-text-middle Select an image
div(uk-form-custom).uk-margin-small-left
input(
type="file",
formenctype="multipart/form-data",
accept=".jpg,.png,image/jpeg,image/png",
data-file-select-container= containerId,
data-file-select="test-image-upload",
data-file-size-element= "file-size",
data-file-max-size= 15 * 1024000,
data-image-id= imageId,
data-cropper-options= cropperOptions,
onchange="return dtp.app.selectImageFile(event);",
)
button(type="button", tabindex="-1").uk-button.uk-button-default Select
#file-info(class="uk-text-center uk-text-left@m", hidden)
#file-name.uk-text-bold
if currentImage
div resolution: #[span#image-resolution-w= numeral(currentImage.metadata.width).format('0,0')]x#[span#image-resolution-h= numeral(currentImage.metadata.height).format('0,0')]
div size: #[span#file-size= numeral(currentImage.metadata.size).format('0,0.00b')]
div last modified: #[span#file-modified= moment(currentImage.created).format('MMM DD, YYYY')]
else
div resolution: #[span#image-resolution-w 512]x#[span#image-resolution-h 512]
div size: #[span#file-size N/A]
div last modified: #[span#file-modified N/A]
.uk-card-footer
div(class="uk-flex-center", uk-grid)
#file-save-btn(hidden).uk-width-auto
button(
type="submit",
).uk-button.uk-button-primary Save

6
app/views/admin/components/menu.pug

@ -12,6 +12,12 @@ ul(uk-nav).uk-nav-default
span.nav-item-icon
i.fas.fa-cog
span.uk-margin-small-left Settings
li(class={ 'uk-active': (adminView === 'image-settings') })
a(href="/admin/settings/images")
span.nav-item-icon
i.fas.fa-image
span.uk-margin-small-left Image Settings
li(class={ 'uk-active': (adminView === 'otp') })
a(href="/admin/otp")

28
app/views/admin/settings/images.pug

@ -0,0 +1,28 @@
extends ../layouts/main
block vendorcss
link(rel='stylesheet', href=`/cropperjs/cropper.min.css?v=${pkg.version}`)
block vendorjs
script(src=`/cropperjs/cropper.min.js?v=${pkg.version}`)
block content
include ../components/file-upload-image
//- h2 Add or replace your site images here
div(uk-grid).uk-flex-middle
.uk-width-expand
fieldset
legend Site Icon
.uk-margin
if siteIcon
p.uk-card-title Replace your site icon below.
else
p.uk-card-title You do not currently have a site icon. Add one below.
+renderFileUploadImage(
`/admin/settings/images/updateSiteIcon`,
'site-icon-upload',
'site-icon-file',
'site-icon-picture',
`/img/icon/dtp-base.png`,
siteIcon,
{ aspectRatio: 1 },
)

2
app/views/otp/welcome.pug

@ -41,7 +41,7 @@ block content
.uk-margin
button(type="submit").uk-button.dtp-button-primary.uk-border-pill Enable 2FA
div(class="uk-width-1-1 uk-text-center uk-text-left@m", hidden)
div(class="uk-width-1-1 uk-text-center uk-text-left@m")
.uk-margin
p Or, if your authenticator doesn't support scanning QR codes, you can enter the OTP configuration information shown here to begin displaying codes:
pre(

48
app/views/welcome/core-home.pug

@ -3,26 +3,34 @@ block content
section.uk-section.uk-section-default
.uk-container
.uk-card.uk-card-default
.uk-card-header
h1.uk-card-title Select Community
.uk-card-body
div(uk-grid).uk-grid-small
each core in connectedCores
div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl")
//- pre= JSON.stringify(connectedCores, null, 2)
a(href=`/auth/core/${core._id}`).uk-display-block.uk-link-reset
.dtp-core-list-item.uk-border-rounded
div(uk-grid).uk-grid-small.uk-flex-middle
.uk-width-auto
img(src=`http://${core.meta.domain}/img/icon/dtp-core.svg`, style="width: 48px; height: auto;")
.uk-width-expand
.core-name= core.meta.name
.core-description= core.meta.description
if Array.isArray(hosts) && (hosts.length > 0)
.uk-card.uk-card-default
.uk-card-header
h1.uk-card-title Select Community
.uk-card-body
div(uk-grid).uk-grid-small
each core in connectedCores
div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl")
//- pre= JSON.stringify(connectedCores, null, 2)
a(href=`/auth/core/${core._id}`).uk-display-block.uk-link-reset
.dtp-core-list-item.uk-border-rounded
div(uk-grid).uk-grid-small.uk-flex-middle
.uk-width-auto
img(src=`http://${core.meta.domain}/img/icon/dtp-core.svg`, style="width: 48px; height: auto;")
.uk-width-expand
.core-name= core.meta.name
.core-description= core.meta.description
.uk-card-footer
div(uk-grid).uk-grid-small
.uk-width-expand
+renderBackButton()
+renderBackButton()
else
.uk-card.uk-card-default
.uk-card-header
h1.uk-card-title There are no communities connected to this site
.uk-card-footer
div(uk-grid).uk-grid-small
.uk-width-expand
+renderBackButton()

37
client/js/site-admin-app.js

@ -415,6 +415,43 @@ export default class DtpSiteAdminHostStatsApp extends DtpApp {
return false;
}
async submitImageForm (event) {
event.preventDefault();
event.stopPropagation();
const formElement = event.currentTarget || event.target;
const form = new FormData(formElement);
this.cropper.getCroppedCanvas().toBlob(async (imageData) => {
try {
form.append('imageFile', imageData, 'icon.png');
this.log.info('submitImageForm', 'updating site image', { event, action: formElement.action });
const response = await fetch(formElement.action, {
method: formElement.method,
body: form,
});
if (!response.ok) {
let json;
try {
json = await response.json();
} catch (error) {
throw new Error('Server error');
}
throw new Error(json.message || 'Server error');
}
await this.processResponse(response);
window.location.reload();
} catch (error) {
UIkit.modal.alert(`Failed to update site image: ${error.message}`);
}
});
return;
}
}
dtp.DtpSiteAdminHostStatsApp = DtpSiteAdminHostStatsApp;
Loading…
Cancel
Save