This commit is contained in:
David Arranz 2024-09-23 18:37:08 +02:00
parent 247d13ffcf
commit 133bcec868
13 changed files with 92 additions and 29 deletions

5
.gitignore vendored
View File

@ -19,7 +19,10 @@ debug*.log*
error*.log*
.*-audit.json
# uploads
server/uploads/dealer-logos/
# misc
.DS_Store
*.pem
*.pem

View File

@ -127,6 +127,8 @@ export const createAxiosDataProvider = (
const formData = new FormData();
formData.append(key || "file", file);
console.log(file);
const response = await httpClient.post<R>(url, formData, {
headers: {
"Content-Type": "multipart/form-data",

1
server/.gitignore vendored
View File

@ -1 +0,0 @@
./uploads/dealer-logos

View File

@ -57,6 +57,7 @@
"dependencies": {
"@joi/date": "^2.1.0",
"@reis/joi-luxon": "^3.0.0",
"@types/mime-types": "^2.1.4",
"bcrypt": "^5.1.1",
"cls-rtracer": "^2.6.3",
"cors": "^2.8.5",

View File

@ -26,7 +26,7 @@ export abstract class ExpressController implements IController {
new URL(`${this.req.protocol}://${this.req.get("host")}${this.req.originalUrl}`).origin
}/api/v1`;
this.file = this.req && this.req["file"]; // <-- ????
//this.file = this.req && this.req["file"]; // <-- ????
this.executeImpl();
}

View File

@ -8,7 +8,10 @@ import { IRepositoryManager } from "@/contexts/common/domain";
import { IInfrastructureError } from "@/contexts/common/infrastructure";
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
import { Result, UniqueID } from "@shared/contexts";
import fs from "fs/promises";
import { IProfileRepository, Profile } from "../domain";
import { ProfileLogotype } from "../domain/entities/ProfileLogotype";
import { IProfileContext } from "../infrastructure";
export interface IUpdateProfileLogoUseCaseRequest extends IUseCaseRequest {
userId: UniqueID;
@ -24,10 +27,12 @@ export class UploadProfileLogoUseCase
{
private _adapter: ISequelizeAdapter;
private _repositoryManager: IRepositoryManager;
private _defaults: Record<string, any>;
constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
constructor(props: IProfileContext) {
this._adapter = props.adapter;
this._repositoryManager = props.repositoryManager;
this._defaults = props.defaults;
}
async execute(
@ -48,13 +53,21 @@ export class UploadProfileLogoUseCase
const profile = exitsOrError.object;
// Borro del logo anterior
this._deleteOldLogo(profile.logo);
// Actualizar el perfil con datos actualizados
profile.logo = file.filename;
profile.logo = ProfileLogotype.create(file.filename).object;
// Guardar los cambios
return this._saveProfile(profile);
}
private async _deleteOldLogo(filename) {
const logo_path = `${this._defaults.dealer_logos_upload_path}/${filename}`;
await fs.rm(logo_path);
}
private async _saveProfile(updatedProfile: Profile) {
// Guardar el contacto
const transaction = this._adapter.startTransaction();

View File

@ -10,6 +10,7 @@ import {
Result,
UniqueID,
} from "@shared/contexts";
import { ProfileLogotype } from "./ProfileLogotype";
export interface IProfileProps {
name: Name;
@ -24,7 +25,7 @@ export interface IProfileProps {
defaultQuoteValidity: Note;
defaultTax: Percentage;
logo: string;
logo: ProfileLogotype;
}
export interface IProfile {
@ -42,7 +43,7 @@ export interface IProfile {
defaultQuoteValidity: Note;
defaultTax: Percentage;
logo: string;
logo: ProfileLogotype;
}
export class Profile extends AggregateRoot<IProfileProps> implements IProfile {
@ -115,11 +116,11 @@ export class Profile extends AggregateRoot<IProfileProps> implements IProfile {
this.props.defaultTax = newDefaultTax;
}
get logo(): string {
get logo(): ProfileLogotype {
return this.props.logo;
}
set logo(newLogo: string) {
set logo(newLogo: ProfileLogotype) {
this.props.logo = newLogo;
}
}

View File

@ -0,0 +1,53 @@
import { config } from "@/config";
import { IValueObjectOptions, Result, ValueObject } from "@shared/contexts";
import fs from "fs/promises";
import mime from "mime-types";
import path from "path";
export interface IProfileLogotypeOptions extends IValueObjectOptions {}
export class ProfileLogotype extends ValueObject<string> {
public static create(value: string, options: IProfileLogotypeOptions = {}) {
const _options = {
label: "logotype",
...options,
};
//const validationResult = ProfileStatus.validate(status, _options);
/*if (validationResult.isFailure) {
return Result.fail(
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options)
);
}*/
return Result.ok(new ProfileLogotype(value));
}
get filename(): string {
return this.props;
}
public async toBase64() {
let data: any;
try {
data = await fs.readFile(this.relativePath());
} catch (err) {
data = await fs.readFile(config.defaults.dealer_logo_placeholder);
}
const mimeType = mime.lookup(data);
return `data:${mimeType};base64,${data.toString("base64")}`;
}
public relativePath() {
// `${context.defaults.dealer_logos_upload_path}/${profile.logo}`
return this.filename
? path.format({
base: config.defaults.dealer_logos_upload_path,
name: this.filename,
})
: config.defaults.dealer_logo_placeholder;
}
public toPrimitive(): string {
return this.filename;
}
}

View File

@ -2,8 +2,6 @@ import { AuthUser } from "@/contexts/auth/domain";
import { Profile } from "@/contexts/profile/domain";
import { IProfileContext } from "@/contexts/profile/infrastructure/Profile.context";
import { IGetProfileResponse_DTO } from "@shared/contexts";
import fs from "fs/promises";
import mime from "mime-types";
export interface IGetProfilePresenter {
map: (
@ -13,22 +11,12 @@ export interface IGetProfilePresenter {
) => Promise<IGetProfileResponse_DTO>;
}
const logoToBase64Image = async (imagePath: string) => {
const data = await fs.readFile(imagePath);
const mimeType = mime.lookup(imagePath);
return `data:${mimeType};base64,${data.toString("base64")}`;
};
export const GetProfilePresenter: IGetProfilePresenter = {
map: async (
profile: Profile,
user: AuthUser,
context: IProfileContext
): Promise<IGetProfileResponse_DTO> => {
const profile_logo = profile.logo
? `${context.defaults.dealer_logos_upload_path}/${profile.logo}`
: context.defaults.dealer_logo_placeholder;
return {
id: user.id.toString(),
name: user.name.toString(),
@ -49,7 +37,7 @@ export const GetProfilePresenter: IGetProfilePresenter = {
default_quote_validity: profile.defaultQuoteValidity.toString(),
default_tax: profile.defaultTax.convertScale(2).toObject(),
logo: await logoToBase64Image(profile_logo),
logo: await profile.logo.toBase64(),
},
};
},

View File

@ -6,8 +6,5 @@ export interface IGetProfileLogoPresenter {
}
export const GetProfileLogoPresenter: IGetProfileLogoPresenter = {
map: (profile: Profile, context: IProfileContext): string =>
profile.logo
? `${context.defaults.dealer_logos_upload_path}/${profile.logo}`
: context.defaults.dealer_logo_placeholder,
map: (profile: Profile, context: IProfileContext): string => profile.logo.relativePath(),
};

View File

@ -32,7 +32,7 @@ export class UploadProfileLogoController extends ExpressController {
async executeImpl() {
const req = this.req as AuthenticatedRequest;
const user = <User>req.user;
const file: Express.Multer.File = this.file;
const file = this.req["file"];
if (!user || !file || !file.filename) {
const errorMessage = "Unexpected missing input data";

View File

@ -14,6 +14,7 @@ import {
UniqueID,
} from "@shared/contexts";
import { IProfileProps, Profile } from "../../domain";
import { ProfileLogotype } from "../../domain/entities/ProfileLogotype";
import { IProfileContext } from "../Profile.context";
export interface IProfileMapper
@ -32,7 +33,7 @@ class ProfileMapper
const status = this.mapsValue(source, "status", DealerStatus.create);
const language = this.mapsValue(source, "lang_code", Language.createFromCode);
const currency = this.mapsValue(source, "currency_code", CurrencyData.createFromCode);
const logo = source.logo;
const logo = this.mapsValue(source, "logo", ProfileLogotype.create);
const contactInformation = this.mapsValue(
source,
@ -95,7 +96,7 @@ class ProfileMapper
default_legal_terms: source.defaultLegalTerms.toPrimitive(),
default_quote_validity: source.defaultQuoteValidity.toPrimitive(),
default_tax: source.defaultTax.convertScale(2).toPrimitive(),
logo: source.logo,
logo: source.logo.toPrimitive(),
};
}
}

View File

@ -978,6 +978,11 @@
resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547"
integrity sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==
"@types/mime-types@^2.1.4":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.4.tgz#93a1933e24fed4fb9e4adc5963a63efcbb3317a2"
integrity sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==
"@types/mime@^1":
version "1.3.5"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690"