mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-02-15 10:30:09 +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/logo",
|
||||||
"GET /api/application-images/background",
|
"GET /api/application-images/background",
|
||||||
"GET /api/application-images/favicon",
|
"GET /api/application-images/favicon",
|
||||||
|
"GET /api/application-images/email",
|
||||||
"GET /_app",
|
"GET /_app",
|
||||||
"GET /fonts",
|
"GET /fonts",
|
||||||
"GET /healthz",
|
"GET /healthz",
|
||||||
|
|||||||
@@ -23,11 +23,13 @@ func NewAppImagesController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
group.GET("/application-images/logo", controller.getLogoHandler)
|
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/background", controller.getBackgroundImageHandler)
|
||||||
group.GET("/application-images/favicon", controller.getFaviconHandler)
|
group.GET("/application-images/favicon", controller.getFaviconHandler)
|
||||||
group.GET("/application-images/default-profile-picture", authMiddleware.Add(), controller.getDefaultProfilePicture)
|
group.GET("/application-images/default-profile-picture", authMiddleware.Add(), controller.getDefaultProfilePicture)
|
||||||
|
|
||||||
group.PUT("/application-images/logo", authMiddleware.Add(), controller.updateLogoHandler)
|
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/background", authMiddleware.Add(), controller.updateBackgroundImageHandler)
|
||||||
group.PUT("/application-images/favicon", authMiddleware.Add(), controller.updateFaviconHandler)
|
group.PUT("/application-images/favicon", authMiddleware.Add(), controller.updateFaviconHandler)
|
||||||
group.PUT("/application-images/default-profile-picture", authMiddleware.Add(), controller.updateDefaultProfilePicture)
|
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)
|
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
|
// getBackgroundImageHandler godoc
|
||||||
// @Summary Get background image
|
// @Summary Get background image
|
||||||
// @Description Get the background image for the application
|
// @Description Get the background image for the application
|
||||||
@@ -124,6 +138,37 @@ func (c *AppImagesController) updateLogoHandler(ctx *gin.Context) {
|
|||||||
ctx.Status(http.StatusNoContent)
|
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
|
// updateBackgroundImageHandler godoc
|
||||||
// @Summary Update background image
|
// @Summary Update background image
|
||||||
// @Description Update the application 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]{
|
data := &email.TemplateData[V]{
|
||||||
AppName: dbConfig.AppName.Value,
|
AppName: dbConfig.AppName.Value,
|
||||||
LogoURL: common.EnvConfig.AppURL + "/api/application-images/logo",
|
LogoURL: common.EnvConfig.AppURL + "/api/application-images/email",
|
||||||
Data: tData,
|
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",
|
"favicon": "Favicon",
|
||||||
"light_mode_logo": "Light Mode Logo",
|
"light_mode_logo": "Light Mode Logo",
|
||||||
"dark_mode_logo": "Dark Mode Logo",
|
"dark_mode_logo": "Dark Mode Logo",
|
||||||
|
"email_logo": "Email Logo",
|
||||||
"background_image": "Background Image",
|
"background_image": "Background Image",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"reset_profile_picture_question": "Reset profile picture?",
|
"reset_profile_picture_question": "Reset profile picture?",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
cachedApplicationLogo,
|
cachedApplicationLogo,
|
||||||
cachedBackgroundImage,
|
cachedBackgroundImage,
|
||||||
cachedDefaultProfilePicture,
|
cachedDefaultProfilePicture,
|
||||||
|
cachedEmailLogo,
|
||||||
cachedProfilePicture
|
cachedProfilePicture
|
||||||
} from '$lib/utils/cached-image-util';
|
} from '$lib/utils/cached-image-util';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
@@ -46,6 +47,14 @@ export default class AppConfigService extends APIService {
|
|||||||
cachedApplicationLogo.bustCache(light);
|
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) => {
|
updateDefaultProfilePicture = async (defaultProfilePicture: File) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', defaultProfilePicture);
|
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 = {
|
export const cachedDefaultProfilePicture: CachableImage = {
|
||||||
getUrl: () =>
|
getUrl: () =>
|
||||||
getCachedImageUrl(
|
getCachedImageUrl(
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
async function updateImages(
|
async function updateImages(
|
||||||
logoLight: File | undefined,
|
logoLight: File | undefined,
|
||||||
logoDark: File | undefined,
|
logoDark: File | undefined,
|
||||||
|
logoEmail: File | undefined,
|
||||||
defaultProfilePicture: File | null | undefined,
|
defaultProfilePicture: File | null | undefined,
|
||||||
backgroundImage: File | undefined,
|
backgroundImage: File | undefined,
|
||||||
favicon: File | undefined
|
favicon: File | undefined
|
||||||
@@ -56,6 +57,10 @@
|
|||||||
? appConfigService.updateLogo(logoDark, false)
|
? appConfigService.updateLogo(logoDark, false)
|
||||||
: Promise.resolve();
|
: Promise.resolve();
|
||||||
|
|
||||||
|
const emailLogoPromise = logoEmail
|
||||||
|
? appConfigService.updateEmailLogo(logoEmail)
|
||||||
|
: Promise.resolve();
|
||||||
|
|
||||||
const defaultProfilePicturePromise =
|
const defaultProfilePicturePromise =
|
||||||
defaultProfilePicture === null
|
defaultProfilePicture === null
|
||||||
? appConfigService.deleteDefaultProfilePicture()
|
? appConfigService.deleteDefaultProfilePicture()
|
||||||
@@ -70,6 +75,7 @@
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
lightLogoPromise,
|
lightLogoPromise,
|
||||||
darkLogoPromise,
|
darkLogoPromise,
|
||||||
|
emailLogoPromise,
|
||||||
defaultProfilePicturePromise,
|
defaultProfilePicturePromise,
|
||||||
backgroundImagePromise,
|
backgroundImagePromise,
|
||||||
faviconPromise
|
faviconPromise
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
import {
|
import {
|
||||||
cachedApplicationLogo,
|
cachedApplicationLogo,
|
||||||
cachedBackgroundImage,
|
cachedBackgroundImage,
|
||||||
cachedDefaultProfilePicture
|
cachedDefaultProfilePicture,
|
||||||
|
cachedEmailLogo
|
||||||
} from '$lib/utils/cached-image-util';
|
} from '$lib/utils/cached-image-util';
|
||||||
import ApplicationImage from './application-image.svelte';
|
import ApplicationImage from './application-image.svelte';
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
callback: (
|
callback: (
|
||||||
logoLight: File | undefined,
|
logoLight: File | undefined,
|
||||||
logoDark: File | undefined,
|
logoDark: File | undefined,
|
||||||
|
logoEmail: File | undefined,
|
||||||
defaultProfilePicture: File | null | undefined,
|
defaultProfilePicture: File | null | undefined,
|
||||||
backgroundImage: File | undefined,
|
backgroundImage: File | undefined,
|
||||||
favicon: File | undefined
|
favicon: File | undefined
|
||||||
@@ -22,6 +24,7 @@
|
|||||||
|
|
||||||
let logoLight = $state<File | undefined>();
|
let logoLight = $state<File | undefined>();
|
||||||
let logoDark = $state<File | undefined>();
|
let logoDark = $state<File | undefined>();
|
||||||
|
let logoEmail = $state<File | undefined>();
|
||||||
let defaultProfilePicture = $state<File | null | undefined>();
|
let defaultProfilePicture = $state<File | null | undefined>();
|
||||||
let backgroundImage = $state<File | undefined>();
|
let backgroundImage = $state<File | undefined>();
|
||||||
let favicon = $state<File | undefined>();
|
let favicon = $state<File | undefined>();
|
||||||
@@ -54,6 +57,15 @@
|
|||||||
imageURL={cachedApplicationLogo.getUrl(false)}
|
imageURL={cachedApplicationLogo.getUrl(false)}
|
||||||
forceColorScheme="dark"
|
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
|
<ApplicationImage
|
||||||
id="default-profile-picture"
|
id="default-profile-picture"
|
||||||
imageClass="size-24"
|
imageClass="size-24"
|
||||||
@@ -75,7 +87,8 @@
|
|||||||
<Button
|
<Button
|
||||||
class="mt-5"
|
class="mt-5"
|
||||||
usePromiseLoading
|
usePromiseLoading
|
||||||
onclick={() => callback(logoLight, logoDark, defaultProfilePicture, backgroundImage, favicon)}
|
onclick={() =>
|
||||||
|
callback(logoLight, logoDark, logoEmail, defaultProfilePicture, backgroundImage, favicon)}
|
||||||
>{m.save()}</Button
|
>{m.save()}</Button
|
||||||
>
|
>
|
||||||
</div>
|
</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();
|
await expect(page.getByLabel('API Key Expiration')).toBeChecked();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Update application images', async ({ page }) => {
|
test.describe('Update application images', () => {
|
||||||
await page.getByRole('button', { name: 'Expand card' }).nth(4).click();
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.getByRole('button', { name: 'Expand card' }).nth(4).click();
|
||||||
|
});
|
||||||
|
|
||||||
await page.getByLabel('Favicon').setInputFiles('assets/w3-schools-favicon.ico');
|
test('should upload images', async ({ page }) => {
|
||||||
await page.getByLabel('Light Mode Logo').setInputFiles('assets/pingvin-share-logo.png');
|
await page.getByLabel('Favicon').setInputFiles('assets/w3-schools-favicon.ico');
|
||||||
await page.getByLabel('Dark Mode Logo').setInputFiles('assets/nextcloud-logo.png');
|
await page.getByLabel('Light Mode Logo').setInputFiles('assets/pingvin-share-logo.png');
|
||||||
await page.getByLabel('Default Profile Picture').setInputFiles('assets/pingvin-share-logo.png');
|
await page.getByLabel('Dark Mode Logo').setInputFiles('assets/cloud-logo.png');
|
||||||
await page.getByLabel('Background Image').setInputFiles('assets/clouds.jpg');
|
await page.getByLabel('Email Logo').setInputFiles('assets/pingvin-share-logo.png');
|
||||||
await page.getByRole('button', { name: 'Save' }).last().click();
|
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(
|
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||||
'Images updated successfully. It may take a few minutes to update.'
|
'Images updated successfully. It may take a few minutes to update.'
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.request
|
await page.request
|
||||||
.get('/api/application-images/favicon')
|
.get('/api/application-images/favicon')
|
||||||
.then((res) => expect.soft(res.status()).toBe(200));
|
.then((res) => expect.soft(res.status()).toBe(200));
|
||||||
await page.request
|
await page.request
|
||||||
.get('/api/application-images/logo?light=true')
|
.get('/api/application-images/logo?light=true')
|
||||||
.then((res) => expect.soft(res.status()).toBe(200));
|
.then((res) => expect.soft(res.status()).toBe(200));
|
||||||
await page.request
|
await page.request
|
||||||
.get('/api/application-images/logo?light=false')
|
.get('/api/application-images/logo?light=false')
|
||||||
.then((res) => expect.soft(res.status()).toBe(200));
|
.then((res) => expect.soft(res.status()).toBe(200));
|
||||||
await page.request
|
await page.request
|
||||||
.get('/api/application-images/background')
|
.get('/api/application-images/email')
|
||||||
.then((res) => expect.soft(res.status()).toBe(200));
|
.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.getByLabel('Name').fill('Nextcloud updated');
|
||||||
await page.getByTestId('callback-url-1').first().fill('http://nextcloud-updated/auth/callback');
|
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.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.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.getByLabel('Client Launch URL').fill(oidcClient.launchURL);
|
||||||
await page.getByRole('button', { name: 'Save' }).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user