add auth and payment api

This commit is contained in:
ItsMalma
2025-11-19 23:53:56 +07:00
parent 8f91994f29
commit 7e7a865368
64 changed files with 9067 additions and 2662 deletions

View File

@@ -0,0 +1,6 @@
export abstract class AbstractEmailService {
public abstract sendVerificationEmail(
to: string,
code: string,
): Promise<void>;
}

View File

@@ -0,0 +1,30 @@
import { AbstractEmailService } from "@/common/services/email-service/abstract.email-service";
import { mailConfig } from "@/configs/mail.config";
import * as nodemailer from "nodemailer";
export class LibraryEmailService extends AbstractEmailService {
private readonly _transporter: nodemailer.Transporter;
public constructor() {
super();
this._transporter = nodemailer.createTransport({
host: mailConfig.host,
port: mailConfig.port,
secure: true,
auth: {
user: mailConfig.username,
pass: mailConfig.password,
},
});
}
public async sendVerificationEmail(to: string, code: string): Promise<void> {
await this._transporter.sendMail({
from: mailConfig.username,
to,
subject: "Email Verification",
text: `Your verification code is: ${code}`,
});
}
}

View File

@@ -0,0 +1,29 @@
import type { Admin } from "@/database/entities/admin.entity";
import type { Partner } from "@/database/entities/partner.entity";
export enum JwtType {
access = "access",
refresh = "refresh",
}
export abstract class AbstractJwtService {
public abstract createAdminToken(
admin: Admin,
type: JwtType,
): Promise<{
token: string;
expiresAt: Date;
}>;
public abstract verifyAdminToken(token: string): Promise<Admin>;
public abstract createPartnerToken(
partner: Partner,
type: JwtType,
): Promise<{
token: string;
expiresAt: Date;
}>;
public abstract verifyPartnerToken(token: string): Promise<Partner>;
}

View File

@@ -0,0 +1,149 @@
import { InvalidJwtError } from "@/common/errors/invalid-jwt.error";
import {
AbstractJwtService,
JwtType,
} from "@/common/services/jwt-service/abstract.jwt-service";
import { jwtConfig } from "@/configs/jwt.config";
import { Admin } from "@/database/entities/admin.entity";
import { Partner } from "@/database/entities/partner.entity";
import { orm } from "@/database/orm";
import * as dateFns from "date-fns";
import * as jwt from "jsonwebtoken";
import { ulid } from "ulid";
import z from "zod";
const adminPayloadSchema = z.object({
sub: z.ulid(),
role: z.literal("admin"),
});
const partnerPayloadSchema = z.object({
sub: z.ulid(),
role: z.literal("partner"),
});
export class LibraryJwtService extends AbstractJwtService {
public async createAdminToken(
admin: Admin,
type: JwtType,
): Promise<{
token: string;
expiresAt: Date;
}> {
const now = new Date();
let expiresAt: Date;
switch (type) {
case JwtType.access:
expiresAt = dateFns.addDays(now, 1);
break;
case JwtType.refresh:
expiresAt = dateFns.addDays(now, 30);
break;
}
const token = jwt.sign(
{
role: "admin",
},
jwtConfig.secret,
{
algorithm: jwtConfig.algorithm,
expiresIn: Math.floor((expiresAt.getTime() - now.getTime()) / 1000),
notBefore: "0 seconds",
subject: admin.id,
issuer: jwtConfig.issuer,
jwtid: ulid(),
},
);
return {
token,
expiresAt,
};
}
public async verifyAdminToken(token: string): Promise<Admin> {
const payload = jwt.verify(token, jwtConfig.secret, {
algorithms: [jwtConfig.algorithm],
issuer: jwtConfig.issuer,
});
const parsePayloadResult = adminPayloadSchema.safeParse(payload);
if (!parsePayloadResult.success) {
throw new InvalidJwtError("Invalid payload.");
}
const adminPayload = parsePayloadResult.data;
const admin = await orm.em.findOne(Admin, {
id: adminPayload.sub,
});
if (!admin) {
throw new InvalidJwtError("Admin not found.");
}
return admin;
}
public async createPartnerToken(
partner: Partner,
type: JwtType,
): Promise<{
token: string;
expiresAt: Date;
}> {
const now = new Date();
let expiresAt: Date;
switch (type) {
case JwtType.access:
expiresAt = dateFns.addDays(now, 1);
break;
case JwtType.refresh:
expiresAt = dateFns.addDays(now, 30);
break;
}
const token = jwt.sign(
{
role: "partner",
},
jwtConfig.secret,
{
algorithm: jwtConfig.algorithm,
expiresIn: Math.floor((expiresAt.getTime() - now.getTime()) / 1000),
notBefore: "0 seconds",
subject: partner.id,
issuer: jwtConfig.issuer,
jwtid: ulid(),
},
);
return {
token,
expiresAt,
};
}
public async verifyPartnerToken(token: string): Promise<Partner> {
const payload = jwt.verify(token, jwtConfig.secret, {
algorithms: [jwtConfig.algorithm],
issuer: jwtConfig.issuer,
});
const parsePayloadResult = partnerPayloadSchema.safeParse(payload);
if (!parsePayloadResult.success) {
throw new InvalidJwtError("Invalid payload.");
}
const partnerPayload = parsePayloadResult.data;
const partner = await orm.em.findOne(Partner, {
id: partnerPayload.sub,
});
if (!partner) {
throw new InvalidJwtError("Partner not found.");
}
return partner;
}
}

