mirror of
https://github.com/immich-app/immich.git
synced 2024-11-15 18:08:48 -07:00
feat(web): send test email button (#10011)
* feat(web): test email button * openapi * UI button * Show notification * pr feedback * remove jobs * send email directly from repository and add feedback * avoid sending many emails * linter * pr feedback * lint * lint * lint
This commit is contained in:
parent
d5f3d98dfc
commit
9ac2ac2fcb
BIN
docs/static/img/ios-app-store-badge.png
vendored
BIN
docs/static/img/ios-app-store-badge.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 70 KiB |
1
mobile/openapi/README.md
generated
1
mobile/openapi/README.md
generated
@ -144,6 +144,7 @@ Class | Method | HTTP request | Description
|
|||||||
*MemoriesApi* | [**removeMemoryAssets**](doc//MemoriesApi.md#removememoryassets) | **DELETE** /memories/{id}/assets |
|
*MemoriesApi* | [**removeMemoryAssets**](doc//MemoriesApi.md#removememoryassets) | **DELETE** /memories/{id}/assets |
|
||||||
*MemoriesApi* | [**searchMemories**](doc//MemoriesApi.md#searchmemories) | **GET** /memories |
|
*MemoriesApi* | [**searchMemories**](doc//MemoriesApi.md#searchmemories) | **GET** /memories |
|
||||||
*MemoriesApi* | [**updateMemory**](doc//MemoriesApi.md#updatememory) | **PUT** /memories/{id} |
|
*MemoriesApi* | [**updateMemory**](doc//MemoriesApi.md#updatememory) | **PUT** /memories/{id} |
|
||||||
|
*NotificationsApi* | [**sendTestEmail**](doc//NotificationsApi.md#sendtestemail) | **POST** /notifications/test-email |
|
||||||
*OAuthApi* | [**finishOAuth**](doc//OAuthApi.md#finishoauth) | **POST** /oauth/callback |
|
*OAuthApi* | [**finishOAuth**](doc//OAuthApi.md#finishoauth) | **POST** /oauth/callback |
|
||||||
*OAuthApi* | [**linkOAuthAccount**](doc//OAuthApi.md#linkoauthaccount) | **POST** /oauth/link |
|
*OAuthApi* | [**linkOAuthAccount**](doc//OAuthApi.md#linkoauthaccount) | **POST** /oauth/link |
|
||||||
*OAuthApi* | [**redirectOAuthToMobile**](doc//OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect |
|
*OAuthApi* | [**redirectOAuthToMobile**](doc//OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect |
|
||||||
|
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@ -43,6 +43,7 @@ part 'api/jobs_api.dart';
|
|||||||
part 'api/libraries_api.dart';
|
part 'api/libraries_api.dart';
|
||||||
part 'api/map_api.dart';
|
part 'api/map_api.dart';
|
||||||
part 'api/memories_api.dart';
|
part 'api/memories_api.dart';
|
||||||
|
part 'api/notifications_api.dart';
|
||||||
part 'api/o_auth_api.dart';
|
part 'api/o_auth_api.dart';
|
||||||
part 'api/partners_api.dart';
|
part 'api/partners_api.dart';
|
||||||
part 'api/people_api.dart';
|
part 'api/people_api.dart';
|
||||||
|
57
mobile/openapi/lib/api/notifications_api.dart
generated
Normal file
57
mobile/openapi/lib/api/notifications_api.dart
generated
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationsApi {
|
||||||
|
NotificationsApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
|
||||||
|
|
||||||
|
final ApiClient apiClient;
|
||||||
|
|
||||||
|
/// Performs an HTTP 'POST /notifications/test-email' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [SystemConfigSmtpDto] systemConfigSmtpDto (required):
|
||||||
|
Future<Response> sendTestEmailWithHttpInfo(SystemConfigSmtpDto systemConfigSmtpDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/notifications/test-email';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = systemConfigSmtpDto;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'POST',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [SystemConfigSmtpDto] systemConfigSmtpDto (required):
|
||||||
|
Future<void> sendTestEmail(SystemConfigSmtpDto systemConfigSmtpDto,) async {
|
||||||
|
final response = await sendTestEmailWithHttpInfo(systemConfigSmtpDto,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3466,6 +3466,41 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/notifications/test-email": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "sendTestEmail",
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/SystemConfigSmtpDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/oauth/authorize": {
|
"/oauth/authorize": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "startOAuth",
|
"operationId": "startOAuth",
|
||||||
|
@ -554,6 +554,19 @@ export type MemoryUpdateDto = {
|
|||||||
memoryAt?: string;
|
memoryAt?: string;
|
||||||
seenAt?: string;
|
seenAt?: string;
|
||||||
};
|
};
|
||||||
|
export type SystemConfigSmtpTransportDto = {
|
||||||
|
host: string;
|
||||||
|
ignoreCert: boolean;
|
||||||
|
password: string;
|
||||||
|
port: number;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
export type SystemConfigSmtpDto = {
|
||||||
|
enabled: boolean;
|
||||||
|
"from": string;
|
||||||
|
replyTo: string;
|
||||||
|
transport: SystemConfigSmtpTransportDto;
|
||||||
|
};
|
||||||
export type OAuthConfigDto = {
|
export type OAuthConfigDto = {
|
||||||
redirectUri: string;
|
redirectUri: string;
|
||||||
};
|
};
|
||||||
@ -990,19 +1003,6 @@ export type SystemConfigMapDto = {
|
|||||||
export type SystemConfigNewVersionCheckDto = {
|
export type SystemConfigNewVersionCheckDto = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
export type SystemConfigSmtpTransportDto = {
|
|
||||||
host: string;
|
|
||||||
ignoreCert: boolean;
|
|
||||||
password: string;
|
|
||||||
port: number;
|
|
||||||
username: string;
|
|
||||||
};
|
|
||||||
export type SystemConfigSmtpDto = {
|
|
||||||
enabled: boolean;
|
|
||||||
"from": string;
|
|
||||||
replyTo: string;
|
|
||||||
transport: SystemConfigSmtpTransportDto;
|
|
||||||
};
|
|
||||||
export type SystemConfigNotificationsDto = {
|
export type SystemConfigNotificationsDto = {
|
||||||
smtp: SystemConfigSmtpDto;
|
smtp: SystemConfigSmtpDto;
|
||||||
};
|
};
|
||||||
@ -2022,6 +2022,15 @@ export function addMemoryAssets({ id, bulkIdsDto }: {
|
|||||||
body: bulkIdsDto
|
body: bulkIdsDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
export function sendTestEmail({ systemConfigSmtpDto }: {
|
||||||
|
systemConfigSmtpDto: SystemConfigSmtpDto;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchText("/notifications/test-email", oazapfts.json({
|
||||||
|
...opts,
|
||||||
|
method: "POST",
|
||||||
|
body: systemConfigSmtpDto
|
||||||
|
})));
|
||||||
|
}
|
||||||
export function startOAuth({ oAuthConfigDto }: {
|
export function startOAuth({ oAuthConfigDto }: {
|
||||||
oAuthConfigDto: OAuthConfigDto;
|
oAuthConfigDto: OAuthConfigDto;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
@ -14,6 +14,7 @@ import { JobController } from 'src/controllers/job.controller';
|
|||||||
import { LibraryController } from 'src/controllers/library.controller';
|
import { LibraryController } from 'src/controllers/library.controller';
|
||||||
import { MapController } from 'src/controllers/map.controller';
|
import { MapController } from 'src/controllers/map.controller';
|
||||||
import { MemoryController } from 'src/controllers/memory.controller';
|
import { MemoryController } from 'src/controllers/memory.controller';
|
||||||
|
import { NotificationController } from 'src/controllers/notification.controller';
|
||||||
import { OAuthController } from 'src/controllers/oauth.controller';
|
import { OAuthController } from 'src/controllers/oauth.controller';
|
||||||
import { PartnerController } from 'src/controllers/partner.controller';
|
import { PartnerController } from 'src/controllers/partner.controller';
|
||||||
import { PersonController } from 'src/controllers/person.controller';
|
import { PersonController } from 'src/controllers/person.controller';
|
||||||
@ -46,6 +47,7 @@ export const controllers = [
|
|||||||
LibraryController,
|
LibraryController,
|
||||||
MapController,
|
MapController,
|
||||||
MemoryController,
|
MemoryController,
|
||||||
|
NotificationController,
|
||||||
OAuthController,
|
OAuthController,
|
||||||
PartnerController,
|
PartnerController,
|
||||||
PersonController,
|
PersonController,
|
||||||
|
19
server/src/controllers/notification.controller.ts
Normal file
19
server/src/controllers/notification.controller.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Body, Controller, HttpCode, Post } from '@nestjs/common';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
||||||
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { NotificationService } from 'src/services/notification.service';
|
||||||
|
|
||||||
|
@ApiTags('Notifications')
|
||||||
|
@Controller('notifications')
|
||||||
|
export class NotificationController {
|
||||||
|
constructor(private service: NotificationService) {}
|
||||||
|
|
||||||
|
@Post('test-email')
|
||||||
|
@HttpCode(200)
|
||||||
|
@Authenticated({ admin: true })
|
||||||
|
sendTestEmail(@Auth() auth: AuthDto, @Body() dto: SystemConfigSmtpDto) {
|
||||||
|
return this.service.sendTestEmail(auth.user.id, dto);
|
||||||
|
}
|
||||||
|
}
|
@ -394,7 +394,7 @@ class SystemConfigSmtpTransportDto {
|
|||||||
password!: string;
|
password!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SystemConfigSmtpDto {
|
export class SystemConfigSmtpDto {
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
enabled!: boolean;
|
enabled!: boolean;
|
||||||
|
|
||||||
|
134
server/src/emails/test.email.tsx
Normal file
134
server/src/emails/test.email.tsx
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
Column,
|
||||||
|
Container,
|
||||||
|
Head,
|
||||||
|
Hr,
|
||||||
|
Html,
|
||||||
|
Img,
|
||||||
|
Link,
|
||||||
|
Preview,
|
||||||
|
Row,
|
||||||
|
Section,
|
||||||
|
Text,
|
||||||
|
} from '@react-email/components';
|
||||||
|
import * as CSS from 'csstype';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { TestEmailProps } from 'src/interfaces/notification.interface';
|
||||||
|
|
||||||
|
export const TestEmail = ({ baseUrl, displayName }: TestEmailProps) => (
|
||||||
|
<Html>
|
||||||
|
<Head />
|
||||||
|
<Preview>This is a test email from Immich</Preview>
|
||||||
|
<Body
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
color: 'rgb(66, 80, 175)',
|
||||||
|
fontFamily: 'Overpass, sans-serif',
|
||||||
|
fontSize: '18px',
|
||||||
|
lineHeight: '24px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container
|
||||||
|
style={{
|
||||||
|
width: '480px',
|
||||||
|
maxWidth: '100%',
|
||||||
|
padding: '10px',
|
||||||
|
margin: '0 auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Section
|
||||||
|
style={{
|
||||||
|
padding: '36px',
|
||||||
|
tableLayout: 'fixed',
|
||||||
|
backgroundColor: 'rgb(226, 232, 240)',
|
||||||
|
border: 'solid 0px rgb(248 113 113)',
|
||||||
|
borderRadius: '50px',
|
||||||
|
textAlign: 'center' as const,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Img
|
||||||
|
src="https://immich.app/img/immich-logo-inline-light.png"
|
||||||
|
alt="Immich"
|
||||||
|
style={{
|
||||||
|
height: 'auto',
|
||||||
|
margin: '0 auto 48px auto',
|
||||||
|
width: '50%',
|
||||||
|
alignSelf: 'center',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text style={text}>
|
||||||
|
Hey <strong>{displayName}</strong>, this is the test email from your Immich Instance
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<Link style={{ marginTop: '50px' }} href={baseUrl}>
|
||||||
|
{baseUrl}
|
||||||
|
</Link>
|
||||||
|
</Row>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Hr style={{ color: 'rgb(66, 80, 175)', marginTop: '24px' }} />
|
||||||
|
|
||||||
|
<Section style={{ textAlign: 'center' }}>
|
||||||
|
<Row>
|
||||||
|
<Column align="center">
|
||||||
|
<Link href="https://play.google.com/store/apps/details?id=app.alextran.immich">
|
||||||
|
<Img src={`https://immich.app/img/google-play-badge.png`} height="96px" alt="Immich" />
|
||||||
|
</Link>
|
||||||
|
<Link href="https://apps.apple.com/sg/app/immich/id1613945652">
|
||||||
|
<Img
|
||||||
|
src={`https://immich.app/img/ios-app-store-badge.png`}
|
||||||
|
alt="Immich"
|
||||||
|
style={{ height: '72px', padding: '14px' }}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: '#6a737d',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
textAlign: 'center' as const,
|
||||||
|
marginTop: '12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link href="https://immich.app">Immich</Link> project is available under GNU AGPL v3 license.
|
||||||
|
</Text>
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
|
||||||
|
TestEmail.PreviewProps = {
|
||||||
|
baseUrl: 'https://demo.immich.app/auth/login',
|
||||||
|
displayName: 'Alan Turing',
|
||||||
|
} as TestEmailProps;
|
||||||
|
|
||||||
|
export default TestEmail;
|
||||||
|
|
||||||
|
const text = {
|
||||||
|
margin: '0 0 24px 0',
|
||||||
|
textAlign: 'left' as const,
|
||||||
|
fontSize: '18px',
|
||||||
|
lineHeight: '24px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const button: CSS.Properties = {
|
||||||
|
backgroundColor: 'rgb(66, 80, 175)',
|
||||||
|
margin: '1em 0',
|
||||||
|
padding: '0.75em 3em',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '1em',
|
||||||
|
fontWeight: 700,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
borderRadius: '9999px',
|
||||||
|
};
|
@ -26,6 +26,8 @@ export type SmtpOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export enum EmailTemplate {
|
export enum EmailTemplate {
|
||||||
|
TEST_EMAIL = 'test',
|
||||||
|
|
||||||
// AUTH
|
// AUTH
|
||||||
WELCOME = 'welcome',
|
WELCOME = 'welcome',
|
||||||
RESET_PASSWORD = 'reset-password',
|
RESET_PASSWORD = 'reset-password',
|
||||||
@ -39,6 +41,10 @@ interface BaseEmailProps {
|
|||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TestEmailProps extends BaseEmailProps {
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WelcomeEmailProps extends BaseEmailProps {
|
export interface WelcomeEmailProps extends BaseEmailProps {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
username: string;
|
username: string;
|
||||||
@ -61,6 +67,10 @@ export interface AlbumUpdateEmailProps extends BaseEmailProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type EmailRenderRequest =
|
export type EmailRenderRequest =
|
||||||
|
| {
|
||||||
|
template: EmailTemplate.TEST_EMAIL;
|
||||||
|
data: TestEmailProps;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
template: EmailTemplate.WELCOME;
|
template: EmailTemplate.WELCOME;
|
||||||
data: WelcomeEmailProps;
|
data: WelcomeEmailProps;
|
||||||
|
@ -4,6 +4,7 @@ import { createTransport } from 'nodemailer';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { AlbumInviteEmail } from 'src/emails/album-invite.email';
|
import { AlbumInviteEmail } from 'src/emails/album-invite.email';
|
||||||
import { AlbumUpdateEmail } from 'src/emails/album-update.email';
|
import { AlbumUpdateEmail } from 'src/emails/album-update.email';
|
||||||
|
import { TestEmail } from 'src/emails/test.email';
|
||||||
import { WelcomeEmail } from 'src/emails/welcome.email';
|
import { WelcomeEmail } from 'src/emails/welcome.email';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import {
|
import {
|
||||||
@ -58,6 +59,10 @@ export class NotificationRepository implements INotificationRepository {
|
|||||||
|
|
||||||
private render({ template, data }: EmailRenderRequest): React.FunctionComponentElement<any> {
|
private render({ template, data }: EmailRenderRequest): React.FunctionComponentElement<any> {
|
||||||
switch (template) {
|
switch (template) {
|
||||||
|
case EmailTemplate.TEST_EMAIL: {
|
||||||
|
return React.createElement(TestEmail, data);
|
||||||
|
}
|
||||||
|
|
||||||
case EmailTemplate.WELCOME: {
|
case EmailTemplate.WELCOME: {
|
||||||
return React.createElement(WelcomeEmail, data);
|
return React.createElement(WelcomeEmail, data);
|
||||||
}
|
}
|
||||||
@ -84,6 +89,7 @@ export class NotificationRepository implements INotificationRepository {
|
|||||||
pass: options.password,
|
pass: options.password,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
connectionTimeout: 5000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
|
||||||
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
|
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { OnServerEvent } from 'src/decorators';
|
import { OnServerEvent } from 'src/decorators';
|
||||||
|
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
||||||
import { AlbumEntity } from 'src/entities/album.entity';
|
import { AlbumEntity } from 'src/entities/album.entity';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
@ -55,6 +56,38 @@ export class NotificationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendTestEmail(id: string, dto: SystemConfigSmtpDto) {
|
||||||
|
const user = await this.userRepository.get(id, { withDeleted: false });
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.notificationRepository.verifySmtp(dto.transport);
|
||||||
|
} catch (error) {
|
||||||
|
throw new HttpException('Failed to verify SMTP configuration', HttpStatus.BAD_REQUEST, { cause: error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { server } = await this.configCore.getConfig();
|
||||||
|
const { html, text } = this.notificationRepository.renderEmail({
|
||||||
|
template: EmailTemplate.TEST_EMAIL,
|
||||||
|
data: {
|
||||||
|
baseUrl: server.externalDomain || DEFAULT_EXTERNAL_DOMAIN,
|
||||||
|
displayName: user.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.notificationRepository.sendEmail({
|
||||||
|
to: user.email,
|
||||||
|
subject: 'Test email from Immich',
|
||||||
|
html,
|
||||||
|
text,
|
||||||
|
from: dto.from,
|
||||||
|
replyTo: dto.replyTo || dto.from,
|
||||||
|
smtp: dto.transport,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async handleUserSignup({ id, tempPassword }: INotifySignupJob) {
|
async handleUserSignup({ id, tempPassword }: INotifySignupJob) {
|
||||||
const user = await this.userRepository.get(id, { withDeleted: false });
|
const user = await this.userRepository.get(id, { withDeleted: false });
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { SystemConfigDto } from '@immich/sdk';
|
import { sendTestEmail, type SystemConfigDto } from '@immich/sdk';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
@ -11,13 +11,57 @@
|
|||||||
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
|
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
|
||||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||||
|
import {
|
||||||
|
NotificationType,
|
||||||
|
notificationController,
|
||||||
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
|
import { user } from '$lib/stores/user.store';
|
||||||
|
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
|
||||||
export let savedConfig: SystemConfigDto;
|
export let savedConfig: SystemConfigDto;
|
||||||
export let defaultConfig: SystemConfigDto;
|
export let defaultConfig: SystemConfigDto;
|
||||||
export let config: SystemConfigDto; // this is the config that is being edited
|
export let config: SystemConfigDto; // this is the config that is being edited
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
|
let isSending = false;
|
||||||
const dispatch = createEventDispatcher<SettingsEventType>();
|
const dispatch = createEventDispatcher<SettingsEventType>();
|
||||||
|
|
||||||
|
const handleSendTestEmail = async () => {
|
||||||
|
if (isSending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSending = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendTestEmail({
|
||||||
|
systemConfigSmtpDto: {
|
||||||
|
enabled: config.notifications.smtp.enabled,
|
||||||
|
transport: {
|
||||||
|
host: config.notifications.smtp.transport.host,
|
||||||
|
port: config.notifications.smtp.transport.port,
|
||||||
|
username: config.notifications.smtp.transport.username,
|
||||||
|
password: config.notifications.smtp.transport.password,
|
||||||
|
ignoreCert: config.notifications.smtp.transport.ignoreCert,
|
||||||
|
},
|
||||||
|
from: config.notifications.smtp.from,
|
||||||
|
replyTo: config.notifications.smtp.from,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
notificationController.show({
|
||||||
|
type: NotificationType.Info,
|
||||||
|
message: $t('admin.notification_email_test_email_sent', { values: { email: $user.email } }),
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch('save', { notifications: config.notifications });
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('admin.notification_email_test_email_failed'));
|
||||||
|
} finally {
|
||||||
|
isSending = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -93,6 +137,15 @@
|
|||||||
bind:value={config.notifications.smtp.from}
|
bind:value={config.notifications.smtp.from}
|
||||||
isEdited={config.notifications.smtp.from !== savedConfig.notifications.smtp.from}
|
isEdited={config.notifications.smtp.from !== savedConfig.notifications.smtp.from}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="flex gap-2 place-items-center">
|
||||||
|
<Button size="sm" disabled={disabled || !config.notifications.smtp.enabled} on:click={handleSendTestEmail}
|
||||||
|
>{$t('admin.notification_email_sent_test_email_button')}
|
||||||
|
</Button>
|
||||||
|
{#if isSending}
|
||||||
|
<LoadingSpinner />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SettingAccordion>
|
</SettingAccordion>
|
||||||
</div>
|
</div>
|
||||||
|
@ -102,6 +102,9 @@
|
|||||||
"notification_email_password_description": "Password to use when authenticating with the email server",
|
"notification_email_password_description": "Password to use when authenticating with the email server",
|
||||||
"notification_email_port_description": "Port of the email server (e.g 25, 465, or 587)",
|
"notification_email_port_description": "Port of the email server (e.g 25, 465, or 587)",
|
||||||
"notification_email_setting_description": "Settings for sending email notifications",
|
"notification_email_setting_description": "Settings for sending email notifications",
|
||||||
|
"notification_email_test_email_failed": "Failed to send test email, check your values",
|
||||||
|
"notification_email_test_email_sent": "A test email has been sent to {email}. Please check your inbox.",
|
||||||
|
"notification_email_sent_test_email_button": "Send test email and save",
|
||||||
"notification_email_username_description": "Username to use when authenticating with the email server",
|
"notification_email_username_description": "Username to use when authenticating with the email server",
|
||||||
"notification_enable_email_notifications": "Enable email notifications",
|
"notification_enable_email_notifications": "Enable email notifications",
|
||||||
"notification_settings": "Notification Settings",
|
"notification_settings": "Notification Settings",
|
||||||
|
Loading…
Reference in New Issue
Block a user