Skip to content

Commit

Permalink
Merge pull request #394 from d02ev/d02ev/issue#332
Browse files Browse the repository at this point in the history
PR for Issue#332
  • Loading branch information
erenfn authored Dec 17, 2024
2 parents 99e5614 + 01e7067 commit 7a310eb
Show file tree
Hide file tree
Showing 11 changed files with 237 additions and 31 deletions.
1 change: 1 addition & 0 deletions backend/config/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module.exports = {
update: [userRole.ADMIN],
changeRole: [userRole.ADMIN],
setOrg: [userRole.ADMIN],
serverUrl: [userRole.ADMIN],
popups: [userRole.ADMIN],
hints: [userRole.ADMIN],
banners: [userRole.ADMIN],
Expand Down
14 changes: 14 additions & 0 deletions backend/migrations/20241216195934-server_url_col_team_rel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict';

module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.addColumn('teams', 'serverUrl', {
type: Sequelize.STRING(255),
allowNull: true,
})
},

async down (queryInterface, Sequelize) {
await queryInterface.removeColumn('teams', 'serverUrl')
}
};
42 changes: 41 additions & 1 deletion backend/src/controllers/team.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const TeamService = require("../service/team.service");
const { internalServerError } = require("../utils/errors.helper");
const { MAX_ORG_NAME_LENGTH, ORG_NAME_REGEX } = require('../utils/constants.helper');
const db = require("../models");
const { validationResult } = require('express-validator');

const Team = db.Team;
const teamService = new TeamService();
Expand Down Expand Up @@ -61,6 +62,21 @@ const getTeamCount = async (req, res) => {
}
};

const getServerUrl = async (req, res) => {
try {
let serverUrl = await teamService.fetchServerUrl();
serverUrl = serverUrl === null ? "" : serverUrl;

return res.status(200).json({ serverUrl });
} catch (err) {
const { statusCode, payload } = internalServerError(
"GET_SERVER_URL_ERROR",
err.message
);
res.status(statusCode).json(payload);
}
};

