Initial version

This commit is contained in:
Flatlogic Bot 2025-03-13 13:53:10 +05:00
commit 435412ef72
29 changed files with 6357 additions and 0 deletions

26
.eslintrc.cjs Normal file
View File

@ -0,0 +1,26 @@
const globals = require('globals');
module.exports = [
{
files: ['**/*.js', '**/*.ts', '**/*.tsx'],
languageOptions: {
ecmaVersion: 2021,
sourceType: 'module',
globals: {
...globals.browser,
...globals.node,
},
parser: '@typescript-eslint/parser',
},
plugins: ['@typescript-eslint'],
rules: {
'no-unused-vars': 'warn',
'no-console': 'off',
'indent': ['error', 2],
'quotes': ['error', 'single'],
'semi': ['error', 'always'],
'@typescript-eslint/no-unused-vars': 'warn',
},
},
];

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules/
*/node_modules/
*/build/

11
.prettierrc Normal file
View File

@ -0,0 +1,11 @@
{
"singleQuote": true,
"tabWidth": 2,
"printWidth": 80,
"trailingComma": "all",
"quoteProps": "as-needed",
"jsxSingleQuote": true,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always"
}

7
.sequelizerc Normal file
View File

@ -0,0 +1,7 @@
const path = require('path');
module.exports = {
"config": path.resolve("src", "db", "db.config.js"),
"models-path": path.resolve("src", "db", "models"),
"seeders-path": path.resolve("src", "db", "seeders"),
"migrations-path": path.resolve("src", "db", "migrations")
};

23
Dockerfile Normal file
View File

@ -0,0 +1,23 @@
FROM node:20.15.1-alpine
RUN apk update && apk add bash
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./
RUN yarn install
# If you are building your code for production
# RUN npm ci --only=production
# Bundle app source
COPY . .
EXPOSE 4000
CMD [ "yarn", "start" ]

13
README.md Normal file
View File

@ -0,0 +1,13 @@
#test - template backend,
#### Run App on local machine:
##### Install local dependencies:
- `yarn install`
---
##### Start build:
- `yarn start`

44
package.json Normal file
View File

@ -0,0 +1,44 @@
{
"name": "app-shell",
"description": "app-shell",
"scripts": {
"start": "nodemon ./src/index.js"
},
"dependencies": {
"@babel/parser": "^7.26.7",
"adm-zip": "^0.5.16",
"axios": "^1.6.7",
"bcrypt": "5.1.1",
"cors": "2.8.5",
"csv-parser": "^3.0.0",
"eslint": "^9.13.0",
"express": "4.18.2",
"formidable": "1.2.2",
"helmet": "4.1.1",
"json2csv": "^5.0.7",
"jsonwebtoken": "8.5.1",
"lodash": "4.17.21",
"moment": "2.30.1",
"multer": "^1.4.4",
"passport": "^0.7.0",
"passport-google-oauth2": "^0.2.0",
"passport-jwt": "^4.0.1",
"passport-microsoft": "^0.1.0",
"postcss": "^8.5.1",
"sequelize-json-schema": "^2.1.1",
"pg": "^8.13.3"
},
"engines": {
"node": ">=18"
},
"private": true,
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.12.2",
"@typescript-eslint/parser": "^8.12.2",
"cross-env": "7.0.3",
"mocha": "8.1.3",
"node-mocks-http": "1.9.0",
"nodemon": "^3.1.7",
"sequelize-cli": "6.6.2"
}
}

68
src/config.js Normal file
View File

