diff --git a/app/controllers/admin.js b/app/controllers/admin.js index cad0613..809feae 100644 --- a/app/controllers/admin.js +++ b/app/controllers/admin.js @@ -90,6 +90,7 @@ class AdminController extends SiteController { }; res.locals.channels = await venueService.getChannels(); + res.locals.pageTitle = `Admin Dashbord for ${this.dtp.config.site.name}`; res.render('admin/index'); } diff --git a/app/controllers/admin/otp.js b/app/controllers/admin/otp.js index 3700b41..0363aa9 100644 --- a/app/controllers/admin/otp.js +++ b/app/controllers/admin/otp.js @@ -25,57 +25,20 @@ class OtpAdminController extends SiteController { return next(); }); - router.param('user', this.populateUser.bind(this)); - - - + // router.param('otp', this.populateOtp.bind(this)); + router.get('/', this.getIndex.bind(this)); - router.post('/generate/:user', this.regenerateOTPTokens.bind(this)); - return router; } - - async populateUser (req, res, next) { - try { - const { otpAuth: otpAuthService } = this.dtp.services; - if (!req.user) { - throw new SiteError(402, "Error getting user"); - } - res.locals.tokens = await otpAuthService.getOTPAccount(req.user, "Admin"); - if (!res.locals.tokens) { - throw new SiteError(402, "Error getting OTP details for user"); - } - return next(); - } catch (error) { - this.log.error('failed to get tokens', { error }); - return next(error); - } - } - - async regenerateOTPTokens (req, res, next) { - try { - const { otpAuth: otpAuthService } = this.dtp.services; - if (!req.user._id.equals(res.locals.tokens.user)) { - throw new SiteError(402, "This is not your account."); - } - res.locals.generate = otpAuthService.generateBackupTokens(req.user, 'Admin'); - res.reload(); - } catch (error) { - this.log.error('failed to get tokens', { error }); - return next(error); - } - } - async getIndex (req, res, next) { try { const { otpAuth: otpAuthService } = this.dtp.services; if (!req.user) { throw new SiteError(402, "Error getting user"); } - const tokens = await otpAuthService.getOTPAccount(req.user, "Admin"); - res.locals.tokens = tokens.backupTokens; + res.locals.tokens = await otpAuthService.getBackupTokens(req.user, "Admin"); res.render('admin/otp/index'); } catch (error) { this.log.error('failed to get tokens', { error }); diff --git a/app/controllers/admin/settings.js b/app/controllers/admin/settings.js index e5eefcb..1c8b967 100644 --- a/app/controllers/admin/settings.js +++ b/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,43 @@ 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) { + const { image: imageService } = this.dtp.services; + try { + const displayList = this.createDisplayList('site-icon'); + await imageService.updateSiteIcon(req.body, req.file); + displayList.showNotification( + 'Site Icon updated successfully.', + 'success', + 'bottom-center', + 2000, + ); + res.status(200).json({ + success: true, + displayList, + }); + } catch (error) { + this.log.error('failed to update site icon', { error }); + return res.status(error.statusCode || 500).json({ + success: false, + message: error.message, + }); + } + } + } module.exports = { diff --git a/app/controllers/home.js b/app/controllers/home.js index 2eb9fd5..c38435a 100644 --- a/app/controllers/home.js +++ b/app/controllers/home.js @@ -77,8 +77,6 @@ class HomeController extends SiteController { res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.posts = await postService.getPosts(res.locals.pagination); - res.locals.newsfeed = await feedService.getNewsfeed(); - res.render('index'); } catch (error) { this.log.error('failed to render home view', { error }); diff --git a/app/controllers/user.js b/app/controllers/user.js index 3accdc5..cf8b649 100644 --- a/app/controllers/user.js +++ b/app/controllers/user.js @@ -33,12 +33,12 @@ class UserController extends SiteController { const otpSetup = otpAuthService.middleware('Account', { adminRequired: false, otpRequired: true, - otpRedirectURL: async (req) => { return `/user/${req.user._id}`; }, + otpRedirectURL: async (req) => { return `/user/${req.user.username}`; }, }); const otpMiddleware = otpAuthService.middleware('Account', { adminRequired: false, otpRequired: false, - otpRedirectURL: async (req) => { return `/user/${req.user._id}`; }, + otpRedirectURL: async (req) => { return `/user/${req.user.username}`; }, }); router.use( @@ -218,7 +218,7 @@ class UserController extends SiteController { if (error) { return next(error); } - res.redirect(`/user/${res.locals.user._id}`); + res.redirect(`/user/${res.locals.user.username}`); }); } catch (error) { this.log.error('failed to create new user', { error }); diff --git a/app/services/feed.js b/app/services/feed.js index a26e25a..27b55f2 100644 --- a/app/services/feed.js +++ b/app/services/feed.js @@ -30,6 +30,17 @@ class FeedService extends SiteService { this.jobQueue = await this.getJobQueue('newsroom', this.dtp.config.jobQueues.newsroom); } + middleware (options) { + options = Object.assign({ maxEntryCount: 5 }, options); + return async (req, res, next) => { + if (this.isSystemRoute(req.path)) { + return next(); // don't load newsfeeds for non-content routes + } + res.locals.newsfeed = await this.getNewsfeed({ skip: 0, cpp: options.maxEntryCount }); + return next(); + }; + } + async create (feedDefinition) { feedDefinition.url = feedDefinition.url.trim(); const feedContent = await this.load(feedDefinition.url); diff --git a/app/services/image.js b/app/services/image.js index ea46e51..4547483 100644 --- a/app/services/image.js +++ b/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, diff --git a/app/services/otp-auth.js b/app/services/otp-auth.js index 6223dd7..c8b1cad 100644 --- a/app/services/otp-auth.js +++ b/app/services/otp-auth.js @@ -222,15 +222,20 @@ class OtpAuthService extends SiteService { return true; } + async destroyOtpSession (req, serviceName) { + delete req.session.otp[serviceName]; + await this.saveSession(req); + } + async removeForUser (user, serviceName) { return await OtpAccount.findOneAndDelete({ user: user, service: serviceName }); } - - async getOTPAccount (user, serviceName) { - const otpAccount = await OtpAccount.findOne({ user: user._id, service: serviceName }) + + async getBackupTokens (user, serviceName) { + const tokens = await OtpAccount.findOne({ user: user._id, service: serviceName }) .select('+backupTokens') .lean(); - return otpAccount; + return tokens.backupTokens; } } diff --git a/app/views/admin/components/file-upload-image.pug b/app/views/admin/components/file-upload-image.pug new file mode 100644 index 0000000..3ca78f6 --- /dev/null +++ b/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 diff --git a/app/views/admin/components/menu.pug b/app/views/admin/components/menu.pug index e3e6fee..f87bd06 100644 --- a/app/views/admin/components/menu.pug +++ b/app/views/admin/components/menu.pug @@ -12,6 +12,18 @@ 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") + span.nav-item-icon + i.fas.fa-cog + span.uk-margin-small-left Otp Settings li(class={ 'uk-active': (adminView === 'otp') }) a(href="/admin/otp") diff --git a/app/views/admin/layouts/main.pug b/app/views/admin/layouts/main.pug index 3e07d52..cc5f546 100644 --- a/app/views/admin/layouts/main.pug +++ b/app/views/admin/layouts/main.pug @@ -16,4 +16,4 @@ block content-container include ../components/menu div(class="uk-width-1-1 uk-flex-first uk-width-expand@m").uk-width-expand - block content + block content \ No newline at end of file diff --git a/app/views/admin/settings/images.pug b/app/views/admin/settings/images.pug new file mode 100644 index 0000000..00220bf --- /dev/null +++ b/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 }, + ) \ No newline at end of file diff --git a/app/views/components/library.pug b/app/views/components/library.pug index 0aa6d3f..9424f97 100644 --- a/app/views/components/library.pug +++ b/app/views/components/library.pug @@ -40,7 +40,7 @@ include section-title function getUserProfileUrl (user) { if (user.core) { - return `/user/core/${user._id}`; + return `/user/core/${user.username}`; } return `/user/${user.username}`; } @@ -60,6 +60,6 @@ mixin renderBackButton (options) mixin renderUserLink (user) if user.coreUserId - a(href=`/user/core/${user._id}`)= `${user.username}@${user.core.meta.domainKey}` + a(href=`/user/core/${user.username}`)= `${user.username}@${user.core.meta.domainKey}` else a(href=`/user/${user.username}`)= user.displayName || user.username \ No newline at end of file diff --git a/app/views/components/navbar.pug b/app/views/components/navbar.pug index f63344f..1cc2f51 100644 --- a/app/views/components/navbar.pug +++ b/app/views/components/navbar.pug @@ -67,7 +67,7 @@ nav(uk-navbar).uk-navbar-container.uk-position-fixed.uk-position-top li.uk-nav-heading.uk-text-center= user.displayName || user.username li.uk-nav-divider li - a(href= user.core ? `/user/core/${user._id}` : `/user/${user.username}`) + a(href= user.core ? `/user/core/${user.username}` : `/user/${user.username}`) span.nav-item-icon i.fas.fa-user span Profile diff --git a/app/views/components/off-canvas.pug b/app/views/components/off-canvas.pug index af75ca8..df67cc1 100644 --- a/app/views/components/off-canvas.pug +++ b/app/views/components/off-canvas.pug @@ -60,7 +60,7 @@ mixin renderMenuItem (iconClass, label) .uk-width-expand Chat li(class={ "uk-active": (currentView === 'user-settings') }) - a(href=`/user/${user._id}`).uk-display-block + a(href=`/user/${user.username}`).uk-display-block div(uk-grid).uk-grid-collapse .uk-width-auto .app-menu-icon diff --git a/app/views/layouts/main-sidebar.pug b/app/views/layouts/main-sidebar.pug index 52cbef8..2a67a50 100644 --- a/app/views/layouts/main-sidebar.pug +++ b/app/views/layouts/main-sidebar.pug @@ -1,6 +1,8 @@ extends main block content-container + block content-header + section.uk-section.uk-section-default.uk-section-small .uk-container div(uk-grid)#dtp-content-grid diff --git a/app/views/newsroom/components/feed-entry-list-item.pug b/app/views/newsroom/components/feed-entry-list-item.pug index da32d31..d50b368 100644 --- a/app/views/newsroom/components/feed-entry-list-item.pug +++ b/app/views/newsroom/components/feed-entry-list-item.pug @@ -1,9 +1,9 @@ mixin renderNewsroomFeedEntryListItem (entry) - .uk-text-bold - a(href= entry.link, target="_blank").uk-link-reset= entry.title - .uk-text-small - div(uk-grid).uk-grid-small - .uk-width-expand + a(href= entry.link, target="_blank").dtp-link + div= entry.title + .uk-article-meta + div(uk-grid).uk-grid-small.uk-grid-divider + .uk-width-auto.uk-text-truncate a(href= entry.feed.link, target="_blank").uk-link-reset= entry.feed.title .uk-width-auto div= moment(entry.published).fromNow() \ No newline at end of file diff --git a/app/views/welcome/core-home.pug b/app/views/welcome/core-home.pug index bb313d9..7298915 100644 --- a/app/views/welcome/core-home.pug +++ b/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() \ No newline at end of file + +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() \ No newline at end of file diff --git a/client/js/site-admin-app.js b/client/js/site-admin-app.js index ea354b7..89e608a 100644 --- a/client/js/site-admin-app.js +++ b/client/js/site-admin-app.js @@ -508,27 +508,41 @@ export default class DtpSiteAdminHostStatsApp extends DtpApp { return false; } - async generateOTPTokens (event) { + async submitImageForm (event) { event.preventDefault(); event.stopPropagation(); - const target = event.currentTarget || event.target; - const user = target.getAttribute('data-user'); - - try { - await UIkit.modal.confirm(`Are you sure you want to regenerate your OTP tokens?`); - } catch (error) { - return false; - } - - try { - const response = await fetch(`/admin/otp/generate/${user}`, { method: 'POST' }); - await this.processResponse(response); - } catch (error) { - UIkit.modal.alert(`Failed to generate OTP tokens: ${error.message}`); - } + 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 false; + return; } } diff --git a/client/less/site/main.less b/client/less/site/main.less index 73700d0..007a841 100644 --- a/client/less/site/main.less +++ b/client/less/site/main.less @@ -50,6 +50,13 @@ body { } } +a.dtp-link { + color: inherit; + + &:hover { + color: @global-link-hover-color; + } +} .dtp-site-footer { .uk-subnav { a.dtp-social-link { diff --git a/dtp-sites.js b/dtp-sites.js index bea4945..bbfc516 100644 --- a/dtp-sites.js +++ b/dtp-sites.js @@ -36,13 +36,15 @@ module.config = { venueService.channelMiddleware(), gabTvService.channelMiddleware() ); + const { feed: feedService } = dtp.services; + app.use(feedService.middleware({ maxEntryCount: 5 })); }, }; module.log = new SiteLog(module, module.config.component); module.shutdown = async ( ) => { - + await SitePlatform.shutdown(); }; (async ( ) => { diff --git a/lib/site-common.js b/lib/site-common.js index 78a1788..3732ce3 100644 --- a/lib/site-common.js +++ b/lib/site-common.js @@ -106,6 +106,13 @@ class SiteCommon extends EventEmitter2 { }); } + isSystemRoute (pathname) { + return pathname.startsWith('/auth') || + pathname.startsWith('/image') || + pathname.startsWith('/manifest') + ; + } + isValidString (text) { return text && (typeof text === 'string') && (text.length > 0); }