diff --git a/src/app/owner/profile/profile.controller.ts b/src/app/owner/profile/profile.controller.ts index 1e6bc83..7492f0b 100644 --- a/src/app/owner/profile/profile.controller.ts +++ b/src/app/owner/profile/profile.controller.ts @@ -2,7 +2,10 @@ import { Me } from '@core/decorators/user.decorator'; import { OwnerAuthGuard } from '@core/guards/auth.guard'; import { OwnerGuard } from '@core/guards/owner.guard'; import { AuthService } from '@core/services/auth.service'; +import { AwsService } from '@core/services/aws.service'; import { PermAct, PermOwner } from '@core/services/role.service'; +import { Media } from '@db/entities/core/media.entity'; +import { Location } from '@db/entities/owner/location.entity'; import { Owner, OwnerStatus } from '@db/entities/owner/owner.entity'; import { OwnerTransformer } from '@db/transformers/owner.transformer'; import { NotPermittedException } from '@lib/exceptions/not-permitted.exception'; @@ -10,13 +13,13 @@ import { ValidationException } from '@lib/exceptions/validation.exception'; import { time } from '@lib/helpers/time.helper'; import { Validator } from '@lib/helpers/validator.helper'; import { Permissions } from '@lib/rbac'; -import { BadRequestException, Body, Controller, Get, Param, Post, Put, Res, UseGuards } from '@nestjs/common'; -import { Location } from '@db/entities/owner/location.entity'; +import { BadRequestException, Body, Controller, Delete, Get, Param, Post, Put, Req, Res, UseGuards } from '@nestjs/common'; +import { isEmpty } from 'lodash'; @Controller() @UseGuards(OwnerAuthGuard()) export class ProfileController { - constructor(private auth: AuthService) {} + constructor(private auth: AuthService, private aws: AwsService) {} @Get() @UseGuards(OwnerGuard) @@ -100,4 +103,30 @@ export class ProfileController { return response.noContent(); } + + @Post('/avatar') + @UseGuards(OwnerGuard) + @Permissions(`${PermOwner.Profile}@${PermAct.C}`) + async uploadAvatar(@Req() request, @Res() response, @Me() me: Owner) { + const file = await this.aws.uploadFile(request, response, 'image', { dynamicPath: `staff/${me.id}/avatar` }); + if (!file || isEmpty(file)) { + throw new BadRequestException('Unable to upload image'); + } + + if (await me.image) { + await this.aws.removeFile(await me.image); + } + + await Media.build(me, file); + + await response.item(me, OwnerTransformer); + } + + @Delete('/avatar') + @UseGuards(OwnerGuard) + @Permissions(`${PermOwner.Profile}@${PermAct.D}`) + async deleteAvatar(@Res() response, @Me() me: Owner) { + await Media.delete({ owner_id: me.id }); + return response.noContent(); + } } diff --git a/src/app/owner/restaurant/location/location.controller.ts b/src/app/owner/restaurant/location/location.controller.ts index 0c6575c..8798caf 100644 --- a/src/app/owner/restaurant/location/location.controller.ts +++ b/src/app/owner/restaurant/location/location.controller.ts @@ -86,6 +86,10 @@ export class LocationController { throw new BadRequestException('Location has already existed.'); } + if (isTrue(body.is_default)) { + await Location.update({ restaurant_id: rest.id }, { is_default: !isTrue(body.is_default) }); + } + const loc = await Location.findOneByOrFail({ id: param.location_id }); loc.name = body.name; loc.is_default = isTrue(body.is_default); diff --git a/src/app/owner/restaurant/restaurant.controller.ts b/src/app/owner/restaurant/restaurant.controller.ts index 628f0b2..724b03e 100644 --- a/src/app/owner/restaurant/restaurant.controller.ts +++ b/src/app/owner/restaurant/restaurant.controller.ts @@ -1,17 +1,23 @@ import { Rest } from '@core/decorators/restaurant.decorator'; import { OwnerAuthGuard } from '@core/guards/auth.guard'; import { OwnerGuard } from '@core/guards/owner.guard'; +import { AwsService } from '@core/services/aws.service'; import { PermAct, PermOwner } from '@core/services/role.service'; +import { Media } from '@db/entities/core/media.entity'; import { Restaurant } from '@db/entities/owner/restaurant.entity'; import { RestaurantTransformer } from '@db/transformers/restaurant.transformer'; import { ValidationException } from '@lib/exceptions/validation.exception'; import { Validator } from '@lib/helpers/validator.helper'; import { Permissions } from '@lib/rbac'; -import { Body, Controller, Get, Put, Res, UseGuards } from '@nestjs/common'; +import AppDataSource from '@lib/typeorm/datasource.typeorm'; +import { BadRequestException, Body, Controller, Delete, Get, Param, Post, Put, Req, Res, UseGuards } from '@nestjs/common'; +import { get, isEmpty } from 'lodash'; @Controller() @UseGuards(OwnerAuthGuard()) export class RestaurantController { + constructor(private aws: AwsService) {} + @Get() @UseGuards(OwnerGuard) @Permissions(`${PermOwner.Restaurant}@${PermAct.R}`) @@ -28,6 +34,7 @@ export class RestaurantController { phone: 'required|phone|unique', email: 'email', website: 'url', + description: '', }; const validation = Validator.init(body, rules); if (validation.fails()) { @@ -38,8 +45,57 @@ export class RestaurantController { rest.phone = body.phone; rest.email = body.email; rest.website = body.website; + rest.description = body.description; await rest.save(); return response.item(rest, RestaurantTransformer); } + + @Post('/image/:type') + @UseGuards(OwnerGuard) + @Permissions(`${PermOwner.Restaurant}@${PermAct.C}`) + async uploadAvatar(@Req() request, @Res() response, @Rest() rest: Restaurant, @Param() param) { + if (!['logo', 'banner'].includes(param.type)) { + throw new BadRequestException('Invalid image type'); + } + + const file = await this.aws.uploadFile(request, response, 'image', { dynamicPath: `restaurant/${rest.id}/avatar` }); + if (!file || isEmpty(file)) { + throw new BadRequestException('Unable to upload image'); + } + + const payload = await Media.getPayload(file); + const url = get(payload, 'url', null); + if (param.type == 'logo') { + rest.logo_url = url; + } else if (param.type == 'banner') { + rest.banner_url = url; + } + + await AppDataSource.transaction(async (manager) => { + await manager.getRepository(Restaurant).save(rest); + }); + + await response.item(rest, RestaurantTransformer); + } + + @Delete('/image/:type') + @UseGuards(OwnerGuard) + @Permissions(`${PermOwner.Restaurant}@${PermAct.D}`) + async deleteAvatar(@Res() response, @Rest() rest: Restaurant, @Param() param) { + if (!['logo', 'banner'].includes(param.type)) { + throw new BadRequestException('Invalid image type'); + } + + if (param.type == 'logo') { + rest.logo_url = null; + } else if (param.type == 'banner') { + rest.banner_url = null; + } + + await AppDataSource.transaction(async (manager) => { + await manager.getRepository(Restaurant).save(rest); + }); + return response.noContent(); + } } diff --git a/src/app/owner/restaurant/staff/staff.controller.ts b/src/app/owner/restaurant/staff/staff.controller.ts index d54803d..cfc54ea 100644 --- a/src/app/owner/restaurant/staff/staff.controller.ts +++ b/src/app/owner/restaurant/staff/staff.controller.ts @@ -29,7 +29,7 @@ export class StaffController { const query = AppDataSource.createQueryBuilder(StaffUser, 't1'); query.where({ restaurant_id: rest.id }); - if (loc.id) { + if (loc && loc.id) { query.andWhere({ location_id: loc.id }); } @@ -46,6 +46,7 @@ export class StaffController { } @Post() + @UseGuards(OwnerGuard) @Permissions(`${PermOwner.Staff}@${PermAct.C}`) async store(@Body() body, @Res() response, @Rest() rest) { const rules = { @@ -110,6 +111,7 @@ export class StaffController { } @Put('/:id') + @UseGuards(OwnerGuard) @Permissions(`${PermOwner.Staff}@${PermAct.U}`) async update(@Param() param, @Body() body, @Res() response) { const rules = { diff --git a/src/database/entities/core/media.entity.ts b/src/database/entities/core/media.entity.ts index 9edd678..5932685 100644 --- a/src/database/entities/core/media.entity.ts +++ b/src/database/entities/core/media.entity.ts @@ -95,6 +95,19 @@ export class Media extends BaseEntity { } as Media; } + static async build(this, entity: T | any, image: IStorageResponse): Promise { + if (!(await entity.image)) { + return this.add(entity, image); + } + + // reload entity + const payload = await Media.getPayload(image); + await entity.image.update({ ...payload }); + await entity.reload(); + + return entity; + } + static async add(entity: T | any, image: IStorageResponse): Promise { const payload = await Media.getPayload(image); const klass = snakeCase(get(entity, 'constructor.name', '')).toLowerCase(); diff --git a/src/database/entities/owner/owner.entity.ts b/src/database/entities/owner/owner.entity.ts index 209fe2b..a9140f8 100644 --- a/src/database/entities/owner/owner.entity.ts +++ b/src/database/entities/owner/owner.entity.ts @@ -83,7 +83,7 @@ export class Owner extends BaseEntity { @Exclude() @OneToOne(() => Media, (media) => media.owner) - image: Media; + image: Promise; get isVerified() { return this.verified_at !== null; diff --git a/src/database/entities/owner/restaurant.entity.ts b/src/database/entities/owner/restaurant.entity.ts index 438c9d6..cebec1b 100644 --- a/src/database/entities/owner/restaurant.entity.ts +++ b/src/database/entities/owner/restaurant.entity.ts @@ -25,6 +25,9 @@ export class Restaurant extends BaseEntity { @PhoneColumn() phone: string; + @Column({ type: 'longtext' }) + description: string; + @Column() slug: string; @@ -37,6 +40,12 @@ export class Restaurant extends BaseEntity { @StatusColumn() status: RestaurantStatus; + @Column({ nullable: true }) + logo_url: string; + + @Column({ nullable: true }) + banner_url: string; + @Exclude() @ForeignColumn() owner_id: string; diff --git a/src/database/migrations/1709828001230-restaurant-desc.ts b/src/database/migrations/1709828001230-restaurant-desc.ts new file mode 100644 index 0000000..1d9de58 --- /dev/null +++ b/src/database/migrations/1709828001230-restaurant-desc.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class restaurantDesc1709828001230 implements MigrationInterface { + name = 'restaurantDesc1709828001230'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`restaurant\` ADD \`description\` longtext NOT NULL`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`restaurant\` DROP COLUMN \`description\``); + } +} diff --git a/src/database/migrations/1710002572201-restaurant-image.ts b/src/database/migrations/1710002572201-restaurant-image.ts new file mode 100644 index 0000000..6b1033f --- /dev/null +++ b/src/database/migrations/1710002572201-restaurant-image.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class restaurantImage1710002572201 implements MigrationInterface { + name = 'restaurantImage1710002572201'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`restaurant\` ADD \`logo_url\` varchar(255) NULL`); + await queryRunner.query(`ALTER TABLE \`restaurant\` ADD \`banner_url\` varchar(255) NULL`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`restaurant\` DROP COLUMN \`banner_url\``); + await queryRunner.query(`ALTER TABLE \`restaurant\` DROP COLUMN \`logo_url\``); + } +} diff --git a/src/database/transformers/owner.transformer.ts b/src/database/transformers/owner.transformer.ts index 4c7ac80..61fcf93 100644 --- a/src/database/transformers/owner.transformer.ts +++ b/src/database/transformers/owner.transformer.ts @@ -1,4 +1,3 @@ -import { Media } from '@db/entities/core/media.entity'; import { Owner } from '@db/entities/owner/owner.entity'; import { encrypt } from '@lib/helpers/encrypt.helper'; import { RequestHelper } from '@lib/helpers/request.helper'; @@ -11,13 +10,13 @@ export class OwnerTransformer extends TransformerAbstract { return ['role', 'restaurant', 'location']; } - transform(entity: Owner) { + async transform(entity: Owner) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { image, ...rest } = entity.toJSON(); return { ...rest, - avatar: Media.getImage(image), + avatar: await entity.getAvatar(), }; } @@ -41,7 +40,7 @@ export class OwnerTransformer extends TransformerAbstract { name: location.name, } : null, - avatar: Media.getImage(entity.image), + avatar: await entity.getAvatar(), }; } diff --git a/templates/mails/staff-register.hbs b/templates/mails/staff-register.hbs index 1d3a011..9490521 100644 --- a/templates/mails/staff-register.hbs +++ b/templates/mails/staff-register.hbs @@ -2,7 +2,7 @@ {{#content "content"}}

Dear {{name}},

-

You are going to use this email address as your username in Kelola Staff!

+

You are going to use this email address as your username in Ordero.

To login to staff portal, please use the following password: {{password}}

If you have any problems, simply reply to this email and we'll get back to you as soon as we can.