@ -0,0 +1,68 @@
const os = require('os');
const config = {
gcloud: {
bucket: "fldemo-files",
hash: "4b305598e0c1657ad008afb48d376a49"
},
bcrypt: {
saltRounds: 12
},
admin_pass: "password",
admin_email: "admin@flatlogic.com",
providers: {
LOCAL: 'local',
GOOGLE: 'google',
MICROSOFT: 'microsoft'
},
secret_key: 'HUEyqESqgQ1yTwzVlO6wprC9Kf1J1xuA',
remote: '',
port: process.env.NODE_ENV === "production" ? "" : "8080",
hostUI: process.env.NODE_ENV === "production" ? "" : "http://localhost",
portUI: process.env.NODE_ENV === "production" ? "" : "3000",
portUIProd: process.env.NODE_ENV === "production" ? "" : ":3000",
swaggerUI: process.env.NODE_ENV === "production" ? "" : "http://localhost",
swaggerPort: process.env.NODE_ENV === "production" ? "" : ":8080",
google: {
clientId: '671001533244-kf1k1gmp6mnl0r030qmvdu6v36ghmim6.apps.googleusercontent.com',
clientSecret: 'Yo4qbKZniqvojzUQ60iKlxqR'
},
microsoft: {
clientId: '4696f457-31af-40de-897c-e00d7d4cff73',
clientSecret: 'm8jzZ.5UpHF3=-dXzyxiZ4e[F8OF54@p'
},
uploadDir: os.tmpdir(),
email: {
from: 'amana <app@flatlogic.app>',
host: 'email-smtp.us-east-1.amazonaws.com',
port: 587,
auth: {
user: 'AKIAVEW7G4PQUBGM52OF',
pass: process.env.EMAIL_PASS,
},
tls: {
rejectUnauthorized: false
}
},
roles: {
admin: 'Administrator',
user: 'User',
},
project_uuid: '43092204-4784-4dde-93a5-ce98718ee7a5',
flHost: process.env.NODE_ENV === 'production' ? 'https://flatlogic.com/projects' : 'http://localhost:3000/projects',
};
config.pexelsKey = 'Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18';
config.pexelsQuery = 'gears and cogs in motion';
config.host = process.env.NODE_ENV === "production" ? config.remote : "http://localhost";
config.apiUrl = `${config.host}${config.port ? `:${config.port}` : ``}/api`;
config.swaggerUrl = `${config.swaggerUI}${config.swaggerPort}`;
config.uiUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}/#`;
config.backUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}`;
module.exports = config;

23
src/helpers.js Normal file
View File

@ -0,0 +1,23 @@
const jwt = require('jsonwebtoken');
const config = require('./config');
module.exports = class Helpers {
static wrapAsync(fn) {
return function (req, res, next) {
fn(req, res, next).catch(next);
};
}
static commonErrorHandler(error, req, res, next) {
if ([400, 403, 404].includes(error.code)) {
return res.status(error.code).send(error.message);
}
console.error(error);
return res.status(500).send(error.message);
}
static jwtSign(data) {
return jwt.sign(data, config.secret_key, { expiresIn: '6h' });
}
};

54
src/index.js Normal file
View File

@ -0,0 +1,54 @@
const express = require('express');
const cors = require('cors');
const app = express();
const bodyParser = require('body-parser');
const checkPermissions = require('./middlewares/check-permissions');
const modifyPath = require('./middlewares/modify-path');
const VCS = require('./services/vcs');
const executorRoutes = require('./routes/executor');
const vcsRoutes = require('./routes/vcs');
// Function to initialize the Git repository
function initRepo() {
const projectId = '27631';
return VCS.initRepo(projectId);
}
// Start the Express app on APP_SHELL_PORT (4000)
function startServer() {
const PORT = 4000;
app.listen(PORT, () => {
console.log(`Listening on port ${PORT}`);
});
}
// Run Git check after the server is up
function runGitCheck() {
initRepo()
.then(result => {
console.log(result.message);
// Here you can add additional logic if needed
})
.catch(err => {
console.error('Error during repo initialization:', err);
// Optionally exit the process if Git check is critical:
process.exit(1);
});
}
app.use(cors({ origin: true }));
app.use(bodyParser.json());
app.use(checkPermissions);
app.use(modifyPath);
app.use('/executor', executorRoutes);
app.use('/vcs', vcsRoutes);
// Start the app_shell server
startServer();
// Now perform Git check
runGitCheck();
module.exports = app;

View File

@ -0,0 +1,17 @@
const config = require('../config');
function checkPermissions(req, res, next) {
const project_uuid = config.project_uuid;
const requiredHeader = 'X-Project-UUID';
const headerValue = req.headers[requiredHeader.toLowerCase()];
// Logging whatever request we're getting
console.log('Request:', req.url, req.method, req.body, req.headers);
if (headerValue && headerValue === project_uuid) {
next();
} else {
res.status(403).send({ error: 'Stop right there, criminal scum! Your project UUID is invalid or missing.' });
}
}
module.exports = checkPermissions;

View File

@ -0,0 +1,8 @@
function modifyPath(req, res, next) {
if (req.body && req.body.path) {
req.body.path = '../../../' + req.body.path;
}
next();
}
module.exports = modifyPath;

288
src/routes/executor.js Normal file
View File

@ -0,0 +1,288 @@
const express = require('express');
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });
const fs = require('fs');
const ExecutorService = require('../services/executor');
const wrapAsync = require('../helpers').wrapAsync;
const router = express.Router();
router.post(
'/read_project_tree',
wrapAsync(async (req, res) => {
const { path } = req.body;
const tree = await ExecutorService.readProjectTree(path);
res.status(200).send(tree);
}),
);
router.post(
'/read_file',
wrapAsync(async (req, res) => {
const { path, showLines } = req.body;
const content = await ExecutorService.readFileContents(path, showLines);
res.status(200).send(content);
}),
);
router.post(
'/count_file_lines',
wrapAsync(async (req, res) => {
const { path } = req.body;
const content = await ExecutorService.countFileLines(path);
res.status(200).send(content);
}),
);
// router.post(
// '/read_file_header',
// wrapAsync(async (req, res) => {
// const { path, N } = req.body;
// try {
// const header = await ExecutorService.readFileHeader(path, N);
// res.status(200).send(header);
// } catch (error) {
// res.status(500).send({
// error: true,
// message: error.message,
// details: error.details || error.stack,
// validation: error.validation
// });
// }
// }),
// );
router.post(
'/read_file_line_context',
wrapAsync(async (req, res) => {
const { path, lineNumber, windowSize, showLines } = req.body;
try {
const context = await ExecutorService.readFileLineContext(path, lineNumber, windowSize, showLines);
res.status(200).send(context);
} catch (error) {
res.status(500).send({
error: true,
message: error.message,
details: error.details || error.stack,
validation: error.validation
});
}
}),
);
router.post(
'/write_file',
wrapAsync(async (req, res) => {
const { path, fileContents, comment } = req.body;
try {
await ExecutorService.writeFile(path, fileContents, comment);
res.status(200).send({ message: 'File written successfully' });
} catch (error) {
res.status(500).send({
error: true,
message: error.message,
details: error.details || error.stack,
validation: error.validation
});
}
}),
);
router.post(
'/insert_file_content',
wrapAsync(async (req, res) => {
const { path, lineNumber, newContent, message } = req.body;
try {
await ExecutorService.insertFileContent(path, lineNumber, newContent, message);
res.status(200).send({ message: 'File written successfully' });
} catch (error) {
res.status(500).send({
error: true,
message: error.message,
details: error.details || error.stack,
validation: error.validation
});
}
}),
);
router.post(
'/replace_file_line',
wrapAsync(async (req, res) => {
const { path, lineNumber, newText } = req.body;
try {
const result = await ExecutorService.replaceFileLine(path, lineNumber, newText);
res.status(200).send(result);
} catch (error) {
res.status(500).send({
error: true,
message: error.message,
details: error.details || error.stack,
validation: error.validation
});
}
}),
);
router.post(
'/replace_file_chunk',
wrapAsync(async (req, res) => {
const { path, startLine, endLine, newCode } = req.body;
try {
const result = await ExecutorService.replaceFileChunk(path, startLine, endLine, newCode);
res.status(200).send(result);
} catch (error) {
res.status(500).send({
error: true,
message: error.message,
details: error.details || error.stack,
validation: error.validation
});
}
}),
);
router.post(
'/delete_file_lines',
wrapAsync(async (req, res) => {
const { path, startLine, endLine, message } = req.body;
try {
const result = await ExecutorService.deleteFileLines(path, startLine, endLine, message);
res.status(200).send(result);
} catch (error) {
res.status(500).send({
error: true,
message: error.message,
details: error.details || error.stack,
validation: error.validation
});
}
}),
);
router.post(
'/validate_file',
wrapAsync(async (req, res) => {
const { path } = req.body;
try {
const validationResult = await ExecutorService.validateFile(path);
res.status(200).send({ validationResult });
} catch (error) {
res.status(500).send({
error: true,
message: error.message,
details: error.details || error.stack,
validation: error.validation
});
}
}),
);
router.post(
'/check_frontend_runtime_error',
wrapAsync(async (req, res) => {
try {
const result = await ExecutorService.checkFrontendRuntimeLogs();
res.status(200).send(result);
} catch (error) {
res.status(500).send({ error: error });
}
}),
);
router.post(
'/replace_code_block',
wrapAsync(async (req, res) => {
const {path, oldCode, newCode, message} = req.body;
try {
const response = await ExecutorService.replaceCodeBlock(path, oldCode, newCode, message);
res.status(200).send(response);
} catch (error) {
res.status(500).send({
error: true,
message: error.message,
details: error.details || error.stack,
validation: error.validation
})
}
})
)
router.post('/update_project_files_from_scheme',
upload.single('file'), // 'file' - name of the field in the form
async (req, res) => {
console.log('Request received');
console.log('Headers:', req.headers);
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
console.log('File info:', {
originalname: req.file.originalname,
path: req.file.path,
size: req.file.size,
mimetype: req.file.mimetype
});
try {
console.log('Starting update process...');
const result = await ExecutorService.updateProjectFilesFromScheme(req.file.path);
console.log('Update completed, result:', result);
console.log('Removing temp file...');
fs.unlinkSync(req.file.path);
console.log('Temp file removed');
console.log('Sending response...');
return res.json(result);
} catch (error) {
console.error('Error in route handler:', error);
if (req.file) {
try {
fs.unlinkSync(req.file.path);
console.log('Temp file removed after error');
} catch (unlinkError) {
console.error('Error removing temp file:', unlinkError);
}
}
console.error('Update project files error:', error);
return res.status(500).json({
error: error.message,
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
});
}
}
);
router.post(
'/get_db_schema',
wrapAsync(async (req, res) => {
try {
const jsonSchema = await ExecutorService.getDBSchema();
res.status(200).send({ jsonSchema });
} catch (error) {
res.status(500).send({ error: error });
}
}),
);
router.post(
'/execute_sql',
wrapAsync(async (req, res) => {
try {
const { query } = req.body;
const result = await ExecutorService.executeSQL(query);
res.status(200).send(result);
} catch (error) {
res.status(500).send({ error: error });
}
}),
);
router.use('/', require('../helpers').commonErrorHandler);
module.exports = router;

40
src/routes/vcs.js Normal file
View File

@ -0,0 +1,40 @@
const express = require('express');
const wrapAsync = require('../helpers').wrapAsync; // Ваша обёртка для обработки асинхронных маршрутов
const VSC = require('../services/vcs');
const router = express.Router();
router.post('/init', wrapAsync(async (req, res) => {
const result = await VSC.initRepo();
res.status(200).send(result);
}));
router.post('/commit', wrapAsync(async (req, res) => {
const { message, files } = req.body;
const result = await VSC.commitChanges(message, files);
res.status(200).send(result);
}));
router.post('/log', wrapAsync(async (req, res) => {
const result = await VSC.getLog();
res.status(200).send(result);
}));
router.post('/rollback', wrapAsync(async (req, res) => {
const { ref } = req.body;
// const result = await VSC.checkout(ref);
const result = await VSC.revert(ref);
res.status(200).send(result);
}));
router.post('/sync-to-stable', wrapAsync(async (req, res) => {
const result = await VSC.mergeDevIntoMaster();
res.status(200).send(result);
}));
router.post('/reset-dev', wrapAsync(async (req, res) => {
const result = await VSC.resetDevBranch();
res.status(200).send(result);
}));
router.use('/', require('../helpers').commonErrorHandler);
module.exports = router;

88
src/services/database.js Normal file
View File

@ -0,0 +1,88 @@
// Database.js
const { Client } = require('pg');
const config = require('../../../backend/src/db/db.config');
const env = process.env.NODE_ENV || 'development';
const dbConfig = config[env];
class Database {
constructor() {
this.client = new Client({
user: dbConfig.username,
password: dbConfig.password,
database: dbConfig.database,
host: dbConfig.host,
port: dbConfig.port
});
// Connect once, reuse the client
this.client.connect().catch(err => {
console.error('Error connecting to the database:', err);
throw err;
});
}
async executeSQL(query) {
try {
const result = await this.client.query(query);
return {
success: true,
rows: result.rows
};
} catch (error) {
console.error('Error executing query:', error);
throw error;
}
}
// Method to fetch simple table/column info from 'information_schema'
// (You can expand this to handle constraints, indexes, etc.)
async getDBSchema(schemaName = 'public') {
try {
const tableQuery = `
SELECT table_name
FROM information_schema.tables
WHERE table_schema = $1
AND table_type = 'BASE TABLE'
ORDER BY table_name
`;
const columnQuery = `
SELECT table_name, column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = $1
ORDER BY table_name, ordinal_position
`;
const [tablesResult, columnsResult] = await Promise.all([
this.client.query(tableQuery, [schemaName]),
this.client.query(columnQuery, [schemaName]),
]);
// Build a simple schema object:
const tables = tablesResult.rows.map(row => row.table_name);
const columnsByTable = {};
columnsResult.rows.forEach(row => {
const { table_name, column_name, data_type, is_nullable } = row;
if (!columnsByTable[table_name]) columnsByTable[table_name] = [];
columnsByTable[table_name].push({ column_name, data_type, is_nullable });
});
// Combine tables with their columns
return tables.map(table => ({
table,
columns: columnsByTable[table] || [],
}));
} catch (error) {
console.error('Error fetching schema:', error);
throw error;
}
}
async close() {
await this.client.end();
}
}
module.exports = new Database();

View File

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html>
<head>
<style>
.email-container {
max-width: 600px;
margin: auto;
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 4px;
overflow: hidden;
}
.email-header {
background-color: #3498db;
color: #fff;
padding: 16px;
text-align: center;
}
.email-body {
padding: 16px;
}
.email-footer {
padding: 16px;
background-color: #f7fafc;
text-align: center;
color: #4a5568;
font-size: 14px;
}
.link-primary {
color: #3498db;
text-decoration: none;
}
</style>
</head>
<body>
<div class="email-container">
<div class="email-header">Verify your email for {appTitle}!</div>
<div class="email-body">
<p>Hello,</p>
<p>Follow this link to verify your email address.</p>
<p>
If you didn't ask to verify this address, you can ignore this email.
</p>
<p><a href="{signupUrl}" class="link-primary">{signupUrl}</a></p>
</div>
<div class="email-footer">
Thanks,<br />
The {appTitle} Team
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html>
<head>
<style>
.email-container {
max-width: 600px;
margin: auto;
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 4px;
overflow: hidden;
}
.email-header {
background-color: #3498db;
color: #fff;
padding: 16px;
text-align: center;
}
.email-body {
padding: 16px;
}
.email-footer {
padding: 16px;
background-color: #f7fafc;
text-align: center;
color: #4a5568;
font-size: 14px;
}
.btn-primary {
background-color: #3498db;
color: #fff !important;
padding: 8px 16px;
border-radius: 4px;
text-decoration: none;
display: inline-block;
}
</style>
</head>
<body>
<div class="email-container">
<div class="email-header">Welcome to {appTitle}!</div>
<div class="email-body">
<p>Hello,</p>
<p>
You've been invited to join {appTitle}. Please click the button below
to set up your account.
</p>
<a href="{signupUrl}" class="btn-primary">Set up account</a>
</div>
<div class="email-footer">
Thanks,<br />
The {appTitle} Team
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<style>
.email-container {
max-width: 600px;
margin: auto;
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 4px;
overflow: hidden;
}
.email-header {
background-color: #3498db;
color: #fff;
padding: 16px;
text-align: center;
}
.email-body {
padding: 16px;
}
.email-footer {
padding: 16px;
background-color: #f7fafc;
text-align: center;
color: #4a5568;
font-size: 14px;
}
.link-primary {
color: #3498db;
text-decoration: none;
}
</style>
</head>
<body>
<div class="email-container">
<div class="email-header">Reset your password for {appTitle}</div>
<div class="email-body">
<p>Hello,</p>
<p>
Follow this link to reset your {appTitle} password for your
{accountName} account.
</p>
<p><a href="{resetUrl}" class="link-primary">{resetUrl}</a></p>
<p>
If you didn't ask to reset your password, you can ignore this email.
</p>
</div>
<div class="email-footer">
Thanks,<br />
The {appTitle} Team
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,41 @@
const config = require('../../config');
const assert = require('assert');
const nodemailer = require('nodemailer');
module.exports = class EmailSender {
constructor(email) {
this.email = email;
}
async send() {
assert(this.email, 'email is required');
assert(this.email.to, 'email.to is required');
assert(this.email.subject, 'email.subject is required');
assert(this.email.html, 'email.html is required');
const htmlContent = await this.email.html();
const transporter = nodemailer.createTransport(this.transportConfig);
const mailOptions = {
from: this.from,
to: this.email.to,
subject: this.email.subject,
html: htmlContent,
};
return transporter.sendMail(mailOptions);
}
static get isConfigured() {
return !!config.email?.auth?.pass && !!config.email?.auth?.user;
}
get transportConfig() {
return config.email;
}
get from() {
return config.email.from;
}
};

View File

@ -0,0 +1,41 @@
const { getNotification } = require('../../notifications/helpers');
const fs = require('fs').promises;
const path = require('path');
module.exports = class EmailAddressVerificationEmail {
constructor(to, link) {
this.to = to;
this.link = link;
}
get subject() {
return getNotification(
'emails.emailAddressVerification.subject',
getNotification('app.title'),
);
}
async html() {
try {
const templatePath = path.join(
__dirname,
'../../email/htmlTemplates/addressVerification/emailAddressVerification.html',
);
const template = await fs.readFile(templatePath, 'utf8');
const appTitle = getNotification('app.title');
const signupUrl = this.link;
let html = template
.replace(/{appTitle}/g, appTitle)
.replace(/{signupUrl}/g, signupUrl)
.replace(/{to}/g, this.to);
return html;
} catch (error) {
console.error('Error generating invitation email HTML:', error);
throw error;
}
}
};

View File

@ -0,0 +1,41 @@
const fs = require('fs').promises;
const path = require('path');
const { getNotification } = require('../../notifications/helpers');
module.exports = class InvitationEmail {
constructor(to, host) {
this.to = to;
this.host = host;
}
get subject() {
return getNotification(
'emails.invitation.subject',
getNotification('app.title'),
);
}
async html() {
try {
const templatePath = path.join(
__dirname,
'../../email/htmlTemplates/invitation/invitationTemplate.html',
);
const template = await fs.readFile(templatePath, 'utf8');
const appTitle = getNotification('app.title');
const signupUrl = `${this.host}&invitation=true`;
let html = template
.replace(/{appTitle}/g, appTitle)
.replace(/{signupUrl}/g, signupUrl)
.replace(/{to}/g, this.to);
return html;
} catch (error) {
console.error('Error generating invitation email HTML:', error);
throw error;
}
}
};

View File

@ -0,0 +1,42 @@
const { getNotification } = require('../../notifications/helpers');
const path = require('path');
const { promises: fs } = require('fs');
module.exports = class PasswordResetEmail {
constructor(to, link) {
this.to = to;
this.link = link;
}
get subject() {
return getNotification(
'emails.passwordReset.subject',
getNotification('app.title'),
);
}
async html() {
try {
const templatePath = path.join(
__dirname,
'../../email/htmlTemplates/passwordReset/passwordResetEmail.html',
);
const template = await fs.readFile(templatePath, 'utf8');
const appTitle = getNotification('app.title');
const resetUrl = this.link;
const accountName = this.to;
let html = template
.replace(/{appTitle}/g, appTitle)
.replace(/{resetUrl}/g, resetUrl)
.replace(/{accountName}/g, accountName);
return html;
} catch (error) {
console.error('Error generating invitation email HTML:', error);
throw error;
}
}
};

999
src/services/executor.js Normal file
View File

@ -0,0 +1,999 @@
const fs = require('fs').promises;
const os = require('os');
const path = require('path');
const AdmZip = require('adm-zip');
const { exec } = require('child_process');
const util = require('util');
// Babel Parser for JS/TS/TSX
const babelParser = require('@babel/parser');
const babelParse = babelParser.parse;
// Local App DB Connection
const database = require('./database');
// PostCSS for CSS
const postcss = require('postcss');
const execAsync = util.promisify(exec);
module.exports = class ExecutorService {
static async readProjectTree (directoryPath) {
const paths = {
frontend: '../../../frontend',
backend: '../../../backend',
default: '../../../'
};
try {
const publicDir = path.join(__dirname, paths[directoryPath] || directoryPath || paths.default);
return await getDirectoryTree(publicDir);
} catch (error) {
console.error('Error reading directory:', error);
throw error;
}
}
static async readFileContents(filePath, showLines) {
try {
const fullPath = path.join(__dirname, filePath);
const content = await fs.readFile(fullPath, 'utf8');
if (showLines) {
const lines = content.split('\n');
const lineObject = {};
lines.forEach((line, index) => {
lineObject[index + 1] = line;
});
return lineObject;
} else {
return content;
}
} catch (error) {
console.error('Error reading file:', error);
throw error;
}
}
static async countFileLines(filePath) {
try {
const fullPath = path.join(__dirname, filePath);
// Check file exists
await fs.access(fullPath);
// Read file content
const content = await fs.readFile(fullPath, 'utf8');
// Split by newline and count
const lines = content.split('\n');
return {
success: true,
lineCount: lines.length
};
} catch (error) {
console.error('Error counting file lines:', error);
return {
success: false,
message: error.message
};
}
}
// static async readFileHeader(filePath, N = 30) {
// try {
// const fullPath = path.join(__dirname, filePath);
// const content = await fs.readFile(fullPath, 'utf8');
// const lines = content.split('\n');
//
// if (lines.length < N) {
// return { error: `File has less than ${N} lines` };
// }
//
// const headerLines = lines.slice(0, Math.min(50, lines.length));
//
// const lineObject = {};
// headerLines.forEach((line, index) => {
// lineObject[index + 1] = line;
// });
//
// return lineObject;
// } catch (error) {
// console.error('Error reading file header:', error);
// throw error;
// }
// }
static async readFileLineContext(filePath, lineNumber, windowSize, showLines) {
try {
const fullPath = path.join(__dirname, filePath);
const content = await fs.readFile(fullPath, 'utf8');
const lines = content.split('\n');
const start = Math.max(0, lineNumber - windowSize);
const end = Math.min(lines.length, lineNumber + windowSize + 1);
const contextLines = lines.slice(start, end);
if (showLines) {
const lineObject = {};
contextLines.forEach((line, index) => {
lineObject[start + index + 1] = line;
});
return lineObject;
} else {
return contextLines.join('\n');
}
} catch (error) {
console.error('Error reading file line context:', error);
throw error;
}
}
static async validateFile(filePath) {
console.log('Validating file:', filePath);
// Read file content
let content;
try {
content = await fs.readFile(filePath, 'utf8');
} catch (err) {
throw new Error(`Could not read file: ${filePath}\n${err.message}`);
}
// Determine file extension
let ext = path.extname(filePath).toLowerCase();
if (ext === '.temp') {
ext = path.extname(filePath.slice(0, -5)).toLowerCase();
}
try {
switch (ext) {
case '.js':
case '.ts':
case '.tsx': {
// Parse JS/TS/TSX with Babel
babelParse(content, {
sourceType: 'module',
// plugins array covers JS, TS, TSX, and optional JS flavors
plugins: ['jsx', 'typescript']
});
break;
}
case '.css': {
// Parse CSS with PostCSS
postcss.parse(content);
break;
}
default: {
// If the extension isn't recognized, assume it's "valid"
// or you could throw an error to force a known extension
console.warn(`No validation implemented for extension "${ext}". Skipping syntax check.`);
}
}
// If parsing succeeded, return true
return true;
} catch (parseError) {
// Rethrow parse errors with a friendlier message
throw parseError;
}
}
static async checkFrontendRuntimeLogs() {
const frontendLogPath = '../frontend/json/runtimeError.json';
try {
// Check if file exists
try {
console.log('Accessing frontend logs:', frontendLogPath);
await fs.access(frontendLogPath);
} catch (error) {
console.log('Frontend logs not found:', error);
// File doesn't exist - return empty object
return { runtime_error: {} };
}
// File exists, try to read it
try {
// Read the entire file instead of using tail
const fileContent = await fs.readFile(frontendLogPath, 'utf8');
console.log('Reading frontend logs:', fileContent);
// Handle empty file
if (!fileContent || fileContent.trim() === '') {
return { runtime_error: {} };
}
// Parse JSON content
const runtime_error = JSON.parse(fileContent);
console.log('Parsed frontend logs:', runtime_error);
return { runtime_error };
} catch (error) {
// Error reading or parsing file
console.error('Error reading frontend runtime logs:', error);
return { runtime_error: {} };
}
} catch (error) {
// Unexpected error
console.log('Error checking frontend logs:', error);
return { runtime_error: {} };
}
}
static async writeFile(filePath, fileContents, comment) {
try {
console.log(comment)
const fullPath = path.join(__dirname, filePath);
// Write to a temp file first
const tempPath = `${fullPath}.temp`;
await fs.writeFile(tempPath, fileContents, 'utf8');
// Validate the temp file
await this.validateFile(tempPath);
// Rename temp file to original path
await fs.rename(tempPath, fullPath);
return true;
} catch (error) {
console.error('Error writing file:', error);
throw error;
}
}
static async insertFileContent(filePath, lineNumber, newContent, message) {
try {
const fullPath = path.join(__dirname, filePath);
// Check file exists
await fs.access(fullPath);
// Read and split by line
const content = await fs.readFile(fullPath, 'utf8');
const lines = content.split('\n');
// Ensure lineNumber is within [1 ... lines.length + 1]
// 1 means "insert at the very first line"
// lines.length + 1 means "append at the end"
if (lineNumber < 1) {
lineNumber = 1;
}
if (lineNumber > lines.length + 1) {
lineNumber = lines.length + 1;
}
// Convert to 0-based index
const insertIndex = lineNumber - 1;
// Prepare preview
const preview = {
insertionLine: lineNumber,
insertedLines: newContent.split('\n')
};
// Insert newContent lines at the specified index
lines.splice(insertIndex, 0, ...newContent.split('\n'));
// Write changes to a temp file first
const updatedContent = lines.join('\n');
const tempPath = `${fullPath}.temp`;
await fs.writeFile(tempPath, updatedContent, 'utf8');
await this.validateFile(tempPath);
// Rename temp file to original path
await fs.rename(tempPath, fullPath);
return {
success: true
};
} catch (error) {
console.error('Error inserting file content:', error);
throw error;
}
}
static async replaceFileLine(filePath, lineNumber, newText, message = null) {
const fullPath = path.join(__dirname, filePath);
try {
try {
await fs.access(fullPath);
} catch (error) {
throw new Error(`File not found: ${filePath}`);
}
const content = await fs.readFile(fullPath, 'utf8');
const lines = content.split('\n');
if (lineNumber < 1 || lineNumber > lines.length) {
throw new Error(`Invalid line number: ${lineNumber}. File has ${lines.length} lines`);
}
if (typeof newText !== 'string') {
throw new Error('New text must be a string');
}
const preview = {
oldLine: lines[lineNumber - 1],
newLine: newText,
lineNumber: lineNumber
};
lines[lineNumber - 1] = newText;
const newContent = lines.join('\n');
const tempPath = `${fullPath}.temp`;
await fs.writeFile(tempPath, newContent, 'utf8');
await this.validateFile(tempPath);
await fs.rename(tempPath, fullPath);
return {
success: true
};
} catch (error) {
console.error('Error updating file line:', error);
try {
await fs.unlink(`${fullPath}.temp`);
} catch {}
throw {
error: error,
message: error.message,
details: error.stack
};
}
}
static async replaceFileChunk(filePath, startLine, endLine, newCode) {
try {
// Check if this is a single-line change
const newCodeLines = newCode.split('\n');
if (newCodeLines.length === 1 && endLine === startLine) {
// Redirect to replace_file_line
return await this.replaceFileLine(filePath, startLine, newCode);
}
const fullPath = path.join(__dirname, filePath);
// Check if file exists
try {
await fs.access(fullPath);
} catch (error) {
throw new Error(`File not found: ${filePath}`);
}
const content = await fs.readFile(fullPath, 'utf8');
const lines = content.split('\n');
// Adjust line numbers to array indices (subtract 1)
const startIndex = startLine - 1;
const endIndex = endLine - 1;
// Validate input parameters
if (startIndex < 0 || endIndex >= lines.length || startIndex > endIndex) {
throw new Error(`Invalid line range: ${startLine}-${endLine}. File has ${lines.length} lines`);
}
// Check type of new code
if (typeof newCode !== 'string') {
throw new Error('New code must be a string');
}
// Create changes preview
const preview = {
oldLines: lines.slice(startIndex, endIndex + 1),
newLines: newCode.split('\n'),
startLine,
endLine
};
// Apply changes to temp file first
lines.splice(startIndex, endIndex - startIndex + 1, ...newCode.split('\n'));
const newContent = lines.join(os.EOL);
const tempPath = `${fullPath}.temp`;
await fs.writeFile(tempPath, newContent, 'utf8');
await this.validateFile(tempPath);
// Apply changes if all validations passed
await fs.rename(tempPath, fullPath);
return {
success: true
};
} catch (error) {
console.error('Error updating file slice:', error);
// Clean up temp file if exists
try {
await fs.unlink(`${fullPath}.temp`);
} catch {}
throw {
error: error,
message: error.message,
details: error.details || error.stack
};
}
}
static async replaceCodeBlock(filePath, oldCode, newCode, message) {
try {
console.log(message);
const fullPath = path.join(__dirname, filePath);
// Check file exists
await fs.access(fullPath);
// Read file content
let content = await fs.readFile(fullPath, 'utf8');
// A small helper to unify line breaks to just `\n`
const unifyLineBreaks = (str) => str.replace(/\r\n/g, '\n');
// Normalize line breaks in file content, oldCode, and newCode
content = unifyLineBreaks(content);
oldCode = unifyLineBreaks(oldCode);
newCode = unifyLineBreaks(newCode);
// Optional: Trim trailing spaces or handle other whitespace normalization if needed
// oldCode = oldCode.trim();
// newCode = newCode.trim();
// Check if oldCode actually exists in the content
const index = content.indexOf(oldCode);
if (index === -1) {
return {
success: false,
message: 'Old code not found in file.'
};
}
// Create a preview before replacing
const preview = {
oldCodeSnippet: oldCode,
newCodeSnippet: newCode
};
// Perform replacement (single occurrence). For multiple, use replaceAll or a loop.
// If you want a global replacement, consider:
// content = content.split(oldCode).join(newCode);
content = content.replace(oldCode, newCode);
// Write to a temp file first
const tempPath = `${fullPath}.temp`;
await fs.writeFile(tempPath, content, 'utf8');
await this.validateFile(tempPath);
// Rename temp file to original
await fs.rename(tempPath, fullPath);
return {
success: true
};
} catch (error) {
console.error('Error replacing code:', error);
return {
error: error,
message: error.message,
details: error.details || error.stack
};
}
}
//todo add validation
static async deleteFileLines(filePath, startLine, endLine, veryShortDescription) {
try {
const fullPath = path.join(__dirname, filePath);
// Check if file exists
await fs.access(fullPath);
// Read file content
const content = await fs.readFile(fullPath, 'utf8');
const lines = content.split('\n');
// Convert to zero-based indices
const startIndex = startLine - 1;
const endIndex = endLine - 1;
// Validate range
if (startIndex < 0 || endIndex >= lines.length || startIndex > endIndex) {
throw new Error(
`Invalid line range: ${startLine}-${endLine}. File has ${lines.length} lines`
);
}
// Prepare a preview of the lines being deleted
const preview = {
deletedLines: lines.slice(startIndex, endIndex + 1),
startLine,
endLine
};
// Remove lines
lines.splice(startIndex, endIndex - startIndex + 1);
// Join remaining lines and write to a temporary file
const newContent = lines.join('\n');
const tempPath = `${fullPath}.temp`;
await fs.writeFile(tempPath, newContent, 'utf8');
await this.validateFile(tempPath);
// Rename temp file to original
await fs.rename(tempPath, fullPath);
return {
success: true
};
} catch (error) {
console.error('Error deleting file lines:', error);
return {
error: error,
message: error.message,
details: error.details || error.stack
};
}
}
static async validateTypeScript(filePath, content = null) {
try {
// Basic validation of JSX syntax
const jsxErrors = [];
if (content !== null) {
// Check for matching braces
if ((content.match(/{/g) || []).length !== (content.match(/}/g) || []).length) {
jsxErrors.push("Unmatched curly braces");
}
// Check for invalid syntax in JSX attributes
if (content.includes('label={')) {
if (!content.match(/label={[^}]+}/)) {
jsxErrors.push("Invalid label attribute syntax");
}
}
if (jsxErrors.length > 0) {
return {
valid: false,
errors: jsxErrors.map(error => ({
code: 'JSX_SYNTAX_ERROR',
severity: 'error',
location: '',
message: error
}))
};
}
}
return {
valid: true,
errors: [],
errorCount: 0,
warningCount: 0
};
} catch (error) {
console.error('TypeScript validation error:', error);
return {
valid: false,
errors: [{
code: 'VALIDATION_FAILED',
severity: 'error',
location: '',
message: `TypeScript validation error: ${error.message}`
}],
errorCount: 1,
warningCount: 0
};
}
}
static async validateBackendFiles(backendPath) {
try {
// Check for syntax errors
await execAsync(`node --check ${backendPath}/src/index.js`);
// Try to run the code in a test environment
const testProcess = exec(
'NODE_ENV=test node -e "try { require(\'./src/index.js\') } catch(e) { console.error(e); process.exit(1) }"',
{ cwd: backendPath }
);
return new Promise((resolve) => {
let output = '';
let error = '';
testProcess.stdout.on('data', (data) => {
output += data;
});
testProcess.stderr.on('data', (data) => {
error += data;
});
testProcess.on('close', (code) => {
if (code === 0) {
resolve({ valid: true });
} else {
resolve({
valid: false,
error: error || output
});
}
});
// Timeout on validation
setTimeout(() => {
testProcess.kill();
resolve({
valid: true,
warning: 'Validation timeout, but no immediate errors found'
});
}, 5000);
});
} catch (error) {
return {
valid: false,
error: error.message
};
}
}
static async createBackup(ROOT_PATH) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupDir = path.join(ROOT_PATH, 'backups', timestamp);
try {
await fs.mkdir(path.join(ROOT_PATH, 'backups'), { recursive: true });
const dirsToBackup = ['frontend', 'backend'];
for (const dir of dirsToBackup) {
const sourceDir = path.join(ROOT_PATH, dir);
const targetDir = path.join(backupDir, dir);
await fs.mkdir(targetDir, { recursive: true });
await execAsync(
`cd "${sourceDir}" && ` +
`find . -type f -not -path "*/node_modules/*" -not -path "*/\\.*" | ` +
`while read file; do ` +
`mkdir -p "${targetDir}/$(dirname "$file")" && ` +
`cp "$file" "${targetDir}/$file"; ` +
`done`
);
}
console.log('Backup created at:', backupDir);
return backupDir;
} catch (error) {
console.error('Error creating backup:', error);
throw error;
}
}
static async restoreFromBackup(backupDir, ROOT_PATH) {
try {
console.log('Restoring from backup:', backupDir);
await execAsync(`rm -rf ${ROOT_PATH}/backend/*`);
await execAsync(`cp -r ${backupDir}/* ${ROOT_PATH}/backend/`);
return true;
} catch (error) {
console.error('Error restoring from backup:', error);
throw error;
}
}
static async updateProjectFilesFromScheme(zipFilePath) {
const MAX_FILE_SIZE = 10 * 1024 * 1024;
const ROOT_PATH = path.join(__dirname, '../../../');
try {
console.log('Checking file access...');
await fs.access(zipFilePath);
console.log('Getting file stats...');
const stats = await fs.stat(zipFilePath);
console.log('File size:', stats.size);
if (stats.size > MAX_FILE_SIZE) {
console.log('File size exceeds limit');
return { success: false, error: 'File size exceeds limit' };
}
// Copying zip file to /tmp
const tempZipPath = path.join('/tmp', path.basename(zipFilePath));
await fs.copyFile(zipFilePath, tempZipPath);
// Launching background update process
const servicesUpdate = (async () => {
try {
console.log('Stopping services...');
await stopServices();
console.log('Creating zip instance...');
const zip = new AdmZip(tempZipPath);
console.log('Extracting files to:', ROOT_PATH);
zip.extractAllTo(ROOT_PATH, true);
console.log('Files extracted');
const removedFilesPath = path.join(ROOT_PATH, 'removed_files.json');
try {
await fs.access(removedFilesPath);
const removedFilesContent = await fs.readFile(removedFilesPath, 'utf8');
const filesToRemove = JSON.parse(removedFilesContent);
await removeFiles(filesToRemove, ROOT_PATH);
await fs.unlink(removedFilesPath);
} catch (error) {
console.log('No removed files to process or error accessing removed_files.json:', error);
}
// Remove temp zip file
await fs.unlink(tempZipPath);
// Start services after a delay
setTimeout(() => {
startServices()
.then(() => console.log('Services started successfully'))
.catch(e => console.error('Failed to start services:', e));
}, 1000);
} catch (error) {
console.error('Error in service update process:', error);
}
})();
servicesUpdate.catch(error => {
console.error('Background update process failed:', error);
});
console.log('Returning immediate response');
return {
success: true,
message: 'Update process initiated'
};
} catch (error) {
console.error('Critical error in updateProjectFilesFromScheme:', error);
return {
success: false,
error: error.message
};
}
}
static async getDBSchema() {
try {
return await database.getDBSchema();
} catch (error) {
console.error('Error reading schema:', error);
throw {
error: error,
message: error.message,
details: error.details || error.stack
};
}
}
static async executeSQL(query) {
try {
return await database.executeSQL(query);
} catch (error) {
console.error('Error executing query:', error);
throw {
error: error,
message: error.message,
details: error.details || error.stack
};
}
}
};
async function getDirectoryTree(dirPath) {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
const result = {};
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory() && (
entry.name === 'node_modules' ||
entry.name === 'app-shell' ||
entry.name === '.git' ||
entry.name === '.idea'
)) {
continue;
}
const relativePath = fullPath.replace('/app', '');
if (entry.isDirectory()) {
const subTree = await getDirectoryTree(fullPath);
Object.keys(subTree).forEach(key => {
result[key.replace('/app', '')] = subTree[key];
});
} else {
const fileContent = await fs.readFile(fullPath, 'utf8');
const lineCount = fileContent.split('\n').length;
result[relativePath] = lineCount;
}
}
return result;
}
async function stopServices() {
try {
console.log('Finding service processes...');
// Frontend stopping
const { stdout: frontendProcess } = await execAsync("ps -o pid,cmd | grep '[n]ext-server' | awk '{print $1}'");
if (frontendProcess.trim()) {
console.log('Stopping frontend, pid:', frontendProcess.trim());
await execAsync(`kill -15 ${frontendProcess.trim()}`);
}
// Backend stopping
const { stdout: backendProcess } = await execAsync("ps -o pid,cmd | grep '[n]ode ./src/index.js' | grep -v app-shell | awk '{print $1}'");
if (backendProcess.trim()) {
console.log('Stopping backend, pid:', backendProcess.trim());
await execAsync(`kill -15 ${backendProcess.trim()}`);
}
await new Promise(resolve => setTimeout(resolve, 2000));
return { success: true };
} catch (error) {
console.error('Error stopping services:', error);
return { success: false, error: error.message };
}
}
async function startServices() {
try {
console.log('Starting services...');
await execAsync('yarn --cwd /app/frontend dev &');
await execAsync('yarn --cwd /app/backend start &');
return { success: true };
} catch (error) {
console.error('Error starting services:', error);
return { success: false, error: error.message };
}
}
async function checkStatus() {
try {
const { stdout } = await execAsync('ps aux');
return {
success: true,
frontendRunning: stdout.includes('next-server'),
backendRunning: stdout.includes('nodemon') && stdout.includes('/app/backend'),
nginxRunning: stdout.includes('nginx: master process')
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
async function validateJSXSyntax(code) {
// Define validation rules for JSX
const rules = [
{
// JSX attribute with expression
pattern: /^[a-zA-Z][a-zA-Z0-9]*={.*}$/,
message: 'Invalid JSX attribute syntax'
},
{
// Invalid sequences
pattern: /,{2,}/,
message: 'Invalid character sequence detected',
shouldNotMatch: true
},
{
// Ternary expressions
pattern: /^[a-zA-Z][a-zA-Z0-9]*={[\w\s]+\?[^}]+:[^}]+}$/,
message: 'Invalid ternary expression in JSX'
}
];
// Validate each line
const lines = code.split('\n');
for (const line of lines) {
const trimmedLine = line.trim();
// Skip empty lines
if (!trimmedLine) continue;
// Check each rule
for (const rule of rules) {
if (rule.shouldNotMatch) {
// For patterns that should not be present
if (rule.pattern.test(trimmedLine)) {
return {
valid: false,
errors: [{
code: 'JSX_SYNTAX_ERROR',
severity: 'error',
location: '',
message: rule.message
}]
};
}
} else {
// For patterns that should match
if (trimmedLine.includes('=') && !rule.pattern.test(trimmedLine)) {
return {
valid: false,
errors: [{
code: 'JSX_SYNTAX_ERROR',
severity: 'error',
location: '',
message: rule.message
}]
};
}
}
}
// Additional JSX-specific checks
if ((trimmedLine.match(/{/g) || []).length !== (trimmedLine.match(/}/g) || []).length) {
return {
valid: false,
errors: [{
code: 'JSX_SYNTAX_ERROR',
severity: 'error',
location: '',
message: 'Unmatched curly braces in JSX'
}]
};
}
}
// If all checks pass
return {
valid: true,
errors: []
};
}
async function removeFiles(files, rootPath) {
try {
for (const file of files) {
const fullPath = path.join(rootPath, file);
try {
await fs.unlink(fullPath);
console.log(`File removed: ${fullPath}`);
} catch (error) {
console.error(`Error when trying to delete a file ${fullPath}:`, error);
}
}
} catch (error) {
console.error('Error removing files:', error);
throw error;
}
}

View File

@ -0,0 +1,16 @@
const { getNotification, isNotification } = require('../helpers');
module.exports = class ForbiddenError extends Error {
constructor(messageCode) {
let message;
if (messageCode && isNotification(messageCode)) {
message = getNotification(messageCode);
}
message = message || getNotification('errors.forbidden.message');
super(message);
this.code = 403;
}
};

View File

@ -0,0 +1,16 @@
const { getNotification, isNotification } = require('../helpers');
module.exports = class ValidationError extends Error {
constructor(messageCode) {
let message;
if (messageCode && isNotification(messageCode)) {
message = getNotification(messageCode);
}
message = message || getNotification('errors.validation.message');
super(message);
this.code = 400;
}
};

View File

@ -0,0 +1,30 @@
const _get = require('lodash/get');
const errors = require('./list');
function format(message, args) {
if (!message) {
return null;
}
return message.replace(/{(\d+)}/g, function (match, number) {
return typeof args[number] != 'undefined' ? args[number] : match;
});
}
const isNotification = (key) => {
const message = _get(errors, key);
return !!message;
};
const getNotification = (key, ...args) => {
const message = _get(errors, key);
if (!message) {
return key;
}
return format(message, args);
};
exports.getNotification = getNotification;
exports.isNotification = isNotification;

View File

@ -0,0 +1,100 @@
const errors = {
app: {
title: 'test',
},
auth: {
userDisabled: 'Your account is disabled',
forbidden: 'Forbidden',
unauthorized: 'Unauthorized',
userNotFound: `Sorry, we don't recognize your credentials`,
wrongPassword: `Sorry, we don't recognize your credentials`,
weakPassword: 'This password is too weak',
emailAlreadyInUse: 'Email is already in use',
invalidEmail: 'Please provide a valid email',
passwordReset: {
invalidToken: 'Password reset link is invalid or has expired',
error: `Email not recognized`,
},
passwordUpdate: {
samePassword: `You can't use the same password. Please create new password`,
},
userNotVerified: `Sorry, your email has not been verified yet`,
emailAddressVerificationEmail: {
invalidToken: 'Email verification link is invalid or has expired',
error: `Email not recognized`,
},
},
iam: {
errors: {
userAlreadyExists: 'User with this email already exists',
userNotFound: 'User not found',
disablingHimself: `You can't disable yourself`,
revokingOwnPermission: `You can't revoke your own owner permission`,
deletingHimself: `You can't delete yourself`,
emailRequired: 'Email is required',
},
},
importer: {
errors: {
invalidFileEmpty: 'The file is empty',
invalidFileExcel: 'Only excel (.xlsx) files are allowed',
invalidFileUpload:
'Invalid file. Make sure you are using the last version of the template.',
importHashRequired: 'Import hash is required',
importHashExistent: 'Data has already been imported',
userEmailMissing: 'Some items in the CSV do not have an email',
},
},
errors: {
forbidden: {
message: 'Forbidden',
},
validation: {
message: 'An error occurred',
},
searchQueryRequired: {
message: 'Search query is required',
},
},
emails: {
invitation: {
subject: `You've been invited to {0}`,
body: `
<p>Hello,</p>
<p>You've been invited to {0} set password for your {1} account.</p>
<p><a href='{2}'>{2}</a></p>
<p>Thanks,</p>
<p>Your {0} team</p>
`,
},
emailAddressVerification: {
subject: `Verify your email for {0}`,
body: `
<p>Hello,</p>
<p>Follow this link to verify your email address.</p>
<p><a href='{0}'>{0}</a></p>
<p>If you didn't ask to verify this address, you can ignore this email.</p>
<p>Thanks,</p>
<p>Your {1} team</p>
`,
},
passwordReset: {
subject: `Reset your password for {0}`,
body: `
<p>Hello,</p>
<p>Follow this link to reset your {0} password for your {1} account.</p>
<p><a href='{2}'>{2}</a></p>
<p>If you didn't ask to reset your password, you can ignore this email.</p>
<p>Thanks,</p>
<p>Your {0} team</p>
`,
},
},
};
module.exports = errors;

392
src/services/vcs.js Normal file
View File

@ -0,0 +1,392 @@
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const path = require('path');
const { promises: fs } = require("fs");
const axios = require('axios');
const ROOT_PATH = './';
const MAX_BUFFER = 1024 * 1024 * 50;
const GITEA_DOMAIN = 'gitea.flatlogic.app';
const USERNAME = 'admin';
const API_TOKEN = 'f22e83489657f49c320d081fc934e5c9daacfa08';
class VCS {
// Main method controller of the repository initialization process
static async initRepo(projectId = 'test') {
try {
// 1. Ensure the remote repository exists (create if needed)
const remoteUrl = await this.setupRemote(projectId);
// 2. Set up the local repository (initialization, fetching and reset)
await this.setupLocalRepo(remoteUrl);
console.log(`[DEBUG] Repository "${projectId}" is ready (remote code applied).`);
return { message: `Repository ${projectId} is ready (remote code applied).` };
} catch (error) {
throw new Error(`Error during repo initialization: ${error.message}`);
}
}
// Checks for the existence of the remote repo and creates it if it doesn't exist
static async setupRemote(projectId) {
console.log(`[DEBUG] Checking remote repository "${projectId}"...`);
let repoData = await this.checkRepoExists(projectId);
if (!repoData) {
console.log(`[DEBUG] Remote repository "${projectId}" does not exist. Creating...`);
repoData = await this.createRemoteRepo(projectId);
console.log(`[DEBUG] Remote repository created: ${JSON.stringify(repoData)}`);
} else {
console.log(`[DEBUG] Remote repository "${projectId}" already exists.`);
}
// Return the URL with token authentication
return `https://${USERNAME}:${API_TOKEN}@${GITEA_DOMAIN}/${USERNAME}/${projectId}.git`;
}
// Sets up the local repository: either fetches/reset if .git exists,
// initializes git in a non-empty directory, or clones the repository if empty.
static async setupLocalRepo(remoteUrl) {
const gitDir = path.join(ROOT_PATH, '.git');
const localRepoExists = await this.exists(gitDir);
if (localRepoExists) {
await this.fetchAndResetRepo();
} else {
const files = await fs.readdir(ROOT_PATH);
if (files.length > 0) {
await this.initializeGitRepo(remoteUrl);
} else {
console.log('[DEBUG] Local directory is empty. Cloning remote repository...');
await exec(`git clone ${remoteUrl} .`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
}
}
}
// Check if a file/directory exists
static async exists(pathToCheck) {
try {
await fs.access(pathToCheck);
return true;
} catch {
return false;
}
}
// If the local repository exists, fetches remote data and resets the repository state
static async fetchAndResetRepo() {
console.log('[DEBUG] Local repository exists. Fetching remote...');
await exec(`git fetch origin`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
const branchReset = await this.tryResetToBranch('fl-dev');
if (!branchReset) {
// If 'fl-dev' branch is not found, try 'master'
const masterReset = await this.tryResetToBranch('master');
if (masterReset) {
// Create 'fl-dev' branch and push it to remote
console.log('[DEBUG] Creating and switching to branch "fl-dev"...');
await exec(`git branch fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
await exec(`git checkout fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Pushing fl-dev branch to remote...');
await exec(`git push -u origin fl-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
} else {
// If neither remote master nor fl-dev exist make an initial commit
console.log('[DEBUG] Neither "origin/fl-dev" nor "origin/master" exist. Creating initial commit...');
await this.commitInitialChanges();
}
}
}
// Tries to check out and reset to the specified branch
static async tryResetToBranch(branchName) {
try {
console.log(`[DEBUG] Checking for remote branch "origin/${branchName}"...`);
await exec(`git rev-parse --verify origin/${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Remote branch "origin/${branchName}" found. Resetting local repository to "origin/${branchName}"...`);
await exec(`git reset --hard origin/${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
await exec(`git checkout ${branchName === 'fl-dev' ? 'fl-dev' : branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
return true;
} catch (e) {
console.log(`[DEBUG] Remote branch "origin/${branchName}" does NOT exist.`);
return false;
}
}
// If remote branch doesn't exist, make the initial commit and set up branches
static async commitInitialChanges() {
console.log('[DEBUG] Adding all files for initial commit...');
await exec(`git add .`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
const { stdout: status } = await exec(`git status --porcelain`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
if (status.trim()) {
await exec(`git commit -m "Initial version"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
await exec(`git push -u origin master --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Creating and switching to branch "fl-dev"...');
await exec(`git branch fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
await exec(`git checkout fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Making fl-dev branch identical to master...');
await exec(`git reset --hard origin/master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Pushing fl-dev branch to remote...');
await exec(`git push -u origin fl-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
} else {
console.log('[DEBUG] No local changes to commit.');
}
}
// If the local directory is not empty but .git doesn't exist, initialize git,
// add .gitignore, configure the user, and add the remote origin.
static async initializeGitRepo(remoteUrl) {
console.log('[DEBUG] Local directory is not empty. Initializing git...');
const gitignorePath = path.join(ROOT_PATH, '.gitignore');
const ignoreContent = `node_modules/\n*/node_modules/\n*/build/\n`;
await fs.writeFile(gitignorePath, ignoreContent, 'utf8');
await exec(`git init`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Configuring git user...');
await exec(`git config user.email "support@flatlogic.com"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
await exec(`git config user.name "Flatlogic Bot"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Adding remote ${remoteUrl}...`);
await exec(`git remote add origin ${remoteUrl}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Fetching remote...');
await exec(`git fetch origin`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
try {
console.log('[DEBUG] Checking for remote branch "origin/fl-dev"...');
await exec(`git rev-parse --verify origin/fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Remote branch "origin/fl-dev" exists. Resetting local repository to origin/fl-dev...');
await exec(`git reset --hard origin/fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Switching to branch "fl-dev"...');
await exec(`git checkout -B fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
} catch (e) {
console.log('[DEBUG] Remote branch "origin/fl-dev" does NOT exist. Creating initial commit...');
await this.commitInitialChanges();
}
}
// Method to check if the repository exists on remote server
static async checkRepoExists(repoName) {
const url = `https://${GITEA_DOMAIN}/api/v1/repos/${USERNAME}/${repoName}`;
try {
const response = await axios.get(url, {
headers: { Authorization: `token ${API_TOKEN}` }
});
return response.data;
} catch (err) {
if (err.response && err.response.status === 404) {
return null;
}
throw new Error('Error checking repository existence: ' + err.message);
}
}
// Method to create a remote repository via API
static async createRemoteRepo(repoName) {
const createUrl = `https://${GITEA_DOMAIN}/api/v1/user/repos`;
console.log("[DEBUG] createUrl", createUrl);
try {
const response = await axios.post(createUrl, {
name: repoName,
description: `Repository for project ${repoName}`,
private: false
}, {
headers: { Authorization: `token ${API_TOKEN}` }
});
return response.data;
} catch (err) {
throw new Error('Error creating repository via API: ' + err.message);
}
}
static async commitChanges(message = "", files = '.') {
// Ensure that we are on branch 'fl-dev' before making any commits
await this._ensureDevBranch();
try {
await exec(`git add ${files}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
const { stdout: status } = await exec('git status --porcelain', { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
if (!status.trim()) {
return { message: "No changes to commit" };
}
const now = new Date();
const commitMessage = message || `Auto commit: ${now.toISOString()}`;
console.log('commitMessage:', commitMessage);
await exec(`git commit -m "${commitMessage}"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
await this._pushChanges();
console.log('Pushed');
return { message: "Changes committed" };
} catch (error) {
throw new Error(`Error during commit: ${error.message}`);
}
}
static async getLog() {
try {
const { stdout } = await exec('git log fl-dev --oneline', { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
const lines = stdout.split(/\r?\n/).filter(line => line.trim() !== '');
const result = {};
lines.forEach((line) => {
const firstSpaceIndex = line.indexOf(' ');
if (firstSpaceIndex > 0) {
const hash = line.substring(0, firstSpaceIndex);
const message = line.substring(firstSpaceIndex + 1).trim();
result[hash] = message;
}
});
return result;
} catch (error) {
throw new Error(`Error during get log: ${error.message}`);
}
}
static async checkout(ref) {
try {
await exec(`git checkout ${ref}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
return { message: `Checked out to ${ref}` };
} catch (error) {
throw new Error(`Error during checkout: ${error.message}`);
}
}
static async revert(commitHash) {
try {
await exec(`git reset --hard`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
// Rollback to the specified commit hash
await exec(
`git revert --no-edit ${commitHash}..HEAD --no-commit`,
{ cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }
);
// Commit the changes
await exec(
`git commit -m "Revert to version ${commitHash}"`,
{ cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }
);
await this._pushChanges();
return { message: `Reverted to commit ${commitHash}` };
} catch (error) {
console.error("Error during range revert:", error.message);
if (error.stdout) {
console.error("Revert stdout:", error.stdout);
}
if (error.stderr) {
console.error("Revert stderr:", error.stderr);
}
throw new Error(`Error during range revert: ${error.message}`);
}
}
static async mergeDevIntoMaster() {
try {
// Switch to branch 'master'
console.log('Switching to branch "master"...');
await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
// Merge branch 'fl-dev' into 'master' with a forced merge.
// Parameter -X theirs is used to resolve conflicts by keeping the changes from the branch being merged in case of conflicts.
console.log('Merging branch "fl-dev" into "master" (force merge with -X theirs)...');
await exec(
`git merge fl-dev --no-ff -X theirs -m "Forced merge: merge fl-dev into master"`,
{ cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }
);
// Push the merged 'master' branch to remote
console.log('Pushing merged master branch to remote...');
const { stdout, stderr } = await exec(`git push origin master`, {
cwd: ROOT_PATH,
maxBuffer: MAX_BUFFER
});
if (stdout) {
console.log("Git push stdout:", stdout);
}
if (stderr) {
console.error("Git push stderr:", stderr);
}
return { message: "Branch fl-dev merged into master and pushed to remote" };
} catch (error) {
console.error("Error during mergeDevIntoMaster:", error.message);
if (error.stdout) {
console.error("Merge stdout:", error.stdout);
}
if (error.stderr) {
console.error("Merge stderr:", error.stderr);
}
throw error;
}
}
static async resetDevBranch() {
try {
console.log('[DEBUG] Switching to branch "master"...');
await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Resetting branch "fl-dev" to be identical to "master"...');
// Command checkout -B fl-dev master creates branch 'fl-dev' from 'master' and switches to it
await exec(`git checkout -B fl-dev master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Pushing updated branch "fl-dev" to remote (force push)...');
await exec(`git push -u origin fl-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
return { message: 'fl-dev branch successfully reset to master.' };
} catch (error) {
console.error("Error during resetting fl-dev branch:", error.message);
if (error.stdout) {
console.error("Reset stdout:", error.stdout);
}
if (error.stderr) {
console.error("Reset stderr:", error.stderr);
}
throw new Error(`Error during resetting fl-dev branch: ${error.message}`);
}
}
static async _pushChanges() {
try {
const { stdout, stderr } = await exec(`git push origin fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
if (stdout) {
console.log("Git push stdout:", stdout);
}
if (stderr) {
console.error("Git push stderr:", stderr);
}
return { message: "Changes pushed to remote repository (fl-dev branch)" };
} catch (error) {
console.error("Git push error:", error.message);
if (error.stdout) {
console.error("Git push stdout:", error.stdout);
}
if (error.stderr) {
console.error("Git push stderr:", error.stderr);
}
}
}
static async _ensureDevBranch() {
try {
// Check if branch 'fl-dev' exists
const { stdout: branchList } = await exec(`git branch --list fl-dev`, {
cwd: ROOT_PATH,
maxBuffer: MAX_BUFFER,
});
if (!branchList || branchList.trim() === '') {
console.log("Branch 'fl-dev' not found. Creating branch 'fl-dev'.");
await exec(`git checkout -b fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
} else {
// Determine current branch
const { stdout: currentBranchStdout } = await exec(`git rev-parse --abbrev-ref HEAD`, {
cwd: ROOT_PATH,
maxBuffer: MAX_BUFFER,
});
const currentBranch = currentBranchStdout.trim();
if (currentBranch !== 'fl-dev') {
console.log(`Switching from branch '${currentBranch}' to 'fl-dev'.`);
await exec(`git checkout fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
} else {
console.log("Already on branch 'fl-dev'.");
}
}
} catch (error) {
console.error("Error ensuring branch 'fl-dev':", error.message);
throw error;
}
}
}
module.exports = VCS;

3763
yarn.lock Normal file

File diff suppressed because it is too large Load Diff