mirror of
https://github.com/OpenSignLabs/OpenSign.git
synced 2026-09-09 11:17:39 +02:00
Merge pull request #1250 from nxglabs/sync-to-public_repo-17512737859
Merge pull request
This commit is contained in:
@@ -4,10 +4,11 @@ import dotenv from 'dotenv';
|
||||
import uploadFile from './uploadFile.js';
|
||||
import docxtopdf, { upload as docxUpload } from './docxtopdf.js';
|
||||
import decryptpdf, { upload as decryptUpload } from './decryptpdf.js';
|
||||
import { deleteUserByAdmin, deleteUserGet, deleteUserPost } from './deleteUser.js';
|
||||
|
||||
export const app = express();
|
||||
|
||||
dotenv.config();
|
||||
dotenv.config({ quiet: true });
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
app.use(express.urlencoded({ limit: '50mb', extended: true }));
|
||||
@@ -15,3 +16,6 @@ app.use(express.urlencoded({ limit: '50mb', extended: true }));
|
||||
app.post('/file_upload', uploadFile);
|
||||
app.post('/docxtopdf', docxUpload.single('file'), docxtopdf);
|
||||
app.post('/decryptpdf', decryptUpload.single('file'), decryptpdf);
|
||||
app.get('/delete-account/:userId', deleteUserGet);
|
||||
app.post('/delete-account/:userId', deleteUserPost);
|
||||
app.post('/deleteuser/:userId', deleteUserByAdmin);
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3';
|
||||
import fs from 'node:fs/promises';
|
||||
import pLimit from 'p-limit';
|
||||
import { serverAppId } from '../../Utils.js';
|
||||
|
||||
// === Configuration ===
|
||||
const serverHost = new URL(process.env.SERVER_URL).hostname;
|
||||
const LOCAL_HOSTS = ['localhost', '127.0.0.1', serverHost];
|
||||
const CONCURRENCY_LIMIT = 5;
|
||||
|
||||
// === S3 Client Setup ===
|
||||
function createS3Client({ region, accessKeyId, secretAccessKey, endpoint = null }) {
|
||||
const config = {
|
||||
region,
|
||||
credentials: {
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
},
|
||||
};
|
||||
|
||||
// Only set custom endpoint if not using AWS
|
||||
if (endpoint && !endpoint.includes('amazonaws.com')) {
|
||||
config.endpoint = `https://${endpoint}`;
|
||||
}
|
||||
|
||||
return new S3Client(config);
|
||||
}
|
||||
|
||||
const s3 = createS3Client({
|
||||
region: process.env.DO_REGION,
|
||||
endpoint: process.env.DO_ENDPOINT,
|
||||
accessKeyId: process.env.DO_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.DO_SECRET_ACCESS_KEY,
|
||||
});
|
||||
|
||||
// === Helpers ===
|
||||
function getS3ParamsFromUrl(fileUrl) {
|
||||
try {
|
||||
const url = new URL(fileUrl);
|
||||
const Bucket = url.hostname.split('.')[0];
|
||||
const Key = decodeURIComponent(url.pathname.slice(1));
|
||||
return { Bucket, Key };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteS3File(fileUrl) {
|
||||
const params = getS3ParamsFromUrl(fileUrl);
|
||||
if (!params) return;
|
||||
|
||||
try {
|
||||
await s3.send(new DeleteObjectCommand(params));
|
||||
// console.log(`✅ Deleted from S3: ${params.Key}`);
|
||||
} catch (err) {
|
||||
console.error(`❌ S3 delete failed: ${params.Key}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteLocalFile(fileUrl) {
|
||||
try {
|
||||
const url = new URL(fileUrl);
|
||||
const filePath = decodeURIComponent(url.pathname);
|
||||
if (!filePath.includes('files')) return;
|
||||
|
||||
const localPath = url?.pathname?.split(`/files/${serverAppId}/`)?.pop();
|
||||
|
||||
if (localPath) {
|
||||
await fs.unlink(`./files/files/${localPath}`);
|
||||
}
|
||||
// console.log(`🗑️ Deleted local file: ${localPath}`);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.warn('⚠️ Local file not found:', fileUrl);
|
||||
} else {
|
||||
console.error('❌ Local delete failed:', err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteFileByUrl(fileUrl) {
|
||||
if (!fileUrl) return;
|
||||
try {
|
||||
const url = new URL(fileUrl);
|
||||
if (LOCAL_HOSTS.includes(url.hostname)) {
|
||||
return deleteLocalFile(fileUrl);
|
||||
} else {
|
||||
return deleteS3File(fileUrl);
|
||||
}
|
||||
} catch {
|
||||
console.warn('⚠️ Invalid URL, skipping:', fileUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// === Main Batch Deletion Function ===
|
||||
export async function deleteInBatches(className, userPointer) {
|
||||
let hasMore = true;
|
||||
const limit = 1000;
|
||||
const limiter = pLimit(CONCURRENCY_LIMIT);
|
||||
|
||||
while (hasMore) {
|
||||
const query = new Parse.Query(className);
|
||||
query.equalTo('CreatedBy', userPointer);
|
||||
query.limit(limit);
|
||||
query.ascending('objectId');
|
||||
|
||||
const results = await query.find({ useMasterKey: true });
|
||||
|
||||
// Step 1: Concurrent file deletions with controlled concurrency
|
||||
const fileDeletePromises = [];
|
||||
|
||||
for (const obj of results) {
|
||||
const urls = ['URL', 'SignedUrl', 'certificateUrl']
|
||||
.map(field => obj.get(field))
|
||||
.filter(Boolean);
|
||||
|
||||
for (const fileUrl of urls) {
|
||||
fileDeletePromises.push(limiter(() => deleteFileByUrl(fileUrl)));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(fileDeletePromises);
|
||||
|
||||
// Step 2: Delete Parse objects
|
||||
if (results.length > 0) {
|
||||
await Parse.Object.destroyAll(results, { useMasterKey: true });
|
||||
console.log(`🧹 Deleted ${results.length} Parse objects from ${className}`);
|
||||
}
|
||||
|
||||
hasMore = results.length === limit;
|
||||
}
|
||||
|
||||
console.log(`✅ Finished deletion from ${className} for user: ${userPointer.objectId}`);
|
||||
}
|
||||
|
||||
export async function deleteDataFiles(className, userPointer) {
|
||||
let hasMore = true;
|
||||
const limit = 1000;
|
||||
const limiter = pLimit(CONCURRENCY_LIMIT);
|
||||
|
||||
while (hasMore) {
|
||||
const query = new Parse.Query(className);
|
||||
query.equalTo('UserId', userPointer);
|
||||
query.limit(limit);
|
||||
query.ascending('objectId');
|
||||
|
||||
const results = await query.find({ useMasterKey: true });
|
||||
|
||||
// Step 1: Concurrent file deletions with controlled concurrency
|
||||
const fileDeletePromises = [];
|
||||
|
||||
for (const obj of results) {
|
||||
const urls = ['FileUrl'].map(field => obj.get(field)).filter(Boolean);
|
||||
for (const fileUrl of urls) {
|
||||
fileDeletePromises.push(limiter(() => deleteFileByUrl(fileUrl)));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(fileDeletePromises);
|
||||
|
||||
// Step 2: Delete Parse objects
|
||||
if (results.length > 0) {
|
||||
await Parse.Object.destroyAll(results, { useMasterKey: true });
|
||||
console.log(`🧹 Deleted ${results.length} Parse objects from ${className}`);
|
||||
}
|
||||
|
||||
hasMore = results.length === limit;
|
||||
}
|
||||
|
||||
console.log(`✅ Finished deletion from ${className} for user: ${userPointer.objectId}`);
|
||||
}
|
||||
|
||||
export async function deleteContactsInBatch(className, userPointer) {
|
||||
let hasMore = true;
|
||||
const limit = 1000;
|
||||
|
||||
while (hasMore) {
|
||||
const query = new Parse.Query(className);
|
||||
query.equalTo('CreatedBy', userPointer);
|
||||
query.limit(limit);
|
||||
query.ascending('objectId');
|
||||
const results = await query.find({ useMasterKey: true });
|
||||
if (results?.length > 0) {
|
||||
await Parse.Object.destroyAll(results, { useMasterKey: true });
|
||||
console.log(`🧹 Deleted ${results.length} Parse objects from ${className}`);
|
||||
}
|
||||
|
||||
hasMore = results.length === limit;
|
||||
}
|
||||
|
||||
console.log(`✅ Finished deletion from ${className} for user: ${userPointer.objectId}`);
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
import axios from 'axios';
|
||||
import { cloudServerUrl, generateId, serverAppId } from '../../Utils.js';
|
||||
import sendmailtoSupport from './sendMailToSupport.js';
|
||||
import { deleteContactsInBatch, deleteDataFiles, deleteInBatches } from './deleteFileUrl.js';
|
||||
const serverUrl = cloudServerUrl;
|
||||
const appId = serverAppId;
|
||||
|
||||
const deleteSessionsAndUser = async (userPointer, userId) => {
|
||||
const Session = Parse.Object.extend('_Session');
|
||||
const sessionQuery = new Parse.Query(Session);
|
||||
sessionQuery.equalTo('user', userPointer);
|
||||
const sessions = await sessionQuery.find({ useMasterKey: true });
|
||||
if (sessions?.length > 0) await Parse.Object.destroyAll(sessions, { useMasterKey: true });
|
||||
|
||||
const userObj = await new Parse.Query(Parse.User).get(userId, { useMasterKey: true });
|
||||
if (userObj) await userObj.destroy({ useMasterKey: true });
|
||||
};
|
||||
|
||||
const resetPasswordAndDeleteSession = async userId => {
|
||||
const password = generateId(16);
|
||||
const user = await new Parse.Query(Parse.User).get(userId, { useMasterKey: true });
|
||||
user.set('password', password);
|
||||
user.set('emailVerified', false);
|
||||
user.unset('ProfilePic');
|
||||
await user.save(null, { useMasterKey: true });
|
||||
|
||||
// Optional: revoke all existing sessions (forces logout everywhere)
|
||||
const sessionQuery = new Parse.Query('_Session');
|
||||
sessionQuery.equalTo('user', user);
|
||||
const sessions = await sessionQuery.find({ useMasterKey: true });
|
||||
if (sessions.length) {
|
||||
await Parse.Object.destroyAll(sessions, { useMasterKey: true });
|
||||
}
|
||||
};
|
||||
export async function deleteUser(userId, adminId) {
|
||||
const userPointer = { __type: 'Pointer', className: '_User', objectId: userId };
|
||||
let userDetails = {
|
||||
UserRole: 'not found',
|
||||
Name: 'not found',
|
||||
Email: 'not found',
|
||||
UserId: userId || 'not found',
|
||||
objectId: 'not found',
|
||||
TenantId: 'not found',
|
||||
};
|
||||
try {
|
||||
// STEP 1: contracts_Users lookup
|
||||
const Users = Parse.Object.extend('contracts_Users');
|
||||
const userQuery = new Parse.Query(Users);
|
||||
userQuery.equalTo('UserId', userPointer);
|
||||
if (adminId) {
|
||||
userQuery.equalTo('CreatedBy', { __type: 'Pointer', className: '_User', objectId: adminId });
|
||||
}
|
||||
const userResult = await userQuery.first({ useMasterKey: true });
|
||||
userDetails = { ...userDetails, UserId: userId };
|
||||
if (!userResult) {
|
||||
const errorMessage = 'User not found.';
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
const contractsUserId = userResult.id;
|
||||
const tenantId = userResult.get('TenantId')?.id;
|
||||
const teamIds = userResult.get('TeamIds') || [];
|
||||
const organizationId = userResult.get('OrganizationId')?.id;
|
||||
const isAdmin = userResult?.get('UserRole') === 'contracts_Admin' ? true : false;
|
||||
userDetails = {
|
||||
...userDetails,
|
||||
UserRole: userResult?.get('UserRole'),
|
||||
Name: userResult?.get('Name'),
|
||||
Email: userResult?.get('Email'),
|
||||
UserId: userResult?.get('UserId')?.id,
|
||||
objectId: userResult?.id,
|
||||
TenantId: userResult?.get('TenantId')?.id,
|
||||
};
|
||||
if (adminId && isAdmin) {
|
||||
const errorMessage = 'An error occurred while deleting your account.';
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
|
||||
// STEP 2: contracts_Document & contracts_Template
|
||||
try {
|
||||
for (const className of ['contracts_Document', 'contracts_Template']) {
|
||||
await deleteInBatches(className, userPointer);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed during contracts_Template cleanup:', err);
|
||||
const errorMessage = 'Failed during contracts_Template cleanup:' + err?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
|
||||
// STEP 3: delete Contacts created by user from contactbook class
|
||||
try {
|
||||
await deleteContactsInBatch('contracts_Contactbook', userPointer);
|
||||
} catch (err) {
|
||||
console.error('Failed during contactbook cleanup:', err);
|
||||
const errorMessage = 'Failed during contactbook cleanup:' + err?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if any rows remain for this UserId
|
||||
const Contactbook = Parse.Object.extend('contracts_Contactbook');
|
||||
const remainingCount = await new Parse.Query(Contactbook)
|
||||
.equalTo('UserId', userPointer)
|
||||
.count({ useMasterKey: true });
|
||||
|
||||
// If no record remains delete from _User class
|
||||
if (remainingCount === 0) {
|
||||
await deleteSessionsAndUser(userPointer, userId);
|
||||
} else {
|
||||
await resetPasswordAndDeleteSession(userId);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed during contactbook current user cleanup:', err);
|
||||
const errorMessage =
|
||||
'Failed during contactbook current user cleanup: ' + (err?.message || err);
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
|
||||
// STEP 4: appToken
|
||||
try {
|
||||
const AppToken = Parse.Object.extend('appToken');
|
||||
const query = new Parse.Query(AppToken);
|
||||
query.equalTo('UserId', userPointer);
|
||||
const tokens = await query.find({ useMasterKey: true });
|
||||
if (tokens?.length) await Parse.Object.destroyAll(tokens, { useMasterKey: true });
|
||||
} catch (err) {
|
||||
console.error('Failed to delete appToken entries:', err);
|
||||
const errorMessage = 'Failed to delete appToken entries:' + err?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
|
||||
// STEP 5: partner_DataFiles
|
||||
try {
|
||||
await deleteDataFiles('partners_DataFiles', userPointer);
|
||||
} catch (err) {
|
||||
console.error('Failed during partners_DataFiles cleanup:', err);
|
||||
const errorMessage = 'Failed during partners_DataFiles cleanup:' + err?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
// STEP 6: contracts_Organizations
|
||||
try {
|
||||
if (organizationId) {
|
||||
const Org = Parse.Object.extend('contracts_Organizations');
|
||||
const query = new Parse.Query(Org);
|
||||
const object = await query.get(organizationId, { useMasterKey: true });
|
||||
await object.destroy({ useMasterKey: true });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete contracts_Organizations entry:', err);
|
||||
const errorMessage = 'Failed to delete contracts_Organizations entry:' + err?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
// STEP 7: Delete each entry in contracts_Teams by objectId from teamIds
|
||||
try {
|
||||
if (teamIds.length > 0) {
|
||||
const Teams = Parse.Object.extend('contracts_Teams');
|
||||
for (const team of teamIds) {
|
||||
try {
|
||||
const teamObj = await new Parse.Query(Teams).get(team.id, { useMasterKey: true });
|
||||
if (teamObj) await teamObj.destroy({ useMasterKey: true });
|
||||
} catch (teamErr) {
|
||||
console.error(`Failed to delete team with ID ${team.id}:`, teamErr);
|
||||
const errorMessage = `Failed to delete team with ID ${team.id}` + teamErr?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed during contracts_Teams deletion loop:', err);
|
||||
const errorMessage = 'Failed during contracts_Teams deletion loop:' + err?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
|
||||
// STEP 8 : partners_Tenant cleanup
|
||||
try {
|
||||
if (tenantId) {
|
||||
const Tenant = Parse.Object.extend('partners_Tenant');
|
||||
const query = new Parse.Query(Tenant);
|
||||
const tenantObj = await query.get(tenantId, { useMasterKey: true });
|
||||
await tenantObj.destroy({ useMasterKey: true });
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = `Failed during partners_Tenant ${'cleanup:'} `;
|
||||
console.error(msg, err);
|
||||
const errorMessage = msg + err?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
|
||||
// STEP 9: partners_TenantCredits cleanup
|
||||
try {
|
||||
if (tenantId) {
|
||||
const tenantCredits = Parse.Object.extend('partners_TenantCredits');
|
||||
const subsByTenant = new Parse.Query(tenantCredits);
|
||||
subsByTenant.equalTo('PartnersTenant', {
|
||||
__type: 'Pointer',
|
||||
className: 'partners_Tenant',
|
||||
objectId: tenantId,
|
||||
});
|
||||
const subs = await subsByTenant.find({ useMasterKey: true });
|
||||
await Parse.Object.destroyAll(subs, { useMasterKey: true });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed during partners_TenantCredits cleanup:', err);
|
||||
const errorMessage = 'Failed during partners_TenantCredits cleanup:' + err?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
}
|
||||
// STEP 10: contracts_Signature
|
||||
try {
|
||||
const Signature = Parse.Object.extend('contracts_Signature');
|
||||
const sigQuery = new Parse.Query(Signature);
|
||||
sigQuery.equalTo('UserId', userPointer);
|
||||
const sigResults = await sigQuery.find({ useMasterKey: true });
|
||||
if (sigResults?.length > 0) await Parse.Object.destroyAll(sigResults, { useMasterKey: true });
|
||||
} catch (err) {
|
||||
console.error('Failed during contracts_Signature cleanup:', err);
|
||||
const errorMessage = 'Failed during contracts_Signature cleanup:' + err?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
|
||||
// STEP 11: contracts_Users
|
||||
try {
|
||||
await userResult.destroy({ useMasterKey: true });
|
||||
} catch (err) {
|
||||
console.error('Failed to delete contracts_Users entry:', err);
|
||||
const errorMessage = 'Failed to delete contracts_Users entry:' + err?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
return { code: 200, message: 'User and all associated data deleted successfully.' };
|
||||
} catch (error) {
|
||||
console.error('User deletion process failed:', error);
|
||||
const errorMessage = `User deletion failed: ${error.message || error}`;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
}
|
||||
|
||||
// 1. HTML Password Prompt Page
|
||||
export const deleteUserGet = async (req, res) => {
|
||||
const { userId } = req.params;
|
||||
|
||||
const extUserQuery = new Parse.Query('Contracts_Users');
|
||||
extUserQuery.equalTo('userId', { __type: 'Pointer', className: '_User', objectId: userId });
|
||||
const extUser = await extUserQuery.first({ useMasterKey: true });
|
||||
if (!extUser) {
|
||||
const errorMessage = 'User not found.';
|
||||
return res.send(errorMessage);
|
||||
}
|
||||
const routePath = process?.env?.SERVER_URL?.includes?.('api') ? '/api' : '';
|
||||
const htmlForm = `
|
||||
<html>
|
||||
<head>
|
||||
<title>Delete Account</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #ffffff;
|
||||
padding: 40px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #dc3545;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #d9534f;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #c9302c;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: #6c757d;
|
||||
font-size: 14px;
|
||||
margin-top: -10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Confirm Account Deletion</h2>
|
||||
<p class="warning">This action is irreversible. Please confirm by entering your password.</p>
|
||||
<form method="POST" action="${routePath}/delete-account/${userId}">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" name="password" id="password" placeholder="Please provide your password" required />
|
||||
<button type="submit">Delete My Account</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
res.send(htmlForm);
|
||||
};
|
||||
|
||||
// 2. Handle Password Verification and Deletion
|
||||
export const deleteUserPost = async (req, res) => {
|
||||
const { userId } = req.params;
|
||||
const routePath = process?.env?.SERVER_URL?.includes?.('api') ? '/api' : '';
|
||||
const { password } = req.body;
|
||||
let userDetails = {
|
||||
UserRole: 'not found',
|
||||
Name: 'not found',
|
||||
Email: 'not found',
|
||||
UserId: userId || 'not found',
|
||||
objectId: 'not found',
|
||||
TenantId: 'not found',
|
||||
};
|
||||
if (!userId) return res.status(404).send('Missing userId parameter.');
|
||||
|
||||
try {
|
||||
// 1. Get the user
|
||||
const userQuery = new Parse.Query(Parse.User);
|
||||
userQuery.equalTo('objectId', userId);
|
||||
const user = await userQuery.first({ useMasterKey: true });
|
||||
if (!user) {
|
||||
const errorMessage = 'User not found.';
|
||||
// sendmailtoSupport(userDetails, errorMessage);
|
||||
// return res.status(404).send(errorMessage);
|
||||
return res.send(errorMessage);
|
||||
}
|
||||
const extUserQuery = new Parse.Query('Contracts_Users');
|
||||
extUserQuery.equalTo('userId', { __type: 'Pointer', className: '_User', objectId: userId });
|
||||
const extUser = await extUserQuery.first({ useMasterKey: true });
|
||||
if (!extUser) {
|
||||
const errorMessage = 'User not found.';
|
||||
return res.send(errorMessage);
|
||||
}
|
||||
// 2. Attempt login to verify password
|
||||
const username = user.get('username'); // assuming 'username' is used for login
|
||||
try {
|
||||
// await Parse.User.logIn(username, password); // Will throw if password invalid
|
||||
// Use REST login to avoid mutating the global Parse current user
|
||||
// Will throw if password invalid
|
||||
const res = await axios.get(serverUrl + '/login', {
|
||||
params: { username, password },
|
||||
headers: { 'X-Parse-Application-Id': appId },
|
||||
});
|
||||
console.log('Res ', res?.data);
|
||||
} catch (err) {
|
||||
console.log('err while validating password: ', err?.response?.data || err);
|
||||
const errorMessage = `Invalid password. <a href="${routePath}/delete-account/${userId}">Try again</a>`;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return res.status(401).send(errorMessage);
|
||||
}
|
||||
|
||||
const response = await deleteUser(userId);
|
||||
const code = response?.code || 500;
|
||||
const message = response?.message || 'An error occurred while deleting your account.';
|
||||
return res.status(code).send(message);
|
||||
} catch (error) {
|
||||
console.error('Account deletion error:', error);
|
||||
const errorMessage = error?.message || 'An error occurred while deleting your account.';
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return res.status(500).send(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Handle Password Verification and Deletion
|
||||
export const deleteUserByAdmin = async (req, res) => {
|
||||
const sessiontoken = req.headers.sessiontoken;
|
||||
const userId = req.params.userId;
|
||||
let userDetails = {
|
||||
UserRole: 'not found',
|
||||
Name: 'not found',
|
||||
Email: 'not found',
|
||||
UserId: userId || 'not found',
|
||||
objectId: 'not found',
|
||||
TenantId: 'not found',
|
||||
};
|
||||
if (!sessiontoken) return res.status(400).json({ message: 'unauthorized.' });
|
||||
if (!userId || userId === ':userId') {
|
||||
return res.status(400).json({ message: 'Missing userId parameter.' });
|
||||
}
|
||||
try {
|
||||
const axiosRes = await axios.get(serverUrl + '/users/me', {
|
||||
headers: {
|
||||
'X-Parse-Application-Id': appId,
|
||||
'X-Parse-Session-Token': sessiontoken,
|
||||
},
|
||||
});
|
||||
const adminId = axiosRes?.data && axiosRes.data?.objectId;
|
||||
|
||||
if (!adminId) {
|
||||
return res.status(400).json({ message: 'Unauthorized.' });
|
||||
}
|
||||
// 1. Get the user
|
||||
const userQuery = new Parse.Query(Parse.User);
|
||||
userQuery.equalTo('objectId', userId);
|
||||
const user = await userQuery.first({ useMasterKey: true });
|
||||
if (!user) {
|
||||
const errorMessage = 'User not found.';
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return res.status(400).json({ message: errorMessage });
|
||||
}
|
||||
const response = await deleteUser(userId, adminId);
|
||||
const code = response?.code || 400;
|
||||
const message = response?.message || 'An error occurred while deleting your account.';
|
||||
return res.status(code).json({ message: message });
|
||||
} catch (error) {
|
||||
const code = error?.response?.data?.code || 400;
|
||||
const errorMessage =
|
||||
error?.response?.data?.error ||
|
||||
error?.message ||
|
||||
'An error occurred while deleting your account.';
|
||||
console.error(`Account deletion error:`, errorMessage);
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return res.status(code).json({ message: errorMessage });
|
||||
}
|
||||
};
|
||||
@@ -141,6 +141,11 @@ export default async function docxtopdf(req, res) {
|
||||
className: 'partners_Tenant',
|
||||
objectId: tenantId,
|
||||
},
|
||||
UserId: {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: userRes.data.objectId,
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { appName, smtpenable } from '../../Utils.js';
|
||||
|
||||
export default async function sendmailtoSupport(userDetails, errorMessage) {
|
||||
const mailsender = smtpenable ? process.env.SMTP_USER_EMAIL : process.env.MAILGUN_SENDER;
|
||||
// Render a simple HTML form. In production, consider using a templating engine.
|
||||
try {
|
||||
await Parse.Cloud.sendEmail({
|
||||
sender: appName + ' <' + mailsender + '>',
|
||||
recipient: 'support@opensignlabs.com',
|
||||
subject: `Error while deleting account – ${appName}`,
|
||||
text: `Error while deleting account – ${appName}`,
|
||||
html: `<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
margin: 0;
|
||||
padding: 40px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.05);
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #dc3545;
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
color: #495057;
|
||||
margin-top: 30px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.details p {
|
||||
margin: 10px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: bold;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #495057;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Error: ${errorMessage}</h1>
|
||||
|
||||
<h2>User Details</h2>
|
||||
<div class="details">
|
||||
<p><span class="label">UserRole:</span> <span class="value">${userDetails?.UserRole}</span></p>
|
||||
<p><span class="label">Name:</span> <span class="value">${userDetails?.Name}</span></p>
|
||||
<p><span class="label">Email:</span> <span class="value">${userDetails?.Email}</span></p>
|
||||
<p><span class="label">UserId:</span> <span class="value">${userDetails?.UserId}</span></p>
|
||||
<p><span class="label">ExtUserId:</span> <span class="value">${userDetails?.objectId}</span></p>
|
||||
<p><span class="label">TenantId:</span> <span class="value">${userDetails?.TenantId}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`,
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('err while sending mail to support', err);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import multerS3 from 'multer-s3';
|
||||
import aws from 'aws-sdk';
|
||||
import dotenv from 'dotenv';
|
||||
import { cloudServerUrl, serverAppId, useLocal } from '../../Utils.js';
|
||||
dotenv.config();
|
||||
dotenv.config({ quiet: true });
|
||||
|
||||
function sanitizeFileName(fileName) {
|
||||
// Remove spaces and invalid characters
|
||||
|
||||
@@ -55,6 +55,7 @@ import recreateDocument from './parsefunction/recreateDocument.js';
|
||||
import loginUser from './parsefunction/loginUser.js';
|
||||
import addUser from './parsefunction/addUser.js';
|
||||
import filterDocs from './parsefunction/filterDocs.js';
|
||||
import sendDeleteUserMail from './parsefunction/sendDeleteUserMail.js';
|
||||
|
||||
// This afterSave function triggers after an object is added or updated in the specified class, allowing for post-processing logic.
|
||||
Parse.Cloud.afterSave('contracts_Document', DocumentAftersave);
|
||||
@@ -120,3 +121,4 @@ Parse.Cloud.define('recreatedoc', recreateDocument);
|
||||
Parse.Cloud.define('loginuser', loginUser);
|
||||
Parse.Cloud.define('adduser', addUser);
|
||||
Parse.Cloud.define('filterdocs', filterDocs);
|
||||
Parse.Cloud.define('senddeleterequest', sendDeleteUserMail);
|
||||
|
||||
@@ -1,45 +1,35 @@
|
||||
import { useLocal } from '../../Utils.js';
|
||||
import { handleValidImage, useLocal } from '../../Utils.js';
|
||||
import getPresignedUrl, { presignedlocalUrl } from './getSignedUrl.js';
|
||||
|
||||
async function DocumentAfterFind(request) {
|
||||
if (request.objects.length === 1) {
|
||||
if (request.objects) {
|
||||
const obj = request.objects[0];
|
||||
if (
|
||||
useLocal !== 'true'
|
||||
) {
|
||||
const SignedUrl = obj?.get('SignedUrl') && obj?.get('SignedUrl');
|
||||
const Url = obj?.get('URL') && obj?.get('URL');
|
||||
const certificateUrl = obj.get('CertificateUrl') && obj.get('CertificateUrl');
|
||||
const SignedUrl = obj?.get('SignedUrl') && obj?.get('SignedUrl');
|
||||
const Url = obj?.get('URL') && obj?.get('URL');
|
||||
const certificateUrl = obj.get('CertificateUrl') && obj.get('CertificateUrl');
|
||||
const isPrefillExist = obj?.get('Placeholders')?.some(x => x.Role === 'prefill');
|
||||
const Placeholder = obj?.get('Placeholders') || [];
|
||||
if (useLocal !== 'true') {
|
||||
if (isPrefillExist) {
|
||||
const updatedPlaceHolder = await handleValidImage(Placeholder);
|
||||
obj.set('Placeholders', updatedPlaceHolder);
|
||||
}
|
||||
if (SignedUrl) {
|
||||
obj.set(
|
||||
'SignedUrl',
|
||||
getPresignedUrl(
|
||||
SignedUrl,
|
||||
)
|
||||
);
|
||||
obj.set('SignedUrl', getPresignedUrl(SignedUrl));
|
||||
}
|
||||
if (Url) {
|
||||
obj.set(
|
||||
'URL',
|
||||
getPresignedUrl(
|
||||
Url,
|
||||
)
|
||||
);
|
||||
obj.set('URL', getPresignedUrl(Url));
|
||||
}
|
||||
if (certificateUrl) {
|
||||
obj.set(
|
||||
'CertificateUrl',
|
||||
getPresignedUrl(
|
||||
certificateUrl,
|
||||
)
|
||||
);
|
||||
obj.set('CertificateUrl', getPresignedUrl(certificateUrl));
|
||||
}
|
||||
return [obj];
|
||||
} else if (useLocal == 'true') {
|
||||
const SignedUrl = obj?.get('SignedUrl') && obj?.get('SignedUrl');
|
||||
const Url = obj?.get('URL') && obj?.get('URL');
|
||||
const certificateUrl = obj.get('CertificateUrl') && obj.get('CertificateUrl');
|
||||
if (isPrefillExist) {
|
||||
const updatedPlaceHolder = await handleValidImage(Placeholder);
|
||||
obj.set('Placeholders', updatedPlaceHolder);
|
||||
}
|
||||
if (SignedUrl) {
|
||||
obj.set('SignedUrl', presignedlocalUrl(SignedUrl));
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ export default async function forwardDoc(request) {
|
||||
}
|
||||
const _docRes = docRes?.toJSON();
|
||||
const docName = _docRes.Name;
|
||||
const fileAdapterId = _docRes?.FileAdapterId || '';
|
||||
const extUserId = _docRes?.ExtUserPtr?.objectId;
|
||||
const TenantAppName = appName;
|
||||
const from = _docRes?.ExtUserPtr?.Email;
|
||||
|
||||
@@ -1,45 +1,35 @@
|
||||
import { useLocal } from '../../Utils.js';
|
||||
import { handleValidImage, useLocal } from '../../Utils.js';
|
||||
import getPresignedUrl, { presignedlocalUrl } from './getSignedUrl.js';
|
||||
|
||||
async function TemplateAfterFind(request) {
|
||||
if (request.objects.length === 1) {
|
||||
if (request.objects) {
|
||||
const obj = request.objects[0];
|
||||
if (
|
||||
useLocal !== 'true'
|
||||
) {
|
||||
const SignedUrl = obj?.get('SignedUrl') && obj?.get('SignedUrl');
|
||||
const Url = obj?.get('URL') && obj?.get('URL');
|
||||
const certificateUrl = obj.get('CertificateUrl') && obj.get('CertificateUrl');
|
||||
const SignedUrl = obj?.get('SignedUrl') && obj?.get('SignedUrl');
|
||||
const Url = obj?.get('URL') && obj?.get('URL');
|
||||
const certificateUrl = obj.get('CertificateUrl') && obj?.get('CertificateUrl');
|
||||
const isPrefillExist = obj?.get('Placeholders')?.some(x => x.Role === 'prefill');
|
||||
const Placeholder = obj?.get('Placeholders') || [];
|
||||
if (useLocal !== 'true') {
|
||||
if (isPrefillExist) {
|
||||
const updatedPlaceHolder = await handleValidImage(Placeholder);
|
||||
obj.set('Placeholders', updatedPlaceHolder);
|
||||
}
|
||||
if (SignedUrl) {
|
||||
obj.set(
|
||||
'SignedUrl',
|
||||
getPresignedUrl(
|
||||
SignedUrl,
|
||||
)
|
||||
);
|
||||
obj.set('SignedUrl', getPresignedUrl(SignedUrl));
|
||||
}
|
||||
if (Url) {
|
||||
obj.set(
|
||||
'URL',
|
||||
getPresignedUrl(
|
||||
Url,
|
||||
)
|
||||
);
|
||||
obj.set('URL', getPresignedUrl(Url));
|
||||
}
|
||||
if (certificateUrl) {
|
||||
obj.set(
|
||||
'CertificateUrl',
|
||||
getPresignedUrl(
|
||||
certificateUrl,
|
||||
)
|
||||
);
|
||||
obj.set('CertificateUrl', getPresignedUrl(certificateUrl));
|
||||
}
|
||||
return [obj];
|
||||
} else if (useLocal == 'true') {
|
||||
const SignedUrl = obj?.get('SignedUrl') && obj?.get('SignedUrl');
|
||||
const Url = obj?.get('URL') && obj?.get('URL');
|
||||
const certificateUrl = obj.get('CertificateUrl') && obj.get('CertificateUrl');
|
||||
if (isPrefillExist) {
|
||||
const updatedPlaceHolder = await handleValidImage(Placeholder);
|
||||
obj.set('Placeholders', updatedPlaceHolder);
|
||||
}
|
||||
if (SignedUrl) {
|
||||
obj.set('SignedUrl', presignedlocalUrl(SignedUrl));
|
||||
}
|
||||
|
||||
@@ -20,8 +20,10 @@ export default async function createBatchContact(req) {
|
||||
UserRole: 'contracts_Guest',
|
||||
TenantId: { __type: 'Pointer', className: 'partners_Tenant', objectId: x.TenantId },
|
||||
CreatedBy: { __type: 'Pointer', className: '_User', objectId: req.user.id },
|
||||
Name: x.Name,
|
||||
Name: x.Name?.trim(),
|
||||
Email: x.Email?.toLowerCase()?.replace(/\s/g, ''),
|
||||
Company: x?.Company?.trim(),
|
||||
JobTitle: x?.JobTitle?.trim(),
|
||||
IsDeleted: false,
|
||||
IsImported: true,
|
||||
...(x?.Phone ? { Phone: `${x?.Phone}` } : {}),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export default async function editContact(request) {
|
||||
const { contactId, name, email, phone, tenantId } = request.params;
|
||||
const company = request.params?.company;
|
||||
const jobTitle = request.params?.jobTitle;
|
||||
if (!request?.user) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'User is not authenticated.');
|
||||
}
|
||||
@@ -25,6 +27,12 @@ export default async function editContact(request) {
|
||||
if (phone) {
|
||||
contactQuery.set('Phone', phone);
|
||||
}
|
||||
if (company) {
|
||||
contactQuery.set('Company', company);
|
||||
}
|
||||
if (jobTitle) {
|
||||
contactQuery.set('JobTitle', jobTitle);
|
||||
}
|
||||
contactQuery.set('Email', email?.toLowerCase()?.replace(/\s/g, ''));
|
||||
contactQuery.set('UserRole', 'contracts_Guest');
|
||||
contactQuery.set('IsDeleted', false);
|
||||
@@ -55,7 +63,7 @@ export default async function editContact(request) {
|
||||
acl.setWriteAccess(createdBy.objectId, true);
|
||||
contactQuery.setACL(acl);
|
||||
|
||||
const res = await contactQuery.save();
|
||||
const res = await contactQuery.save(null, { useMasterKey: true });
|
||||
const parseData = JSON.parse(JSON.stringify(res));
|
||||
return parseData;
|
||||
}
|
||||
@@ -76,7 +84,7 @@ export default async function editContact(request) {
|
||||
acl.setReadAccess(createdBy.objectId, true);
|
||||
acl.setWriteAccess(createdBy.objectId, true);
|
||||
contactQuery.setACL(acl);
|
||||
const res = await contactQuery.save();
|
||||
const res = await contactQuery.save(null, { useMasterKey: true });
|
||||
const parseData = JSON.parse(JSON.stringify(res));
|
||||
return parseData;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import fs from 'node:fs';
|
||||
import dotenv from 'dotenv';
|
||||
import GenerateCertificate from './pdf/GenerateCertificate.js';
|
||||
import { getSecureUrl } from '../../Utils.js';
|
||||
dotenv.config();
|
||||
dotenv.config({ quiet: true });
|
||||
const eSignName = 'OpenSign';
|
||||
const eSigncontact = 'hello@opensignlabs.com';
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { cloudServerUrl, serverAppId } from '../../Utils.js';
|
||||
export default async function getDocument(request) {
|
||||
const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
|
||||
const docId = request.params.docId;
|
||||
const include = request?.params?.include || '';
|
||||
const sessiontoken = request?.headers?.sessiontoken || '';
|
||||
try {
|
||||
if (docId) {
|
||||
@@ -17,6 +18,9 @@ export default async function getDocument(request) {
|
||||
query.include('Placeholders');
|
||||
query.include('DeclineBy');
|
||||
query.notEqualTo('IsArchive', true);
|
||||
if (include) {
|
||||
query?.include(include);
|
||||
}
|
||||
const res = await query.first({ useMasterKey: true });
|
||||
if (res) {
|
||||
const IsEnableOTP = res?.get('IsEnableOTP') || false;
|
||||
|
||||
@@ -2,27 +2,21 @@ import AWS from 'aws-sdk';
|
||||
import { useLocal } from '../../Utils.js';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
dotenv.config({ quiet: true });
|
||||
|
||||
export default function getPresignedUrl(
|
||||
url,
|
||||
) {
|
||||
export default function getPresignedUrl(url) {
|
||||
if (url?.includes('files')) {
|
||||
return presignedlocalUrl(url);
|
||||
} else {
|
||||
const credentials = {
|
||||
accessKeyId:
|
||||
process.env.DO_ACCESS_KEY_ID,
|
||||
secretAccessKey:
|
||||
process.env.DO_SECRET_ACCESS_KEY,
|
||||
accessKeyId: process.env.DO_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.DO_SECRET_ACCESS_KEY,
|
||||
};
|
||||
AWS.config.update({
|
||||
credentials: credentials,
|
||||
region:
|
||||
process.env.DO_REGION,
|
||||
region: process.env.DO_REGION,
|
||||
});
|
||||
const spacesEndpoint =
|
||||
new AWS.Endpoint(process.env.DO_ENDPOINT);
|
||||
const spacesEndpoint = new AWS.Endpoint(process.env.DO_ENDPOINT);
|
||||
|
||||
const s3 = new AWS.S3({ endpoint: spacesEndpoint, signatureVersion: 'v4' });
|
||||
|
||||
@@ -35,8 +29,7 @@ export default function getPresignedUrl(
|
||||
|
||||
// presignedGETURL return presignedUrl with expires time
|
||||
const presignedGETURL = s3.getSignedUrl('getObject', {
|
||||
Bucket:
|
||||
process.env.DO_SPACE,
|
||||
Bucket: process.env.DO_SPACE,
|
||||
Key: filename, //filename
|
||||
Expires: 160, //time to expire in seconds
|
||||
});
|
||||
@@ -53,35 +46,22 @@ export async function getSignedUrl(request) {
|
||||
try {
|
||||
if (url?.includes('files')) {
|
||||
return presignedlocalUrl(url);
|
||||
} else if (
|
||||
useLocal !== 'true'
|
||||
) {
|
||||
} else if (useLocal !== 'true') {
|
||||
const query = new Parse.Query(docId ? 'contracts_Document' : 'contracts_Template');
|
||||
query.equalTo('objectId', docId ? docId : templateId);
|
||||
query.include('ExtUserPtr.TenantId');
|
||||
query.notEqualTo('IsArchive', true);
|
||||
const res = await query.first({ useMasterKey: true });
|
||||
if (res) {
|
||||
const _resDoc = JSON.parse(JSON.stringify(res));
|
||||
if (_resDoc?.IsEnableOTP) {
|
||||
if (!request?.user) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_SESSION_TOKEN,
|
||||
'User is not authenticated.'
|
||||
);
|
||||
} else {
|
||||
const presignedUrl = getPresignedUrl(
|
||||
url,
|
||||
);
|
||||
return presignedUrl;
|
||||
}
|
||||
} else {
|
||||
const presignedUrl = getPresignedUrl(
|
||||
url,
|
||||
);
|
||||
return presignedUrl;
|
||||
}
|
||||
if (!res) return url;
|
||||
|
||||
const _resDoc = res?.toJSON();
|
||||
// Ensure user is authenticated if OTP is required
|
||||
if (_resDoc?.IsEnableOTP && !request?.user) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'User is not authenticated.');
|
||||
}
|
||||
|
||||
const presignedUrl = getPresignedUrl(url);
|
||||
return presignedUrl;
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
|
||||
@@ -35,8 +35,7 @@ export default async function getSigners(request) {
|
||||
searchObj.CreatedBy = { __type: 'Pointer', className: '_User', objectId: request?.user?.id };
|
||||
searchObj.sessionToken = request.user.getSessionToken();
|
||||
return await getContacts(searchObj);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
async function getTenantByUserId(userId, contactId) {
|
||||
try {
|
||||
if (contactId) {
|
||||
@@ -53,8 +52,7 @@ export default async function getTenant(request) {
|
||||
|
||||
if (userId || contactId) {
|
||||
return await getTenantByUserId(userId, contactId);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,7 @@ async function getUserDetails(request) {
|
||||
const msg = err?.message || 'Something went wrong.';
|
||||
throw new Parse.Error(code, msg);
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'User is not authenticated.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
export default async function isUserInContactBook(request) {
|
||||
try {
|
||||
if (request.user) {
|
||||
|
||||
@@ -6,6 +6,12 @@ const saveRoleContact = async contact => {
|
||||
if (contact?.Phone) {
|
||||
contactQuery.set('Phone', contact.Phone);
|
||||
}
|
||||
if (contact?.JobTitle) {
|
||||
contactQuery.set('JobTitle', contact.JobTitle);
|
||||
}
|
||||
if (contact?.Company) {
|
||||
contactQuery.set('Company', contact.Company);
|
||||
}
|
||||
contactQuery.set('CreatedBy', contact.CreatedBy);
|
||||
contactQuery.set('UserId', contact.UserId);
|
||||
contactQuery.set('UserRole', 'contracts_Guest');
|
||||
@@ -23,7 +29,7 @@ const saveRoleContact = async contact => {
|
||||
acl.setReadAccess(contact.UserId.objectId, true);
|
||||
acl.setWriteAccess(contact.UserId.objectId, true);
|
||||
contactQuery.setACL(acl);
|
||||
const contactRes = await contactQuery.save();
|
||||
const contactRes = await contactQuery.save(null, { useMasterKey: true });
|
||||
if (contactRes) {
|
||||
return contactRes;
|
||||
}
|
||||
@@ -37,6 +43,8 @@ export default async function linkContactToDoc(req) {
|
||||
const docId = req.params.docId;
|
||||
const name = req.params.name;
|
||||
const phone = req.params.phone;
|
||||
const jobTitle = req.params.jobTitle;
|
||||
const company = req.params.company;
|
||||
try {
|
||||
if (docId) {
|
||||
// Execute the query to get the document with the specified 'docId'
|
||||
@@ -110,6 +118,8 @@ export default async function linkContactToDoc(req) {
|
||||
UserId: _extUser.UserId,
|
||||
Name: _extUser.Name,
|
||||
Email: email,
|
||||
JobTitle: _extUser?.JobTitle || '',
|
||||
Company: _extUser?.Company || '',
|
||||
Phone: _extUser?.Phone ? _extUser.Phone : '',
|
||||
CreatedBy: _docRes.CreatedBy,
|
||||
TenantId: _docRes.ExtUserPtr?.TenantId?.objectId,
|
||||
@@ -163,6 +173,8 @@ export default async function linkContactToDoc(req) {
|
||||
Name: name,
|
||||
Email: email,
|
||||
Phone: phone,
|
||||
JobTitle: jobTitle,
|
||||
Company: company,
|
||||
CreatedBy: _docRes.CreatedBy,
|
||||
TenantId: _docRes.ExtUserPtr?.TenantId?.objectId,
|
||||
};
|
||||
@@ -220,6 +232,8 @@ export default async function linkContactToDoc(req) {
|
||||
Name: name,
|
||||
Email: email,
|
||||
Phone: phone,
|
||||
JobTitle: jobTitle,
|
||||
Company: company,
|
||||
CreatedBy: _docRes.CreatedBy,
|
||||
TenantId: _docRes.ExtUserPtr?.TenantId?.objectId,
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import { pdflibAddPlaceholder } from '@signpdf/placeholder-pdf-lib';
|
||||
import { Placeholder } from './Placeholder.js';
|
||||
import { SignPdf } from '@signpdf/signpdf';
|
||||
import { P12Signer } from '@signpdf/signer-p12';
|
||||
import { buildDownloadFilename } from '../../../utils/fileUtils.js';
|
||||
|
||||
const serverUrl = cloudServerUrl; // process.env.SERVER_URL;
|
||||
const APPID = serverAppId;
|
||||
@@ -79,7 +80,10 @@ async function updateDoc(docId, url, userId, ipAddress, data, className, sign) {
|
||||
const auditTrail = updateAuditTrail.filter(x => x.Activity === 'Signed');
|
||||
let isCompleted = false;
|
||||
if (data.Signers && data.Signers.length > 0) {
|
||||
if (auditTrail.length === data.Placeholders.length) {
|
||||
//'removePrefill' is used to remove prefill role from placeholders filed then compare length to change status of document
|
||||
const removePrefill =
|
||||
data.Placeholders.length > 0 && data.Placeholders.filter(x => x.Role !== 'prefill');
|
||||
if (auditTrail.length === removePrefill?.length) {
|
||||
isCompleted = true;
|
||||
}
|
||||
} else {
|
||||
@@ -108,9 +112,11 @@ async function sendNotifyMail(doc, signUser, mailProvider, publicUrl) {
|
||||
"<img src='https://qikinnovation.ams3.digitaloceanspaces.com/logo.png' height='50' style='padding:20px'/>";
|
||||
const opurl = ` <a href=www.opensignlabs.com target=_blank>here</a>`;
|
||||
const auditTrailCount = doc?.AuditTrail?.filter(x => x.Activity === 'Signed')?.length || 0;
|
||||
const signersCount = doc?.Placeholders?.length;
|
||||
const remaingsign = signersCount - auditTrailCount;
|
||||
if (remaingsign > 1 && doc?.NotifyOnSignatures) {
|
||||
const removePrefill =
|
||||
doc?.Placeholders?.length > 0 && doc?.Placeholders?.filter(x => x?.Role !== 'prefill');
|
||||
const signersCount = removePrefill?.length;
|
||||
const remainingSign = signersCount - auditTrailCount;
|
||||
if (remainingSign > 1 && doc?.NotifyOnSignatures) {
|
||||
const sender = doc.ExtUserPtr;
|
||||
const pdfName = doc.Name;
|
||||
const creatorName = doc.ExtUserPtr.Name;
|
||||
@@ -227,6 +233,13 @@ async function sendCompletedMail(obj) {
|
||||
}
|
||||
const Bcc = doc?.Bcc?.length > 0 ? doc.Bcc.map(x => x.Email) : [];
|
||||
const updatedBcc = doc?.SenderMail ? [...Bcc, doc?.SenderMail] : Bcc;
|
||||
const formatId = doc?.ExtUserPtr?.DownloadFilenameFormat;
|
||||
const filename = pdfName?.length > 100 ? pdfName?.slice(0, 100) : pdfName;
|
||||
const docName = buildDownloadFilename(formatId, {
|
||||
docName: filename,
|
||||
email: doc?.ExtUserPtr?.Email,
|
||||
isSigned: true,
|
||||
});
|
||||
const params = {
|
||||
extUserId: sender.objectId,
|
||||
url: url,
|
||||
@@ -239,7 +252,7 @@ async function sendCompletedMail(obj) {
|
||||
mailProvider: obj.mailProvider,
|
||||
bcc: updatedBcc?.length > 0 ? updatedBcc : '',
|
||||
certificatePath: `./exports/signed_certificate_${doc.objectId}.pdf`,
|
||||
filename: obj?.filename,
|
||||
filename: docName,
|
||||
};
|
||||
try {
|
||||
const res = await axios.post(serverUrl + '/functions/sendmailv3', params, {
|
||||
|
||||
@@ -1,15 +1,41 @@
|
||||
export default function reportJson(id, userId) {
|
||||
const currentUserId = userId;
|
||||
export default function reportJson(id, currentUserId) {
|
||||
const commanKeys = [
|
||||
'IsSignyourself',
|
||||
'URL',
|
||||
'Name',
|
||||
'Note',
|
||||
'SignedUrl',
|
||||
'AuditTrail',
|
||||
'Folder.Name',
|
||||
'ExtUserPtr.Name',
|
||||
'ExtUserPtr.Email',
|
||||
'ExtUserPtr.DownloadFilenameFormat',
|
||||
'Signers.Name',
|
||||
'Signers.Email',
|
||||
'Signers.Phone',
|
||||
'Placeholders',
|
||||
'TemplateId',
|
||||
'ExpiryDate',
|
||||
];
|
||||
const inProgressKeys = [
|
||||
...commanKeys,
|
||||
'AuditTrail.UserPtr',
|
||||
'SendMail',
|
||||
'RequestBody',
|
||||
'RequestSubject',
|
||||
'ExtUserPtr.TenantId.RequestBody',
|
||||
'ExtUserPtr.TenantId.RequestSubject',
|
||||
];
|
||||
const filterKeys = [
|
||||
'TimeToCompleteDays',
|
||||
'AllowModifications',
|
||||
'IsEnableOTP',
|
||||
'IsTourEnabled',
|
||||
'NotifyOnSignatures',
|
||||
'RedirectUrl',
|
||||
'SendinOrder',
|
||||
];
|
||||
const needYourSignKeys = [...commanKeys, 'Signers.UserId'];
|
||||
switch (id) {
|
||||
// draft documents report
|
||||
case 'ByHuevtCFY':
|
||||
@@ -23,7 +49,7 @@ export default function reportJson(id, userId) {
|
||||
SignedUrl: { $exists: false },
|
||||
CreatedBy: { __type: 'Pointer', className: '_User', objectId: currentUserId },
|
||||
},
|
||||
keys: [...commanKeys, 'Note', 'Folder.Name', 'IsSignyourself'],
|
||||
keys: [...commanKeys, ...filterKeys],
|
||||
};
|
||||
// Need your sign report
|
||||
case '4Hhwbp482K':
|
||||
@@ -44,16 +70,7 @@ export default function reportJson(id, userId) {
|
||||
},
|
||||
},
|
||||
},
|
||||
keys: [
|
||||
...commanKeys,
|
||||
'Note',
|
||||
'Folder.Name',
|
||||
'ExtUserPtr.Email',
|
||||
'Signers.UserId',
|
||||
'AuditTrail',
|
||||
'SignedUrl',
|
||||
'ExpiryDate',
|
||||
],
|
||||
keys: [...needYourSignKeys, ...filterKeys],
|
||||
};
|
||||
// In progress report
|
||||
case '1MwEuxLEkF':
|
||||
@@ -69,21 +86,7 @@ export default function reportJson(id, userId) {
|
||||
CreatedBy: { __type: 'Pointer', className: '_User', objectId: currentUserId },
|
||||
ExpiryDate: { $gt: { __type: 'Date', iso: new Date().toISOString() } },
|
||||
},
|
||||
keys: [
|
||||
...commanKeys,
|
||||
'Note',
|
||||
'Folder.Name',
|
||||
'ExtUserPtr.Email',
|
||||
'AuditTrail',
|
||||
'AuditTrail.UserPtr',
|
||||
'ExpiryDate',
|
||||
'SendMail',
|
||||
'SignedUrl',
|
||||
'RequestBody',
|
||||
'RequestSubject',
|
||||
'ExtUserPtr.TenantId.RequestBody',
|
||||
'ExtUserPtr.TenantId.RequestSubject',
|
||||
],
|
||||
keys: [...inProgressKeys, ...filterKeys],
|
||||
};
|
||||
// completed documents report
|
||||
case 'kQUoW4hUXz':
|
||||
@@ -115,17 +118,7 @@ export default function reportJson(id, userId) {
|
||||
},
|
||||
],
|
||||
},
|
||||
keys: [
|
||||
...commanKeys,
|
||||
'Note',
|
||||
'Folder.Name',
|
||||
'SignedUrl',
|
||||
'TimeToCompleteDays',
|
||||
'IsSignyourself',
|
||||
'IsCompleted',
|
||||
'ExpiryDate',
|
||||
'IsSignyourself',
|
||||
],
|
||||
keys: [...commanKeys, ...filterKeys, 'IsCompleted'],
|
||||
};
|
||||
// declined documents report
|
||||
case 'UPr2Fm5WY3':
|
||||
@@ -137,7 +130,7 @@ export default function reportJson(id, userId) {
|
||||
IsDeclined: true,
|
||||
CreatedBy: { __type: 'Pointer', className: '_User', objectId: currentUserId },
|
||||
},
|
||||
keys: [...commanKeys, 'Note', 'Folder.Name', 'DeclineReason', 'SignedUrl'],
|
||||
keys: [...commanKeys, 'DeclineReason'],
|
||||
};
|
||||
// Expired Documents report
|
||||
case 'zNqBHXHsYH':
|
||||
@@ -152,7 +145,7 @@ export default function reportJson(id, userId) {
|
||||
ExpiryDate: { $lt: { __type: 'Date', iso: new Date().toISOString() } },
|
||||
CreatedBy: { __type: 'Pointer', className: '_User', objectId: currentUserId },
|
||||
},
|
||||
keys: [...commanKeys, 'Note', 'Folder.Name', 'SignedUrl', 'ExpiryDate'],
|
||||
keys: [...commanKeys, ...filterKeys],
|
||||
};
|
||||
// Recently sent for signatures report show on dashboard
|
||||
case 'd9k3UfYHBc':
|
||||
@@ -168,19 +161,7 @@ export default function reportJson(id, userId) {
|
||||
CreatedBy: { __type: 'Pointer', className: '_User', objectId: currentUserId },
|
||||
ExpiryDate: { $gt: { __type: 'Date', iso: new Date().toISOString() } },
|
||||
},
|
||||
keys: [
|
||||
...commanKeys,
|
||||
'Folder.Name',
|
||||
'ExtUserPtr.Email',
|
||||
'AuditTrail',
|
||||
'AuditTrail.UserPtr',
|
||||
'ExpiryDate',
|
||||
'SignedUrl',
|
||||
'RequestBody',
|
||||
'RequestSubject',
|
||||
'ExtUserPtr.TenantId.RequestBody',
|
||||
'ExtUserPtr.TenantId.RequestSubject',
|
||||
],
|
||||
keys: inProgressKeys,
|
||||
};
|
||||
// Recent signature requests report show on dashboard
|
||||
case '5Go51Q7T8r':
|
||||
@@ -201,14 +182,7 @@ export default function reportJson(id, userId) {
|
||||
},
|
||||
},
|
||||
},
|
||||
keys: [
|
||||
...commanKeys,
|
||||
'ExtUserPtr.Email',
|
||||
'Signers.UserId',
|
||||
'AuditTrail',
|
||||
'SignedUrl',
|
||||
'ExpiryDate',
|
||||
],
|
||||
keys: needYourSignKeys,
|
||||
};
|
||||
// Drafts report show on dashboard
|
||||
case 'kC5mfynCi4':
|
||||
@@ -222,7 +196,7 @@ export default function reportJson(id, userId) {
|
||||
SignedUrl: { $exists: false },
|
||||
CreatedBy: { __type: 'Pointer', className: '_User', objectId: currentUserId },
|
||||
},
|
||||
keys: [...commanKeys, 'Note', 'Folder.Name'],
|
||||
keys: commanKeys,
|
||||
};
|
||||
// contact book report
|
||||
case 'contacts':
|
||||
@@ -233,7 +207,7 @@ export default function reportJson(id, userId) {
|
||||
CreatedBy: { __type: 'Pointer', className: '_User', objectId: currentUserId },
|
||||
IsDeleted: { $ne: true },
|
||||
},
|
||||
keys: ['Name', 'Email', 'Phone'],
|
||||
keys: ['Name', 'Email', 'Phone', 'JobTitle', 'Company'],
|
||||
};
|
||||
// Templates report
|
||||
case '6TeaPr321t':
|
||||
@@ -243,8 +217,7 @@ export default function reportJson(id, userId) {
|
||||
params: { Type: { $ne: 'Folder' }, IsArchive: { $ne: true } },
|
||||
keys: [
|
||||
...commanKeys,
|
||||
'Note',
|
||||
'Folder.Name',
|
||||
...filterKeys,
|
||||
'IsPublic',
|
||||
'SharedWith.Name',
|
||||
'SendinOrder',
|
||||
|
||||
@@ -32,7 +32,6 @@ export default async function saveAsTemplate(request) {
|
||||
templateCls.set('EmailSenderName', _docRes?.EmailSenderName);
|
||||
templateCls.set('SenderName', _docRes?.SenderName);
|
||||
templateCls.set('SenderMail', _docRes?.SenderMail);
|
||||
templateCls.set('FileAdapterId', _docRes?.FileAdapterId);
|
||||
templateCls.set('RequestBody', _docRes?.RequestBody);
|
||||
templateCls.set('RequestSubject', _docRes?.RequestSubject);
|
||||
templateCls.set('NextReminderDate', _docRes?.NextReminderDate);
|
||||
@@ -56,6 +55,7 @@ export default async function saveAsTemplate(request) {
|
||||
...pageItem,
|
||||
pos: pageItem.pos.map(p => ({
|
||||
...p,
|
||||
type: p.type === 'text' ? 'text input' : p.type,
|
||||
options: {
|
||||
...p.options,
|
||||
status: 'required',
|
||||
@@ -74,14 +74,14 @@ export default async function saveAsTemplate(request) {
|
||||
};
|
||||
templateCls.set('Placeholders', [placeHolders]);
|
||||
} else {
|
||||
const placeHolders = _docRes?.Placeholders.map((signer, signerIndex) => ({
|
||||
const removePrefill = _docRes?.Placeholders?.filter(x => x.Role !== 'prefill');
|
||||
const placeHolders = removePrefill.map((signer, signerIndex) => ({
|
||||
// copy everything else, then overwrite these fields:
|
||||
...signer,
|
||||
signerObjId: '',
|
||||
signerPtr: {},
|
||||
Role: `Role ${signerIndex + 1}`,
|
||||
Role: signer?.Role ? signer.Role : `Role ${signerIndex + 1}`,
|
||||
email: '',
|
||||
|
||||
// rebuild placeHolder/pages
|
||||
placeHolder: (signer.placeHolder || []).map(page => ({
|
||||
...page,
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
flattenPdf,
|
||||
getSecureUrl,
|
||||
} from '../../Utils.js';
|
||||
import { flattenPdf, getSecureUrl } from '../../Utils.js';
|
||||
export default async function saveFile(request) {
|
||||
if (!request.params.fileBase64) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Please provide file.');
|
||||
@@ -16,29 +13,28 @@ export default async function saveFile(request) {
|
||||
const resExt = await extCls.first({ useMasterKey: true });
|
||||
if (resExt) {
|
||||
const _resExt = JSON.parse(JSON.stringify(resExt));
|
||||
const fileName = request.params.fileName;
|
||||
const ext = request.params.fileName?.split('.')?.pop();
|
||||
let mimeType;
|
||||
let file;
|
||||
if (ext === 'pdf') {
|
||||
mimeType = 'application/pdf';
|
||||
const flatPdf = await flattenPdf(fileBase64);
|
||||
file = [...flatPdf];
|
||||
} else if (ext === 'png' || ext === 'jpeg' || ext === 'jpg') {
|
||||
mimeType = `image/${ext}`;
|
||||
file = { base64: fileBase64 };
|
||||
}
|
||||
const pdfFile = new Parse.File(fileName, file, mimeType);
|
||||
// Save the Parse File if needed
|
||||
const pdfData = await pdfFile.save({ useMasterKey: true });
|
||||
const presignedUrl = pdfData.url();
|
||||
const fileRes = getSecureUrl(presignedUrl);
|
||||
return { url: fileRes.url };
|
||||
const fileName = request.params.fileName;
|
||||
const ext = request.params.fileName?.split('.')?.pop();
|
||||
let mimeType;
|
||||
let file;
|
||||
if (ext === 'pdf') {
|
||||
mimeType = 'application/pdf';
|
||||
const flatPdf = await flattenPdf(fileBase64);
|
||||
file = [...flatPdf];
|
||||
} else if (ext === 'png' || ext === 'jpeg' || ext === 'jpg') {
|
||||
mimeType = `image/${ext}`;
|
||||
file = { base64: fileBase64 };
|
||||
}
|
||||
const pdfFile = new Parse.File(fileName, file, mimeType);
|
||||
// Save the Parse File if needed
|
||||
const pdfData = await pdfFile.save({ useMasterKey: true });
|
||||
const presignedUrl = pdfData.url();
|
||||
const fileRes = getSecureUrl(presignedUrl);
|
||||
return { url: fileRes.url };
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'User not found.');
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'User is not authenticated.');
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -4,6 +4,8 @@ export default async function savecontact(request) {
|
||||
const requestemail = request.params?.email;
|
||||
const email = requestemail?.toLowerCase()?.replace(/\s/g, '');
|
||||
const tenantId = request.params.tenantId;
|
||||
const company = request.params?.company;
|
||||
const jobTitle = request.params?.jobTitle;
|
||||
|
||||
if (request.user) {
|
||||
const currentUser = request?.user;
|
||||
@@ -16,12 +18,18 @@ export default async function savecontact(request) {
|
||||
if (!res) {
|
||||
const contactQuery = new Parse.Object('contracts_Contactbook');
|
||||
contactQuery.set('Name', name);
|
||||
if (phone) {
|
||||
contactQuery.set('Phone', phone);
|
||||
}
|
||||
contactQuery.set('Email', email);
|
||||
contactQuery.set('UserRole', 'contracts_Guest');
|
||||
contactQuery.set('IsDeleted', false);
|
||||
if (phone) {
|
||||
contactQuery.set('Phone', phone);
|
||||
}
|
||||
if (company) {
|
||||
contactQuery.set('Company', company);
|
||||
}
|
||||
if (jobTitle) {
|
||||
contactQuery.set('JobTitle', jobTitle);
|
||||
}
|
||||
if (tenantId) {
|
||||
contactQuery.set('TenantId', {
|
||||
__type: 'Pointer',
|
||||
@@ -29,7 +37,6 @@ export default async function savecontact(request) {
|
||||
objectId: tenantId,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const _users = Parse.Object.extend('User');
|
||||
const _user = new _users();
|
||||
@@ -40,7 +47,6 @@ export default async function savecontact(request) {
|
||||
if (phone) {
|
||||
_user.set('phone', phone);
|
||||
}
|
||||
|
||||
const user = await _user.save();
|
||||
if (user) {
|
||||
contactQuery.set('CreatedBy', currentUserPtr);
|
||||
@@ -52,7 +58,7 @@ export default async function savecontact(request) {
|
||||
acl.setWriteAccess(currentUser.id, true);
|
||||
contactQuery.setACL(acl);
|
||||
|
||||
const res = await contactQuery.save();
|
||||
const res = await contactQuery.save(null, { useMasterKey: true });
|
||||
const parseData = JSON.parse(JSON.stringify(res));
|
||||
return parseData;
|
||||
}
|
||||
@@ -73,7 +79,7 @@ export default async function savecontact(request) {
|
||||
acl.setReadAccess(currentUser.id, true);
|
||||
acl.setWriteAccess(currentUser.id, true);
|
||||
contactQuery.setACL(acl);
|
||||
const res = await contactQuery.save();
|
||||
const res = await contactQuery.save(null, { useMasterKey: true });
|
||||
const parseData = JSON.parse(JSON.stringify(res));
|
||||
return parseData;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { appName, smtpenable } from '../../Utils.js';
|
||||
|
||||
export const errHtml = err => {
|
||||
return `<html><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8" /><title>Reset Password</title></head>
|
||||
<body><h1 style="color:#1a5fa0; margin-bottom:16px;">${err}</h1></body></html>`;
|
||||
};
|
||||
const sendDeleteUserMail = async req => {
|
||||
const app = req.params.app || appName;
|
||||
if (!req.user) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'User is not authenticated.');
|
||||
}
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
if (!userId) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Missing userId parameter.');
|
||||
}
|
||||
|
||||
const userPointer = { __type: 'Pointer', className: '_User', objectId: userId };
|
||||
|
||||
const createdByPointer = { __type: 'Pointer', className: '_User', objectId: req.user.id };
|
||||
|
||||
const userCondition = new Parse.Query('contracts_Users');
|
||||
userCondition.equalTo('UserId', userPointer);
|
||||
|
||||
const userAndCreatorCondition = new Parse.Query('contracts_Users');
|
||||
userAndCreatorCondition.equalTo('UserId', userPointer);
|
||||
userAndCreatorCondition.equalTo('CreatedBy', createdByPointer);
|
||||
|
||||
const mainQuery = Parse.Query.or(userCondition, userAndCreatorCondition);
|
||||
|
||||
const result = await mainQuery.first({ useMasterKey: true });
|
||||
const username = result.get('Email')?.toLowerCase()?.replace(/\s/g, '');
|
||||
const name = result?.get('Name') ? `<b>${result?.get('Name')}</b>` : '';
|
||||
const isAdmin = result?.get('UserRole') === 'contracts_Admin';
|
||||
if (!isAdmin) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.SCRIPT_FAILED,
|
||||
'This action is not permitted. Kindly contact your administrator to request account deletion.'
|
||||
);
|
||||
}
|
||||
|
||||
const serverUrl = process.env?.SERVER_URL?.replace(/\/app\/?$/, '/');
|
||||
const deleteUrl = `${serverUrl}delete-account/${userId}`;
|
||||
const mailsender = smtpenable ? process.env.SMTP_USER_EMAIL : process.env.MAILGUN_SENDER;
|
||||
// Render a simple HTML form. In production, consider using a templating engine.
|
||||
|
||||
await Parse.Cloud.sendEmail({
|
||||
sender: app + ' <' + mailsender + '>',
|
||||
recipient: username,
|
||||
subject: `Account Deletion Request for ${username} – ${app}`,
|
||||
text: `Account Deletion Request for ${username} – ${app}`,
|
||||
html: `<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Account Deletion Request - ${app}</title>
|
||||
</head>
|
||||
<body style="margin:0; padding:0; font-family:Arial, sans-serif; background-color:#f4f4f4; color:#333;">
|
||||
<div
|
||||
style="max-width:600px; margin:50px auto; padding:30px; background-color:#ffffff; border:1px solid #e0e0e0; border-radius:8px;">
|
||||
<h2 style="color:#d9534f;">Request to Delete Your Account</h2>
|
||||
<p style="font-size:16px; line-height:1.5;">
|
||||
Hello ${name},
|
||||
</p>
|
||||
<p style="font-size:16px; line-height:1.5;">
|
||||
We have received a request to permanently delete your <b>${app}</b> account associated with <b>${username}</b>.
|
||||
</p>
|
||||
<p style="font-size:16px; line-height:1.5;">
|
||||
If you did not make this request, please ignore this email. Otherwise, click the button below to proceed
|
||||
with the deletion.
|
||||
</p>
|
||||
<p style="text-align:center; margin:30px 0;">
|
||||
<a href="${deleteUrl}"
|
||||
style="background-color:#d9534f; color:#ffffff; padding:12px 24px; border-radius:5px; text-decoration:none; font-size:16px;">
|
||||
Confirm Account Deletion
|
||||
</a>
|
||||
</p>
|
||||
<p style="font-size:16px; line-height:1.5;">
|
||||
If the button above doesn't work, please copy and open the following link with your browser.
|
||||
</p>
|
||||
<p style="font-size:16px; text-align:center; margin:0px 0px 30px 0px;">
|
||||
<a href="${deleteUrl}">${deleteUrl}</a>
|
||||
</p>
|
||||
<p style="font-size:14px; color:#777;">
|
||||
Note: This action is irreversible and all your data will be permanently removed from our systems.
|
||||
</p>
|
||||
<hr style="margin:30px 0; border:none; border-top:1px solid #eee;">
|
||||
<p style="font-size:12px; color:#999;">
|
||||
If you have any questions or need assistance, please contact our support team.
|
||||
</p>
|
||||
<p style="font-size:12px; color:#999;">
|
||||
© ${new Date().getFullYear()} ${app}. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`,
|
||||
});
|
||||
return 'mail sent.';
|
||||
} catch (err) {
|
||||
console.log('Err in sending delete user email ', err);
|
||||
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, err.message);
|
||||
}
|
||||
};
|
||||
export default sendDeleteUserMail;
|
||||
@@ -57,6 +57,9 @@ export default async function updatePreferences(request) {
|
||||
if (request.params.IsLTVEnabled !== undefined) {
|
||||
newOrg.set('IsLTVEnabled', request.params.IsLTVEnabled);
|
||||
}
|
||||
if (request.params.DownloadFilenameFormat) {
|
||||
newOrg.set('DownloadFilenameFormat', request.params.DownloadFilenameFormat);
|
||||
}
|
||||
const updateUserRes = await newOrg.save(null, { useMasterKey: true });
|
||||
if (updateUserRes) {
|
||||
const _updateUserRes = JSON.parse(JSON.stringify(updateUserRes));
|
||||
|
||||
@@ -15,8 +15,7 @@ export default async function updateTourStatus(request) {
|
||||
const msg = err?.message || 'Something went wrong.';
|
||||
throw new Parse.Error(code, msg);
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'User is not authenticated.');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user