const getTeamDetails = async (req, res) => {
try {
const data = await teamService.getTeam();
Expand Down Expand Up @@ -101,6 +117,30 @@ const updateTeamDetails = async (req, res) => {
}
};

const setServerUrl = async (req, res) => {
const validationErrors = validationResult(req);

if (!validationErrors.isEmpty()) {
const errors = [];
validationErrors.array().forEach(err => {
errors.push(err.msg);
});
return res.status(400).json({ errors });
}

try {
const { serverUrl } = req.body;
await teamService.addServerUrl(serverUrl);
return res.status(200).json({ message: "Server URL Set Successfully" });
} catch (err) {
const { statusCode, payload } = internalServerError(
"SET_SERVER_URL_ERROR",
err.message
)
res.status(statusCode).json(payload);
}
}

const removeMember = async (req, res) => {
const userId = req.user.id;
const { memberId } = req.params;
Expand Down Expand Up @@ -130,4 +170,4 @@ const changeRole = async (req, res) => {
}
}

module.exports = { setOrganisation, getTeamDetails, updateTeamDetails, removeMember, changeRole, getTeamCount, teamService };
module.exports = { setOrganisation, getTeamDetails, updateTeamDetails, removeMember, changeRole, getTeamCount, getServerUrl, setServerUrl, teamService };
4 changes: 4 additions & 0 deletions backend/src/models/Team.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ module.exports = (sequelize, DataTypes) => {
type: DataTypes.STRING(50),
allowNull: false,
},
serverUrl: {
type: DataTypes.STRING(255),
allowNull: true,
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
Expand Down
5 changes: 5 additions & 0 deletions backend/src/routes/team.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const {
setOrganisation,
getTeamDetails,
getTeamCount,
getServerUrl,
setServerUrl,
updateTeamDetails,
removeMember,
changeRole
Expand All @@ -14,17 +16,20 @@ const {
const authenticateJWT = require("../middleware/auth.middleware");
const accessGuard = require("../middleware/accessGuard.middleware");
const settings = require("../../config/settings");
const { validateSetServerUrl } = require('../utils/team.helper');

const router = express.Router();
const teamPermissions = settings.team.permissions;

router.get("/details", authenticateJWT, getTeamDetails);
router.get("/count", getTeamCount);
router.get('/server-url', authenticateJWT, accessGuard(teamPermissions.serverUrl), getServerUrl);

router.post("/set-organisation", authenticateJWT, accessGuard(teamPermissions.setOrg), setOrganisation);
router.post("/invite", authenticateJWT, accessGuard(teamPermissions.invite), sendTeamInvite);
router.put("/update", authenticateJWT, accessGuard(teamPermissions.update), updateTeamDetails);
router.put("/change-role", authenticateJWT, accessGuard(teamPermissions.changeRole), changeRole);
router.put('/server-url', authenticateJWT, accessGuard(teamPermissions.serverUrl), validateSetServerUrl, setServerUrl);

router.delete("/remove/:memberId", authenticateJWT, accessGuard(teamPermissions.removeUser), removeMember);
router.get('/get-all-invites', authenticateJWT, accessGuard(teamPermissions.removeUser), getAllInvites);
Expand Down
23 changes: 23 additions & 0 deletions backend/src/service/team.service.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const { where } = require("sequelize");
const settings = require("../../config/settings");
const db = require("../models");
const Team = db.Team;
Expand Down Expand Up @@ -42,6 +43,28 @@ class TeamService {
}
};

async fetchServerUrl() {
try {
const { serverUrl } = await Team.findOne();
return serverUrl;
} catch (err) {
throw new Error("Failed to fetch server url");
}
}

async addServerUrl(serverUrl) {
const transaction = await sequelize.transaction();
try {
await Team.update({
serverUrl
}, { where: {} }, { transaction });
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw new Error("Failed to add server url")
}
}

async updateTeam(name) {
const transaction = await sequelize.transaction();
try {
Expand Down
2 changes: 2 additions & 0 deletions backend/src/utils/constants.helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ module.exports = Object.freeze({
},
MAX_ORG_NAME_LENGTH: 100,
ORG_NAME_REGEX: /^[a-zA-Z0-9\s\-_&.]+$/,
URL_PROTOCOL_REGEX: /^(https?:\/\/)/,
URL_DOMAIN_REGEX: /^https?:\/\/([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/,
});

50 changes: 50 additions & 0 deletions backend/src/utils/team.helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const { URL_PROTOCOL_REGEX, URL_DOMAIN_REGEX } = require('./constants.helper');
const { check } = require('express-validator');

require('dotenv').config();

const validateServerUrl = url => {
if (url === "") {
return { valid: true, errors: null }
}

const errors = [];

if (!URL_PROTOCOL_REGEX.test(url)) {
errors.push("Invalid or missing protocol (must be 'http://' or 'https://').")
}

const domainMatch = url.match(URL_DOMAIN_REGEX);
if (!domainMatch) {
errors.push("Invalid domain name (must include a valid top-level domain like '.com').");
} else {
const domain = domainMatch[1];
if (!/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(domain)) {
errors.push(`Malformed domain: '${domain}'.`);
}
}

if (errors.length === 0) {
return { valid: true, errors: null }
}

return { valid: false, errors }
};

const validateSetServerUrl = [
check('serverUrl')
.optional({
values: ["", null, undefined]
})
.isString().withMessage('Server URL must be a string')
.trim()
.custom(value => {
const result = validateServerUrl(value);
if (result.valid) {
return true;
}
throw new Error(result.errors);
})
];

module.exports = { validateSetServerUrl };
97 changes: 70 additions & 27 deletions frontend/src/scenes/settings/CodeTab/CodeTab.jsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,101 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import styles from "./CodeTab.module.css";
import CustomTextField from "@components/TextFieldComponents/CustomTextField/CustomTextField";
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
import Button from "@components/Button/Button";
import ContentCopyOutlinedIcon from '@mui/icons-material/ContentCopyOutlined';
import { generateApiKey } from "../../../utils/generalHelper";
import { emitToastError } from "../../../utils/guideHelper";
import { getServerUrl, addServerUrl } from '../../../services/teamServices';
import toastEmitter, { TOAST_EMITTER_KEY } from "../../../utils/toastEmitter";
import { URL_REGEX } from "../../../utils/constants";

const CodeTab = () => {
const [apiKey, setApiKey] = useState('')
const [serverUrl, setServerUrl] = useState('')
const [isLoading, setIsLoading] = useState(false);

const validateServerUrl = url => {
const errors = [];

if (url === "") {
return { valid: true, errors: null };
}

if (!URL_REGEX.PROTOCOL.test(url)) {
errors.push("Invalid or missing protocol (must be 'http://' or 'https://').")
}

const domainMatch = url.match(URL_REGEX.DOMAIN);
if (!domainMatch) {
errors.push("Invalid domain name (must include a valid top-level domain like '.com').");
} else {
const domain = domainMatch[1];
if (!/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(domain)) {
errors.push(`Malformed domain: '${domain}'.`);
}
}

if (errors.length === 0) {
return { valid: true, errors: null }
}

return { valid: false, errors }
};

useEffect(() => {
const fetchServerUrl = async () => {
try {
const { serverUrl } = await getServerUrl();
setServerUrl(serverUrl);
} catch (err) {
console.error('Error fetching server url: ', err);
}
}
fetchServerUrl();
}, [])

const handleUrlChange = (e) => {
setServerUrl(e.target.value);
};

const handleApiKeyChange = (e) => {
setApiKey(e.target.value);
};
const onSave = async () => {
const { valid, errors } = validateServerUrl(serverUrl);

if (!valid) {
errors.forEach(err => {
toastEmitter.emit(TOAST_EMITTER_KEY, err);
});
return;
}

const deleteApiKey = () => {
setApiKey('');
}
try {
setIsLoading(true);
const response = await addServerUrl(serverUrl);
toastEmitter.emit(TOAST_EMITTER_KEY, response.message);
} catch (err) {
emitToastError(err);
} finally {
setIsLoading(false);
}
};

return (
<section className={styles.container}>
<h2>API key management</h2>
<p className={styles.content}>Manage the key that Onboarding app uses to authenticate the agent code.</p>

{/* api key */}
<div className={styles.block}>
<p style={{marginRight:'2rem'}}>API key:</p>
<CustomTextField
value={apiKey}
onChange={handleApiKeyChange}
style={{textAlign: 'right' }}
TextFieldWidth="550px"
/>
<DeleteOutlinedIcon onClick={deleteApiKey} style={{ cursor: 'pointer', fontSize: '24px', color: 'var(--main-text-color)' }} />
<Button text='Regenerate' onClick={() => setApiKey(generateApiKey())} sx={{width:'120px'}}/>
</div>
{/* server url */}
<div className={styles.block}>
<p className={styles.label}>Server URL:</p>
<CustomTextField
value={serverUrl}
onChange={handleUrlChange}
style={{textAlign: 'right' }}
style={{ textAlign: 'right' }}
TextFieldWidth="550px"
/>
<span/>
<Button text='Save' sx={{width:'120px'}}/>
<span />
<Button text='Save' sx={{ width: '120px' }} onClick={onSave} />
</div>
<h2 style={{marginTop: '25px'}}>Code in your webpage</h2>
<h2 style={{ marginTop: '25px' }}>Code in your webpage</h2>
<div className={styles.informativeBlock}>
<p className={styles.content}>
Code snippet to copy in your web page between {"<head>"} and {"</head>"}. Make sure you edit the API URL.
Expand All @@ -64,14 +108,13 @@ const CodeTab = () => {
{`<!-- Client-side HTML/JS Snippet to be integrated into their website -->
<script>
(function() {
const apiKey = '${apiKey}';
const apiUrl = '${serverUrl}';
var s=document.createElement("script");
s.type="text/javascript";
s.async=false;
s.onerror=()=>{console.log("onboard not loaded");};
s.src = 'http://localhost:8082/main.js=${apiKey}';
s.src = 'http://localhost:8082;
(document.getElementsByTagName("head")[0] || document.getElementsByTagName("body")[0]).appendChild(script);
})();
</script>
Expand Down
22 changes: 21 additions & 1 deletion frontend/src/services/teamServices.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,24 @@ export const getTeamCount = async () => {
console.error('Error getting team count: ', err);
throw err;
}
}
}

export const addServerUrl = async url => {
try {
const response = await apiClient.put(`${baseEndpoint}/server-url`, { serverUrl: url });
return response.data;
} catch (err) {
console.error('Error setting server url: ', err);
throw err;
}
}

export const getServerUrl = async () => {
try {
const response = await apiClient.get(`${baseEndpoint}/server-url`);
return response.data;
} catch (err) {
console.error('Error getting server url: ', err);
throw err;
}
};
Loading

0 comments on commit 7a310eb

Please sign in to comment.