View File

@@ -0,0 +1,5 @@
import type { Order } from "@/database/entities/order.entity";
export abstract class AbstractPaymentService {
public abstract createPaymentUrl(order: Order): Promise<string>;
}

View File

@@ -0,0 +1,141 @@
import { PaymentError } from "@/common/errors/payment.error";
import { AbstractPaymentService } from "@/common/services/payment-service/abstract.payment-service";
import { midtransConfig } from "@/configs/midtrans.config";
import type { OrderDetail } from "@/database/entities/order-detail.entity";
import type { Order } from "@/database/entities/order.entity";
import { RoomType } from "@/database/enums/room-type.enum";
type CreateTransactionResponseSuccess = {
token: string;
redirect_url: string;
};
type CreateTransactionResponseFailed = {
error_messages: string[];
};
export class MidtransPaymentService extends AbstractPaymentService {
private readonly _basicAuth: string;
public constructor() {
super();
this._basicAuth = `Basic ${Buffer.from(`${midtransConfig.serverKey}:`).toBase64()}`;
}
private calculateOrderDetailsPrice(orderDetails: OrderDetail[]): number {
let price = 0;
for (const orderDetail of orderDetails) {
switch (orderDetail.roomType) {
case RoomType.double:
price += orderDetail.order.package.doublePrice;
break;
case RoomType.triple:
price += orderDetail.order.package.triplePrice;
break;
case RoomType.quad:
price += orderDetail.order.package.quadPrice;
break;
case RoomType.infant:
price += orderDetail.order.package.infantPrice ?? 0;
break;
}
}
return price;
}
public async createPaymentUrl(order: Order): Promise<string> {
const doubleOrderDetails = order.details.filter(
(orderDetail) => orderDetail.roomType === RoomType.double,
);
const tripleOrderDetails = order.details.filter(
(orderDetail) => orderDetail.roomType === RoomType.double,
);
const quadOrderDetails = order.details.filter(
(orderDetail) => orderDetail.roomType === RoomType.double,
);
const infantOrderDetails = order.details.filter(
(orderDetail) => orderDetail.roomType === RoomType.double,
);
const response = await fetch(`${midtransConfig.baseUrl}/transactions`, {
method: "POST",
headers: {
Accept: "application/json",
Authorization: this._basicAuth,
"Content-Type": "application/json",
},
body: JSON.stringify({
transaction_details: {
order_id: order.id,
gross_amount: this.calculateOrderDetailsPrice(
order.details.getItems(),
),
},
item_details: [
doubleOrderDetails.length > 0
? {
id: doubleOrderDetails[0].id,
price: order.package.doublePrice,
quantity: doubleOrderDetails.length,
name: `${order.package.package.name} / Double`,
brand: "GoUmrah",
category: "Paket",
merchant_name: "GoUmrah",
}
: undefined,
tripleOrderDetails.length > 0
? {
id: tripleOrderDetails[0].id,
price: order.package.triplePrice,
quantity: tripleOrderDetails.length,
name: `${order.package.package.name} / Triple`,
brand: "GoUmrah",
category: "Paket",
merchant_name: "GoUmrah",
}
: undefined,
quadOrderDetails.length > 0
? {
id: quadOrderDetails[0].id,
price: order.package.quadPrice,
quantity: quadOrderDetails.length,
name: `${order.package.package.name} / Quad`,
brand: "GoUmrah",
category: "Paket",
merchant_name: "GoUmrah",
}
: undefined,
infantOrderDetails.length > 0
? {
id: infantOrderDetails[0].id,
price: order.package.infantPrice,
quantity: infantOrderDetails.length,
name: `${order.package.package.name} / Infant`,
brand: "GoUmrah",
category: "Paket",
merchant_name: "GoUmrah",
}
: undefined,
],
customer_details: {
first_name: order.name,
phone: order.whatsapp,
},
credit_card: {
secure: true,
},
}),
});
if (response.status === 201) {
const responseBody =
(await response.json()) as CreateTransactionResponseSuccess;
return responseBody.redirect_url;
} else {
const responseBody =
(await response.json()) as CreateTransactionResponseFailed;
throw new PaymentError(responseBody.error_messages[0]);
}
}
}