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* error*.log*
.*-audit.json .*-audit.json
# uploads
server/uploads/dealer-logos/
# misc # misc
.DS_Store .DS_Store
*.pem *.pem

View File

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

1
server/.gitignore vendored
View File

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

View File

@ -57,6 +57,7 @@
"dependencies": { "dependencies": {
"@joi/date": "^2.1.0", "@joi/date": "^2.1.0",
"@reis/joi-luxon": "^3.0.0", "@reis/joi-luxon": "^3.0.0",
"@types/mime-types": "^2.1.4",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"cls-rtracer": "^2.6.3", "cls-rtracer": "^2.6.3",
"cors": "^2.8.5", "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 new URL(`${this.req.protocol}://${this.req.get("host")}${this.req.originalUrl}`).origin
}/api/v1`; }/api/v1`;
this.file = this.req && this.req["file"]; // <-- ???? //this.file = this.req && this.req["file"]; // <-- ????
this.executeImpl(); this.executeImpl();
} }

View File

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

View File

@ -10,6 +10,7 @@ import {
Result, Result,
UniqueID, UniqueID,
} from "@shared/contexts"; } from "@shared/contexts";
import { ProfileLogotype } from "./ProfileLogotype";
export interface IProfileProps { export interface IProfileProps {
name: Name; name: Name;
@ -24,7 +25,7 @@ export interface IProfileProps {
defaultQuoteValidity: Note; defaultQuoteValidity: Note;
defaultTax: Percentage; defaultTax: Percentage;
logo: string; logo: ProfileLogotype;
} }
export interface IProfile { export interface IProfile {
@ -42,7 +43,7 @@ export interface IProfile {
defaultQuoteValidity: Note; defaultQuoteValidity: Note;
defaultTax: Percentage; defaultTax: Percentage;
logo: string; logo: ProfileLogotype;
} }
export class Profile extends AggregateRoot<IProfileProps> implements IProfile { export class Profile extends AggregateRoot<IProfileProps> implements IProfile {
@ -115,11 +116,11 @@ export class Profile extends AggregateRoot<IProfileProps> implements IProfile {
this.props.defaultTax = newDefaultTax; this.props.defaultTax = newDefaultTax;
} }
get logo(): string { get logo(): ProfileLogotype {
return this.props.logo; return this.props.logo;
} }
set logo(newLogo: string) { set logo(newLogo: ProfileLogotype) {
this.props.logo = newLogo; 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 { Profile } from "@/contexts/profile/domain";
import { IProfileContext } from "@/contexts/profile/infrastructure/Profile.context"; import { IProfileContext } from "@/contexts/profile/infrastructure/Profile.context";
import { IGetProfileResponse_DTO } from "@shared/contexts"; import { IGetProfileResponse_DTO } from "@shared/contexts";
import fs from "fs/promises";
import mime from "mime-types";
export interface IGetProfilePresenter { export interface IGetProfilePresenter {
map: ( map: (
@ -13,22 +11,12 @@ export interface IGetProfilePresenter {
) => Promise<IGetProfileResponse_DTO>; ) => 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 = { export const GetProfilePresenter: IGetProfilePresenter = {
map: async ( map: async (
profile: Profile, profile: Profile,
user: AuthUser, user: AuthUser,
context: IProfileContext context: IProfileContext
): Promise<IGetProfileResponse_DTO> => { ): Promise<IGetProfileResponse_DTO> => {
const profile_logo = profile.logo
? `${context.defaults.dealer_logos_upload_path}/${profile.logo}`
: context.defaults.dealer_logo_placeholder;
return { return {
id: user.id.toString(), id: user.id.toString(),
name: user.name.toString(), name: user.name.toString(),
@ -49,7 +37,7 @@ export const GetProfilePresenter: IGetProfilePresenter = {
default_quote_validity: profile.defaultQuoteValidity.toString(), default_quote_validity: profile.defaultQuoteValidity.toString(),
default_tax: profile.defaultTax.convertScale(2).toObject(), 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 = { export const GetProfileLogoPresenter: IGetProfileLogoPresenter = {
map: (profile: Profile, context: IProfileContext): string => map: (profile: Profile, context: IProfileContext): string => profile.logo.relativePath(),
profile.logo
? `${context.defaults.dealer_logos_upload_path}/${profile.logo}`
: context.defaults.dealer_logo_placeholder,
}; };

View File

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

View File

@ -14,6 +14,7 @@ import {
UniqueID, UniqueID,
} from "@shared/contexts"; } from "@shared/contexts";
import { IProfileProps, Profile } from "../../domain"; import { IProfileProps, Profile } from "../../domain";
import { ProfileLogotype } from "../../domain/entities/ProfileLogotype";
import { IProfileContext } from "../Profile.context"; import { IProfileContext } from "../Profile.context";
export interface IProfileMapper export interface IProfileMapper
@ -32,7 +33,7 @@ class ProfileMapper
const status = this.mapsValue(source, "status", DealerStatus.create); const status = this.mapsValue(source, "status", DealerStatus.create);
const language = this.mapsValue(source, "lang_code", Language.createFromCode); const language = this.mapsValue(source, "lang_code", Language.createFromCode);
const currency = this.mapsValue(source, "currency_code", CurrencyData.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( const contactInformation = this.mapsValue(
source, source,
@ -95,7 +96,7 @@ class ProfileMapper
default_legal_terms: source.defaultLegalTerms.toPrimitive(), default_legal_terms: source.defaultLegalTerms.toPrimitive(),
default_quote_validity: source.defaultQuoteValidity.toPrimitive(), default_quote_validity: source.defaultQuoteValidity.toPrimitive(),
default_tax: source.defaultTax.convertScale(2).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" resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547"
integrity sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ== 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": "@types/mime@^1":
version "1.3.5" version "1.3.5"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690"