feat(web)!: SPA (#5069)

* feat(web): SPA

* chore: remove unnecessary prune

* feat(web): merge with immich-server

* Correct method name

* fix: bugs, docs, workflows, etc.

* chore: keep dockerignore for dev

* chore: remove license

* fix: expose 2283

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Jason Rasmussen 2023-11-17 23:13:36 -05:00 committed by GitHub
parent 5118d261ab
commit adae5dd758
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
115 changed files with 730 additions and 1446 deletions

View file

@ -15,7 +15,7 @@ export class EnablePasswordLoginCommand extends CommandRunner {
const config = await this.configService.getConfig();
config.passwordLogin.enabled = true;
await this.configService.updateConfig(config);
await axios.post('http://localhost:3001/refresh-config');
await axios.post('http://localhost:3001/api/refresh-config');
console.log('Password login has been enabled.');
}
}
@ -33,7 +33,7 @@ export class DisablePasswordLoginCommand extends CommandRunner {
const config = await this.configService.getConfig();
config.passwordLogin.enabled = false;
await this.configService.updateConfig(config);
await axios.post('http://localhost:3001/refresh-config');
await axios.post('http://localhost:3001/api/refresh-config');
console.log('Password login has been disabled.');
}
}

View file

@ -306,9 +306,9 @@ describe(SystemConfigService.name, () => {
});
});
describe('getTheme', () => {
describe('getCustomCss', () => {
it('should return the default theme', async () => {
await expect(sut.getTheme()).resolves.toEqual(defaults.theme);
await expect(sut.getCustomCss()).resolves.toEqual(defaults.theme.customCss);
});
});
});

View file

@ -1,7 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { JobName } from '../job';
import { CommunicationEvent, ICommunicationRepository, IJobRepository, ISystemConfigRepository } from '../repositories';
import { SystemConfigThemeDto } from './dto/system-config-theme.dto';
import { SystemConfigDto, mapConfig } from './dto/system-config.dto';
import { SystemConfigTemplateStorageOptionDto } from './response-dto/system-config-template-storage-option.dto';
import {
@ -31,11 +30,6 @@ export class SystemConfigService {
return this.core.config$;
}
async getTheme(): Promise<SystemConfigThemeDto> {
const { theme } = await this.core.getConfig();
return theme;
}
async getConfig(): Promise<SystemConfigDto> {
const config = await this.core.getConfig();
return mapConfig(config);
@ -87,4 +81,9 @@ export class SystemConfigService {
return JSON.parse(await this.repository.readFile(`./assets/style-${theme}.json`));
}
async getCustomCss(): Promise<string> {
const { theme } = await this.core.getConfig();
return theme.customCss;
}
}

View file

@ -13,6 +13,7 @@ import {
SwaggerDocumentOptions,
SwaggerModule,
} from '@nestjs/swagger';
import { NextFunction, Request, Response } from 'express';
import { writeFileSync } from 'fs';
import path from 'path';
@ -56,6 +57,12 @@ const patchOpenAPI = (document: OpenAPIObject) => {
document.components.schemas = sortKeys(document.components.schemas);
}
for (const [key, value] of Object.entries(document.paths)) {
const newKey = key.replace('/api/', '/');
delete document.paths[key];
document.paths[newKey] = value;
}
for (const path of Object.values(document.paths)) {
const operations = {
get: path.get,
@ -94,6 +101,14 @@ const patchOpenAPI = (document: OpenAPIObject) => {
return document;
};
export const indexFallback = (excludePaths: string[]) => (req: Request, res: Response, next: NextFunction) => {
if (req.url.startsWith('/api') || req.method.toLowerCase() !== 'get' || excludePaths.indexOf(req.url) !== -1) {
next();
} else {
res.sendFile('/www/index.html', { root: process.cwd() });
}
};
export const useSwagger = (app: INestApplication, isDev: boolean) => {
const config = new DocumentBuilder()
.setTitle('Immich')

View file

@ -1,15 +1,34 @@
import { SystemConfigService } from '@app/domain';
import { Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { Controller, Get, Header, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiExcludeEndpoint } from '@nestjs/swagger';
import { PublicRoute } from '../app.guard';
@Controller()
export class AppController {
constructor(private configService: SystemConfigService) {}
constructor(private service: SystemConfigService) {}
@ApiExcludeEndpoint()
@Get('.well-known/immich')
getImmichWellKnown() {
return {
api: {
endpoint: '/api',
},
};
}
@ApiExcludeEndpoint()
@PublicRoute()
@Get('custom.css')
@Header('Content-Type', 'text/css')
getCustomCss() {
return this.service.getCustomCss();
}
@ApiExcludeEndpoint()
@Post('refresh-config')
@HttpCode(HttpStatus.OK)
public reloadConfig() {
return this.configService.refreshConfig();
return this.service.refreshConfig();
}
}

View file

@ -6,7 +6,7 @@ import { NestExpressApplication } from '@nestjs/platform-express';
import { json } from 'body-parser';
import cookieParser from 'cookie-parser';
import { AppModule } from './app.module';
import { useSwagger } from './app.utils';
import { indexFallback, useSwagger } from './app.utils';
const logger = new Logger('ImmichServer');
const port = Number(process.env.SERVER_PORT) || 3001;
@ -24,6 +24,11 @@ export async function bootstrap() {
app.useWebSocketAdapter(new RedisIoAdapter(app));
useSwagger(app, isDev);
const excludePaths = ['/.well-known/immich', '/custom.css'];
app.setGlobalPrefix('api', { exclude: excludePaths });
app.useStaticAssets('www');
app.use(indexFallback(excludePaths));
const server = await app.listen(port);
server.requestTimeout = 30 * 60 * 1000;

View file

@ -3,7 +3,7 @@ import { Logger } from '@nestjs/common';
import { OnGatewayConnection, OnGatewayDisconnect, WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway({ cors: true })
@WebSocketGateway({ cors: true, path: '/api/socket.io' })
export class CommunicationRepository implements OnGatewayConnection, OnGatewayDisconnect, ICommunicationRepository {
private logger = new Logger(CommunicationRepository.name);
private onConnectCallbacks: Callback[] = [];