mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-02-04 15:39:45 +00:00
feat: add email logo customization (#1150)
This commit is contained in:
@@ -198,6 +198,7 @@ func initLogger(r *gin.Engine) {
|
||||
"GET /api/application-images/logo",
|
||||
"GET /api/application-images/background",
|
||||
"GET /api/application-images/favicon",
|
||||
"GET /api/application-images/email",
|
||||
"GET /_app",
|
||||
"GET /fonts",
|
||||
"GET /healthz",
|
||||
|
||||
@@ -23,11 +23,13 @@ func NewAppImagesController(
|
||||
}
|
||||
|
||||
group.GET("/application-images/logo", controller.getLogoHandler)
|
||||
group.GET("/application-images/email", controller.getEmailLogoHandler)
|
||||
group.GET("/application-images/background", controller.getBackgroundImageHandler)
|
||||
group.GET("/application-images/favicon", controller.getFaviconHandler)
|
||||
group.GET("/application-images/default-profile-picture", authMiddleware.Add(), controller.getDefaultProfilePicture)
|
||||
|
||||
group.PUT("/application-images/logo", authMiddleware.Add(), controller.updateLogoHandler)
|
||||
group.PUT("/application-images/email", authMiddleware.Add(), controller.updateEmailLogoHandler)
|
||||
group.PUT("/application-images/background", authMiddleware.Add(), controller.updateBackgroundImageHandler)
|
||||
group.PUT("/application-images/favicon", authMiddleware.Add(), controller.updateFaviconHandler)
|
||||
group.PUT("/application-images/default-profile-picture", authMiddleware.Add(), controller.updateDefaultProfilePicture)
|
||||
@@ -59,6 +61,18 @@ func (c *AppImagesController) getLogoHandler(ctx *gin.Context) {
|
||||
c.getImage(ctx, imageName)
|
||||
}
|
||||
|
||||
// getEmailLogoHandler godoc
|
||||
// @Summary Get email logo image
|
||||
// @Description Get the email logo image for use in emails
|
||||
// @Tags Application Images
|
||||
// @Produce image/png
|
||||
// @Produce image/jpeg
|
||||
// @Success 200 {file} binary "Email logo image"
|
||||
// @Router /api/application-images/email [get]
|
||||
func (c *AppImagesController) getEmailLogoHandler(ctx *gin.Context) {
|
||||
c.getImage(ctx, "logoEmail")
|
||||
}
|
||||
|
||||
// getBackgroundImageHandler godoc
|
||||
// @Summary Get background image
|
||||
// @Description Get the background image for the application
|
||||
@@ -124,6 +138,37 @@ func (c *AppImagesController) updateLogoHandler(ctx *gin.Context) {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// updateEmailLogoHandler godoc
|
||||
// @Summary Update email logo
|
||||
// @Description Update the email logo for use in emails
|
||||
// @Tags Application Images
|
||||
// @Accept multipart/form-data
|
||||
// @Param file formData file true "Email logo image file"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api/application-images/email [put]
|
||||
func (c *AppImagesController) updateEmailLogoHandler(ctx *gin.Context) {
|
||||
file, err := ctx.FormFile("file")
|
||||
if err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
fileType := utils.GetFileExtension(file.Filename)
|
||||
mimeType := utils.GetImageMimeType(fileType)
|
||||
|
||||
if mimeType != "image/png" && mimeType != "image/jpeg" {
|
||||
_ = ctx.Error(&common.WrongFileTypeError{ExpectedFileType: ".png or .jpg/jpeg"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.appImagesService.UpdateImage(ctx.Request.Context(), file, "logoEmail"); err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// updateBackgroundImageHandler godoc
|
||||
// @Summary Update background image
|
||||
// @Description Update the application background image
|
||||
|
||||
@@ -78,7 +78,7 @@ func SendEmail[V any](ctx context.Context, srv *EmailService, toEmail email.Addr
|
||||
|
||||
data := &email.TemplateData[V]{
|
||||
AppName: dbConfig.AppName.Value,
|
||||
LogoURL: common.EnvConfig.AppURL + "/api/application-images/logo",
|
||||
LogoURL: common.EnvConfig.AppURL + "/api/application-images/email",
|
||||
Data: tData,
|
||||
}
|
||||
|
||||
|
||||
BIN
backend/resources/images/logoEmail.png
Normal file
BIN
backend/resources/images/logoEmail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 566 B |
@@ -311,6 +311,7 @@
|
||||
"favicon": "Favicon",
|
||||
"light_mode_logo": "Light Mode Logo",
|
||||
"dark_mode_logo": "Dark Mode Logo",
|
||||
"email_logo": "Email Logo",
|
||||
"background_image": "Background Image",
|
||||
"language": "Language",
|
||||
"reset_profile_picture_question": "Reset profile picture?",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
cachedApplicationLogo,
|
||||
cachedBackgroundImage,
|
||||
cachedDefaultProfilePicture,
|
||||
cachedEmailLogo,
|
||||
cachedProfilePicture
|
||||
} from '$lib/utils/cached-image-util';
|
||||
import { get } from 'svelte/store';
|
||||
@@ -46,6 +47,14 @@ export default class AppConfigService extends APIService {
|
||||
cachedApplicationLogo.bustCache(light);
|
||||
};
|
||||
|
||||
updateEmailLogo = async (emailLogo: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', emailLogo);
|
||||
|
||||
await this.api.put(`/application-images/email`, formData);
|
||||
cachedEmailLogo.bustCache();
|
||||
};
|
||||
|
||||
updateDefaultProfilePicture = async (defaultProfilePicture: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', defaultProfilePicture);
|
||||
|
||||
@@ -20,6 +20,11 @@ export const cachedApplicationLogo: CachableImage = {
|
||||
}
|
||||
};
|
||||
|
||||
export const cachedEmailLogo: CachableImage = {
|
||||
getUrl: () => getCachedImageUrl(new URL('/api/application-images/email', window.location.origin)),
|
||||
bustCache: () => bustImageCache(new URL('/api/application-images/email', window.location.origin))
|
||||
};
|
||||
|
||||
export const cachedDefaultProfilePicture: CachableImage = {
|
||||
getUrl: () =>
|
||||
getCachedImageUrl(
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
async function updateImages(
|
||||
logoLight: File | undefined,
|
||||
logoDark: File | undefined,
|
||||
logoEmail: File | undefined,
|
||||
defaultProfilePicture: File | null | undefined,
|
||||
backgroundImage: File | undefined,
|
||||
favicon: File | undefined
|
||||
@@ -56,6 +57,10 @@
|
||||
? appConfigService.updateLogo(logoDark, false)
|
||||
: Promise.resolve();
|
||||
|
||||
const emailLogoPromise = logoEmail
|
||||
? appConfigService.updateEmailLogo(logoEmail)
|
||||
: Promise.resolve();
|
||||
|
||||
const defaultProfilePicturePromise =
|
||||
defaultProfilePicture === null
|
||||
? appConfigService.deleteDefaultProfilePicture()
|
||||
@@ -70,6 +75,7 @@
|
||||
await Promise.all([
|
||||
lightLogoPromise,
|
||||
darkLogoPromise,
|
||||
emailLogoPromise,
|
||||
defaultProfilePicturePromise,
|
||||
backgroundImagePromise,
|
||||
faviconPromise
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
import {
|
||||
cachedApplicationLogo,
|
||||
cachedBackgroundImage,
|
||||
cachedDefaultProfilePicture
|
||||
cachedDefaultProfilePicture,
|
||||
cachedEmailLogo
|
||||
} from '$lib/utils/cached-image-util';
|
||||
import ApplicationImage from './application-image.svelte';
|
||||
|
||||
@@ -14,6 +15,7 @@
|
||||
callback: (
|
||||
logoLight: File | undefined,
|
||||
logoDark: File | undefined,
|
||||
logoEmail: File | undefined,
|
||||
defaultProfilePicture: File | null | undefined,
|
||||
backgroundImage: File | undefined,
|
||||
favicon: File | undefined
|
||||
@@ -22,6 +24,7 @@
|
||||
|
||||
let logoLight = $state<File | undefined>();
|
||||
let logoDark = $state<File | undefined>();
|
||||
let logoEmail = $state<File | undefined>();
|
||||
let defaultProfilePicture = $state<File | null | undefined>();
|
||||
let backgroundImage = $state<File | undefined>();
|
||||
let favicon = $state<File | undefined>();
|
||||
@@ -54,6 +57,15 @@
|
||||
imageURL={cachedApplicationLogo.getUrl(false)}
|
||||
forceColorScheme="dark"
|
||||
/>
|
||||
<ApplicationImage
|
||||
id="logo-email"
|
||||
imageClass="size-24"
|
||||
label={m.email_logo()}
|
||||
bind:image={logoEmail}
|
||||
imageURL={cachedEmailLogo.getUrl()}
|
||||
accept="image/png, image/jpeg"
|
||||
forceColorScheme="light"
|
||||
/>
|
||||
<ApplicationImage
|
||||
id="default-profile-picture"
|
||||
imageClass="size-24"
|
||||
@@ -75,7 +87,8 @@
|
||||
<Button
|
||||
class="mt-5"
|
||||
usePromiseLoading
|
||||
onclick={() => callback(logoLight, logoDark, defaultProfilePicture, backgroundImage, favicon)}
|
||||
onclick={() =>
|
||||
callback(logoLight, logoDark, logoEmail, defaultProfilePicture, backgroundImage, favicon)}
|
||||
>{m.save()}</Button
|
||||
>
|
||||
</div>
|
||||
|
||||
BIN
tests/assets/cloud-logo.png
Normal file
BIN
tests/assets/cloud-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
3
tests/assets/cloud-logo.svg
Normal file
3
tests/assets/cloud-logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 88 KiB |
@@ -116,30 +116,49 @@ test('Update email configuration', async ({ page }) => {
|
||||
await expect(page.getByLabel('API Key Expiration')).toBeChecked();
|
||||
});
|
||||
|
||||
test('Update application images', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Expand card' }).nth(4).click();
|
||||
test.describe('Update application images', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Expand card' }).nth(4).click();
|
||||
});
|
||||
|
||||
await page.getByLabel('Favicon').setInputFiles('assets/w3-schools-favicon.ico');
|
||||
await page.getByLabel('Light Mode Logo').setInputFiles('assets/pingvin-share-logo.png');
|
||||
await page.getByLabel('Dark Mode Logo').setInputFiles('assets/nextcloud-logo.png');
|
||||
await page.getByLabel('Default Profile Picture').setInputFiles('assets/pingvin-share-logo.png');
|
||||
await page.getByLabel('Background Image').setInputFiles('assets/clouds.jpg');
|
||||
await page.getByRole('button', { name: 'Save' }).last().click();
|
||||
test('should upload images', async ({ page }) => {
|
||||
await page.getByLabel('Favicon').setInputFiles('assets/w3-schools-favicon.ico');
|
||||
await page.getByLabel('Light Mode Logo').setInputFiles('assets/pingvin-share-logo.png');
|
||||
await page.getByLabel('Dark Mode Logo').setInputFiles('assets/cloud-logo.png');
|
||||
await page.getByLabel('Email Logo').setInputFiles('assets/pingvin-share-logo.png');
|
||||
await page.getByLabel('Default Profile Picture').setInputFiles('assets/pingvin-share-logo.png');
|
||||
await page.getByLabel('Background Image').setInputFiles('assets/clouds.jpg');
|
||||
await page.getByRole('button', { name: 'Save' }).last().click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'Images updated successfully. It may take a few minutes to update.'
|
||||
);
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'Images updated successfully. It may take a few minutes to update.'
|
||||
);
|
||||
|
||||
await page.request
|
||||
.get('/api/application-images/favicon')
|
||||
.then((res) => expect.soft(res.status()).toBe(200));
|
||||
await page.request
|
||||
.get('/api/application-images/logo?light=true')
|
||||
.then((res) => expect.soft(res.status()).toBe(200));
|
||||
await page.request
|
||||
.get('/api/application-images/logo?light=false')
|
||||
.then((res) => expect.soft(res.status()).toBe(200));
|
||||
await page.request
|
||||
.get('/api/application-images/background')
|
||||
.then((res) => expect.soft(res.status()).toBe(200));
|
||||
});
|
||||
await page.request
|
||||
.get('/api/application-images/favicon')
|
||||
.then((res) => expect.soft(res.status()).toBe(200));
|
||||
await page.request
|
||||
.get('/api/application-images/logo?light=true')
|
||||
.then((res) => expect.soft(res.status()).toBe(200));
|
||||
await page.request
|
||||
.get('/api/application-images/logo?light=false')
|
||||
.then((res) => expect.soft(res.status()).toBe(200));
|
||||
await page.request
|
||||
.get('/api/application-images/email')
|
||||
.then((res) => expect.soft(res.status()).toBe(200));
|
||||
await page.request
|
||||
.get('/api/application-images/background')
|
||||
.then((res) => expect.soft(res.status()).toBe(200));
|
||||
});
|
||||
|
||||
test('should only allow png/jpeg for email logo', async ({ page }) => {
|
||||
const emailLogoInput = page.getByLabel('Email Logo');
|
||||
|
||||
await emailLogoInput.setInputFiles('assets/cloud-logo.svg');
|
||||
await page.getByRole('button', { name: 'Save' }).last().click();
|
||||
|
||||
await expect(page.locator('[data-type="error"]')).toHaveText(
|
||||
'File must be of type .png or .jpg/jpeg'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -71,9 +71,9 @@ test('Edit OIDC client', async ({ page }) => {
|
||||
await page.getByLabel('Name').fill('Nextcloud updated');
|
||||
await page.getByTestId('callback-url-1').first().fill('http://nextcloud-updated/auth/callback');
|
||||
await page.locator('[role="tab"][data-value="light-logo"]').first().click();
|
||||
await page.setInputFiles('#oidc-client-logo-light', 'assets/nextcloud-logo.png');
|
||||
await page.setInputFiles('#oidc-client-logo-light', 'assets/cloud-logo.png');
|
||||
await page.locator('[role="tab"][data-value="dark-logo"]').first().click();
|
||||
await page.setInputFiles('#oidc-client-logo-dark', 'assets/nextcloud-logo.png');
|
||||
await page.setInputFiles('#oidc-client-logo-dark', 'assets/cloud-logo.png');
|
||||
await page.getByLabel('Client Launch URL').fill(oidcClient.launchURL);
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user