-ตัวอย่าง microservice
This commit is contained in:
@@ -25,7 +25,7 @@ app.use((err, req, res, next) => {
|
|||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use('/api', router)
|
app.use('/api/web', router)
|
||||||
|
|
||||||
app.listen(process.env.PORT, () => {
|
app.listen(process.env.PORT, () => {
|
||||||
console.log(`✅ ${process.env.PJ_NAME} running on port ${process.env.PORT}`)
|
console.log(`✅ ${process.env.PJ_NAME} running on port ${process.env.PORT}`)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LoginService } from '../services/loginservice.js'
|
import { AccountingSetupService } from '../services/accountingSetupService.js'
|
||||||
import { sendError } from '../utils/response.js'
|
import { sendError } from '../utils/response.js'
|
||||||
// import { OftenError } from '../utils/oftenError.js'
|
// import { OftenError } from '../utils/oftenError.js'
|
||||||
import { GeneralService } from '../share/generalservice.js';
|
import { GeneralService } from '../share/generalservice.js';
|
||||||
@@ -9,7 +9,7 @@ export class accountingSetup {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.generalService = new GeneralService();
|
this.generalService = new GeneralService();
|
||||||
this.loginService = new LoginService();
|
this.AccountingSetupService = new AccountingSetupService();
|
||||||
}
|
}
|
||||||
|
|
||||||
async onNavigate(req, res) {
|
async onNavigate(req, res) {
|
||||||
@@ -23,18 +23,23 @@ export class accountingSetup {
|
|||||||
let idx = -1
|
let idx = -1
|
||||||
let result = []
|
let result = []
|
||||||
try {
|
try {
|
||||||
let username = req.body.request.username;
|
// let username = req.body.request.username;
|
||||||
let password = req.body.request.password;
|
// let password = req.body.request.password;
|
||||||
|
|
||||||
result = await this.loginService.verifyLogin(database, username, password); // เช็คกับ db กลาง ส่ง jwttoken ออกมา
|
result = await this.AccountingSetupService.getAccountingSetup(database); // เช็คกับ db กลาง ส่ง jwttoken ออกมา
|
||||||
// this.generalService.devhint(1, 'accountingSetup.js', 'Login success');
|
// this.generalService.devhint(1, 'accountingSetup.js', 'Login success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
idx = 1;
|
idx = 1;
|
||||||
} finally {
|
} finally {
|
||||||
if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error');
|
if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error');
|
||||||
if (!result) return sendError('ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง', 'Invalid credentials');
|
if (!result) return sendError('ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง', 'Invalid credentials');
|
||||||
if(result) { return result }
|
// แยกกลุ่ม income / expense
|
||||||
return null
|
let income = result.filter(item => item.dtltblcod === 'ACTCAT_INC').map(({ dtltblcod, ...rest }) => rest);
|
||||||
|
|
||||||
|
let expense = result.filter(item => item.dtltblcod === 'ACTCAT_EXP').map(({ dtltblcod, ...rest }) => rest);
|
||||||
|
|
||||||
|
let arydiy = { income , expense };
|
||||||
|
return arydiy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
// import { loginController } from '../controllers/logincontroller.js'
|
import { accountingSetup } from '../controllers/accountingSetup.js'
|
||||||
// import { authMiddleware } from '../middlewares/auth.js'
|
// import { authMiddleware } from '../middlewares/auth.js'
|
||||||
// import { sendResponse } from '../utils/response.js'
|
// import { sendResponse } from '../utils/response.js'
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
// const controller_login_post = new loginController()
|
const controller_accountingSetup_post = new accountingSetup()
|
||||||
|
|
||||||
// ===================================================
|
router.post('/accountingSetup', async (req, res) => {
|
||||||
// 🔹 LOGIN ปกติ
|
const result = await controller_accountingSetup_post.onNavigate(req, res)
|
||||||
// ===================================================
|
if (result) return res.json(result)
|
||||||
// router.post('/login', async (req, res) => {
|
})
|
||||||
// const data = await controller_login_post.onNavigate(req, res)
|
|
||||||
// if (data)
|
|
||||||
// return sendResponse(res, 200, 'เข้าสู่ระบบสำเร็จ', 'Login success', data)
|
|
||||||
// })
|
|
||||||
|
|
||||||
// // ===================================================
|
// // ===================================================
|
||||||
// // 🔹 BIOMETRIC LOGIN
|
// // 🔹 BIOMETRIC LOGIN
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { GeneralService } from '../share/generalservice.js'
|
||||||
|
|
||||||
|
export class AccountingSetupService {
|
||||||
|
constructor() {
|
||||||
|
this.generalService = new GeneralService()
|
||||||
|
}
|
||||||
|
async getAccountingSetup(database) {
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
dtlnam,
|
||||||
|
dtlcod,
|
||||||
|
dtltblcod
|
||||||
|
FROM ${database}.dtlmst
|
||||||
|
WHERE dtltblcod IN ('ACTTYP', 'ACTCAT_INC', 'ACTCAT_EXP');
|
||||||
|
`
|
||||||
|
const params = []
|
||||||
|
const result = await this.generalService.executeQueryParam(database, sql, params);
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// SELECT
|
||||||
|
// acttyp,
|
||||||
|
// dtlname
|
||||||
|
// FROM ${database}.actmst
|
||||||
|
// LEFT JOIN ${database}.dtlmst ON dtltblcod = 'acttyp' and acttyp = dtlcod
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import bcrypt from 'bcrypt'
|
|
||||||
import { GeneralService } from '../share/generalservice.js'
|
|
||||||
import { generateToken } from '../utils/token.js'
|
|
||||||
|
|
||||||
export class LoginService {
|
|
||||||
constructor() {
|
|
||||||
this.generalService = new GeneralService()
|
|
||||||
}
|
|
||||||
async verifyLogin(database, username, password) {
|
|
||||||
this.generalService.devhint(2, 'loginservice.js', `verifyLogin() start for username=${username}`)
|
|
||||||
|
|
||||||
let user = null
|
|
||||||
let token = null
|
|
||||||
|
|
||||||
let sql = `
|
|
||||||
SELECT usrseq, usrnam, usrorg, usrrol, usrpwd, usrthinam, usrthilstnam
|
|
||||||
FROM nuttakit.usrmst
|
|
||||||
WHERE usrnam = $1
|
|
||||||
`
|
|
||||||
let params = [username]
|
|
||||||
const rows = await this.generalService.executeQueryParam(database, sql, params)
|
|
||||||
this.generalService.devhint(3, 'loginservice.js', `query done, found=${rows.length}`)
|
|
||||||
|
|
||||||
if (rows.length === 0) {
|
|
||||||
this.generalService.devhint(2, 'loginservice.js', 'no user found')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
user = rows[0]
|
|
||||||
const match = await bcrypt.compare(password, user.usrpwd)
|
|
||||||
if (match === false) {
|
|
||||||
this.generalService.devhint(2, 'loginservice.js', 'password mismatch')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
token = generateToken({
|
|
||||||
id: user.usrseq,
|
|
||||||
name: user.usrnam,
|
|
||||||
realname: user.usrthinam,
|
|
||||||
lastname: user.usrthilstnam,
|
|
||||||
role: user.usrrol,
|
|
||||||
organization: user.usrorg
|
|
||||||
})
|
|
||||||
this.generalService.devhint(2, 'loginservice.js', 'token generated successfully')
|
|
||||||
|
|
||||||
|
|
||||||
delete user.usrseq
|
|
||||||
delete user.usrnam
|
|
||||||
delete user.usrrol
|
|
||||||
delete user.usrpwd
|
|
||||||
delete user.usrorg
|
|
||||||
return {
|
|
||||||
token,
|
|
||||||
...user
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loginWithBiometric(database, biometric_id) {
|
|
||||||
this.generalService.devhint(2, 'loginservice.js', `loginWithBiometric() start for biometric_id=${biometric_id}`)
|
|
||||||
|
|
||||||
let sql = ''
|
|
||||||
let params = []
|
|
||||||
|
|
||||||
sql = `
|
|
||||||
SELECT usrid, usrnam, usrrol
|
|
||||||
FROM ${database}.usrmst
|
|
||||||
WHERE biometric_id = $1
|
|
||||||
`
|
|
||||||
params = [biometric_id]
|
|
||||||
const rows = await this.generalService.executeQueryParam(database, sql, params)
|
|
||||||
if (rows.length === 0) {
|
|
||||||
this.generalService.devhint(2, 'loginservice.js', 'no biometric found')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = rows[0]
|
|
||||||
const token = generateToken({
|
|
||||||
id: user.usrid,
|
|
||||||
name: user.usrnam,
|
|
||||||
role: user.usrrol,
|
|
||||||
organization: database
|
|
||||||
})
|
|
||||||
|
|
||||||
this.generalService.devhint(2, 'loginservice.js', 'biometric token generated')
|
|
||||||
return { token, user }
|
|
||||||
}
|
|
||||||
|
|
||||||
async registerBiometric(database, usrid, biometric_id) {
|
|
||||||
this.generalService.devhint(2, 'loginservice.js', `registerBiometric() start user=${usrid}`)
|
|
||||||
|
|
||||||
|
|
||||||
let sql = ''
|
|
||||||
let params = []
|
|
||||||
|
|
||||||
sql = `
|
|
||||||
UPDATE ${database}.usrmst
|
|
||||||
SET biometric_id = $1
|
|
||||||
WHERE usrid = $2
|
|
||||||
`
|
|
||||||
params = [biometric_id, usrid]
|
|
||||||
await this.generalService.executeQueryParam(database, sql, params)
|
|
||||||
|
|
||||||
this.generalService.devhint(2, 'loginservice.js', 'biometric registered')
|
|
||||||
return { message: 'Biometric registered successfully' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { generateOTP } from '../utils/otp.js'
|
|
||||||
import { sendMockOtpMail } from '../utils/mailer.js'
|
|
||||||
import { saveOtp, verifyOtp, removeOtp } from '../utils/redis.js'
|
|
||||||
import { sendError } from '../utils/response.js'
|
|
||||||
|
|
||||||
export class OtpService {
|
|
||||||
async sendOtp(email) {
|
|
||||||
try {
|
|
||||||
const otp = generateOTP()
|
|
||||||
await saveOtp(email, otp)
|
|
||||||
await sendMockOtpMail(email, otp)
|
|
||||||
return { email, otp}
|
|
||||||
} catch (error) {
|
|
||||||
return sendError('ไม่สามารถส่ง OTP ได้', 'Failed to send OTP')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import Redis from 'ioredis';
|
|
||||||
import crypto from 'crypto';
|
|
||||||
import { sendError } from '../utils/response.js';
|
|
||||||
import { GeneralService } from '../share/generalservice.js';
|
|
||||||
|
|
||||||
export class OTPVerifyService {
|
|
||||||
constructor() {
|
|
||||||
this.redis = new Redis();
|
|
||||||
this.generalService = new GeneralService();
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyOtp(email, otp) {
|
|
||||||
const storedOtp = await this.redis.get(`otp:${email}`);
|
|
||||||
if (!storedOtp || storedOtp !== otp) {
|
|
||||||
throw sendError('รหัส OTP ไม่ถูกต้องหรือหมดอายุ', 'Invalid OTP');
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.redis.del(`otp:${email}`);
|
|
||||||
|
|
||||||
const resetToken = crypto.randomBytes(32).toString('hex');
|
|
||||||
await this.redis.set(`reset:${email}`, resetToken, 'EX', 600); // TTL 10 นาที
|
|
||||||
|
|
||||||
this.generalService.devhint(1, 'otpverifyservice.js', `OTP Verified → Reset Token issued (${email})`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
resetToken:resetToken
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import Redis from 'ioredis';
|
|
||||||
import bcrypt from 'bcrypt';
|
|
||||||
import crypto from 'crypto';
|
|
||||||
import nodemailer from 'nodemailer';
|
|
||||||
import { GeneralService } from '../share/generalservice.js';
|
|
||||||
import { sendError } from '../utils/response.js';
|
|
||||||
|
|
||||||
export class RegisterService {
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.redis = new Redis();
|
|
||||||
this.generalService = new GeneralService();
|
|
||||||
}
|
|
||||||
|
|
||||||
async requestRegistration(database, email, fname, lname, password) {
|
|
||||||
let result = [];
|
|
||||||
try {
|
|
||||||
let sql = `
|
|
||||||
SELECT usrseq FROM ${database}.usrmst WHERE usrnam = $1
|
|
||||||
`;
|
|
||||||
let param = [email];
|
|
||||||
const userCheck = await this.generalService.executeQueryParam(database, sql, param);
|
|
||||||
|
|
||||||
if (userCheck.length > 0) {
|
|
||||||
this.generalService.devhint(1, 'registerservice.js', `❌ Duplicate email (${email})`);
|
|
||||||
throw sendError('อีเมลนี้ถูกใช้แล้ว', 'Email already registered');
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashedPwd = await bcrypt.hash(password, 10);
|
|
||||||
const token = crypto.randomBytes(32).toString('hex');
|
|
||||||
|
|
||||||
|
|
||||||
const payload = JSON.stringify({ fname, lname, hashedPwd, token, database });
|
|
||||||
await this.redis.set(`verify:${email}`, payload, 'EX', 86400); // 24h
|
|
||||||
|
|
||||||
|
|
||||||
const verifyUrl = `http://localhost:1012/login/verify-email?token=${token}&email=${encodeURIComponent(email)}&organization=${database}`;
|
|
||||||
await this.sendVerifyEmail(email, verifyUrl);
|
|
||||||
|
|
||||||
this.generalService.devhint(2, 'registerservice.js', `✅ Verify link sent to ${email}`);
|
|
||||||
|
|
||||||
result = {
|
|
||||||
code: '200',
|
|
||||||
message_th: 'ส่งลิงก์ยืนยันอีเมลแล้ว',
|
|
||||||
data: {}
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
this.generalService.devhint(1, 'registerservice.js', '❌ Registration Error', error.message);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendVerifyEmail(email, verifyUrl) {
|
|
||||||
try {
|
|
||||||
const transporter = nodemailer.createTransport({
|
|
||||||
service: 'gmail',
|
|
||||||
auth: {
|
|
||||||
user: process.env.SMTP_USER,
|
|
||||||
pass: process.env.SMTP_PASS,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const html = `
|
|
||||||
<div style="font-family: sans-serif;">
|
|
||||||
<h2>ยืนยันการสมัครสมาชิก</h2>
|
|
||||||
<p>กรุณากดยืนยันภายใน 24 ชั่วโมง เพื่อเปิดใช้งานบัญชีของคุณ</p>
|
|
||||||
<a href="${verifyUrl}"
|
|
||||||
style="display:inline-block;background:#0078d4;color:white;
|
|
||||||
padding:10px 20px;text-decoration:none;border-radius:5px;">ยืนยันอีเมล</a>
|
|
||||||
<p style="margin-top:16px;font-size:13px;color:#555;">หากคุณไม่ได้สมัคร โปรดละเว้นอีเมลนี้</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
await transporter.sendMail({
|
|
||||||
from: `"System" <${process.env.SMTP_USER}>`,
|
|
||||||
to: email,
|
|
||||||
subject: '📩 ยืนยันอีเมลสำหรับสมัครสมาชิก',
|
|
||||||
html,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.generalService.devhint(2, 'registerservice.js', `📤 Verification email sent (${email})`);
|
|
||||||
} catch (error) {
|
|
||||||
this.generalService.devhint(1, 'registerservice.js', '❌ Email Send Failed', error.message);
|
|
||||||
throw sendError('ไม่สามารถส่งอีเมลได้', 'Email send failed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import Redis from 'ioredis';
|
|
||||||
import bcrypt from 'bcrypt';
|
|
||||||
import { sendError } from '../utils/response.js';
|
|
||||||
import { GeneralService } from '../share/generalservice.js';
|
|
||||||
|
|
||||||
export class ResetPasswordService {
|
|
||||||
constructor() {
|
|
||||||
this.redis = new Redis();
|
|
||||||
this.generalService = new GeneralService();
|
|
||||||
}
|
|
||||||
|
|
||||||
async resetPassword(email, token, newPassword) {
|
|
||||||
let database = '';
|
|
||||||
|
|
||||||
const storedToken = await this.redis.get(`reset:${email}`);
|
|
||||||
if (!storedToken || storedToken !== token) {
|
|
||||||
throw sendError('Token ไม่ถูกต้องหรือหมดอายุ', 'Invalid or expired token');
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.redis.del(`reset:${email}`);
|
|
||||||
|
|
||||||
// อัปเดตรหัสผ่านในฐานข้อมูลจริง
|
|
||||||
const hashedPwd = await bcrypt.hash(newPassword, 10);
|
|
||||||
let sql = `
|
|
||||||
UPDATE usrmst SET usrpwd = $1 WHERE usrnam = $2
|
|
||||||
`
|
|
||||||
let param = [hashedPwd, email];
|
|
||||||
await this.generalService.executeQueryParam(database, sql, param);
|
|
||||||
|
|
||||||
this.generalService.devhint(1, 'resetpasswordservice.js', `Password reset successful (${email})`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
code: '200',
|
|
||||||
message: 'successful',
|
|
||||||
message_th: 'รีเซ็ตรหัสผ่านสำเร็จ'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { executeQueryParam } from '../share/generalservice.js'
|
|
||||||
|
|
||||||
export class userservice {
|
|
||||||
constructor() {
|
|
||||||
this.generalService = new GeneralService()
|
|
||||||
}
|
|
||||||
async createUser(database, usrnam, usreml) {
|
|
||||||
const sql = `
|
|
||||||
SELECT * FROM ${database}.usrmst
|
|
||||||
WHERE usrnam = $1 OR usreml = $2
|
|
||||||
`
|
|
||||||
const params = [usrnam, usreml]
|
|
||||||
const result = await executeQueryParam(sql, database, params)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import Redis from 'ioredis';
|
|
||||||
import { GeneralService } from '../share/generalservice.js';
|
|
||||||
import { sendError } from '../utils/response.js';
|
|
||||||
|
|
||||||
export class VerifyEmailService {
|
|
||||||
constructor() {
|
|
||||||
this.redis = new Redis();
|
|
||||||
this.generalService = new GeneralService();
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyAndCreate({ email, token, schema = 'nuttakit' }) {
|
|
||||||
// ✅ STEP 1: โหลด payload จาก Redis
|
|
||||||
const key = `verify:${email}`;
|
|
||||||
const stored = await this.redis.get(key);
|
|
||||||
if (!stored) {
|
|
||||||
throw sendError('ลิงก์หมดอายุหรือไม่ถูกต้อง', 'Verification link expired or invalid', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsed;
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(stored);
|
|
||||||
} catch (ex) {
|
|
||||||
await this.redis.del(key).catch(() => {});
|
|
||||||
throw sendError('ข้อมูลการยืนยันไม่ถูกต้อง', 'Invalid verify payload', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ STEP 2: ตรวจสอบ token
|
|
||||||
if (parsed.token !== token) {
|
|
||||||
throw sendError('Token ไม่ถูกต้อง', 'Invalid token', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ STEP 3: ตรวจสอบว่าอีเมลนี้เคยถูกสร้างใน schema แล้วหรือยัง
|
|
||||||
const checkSql = `
|
|
||||||
SELECT usrseq FROM \${database}.usrmst WHERE usrnam = $1
|
|
||||||
`;
|
|
||||||
const checkResult = await this.generalService.executeQueryParam(schema, checkSql, [email]);
|
|
||||||
|
|
||||||
if (checkResult && checkResult.length > 0) {
|
|
||||||
await this.redis.del(key).catch(() => {});
|
|
||||||
throw sendError('อีเมลนี้ถูกใช้แล้วในองค์กรนี้', 'Email already registered in this organization', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ STEP 4: Insert ข้อมูลลงในตารางจริง
|
|
||||||
const insertSql = `
|
|
||||||
INSERT INTO ${database}.usrmst (usrnam, usrthinam, usrthilstnam, usrpwd, usrrol)
|
|
||||||
VALUES ($1, $2, $3, $4, 'U')
|
|
||||||
`;
|
|
||||||
const params = [email, parsed.fname, parsed.lname, parsed.hashedPwd];
|
|
||||||
await this.generalService.executeQueryParam(schema, insertSql, params);
|
|
||||||
|
|
||||||
// ✅ STEP 5: ลบ Redis Key (เคลียร์ payload)
|
|
||||||
await this.redis.del(key).catch(() => {});
|
|
||||||
|
|
||||||
this.generalService.devhint(2, 'verifyemailservice.js', `✅ Account verified (${email})`);
|
|
||||||
|
|
||||||
// ✅ STEP 6: ส่งผลลัพธ์กลับ
|
|
||||||
return {
|
|
||||||
code: '200',
|
|
||||||
message_th: 'ยืนยันอีเมลสำเร็จ บัญชีถูกสร้างแล้ว',
|
|
||||||
data: {
|
|
||||||
email,
|
|
||||||
schema,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user