From e9139c6737729dbfce16a6821e9ce0e2ec806c29 Mon Sep 17 00:00:00 2001 From: Shreyash Neeraj Date: Mon, 15 Jun 2026 22:56:01 +0530 Subject: [PATCH 1/5] Fix login loop, refactor navbar, implement project bundle actions, and resolve edit details description validation bug --- eda-frontend/src/App.js | 3 +- .../src/components/Dashboard/CircuitCard.js | 95 +- .../src/components/Dashboard/DashboardHome.js | 98 -- .../components/Dashboard/DashboardSidebar.js | 10 +- .../components/Dashboard/SchematicsList.js | 963 ++++++++++++------ .../src/components/Project/CreateProject.js | 18 +- .../src/components/SchematicEditor/Header.js | 16 - .../SchematicEditor/PropertiesSidebar.js | 25 +- .../SchematicEditor/SchematicToolbar.js | 146 +-- .../SchematicEditor/VersionComponent.js | 29 +- eda-frontend/src/components/Shared/Layout.js | 30 +- eda-frontend/src/components/Shared/Navbar.js | 15 +- eda-frontend/src/pages/Dashboard.js | 15 +- eda-frontend/src/pages/LTISetup.js | 16 +- eda-frontend/src/pages/SubmissionPage.js | 16 +- eda-frontend/src/pages/signUp.js | 219 ++-- eda-frontend/src/redux/actions/authActions.js | 39 +- .../src/redux/actions/dashboardActions.js | 23 + .../src/redux/actions/saveSchematicActions.js | 7 +- esim-cloud-backend/authAPI/models.py | 9 + .../authAPI/templates/account_dashboard.html | 409 ++++++++ esim-cloud-backend/authAPI/urls.py | 3 +- esim-cloud-backend/authAPI/views.py | 169 +++ esim-cloud-backend/esimCloud/settings.py | 22 +- esim-cloud-backend/esimCloud/urls.py | 3 + esim-cloud-backend/publishAPI/serializers.py | 26 +- esim-cloud-backend/publishAPI/urls.py | 6 + esim-cloud-backend/publishAPI/views.py | 96 +- esim-cloud-backend/saveAPI/views.py | 16 +- 29 files changed, 1851 insertions(+), 691 deletions(-) delete mode 100644 eda-frontend/src/components/Dashboard/DashboardHome.js create mode 100644 esim-cloud-backend/authAPI/templates/account_dashboard.html diff --git a/eda-frontend/src/App.js b/eda-frontend/src/App.js index 7685412e6..e71817723 100644 --- a/eda-frontend/src/App.js +++ b/eda-frontend/src/App.js @@ -87,7 +87,8 @@ function App () { - // TODO: restore PrivateRoute when login is fixed + {/* TODO: restore PrivateRoute when login is fixed */} + diff --git a/eda-frontend/src/components/Dashboard/CircuitCard.js b/eda-frontend/src/components/Dashboard/CircuitCard.js index 6199e3e8f..361d43edc 100644 --- a/eda-frontend/src/components/Dashboard/CircuitCard.js +++ b/eda-frontend/src/components/Dashboard/CircuitCard.js @@ -32,7 +32,7 @@ import StarBorderIcon from '@material-ui/icons/StarBorder' import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser' import { Link as RouterLink } from 'react-router-dom' import { useDispatch, useSelector } from 'react-redux' -import { deleteSchematic, togglePinSave } from '../../redux/actions/index' +import { deleteSchematic, togglePinSave, removeFromProject } from '../../redux/actions/index' // ── Styles ──────────────────────────────────────────────────────────────────── const useStyles = makeStyles((theme) => ({ @@ -124,7 +124,7 @@ function timeSince (jsonDate) { } // ── Component ───────────────────────────────────────────────────────────────── -export default function CircuitCard ({ sch, onRefresh }) { +export default function CircuitCard ({ sch, onRefresh, inProjectFolder = false }) { const classes = useStyles() const dispatch = useDispatch() const auth = useSelector(state => state.authReducer) @@ -164,6 +164,18 @@ export default function CircuitCard ({ sch, onRefresh }) { } } + const handleRemoveFromProject = () => { + if (!hasToken()) { + setSnackOpen(true) + return + } + if (window.confirm(`Remove "${sch.name || sch.save_id}" from project?`)) { + Promise.resolve(dispatch(removeFromProject(sch.save_id))) + .then(() => { if (onRefresh) onRefresh() }) + .catch((err) => console.error(err)) + } + } + // ── Render ──────────────────────────────────────────────────────────────── return ( @@ -232,31 +244,62 @@ export default function CircuitCard ({ sch, onRefresh }) { - {/* Pin / Unpin */} - - - + {inProjectFolder ? ( + <> + {/* Remove from Project */} + + + - {/* Delete */} - - - + {/* Delete */} + + + + + ) : ( + <> + {/* Pin / Unpin */} + + + + + {/* Delete */} + + + + + )} diff --git a/eda-frontend/src/components/Dashboard/DashboardHome.js b/eda-frontend/src/components/Dashboard/DashboardHome.js deleted file mode 100644 index 3230112ba..000000000 --- a/eda-frontend/src/components/Dashboard/DashboardHome.js +++ /dev/null @@ -1,98 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { - Card, - Grid, - Button, - Typography, - CardActions, - CardContent -} from '@material-ui/core' -import { makeStyles } from '@material-ui/core/styles' -import { Link as RouterLink } from 'react-router-dom' -import { useSelector } from 'react-redux' - -import ProgressPanel from './ProgressPanel' - -const useStyles = makeStyles((theme) => ({ - mainHead: { - width: '100%', - backgroundColor: '#404040', - color: '#fff' - }, - title: { - fontSize: 14, - color: '#80ff80' - } -})) - -// Card displaying user dashboard home page header. -function MainCard () { - const classes = useStyles() - const auth = useSelector(state => state.authReducer) - - return ( - - - - Welcome to your EDA Dashboard - - - Welcome {auth.user.username}... - - - - - - - ) -} - -export default function DashboardHome ({ ltiDetails = null }) { - const classes = useStyles() - const auth = useSelector(state => state.authReducer) - - return ( - <> - - {/* User Dashboard Home Header */} - - - - - - - - Hey {auth.user.username} , Track your schematics status here... - - - - - {/* List recent schematics saved by user */} - - - - - - - - ) -} - -DashboardHome.propTypes = { - ltiDetails: PropTypes.string -} diff --git a/eda-frontend/src/components/Dashboard/DashboardSidebar.js b/eda-frontend/src/components/Dashboard/DashboardSidebar.js index 14c17d422..84b0b6041 100644 --- a/eda-frontend/src/components/Dashboard/DashboardSidebar.js +++ b/eda-frontend/src/components/Dashboard/DashboardSidebar.js @@ -91,15 +91,7 @@ export default function DashSidebar (props) { /> - - - + ({ - mainHead: { - width: '100%', - backgroundColor: '#404040', - color: '#fff' - }, - title: { - fontSize: 14, - color: '#80ff80' - }, typography: { padding: theme.spacing(2) }, @@ -87,7 +87,8 @@ const useStyles = makeStyles((theme) => ({ sectionHeader: { display: 'flex', alignItems: 'center', - marginBottom: theme.spacing(1.5) + marginBottom: theme.spacing(1.5), + marginTop: theme.spacing(2) }, sectionTitle: { fontWeight: 700, @@ -101,7 +102,8 @@ const useStyles = makeStyles((theme) => ({ emptyText: { color: theme.palette.text.secondary, fontSize: '0.875rem', - padding: theme.spacing(1.5, 0) + padding: theme.spacing(2, 0), + textAlign: 'center' }, // ── Loading / error states ───────────────────────────────────────────────── centeredSpinner: { @@ -110,13 +112,11 @@ const useStyles = makeStyles((theme) => ({ alignItems: 'center', minHeight: 180 }, - errorBox: { - padding: theme.spacing(2), - backgroundColor: '#fdecea', - border: '1px solid #f44336', - borderRadius: 8, - color: '#c62828', - fontSize: '0.875rem', + filterContainer: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + width: '100%', marginBottom: theme.spacing(2) } })) @@ -126,7 +126,7 @@ function TabPanel (props) { return ( {value === index && ( - <>{children} + {children} )} ) @@ -138,42 +138,79 @@ TabPanel.propTypes = { value: PropTypes.any.isRequired } -// ── Header card (unchanged from original) ───────────────────────────────────── -function MainCard () { - const classes = useStyles() +// Helper to render pinned & recent circuits for a given tab's data +function CircuitListSection ({ circuits, emptyMessage, onRefresh, classes, inProjectFolder = false }) { + const pinnedCircuits = circuits.filter(s => s.pinned === true) + const recentCircuits = circuits.filter(s => s.pinned !== true) + return ( - - - - All schematics are Listed Below - - - My Schematics - - - - - - - + + {/* Pinned Section */} + {pinnedCircuits.length > 0 && ( + +
+ + ★ Pinned + + + ({pinnedCircuits.length}) + +
+ + {pinnedCircuits.map((sch) => ( + + + + ))} + + +
+ )} + + {/* Recent Section */} + + {pinnedCircuits.length > 0 && recentCircuits.length > 0 && ( +
+ + 🕒 Recent + + + ({recentCircuits.length}) + +
+ )} + + {recentCircuits.length === 0 && pinnedCircuits.length === 0 ? ( + + {emptyMessage} + + ) : ( + + {recentCircuits.map((sch) => ( + + + + ))} + + )} +
+
) } -// ── Main component ───────────────────────────────────────────────────────────── +CircuitListSection.propTypes = { + circuits: PropTypes.array.isRequired, + emptyMessage: PropTypes.string.isRequired, + onRefresh: PropTypes.func.isRequired, + classes: PropTypes.object.isRequired, + inProjectFolder: PropTypes.bool +} + export default function SchematicsList ({ ltiDetails = null }) { const classes = useStyles() const auth = useSelector(state => state.authReducer) const schematics = useSelector(state => state.dashboardReducer.schematics) + const myProjects = useSelector(state => state.dashboardReducer.myProjects) const [saves, setSaves] = React.useState(schematics) const dispatch = useDispatch() const [anchorEl, setAnchorEl] = React.useState(null) @@ -186,7 +223,125 @@ export default function SchematicsList ({ ltiDetails = null }) { const [isLoading, setIsLoading] = React.useState(false) const [fetchError, setFetchError] = React.useState(null) - // ── Fetch helper — also used as onRefresh callback for CircuitCard ───────── + // Project dialog and create variables + const [projectDialogOpen, setProjectDialogOpen] = React.useState(false) + const [projectTitle, setProjectTitle] = React.useState('') + const [projectDescription, setProjectDescription] = React.useState('') + const [selectedCircuitIds, setSelectedCircuitIds] = React.useState([]) + const [projectError, setProjectError] = React.useState('') + const [isSubmittingProject, setIsSubmittingProject] = React.useState(false) + const [activeProjectId, setActiveProjectId] = React.useState(null) + + // Edit Project variables + const [editProjectDialogOpen, setEditProjectDialogOpen] = React.useState(false) + const [editProjectTitle, setEditProjectTitle] = React.useState('') + const [editProjectDescription, setEditProjectDescription] = React.useState('') + const [editProjectError, setEditProjectError] = React.useState('') + const [isSubmittingEditProject, setIsSubmittingEditProject] = React.useState(false) + + // Add Schematic variables + const [addSchematicDialogOpen, setAddSchematicDialogOpen] = React.useState(false) + const [selectedAddCircuitIds, setSelectedAddCircuitIds] = React.useState([]) + const [addCircuitError, setAddCircuitError] = React.useState('') + const [isSubmittingAddCircuit, setIsSubmittingAddCircuit] = React.useState(false) + + const handleOpenEditProjectDialog = (project) => { + setEditProjectTitle(project.title) + setEditProjectDescription(project.description || '') + setEditProjectError('') + setEditProjectDialogOpen(true) + } + + const handleCloseEditProjectDialog = () => { + setEditProjectDialogOpen(false) + } + + const handleEditProject = () => { + if (!editProjectTitle.trim()) { + setEditProjectError('Project title is required') + return + } + + setIsSubmittingEditProject(true) + setEditProjectError('') + + const token = auth.token || localStorage.getItem('esim_token') + const config = { + headers: { + 'Content-Type': 'application/json' + } + } + if (token) { + config.headers.Authorization = `Token ${token}` + } + + api.patch(`/publish/myproject/${activeProjectId}/`, { + title: editProjectTitle, + description: editProjectDescription + }, config) + .then(() => { + setIsSubmittingEditProject(false) + setEditProjectDialogOpen(false) + doFetch() + }) + .catch((err) => { + setIsSubmittingEditProject(false) + setEditProjectError(err.response?.data?.error || 'Failed to update project details') + }) + } + + const handleOpenAddSchematicDialog = (project) => { + setSelectedAddCircuitIds([]) + setAddCircuitError('') + setAddSchematicDialogOpen(true) + } + + const handleCloseAddSchematicDialog = () => { + setAddSchematicDialogOpen(false) + } + + const handleToggleAddCircuitSelection = (saveId) => { + if (selectedAddCircuitIds.includes(saveId)) { + setSelectedAddCircuitIds(selectedAddCircuitIds.filter(id => id !== saveId)) + } else { + setSelectedAddCircuitIds([...selectedAddCircuitIds, saveId]) + } + } + + const handleAddSchematics = () => { + if (selectedAddCircuitIds.length === 0) { + setAddCircuitError('Please select at least one circuit to add') + return + } + + setIsSubmittingAddCircuit(true) + setAddCircuitError('') + + const token = auth.token || localStorage.getItem('esim_token') + const config = { + headers: { + 'Content-Type': 'application/json' + } + } + if (token) { + config.headers.Authorization = `Token ${token}` + } + + api.post('/publish/add_schematic/', { + project_id: activeProjectId, + save_ids: selectedAddCircuitIds + }, config) + .then(() => { + setIsSubmittingAddCircuit(false) + setAddSchematicDialogOpen(false) + doFetch() + }) + .catch((err) => { + setIsSubmittingAddCircuit(false) + setAddCircuitError(err.response?.data?.error || 'Failed to add schematics to project') + }) + } + const doFetch = useCallback(() => { const hasToken = auth.token || localStorage.getItem('esim_token') if (!hasToken) { @@ -195,7 +350,10 @@ export default function SchematicsList ({ ltiDetails = null }) { setIsLoading(true) setFetchError(null) - Promise.resolve(dispatch(fetchSchematics())) + Promise.all([ + Promise.resolve(dispatch(fetchSchematics())), + Promise.resolve(dispatch(fetchMyProjects())) + ]) .catch((err) => { if (err.response && err.response.status === 401) { localStorage.removeItem('esim_token') @@ -213,13 +371,6 @@ export default function SchematicsList ({ ltiDetails = null }) { useEffect(() => { doFetch() }, [doFetch]) useEffect(() => { setSaves(schematics) }, [schematics]) - // ── Derive pinned and recent lists from Redux schematics ─────────────────── - // Treat missing pinned field as false (backwards-compat with old records) - const pinnedCircuits = schematics.filter(s => s.pinned === true) - // Recent = all circuits that are NOT pinned - const recentCircuits = schematics.filter(s => s.pinned !== true) - - // ── Search / sort (unchanged from original) ──────────────────────────────── const onSearch = (e) => { setSaves(schematics.filter((o) => // eslint-disable-next-line @@ -240,21 +391,22 @@ export default function SchematicsList ({ ltiDetails = null }) { } const sortSaves = (sorting, order) => { + const sorted = [...saves] if (order === 'ascending') { if (sorting === 'name') { - setSaves(saves.sort((a, b) => (a.name > b.name) ? 1 : -1)) + setSaves(sorted.sort((a, b) => (a.name > b.name) ? 1 : -1)) } else if (sorting === 'created_at') { - setSaves(saves.sort((a, b) => (a.create_time > b.create_time) ? 1 : -1)) + setSaves(sorted.sort((a, b) => (a.create_time > b.create_time) ? 1 : -1)) } else if (sorting === 'updated_at') { - setSaves(saves.sort((a, b) => (a.save_time < b.save_time) ? 1 : -1)) + setSaves(sorted.sort((a, b) => (a.save_time < b.save_time) ? 1 : -1)) } } else { if (sorting === 'name') { - setSaves(saves.sort((a, b) => (a.name < b.name) ? 1 : -1)) + setSaves(sorted.sort((a, b) => (a.name < b.name) ? 1 : -1)) } else if (sorting === 'created_at') { - setSaves(saves.sort((a, b) => (a.create_time < b.create_time) ? 1 : -1)) + setSaves(sorted.sort((a, b) => (a.create_time < b.create_time) ? 1 : -1)) } else if (sorting === 'updated_at') { - setSaves(saves.sort((a, b) => (a.save_time > b.save_time) ? 1 : -1)) + setSaves(sorted.sort((a, b) => (a.save_time > b.save_time) ? 1 : -1)) } } } @@ -273,9 +425,68 @@ export default function SchematicsList ({ ltiDetails = null }) { const handleChange = (event, newValue) => { setValue(newValue) + setActiveProjectId(null) + } + + const handleOpenProjectDialog = () => { + setProjectTitle('') + setProjectDescription('') + setSelectedCircuitIds([]) + setProjectError('') + setProjectDialogOpen(true) + } + + const handleCloseProjectDialog = () => { + setProjectDialogOpen(false) + } + + const handleToggleCircuitSelection = (saveId) => { + if (selectedCircuitIds.includes(saveId)) { + setSelectedCircuitIds(selectedCircuitIds.filter(id => id !== saveId)) + } else { + setSelectedCircuitIds([...selectedCircuitIds, saveId]) + } + } + + const handleCreateProject = () => { + if (!projectTitle.trim()) { + setProjectError('Project title is required') + return + } + if (selectedCircuitIds.length === 0) { + setProjectError('Please select at least one circuit to bundle') + return + } + + setIsSubmittingProject(true) + setProjectError('') + + const token = auth.token || localStorage.getItem('esim_token') + const config = { + headers: { + 'Content-Type': 'application/json' + } + } + if (token) { + config.headers.Authorization = `Token ${token}` + } + + api.post('/publish/create_project/', { + title: projectTitle, + description: projectDescription, + save_ids: selectedCircuitIds + }, config) + .then(() => { + setIsSubmittingProject(false) + setProjectDialogOpen(false) + doFetch() + }) + .catch((err) => { + setIsSubmittingProject(false) + setProjectError(err.response?.data?.error || 'Failed to create project') + }) } - // ── Render ───────────────────────────────────────────────────────────────── return ( <> - {/* ── Quick Actions bar ─────────────────────────────────────────────── */} + {/* Quick Actions Bar */} @@ -332,237 +543,413 @@ export default function SchematicsList ({ ltiDetails = null }) { ) : ( <> - {/* ── Error state ───────────────────────────────────────────────────── */} + {/* Error view */} {fetchError && ( {fetchError} )} - {/* ── Loading state ─────────────────────────────────────────────────── */} - {isLoading - ? ( - - - - - - ) - : ( - <> - {/* ── PINNED section ─────────────────────────────────────────────── */} - -
- - ★ Pinned - - - ({pinnedCircuits.length}) - -
+ {/* Filter and Tab Options */} + + + + Welcome, {auth.user ? auth.user.username : ''} + + + onSearch(e)} placeholder='Search circuits...' /> + {schematics && ( + + + + )} + + + + + + Select Sort + + + + Select Order + + + + + + + + + + + + + - {pinnedCircuits.length === 0 - ? ( - - No pinned circuits yet. Click Pin on any circuit card to pin it here. - + {/* Loading / Results display */} + {isLoading ? ( + + + + + + ) : ( + + {/* TabPanel 0: Schematics */} + + x.lti_id == null && x.is_submission == null)} + emptyMessage={`Hey ${auth.user.username}, you don't have any saved schematics...`} + onRefresh={doFetch} + classes={classes} + /> + + + {/* TabPanel 1: Projects */} + + {activeProjectId && myProjects.find(p => p.project_id === activeProjectId) ? ( + (() => { + const activeProject = myProjects.find(p => p.project_id === activeProjectId) + return ( +
+ + + + + {activeProject.title} + + + + + + + {activeProject.description || 'No description provided.'} + + + +
) - : ( - - {pinnedCircuits.map((sch) => ( - - - - ))} - - )} -
- - - - - - {/* ── RECENT section ─────────────────────────────────────────────── */} - -
- - 🕒 Recent - - - ({recentCircuits.length} total) - -
- - {recentCircuits.length === 0 - ? ( + })() + ) : ( +
+ + + + {myProjects.length === 0 ? ( - No saved circuits yet. Create your first circuit above! + Hey {auth.user.username}, you don't have any saved projects... - ) - : ( - - {recentCircuits.map((sch) => ( - - + ) : ( + + {myProjects.map((proj) => ( + + + setActiveProjectId(proj.project_id)} style={{ flexGrow: 1 }}> + + + +
+ + {proj.title} + + + {(proj.schematics || []).length} {(proj.schematics || []).length === 1 ? 'circuit' : 'circuits'} + +
+
+ + {proj.description || 'No description provided.'} + +
+
+ + + +
))}
)} -
- - )} +
+ )} + + + {/* TabPanel 2: LTI Apps */} + + x.lti_id != null)} + emptyMessage={`Hey ${auth.user.username}, you don't have any saved LTI apps...`} + onRefresh={doFetch} + classes={classes} + /> + + + {/* TabPanel 3: LTI Submissions */} + + x.is_submission != null)} + emptyMessage={`Hey ${auth.user.username}, you don't have any saved submissions...`} + onRefresh={doFetch} + classes={classes} + /> + +
+ )} )} +
- {/* ════════════════════════════════════════════════════════════════════ - Everything below this line is the ORIGINAL SchematicsList content, - preserved exactly as it was — no existing functionality removed. - ════════════════════════════════════════════════════════════════════ */} - - {/* User Dashboard My Schematic Header */} - - - - + {/* Create Project Bundle Dialog */} + + Create New Project Bundle + + {projectError && ( + + {projectError} + + )} + + setProjectTitle(e.target.value)} + style={{ marginBottom: '16px' }} + /> + + setProjectDescription(e.target.value)} + style={{ marginBottom: '20px' }} + /> + + + Select Saved Circuits to Bundle: + + + + {saves.filter(x => x.lti_id == null && x.is_submission == null).length === 0 ? ( + + No saved circuits available. + + ) : ( + + {saves.filter(x => x.lti_id == null && x.is_submission == null).map((sch) => ( + handleToggleCircuitSelection(sch.save_id)} + color="primary" + /> + } + label={sch.name || String(sch.save_id).slice(0, 8)} + /> + ))} + + )} - - - - {schematics && } - {schematics && onSearch(e)} placeholder='Search' />} - + + + + + + + {/* Edit Project Dialog */} + + Edit Project Details + + {editProjectError && ( + + {editProjectError} + + )} + + setEditProjectTitle(e.target.value)} + style={{ marginBottom: '16px' }} + /> + + setEditProjectDescription(e.target.value)} + style={{ marginBottom: '20px' }} + /> + + + + + + + + {/* Add Schematic Dialog */} + + Add Schematics to Project + + {addCircuitError && ( + + {addCircuitError} + + )} + + + Select Saved Circuits to Add: + + + + {saves.filter(x => x.lti_id == null && x.is_submission == null && x.project_id !== activeProjectId).length === 0 ? ( + + No additional saved circuits available. + + ) : ( + + {saves.filter(x => x.lti_id == null && x.is_submission == null && x.project_id !== activeProjectId).map((sch) => ( + handleToggleAddCircuitSelection(sch.save_id)} + color="primary" + /> + } + label={sch.name || String(sch.save_id).slice(0, 8)} + /> + ))} + + )} + + + + + + + ) } diff --git a/eda-frontend/src/components/Project/CreateProject.js b/eda-frontend/src/components/Project/CreateProject.js index f7b5f8e22..0c35a1448 100644 --- a/eda-frontend/src/components/Project/CreateProject.js +++ b/eda-frontend/src/components/Project/CreateProject.js @@ -35,6 +35,7 @@ import { changeStatus, createProject, deleteProject, getStatus } from '../../red import api from '../../utils/Api' import ProjectTimeline from './ProjectTimeline' import ProjectSimulationParameters from './ProjectSimulationParameters' +import queryString from 'query-string' const useStyles = makeStyles((theme) => ({ appBar: { @@ -122,6 +123,15 @@ function CreateProject () { inputVoltageSource: '' }) const [selectedSimulation, setSelectedSimulation] = useState('') + + const getQueryParams = () => { + const hashIndex = window.location.href.indexOf('#') + const searchString = hashIndex !== -1 + ? window.location.href.slice(hashIndex).split('?')[1] + : window.location.href.split('?')[1] + return searchString ? queryString.parse(searchString) : {} + } + useEffect(() => { if (open && project.details?.project_id) { dispatch(getStatus(project.details?.project_id)) @@ -166,11 +176,11 @@ function CreateProject () { if (token) { config.headers.Authorization = `Token ${token}` } - if (window.location.href.split('?id=')[1]) { + const query = getQueryParams() + if (query.id) { api .get( - 'save/versions/' + - window.location.href.split('?id=')[1].substring(0, 36), + 'save/versions/' + query.id, config ) .then((resp) => { @@ -338,7 +348,7 @@ function CreateProject () { } return (
- {(window.location.href.split('?id=')[1] && auth.user?.username === owner) && + {(getQueryParams().id && auth.user?.username === owner) && - - My Profile - - - My Schematics - { + const hashIndex = window.location.href.indexOf('#') + const searchString = hashIndex !== -1 + ? window.location.href.slice(hashIndex).split('?')[1] + : window.location.href.split('?')[1] + return searchString ? queryString.parse(searchString) : {} + } + React.useEffect(() => { const config = { headers: { @@ -153,11 +162,11 @@ export default function PropertiesSidebar ({ gridRef, outlineRef }) { if (token) { config.headers.Authorization = `Token ${token}` } - if (window.location.href.split('?id=')[1] && !window.location.href.split('?id=')[1].includes('gallery')) { + const query = getQueryParams() + if (query.id && !query.id.includes('gallery')) { api .get( - 'save/versions/' + - window.location.href.split('?id=')[1].substring(0, 36), + 'save/versions/' + query.id, config ) .then((resp) => { @@ -188,8 +197,9 @@ export default function PropertiesSidebar ({ gridRef, outlineRef }) { }) setVersions(versionsArray) const temp = [] + const currentBranch = query.branch ? decodeURI(query.branch) : 'master' for (let j = 0; j < versionsArray.length; j++) { - if (decodeURI(window.location.href.split('branch=')[1]) === versionsArray[j][0]) { temp.push(true) } else { temp.push(false) } + if (currentBranch === versionsArray[j][0]) { temp.push(true) } else { temp.push(false) } } const popoverTemp = new Array(versionsArray.length) popoverTemp.fill(false) @@ -301,7 +311,8 @@ export default function PropertiesSidebar ({ gridRef, outlineRef }) { if (token) { config.headers.Authorization = `Token ${token}` } - const saveId = window.location.href.split('id=')[1].substr(0, 36) + const query = getQueryParams() + const saveId = query.id || '' api.delete(`/save/versions/${saveId}/${branchName}`, config).then(resp => { const temp = versions.filter(version => version[0] !== branchName) const tempBranch = branchOpen @@ -336,7 +347,9 @@ export default function PropertiesSidebar ({ gridRef, outlineRef }) { } const checkActiveOrProject = (branch) => { - if (decodeURI(window.location.href.split('branch=')[1]) === branch) return false + const query = getQueryParams() + const currentBranch = query.branch ? decodeURI(query.branch) : 'master' + if (currentBranch === branch) return false if (branch === projectBranch) return false return true } diff --git a/eda-frontend/src/components/SchematicEditor/SchematicToolbar.js b/eda-frontend/src/components/SchematicEditor/SchematicToolbar.js index 90249b411..ba28d91ce 100644 --- a/eda-frontend/src/components/SchematicEditor/SchematicToolbar.js +++ b/eda-frontend/src/components/SchematicEditor/SchematicToolbar.js @@ -219,13 +219,14 @@ export default function SchematicToolbar ({ const handleSave = (version, newSave, save_id) => { if (!newSave) { - window.location = - '#/editor?id=' + - window.location.href.split('id=')[1].substr(0, 36) + - '&version=' + - version + - '&branch=' + - window.location.href.split('branch=')[1].substr(0) + const hashIndex = window.location.href.indexOf('#') + const searchString = hashIndex !== -1 + ? window.location.href.slice(hashIndex).split('?')[1] + : window.location.href.split('?')[1] + const query = searchString ? queryString.parse(searchString) : {} + const id = query.id || '' + const branch = query.branch ? decodeURI(query.branch) : 'master' + window.location = `#/editor?id=${id}&version=${version}&branch=${branch}` window.location.reload() } else { window.location = @@ -430,62 +431,89 @@ export default function SchematicToolbar ({ // Image Export of Schematic Diagram async function exportImage (type) { - const svg = document.querySelector('#divGrid > svg').cloneNode(true) - svg.removeAttribute('style') - svg.setAttribute('width', gridRef.current.scrollWidth) - svg.setAttribute('height', gridRef.current.scrollHeight) - const canvas = document.createElement('canvas') - canvas.width = gridRef.current.scrollWidth - canvas.height = gridRef.current.scrollHeight - canvas.style.width = canvas.width + 'px' - canvas.style.height = canvas.height + 'px' - const images = svg.getElementsByTagName('image') - for (const image of images) { - const data = await fetch(image.getAttribute('xlink:href')).then((v) => { - return v.text() - }) - image.removeAttribute('xlink:href') - image.setAttribute( - 'href', - 'data:image/svg+xml;base64,' + window.btoa(data) - ) - } - const ctx = canvas.getContext('2d') - ctx.mozImageSmoothingEnabled = true - ctx.webkitImageSmoothingEnabled = true - ctx.msImageSmoothingEnabled = true - ctx.imageSmoothingEnabled = true - const pixelRatio = window.devicePixelRatio || 1 - ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0) - return new Promise((resolve) => { - if (type === 'SVG') { - const svgdata = new XMLSerializer().serializeToString(svg) - resolve('' + svgdata) - return + try { + const svg = document.querySelector('#divGrid > svg').cloneNode(true) + svg.removeAttribute('style') + svg.setAttribute('width', gridRef.current.scrollWidth) + svg.setAttribute('height', gridRef.current.scrollHeight) + const canvas = document.createElement('canvas') + canvas.width = gridRef.current.scrollWidth + canvas.height = gridRef.current.scrollHeight + canvas.style.width = canvas.width + 'px' + canvas.style.height = canvas.height + 'px' + const images = svg.getElementsByTagName('image') + for (const image of images) { + const href = image.getAttribute('xlink:href') || image.getAttribute('href') + if (!href) continue + try { + const data = await fetch(href).then((v) => v.text()) + image.removeAttribute('xlink:href') + image.setAttribute( + 'href', + 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(data))) + ) + } catch (e) { + console.warn('Failed to fetch/encode image element:', href, e) + } } - const v = Canvg.fromString(ctx, svg.outerHTML) - v.render().then(() => { - let image = '' - if (type === 'JPG') { - const imgdata = ctx.getImageData(0, 0, canvas.width, canvas.height) - for (let i = 0; i < imgdata.data.length; i += 4) { - if (imgdata.data[i + 3] === 0) { - imgdata.data[i] = 255 - imgdata.data[i + 1] = 255 - imgdata.data[i + 2] = 255 - imgdata.data[i + 3] = 255 - } - } - ctx.putImageData(imgdata, 0, 0) - image = canvas.toDataURL('image/jpeg', 1.0) - } else { - if (type === 'PNG') { - image = canvas.toDataURL('image/png') + const ctx = canvas.getContext('2d') + ctx.mozImageSmoothingEnabled = true + ctx.webkitImageSmoothingEnabled = true + ctx.msImageSmoothingEnabled = true + ctx.imageSmoothingEnabled = true + const pixelRatio = window.devicePixelRatio || 1 + ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0) + + return new Promise((resolve) => { + if (type === 'SVG') { + try { + const svgdata = new XMLSerializer().serializeToString(svg) + resolve('' + svgdata) + } catch (err) { + resolve('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=') } + return + } + try { + const v = Canvg.fromString(ctx, svg.outerHTML) + v.render().then(() => { + try { + let image = '' + if (type === 'JPG') { + const imgdata = ctx.getImageData(0, 0, canvas.width, canvas.height) + for (let i = 0; i < imgdata.data.length; i += 4) { + if (imgdata.data[i + 3] === 0) { + imgdata.data[i] = 255 + imgdata.data[i + 1] = 255 + imgdata.data[i + 2] = 255 + imgdata.data[i + 3] = 255 + } + } + ctx.putImageData(imgdata, 0, 0) + image = canvas.toDataURL('image/jpeg', 1.0) + } else { + if (type === 'PNG') { + image = canvas.toDataURL('image/png') + } + } + resolve(image) + } catch (innerErr) { + console.error('Inner Canvg render error:', innerErr) + resolve('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=') + } + }).catch((renderErr) => { + console.error('Canvg render promise rejected:', renderErr) + resolve('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=') + }) + } catch (err) { + console.error('Canvg initialization error:', err) + resolve('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=') } - resolve(image) }) - }) + } catch (err) { + console.error('Global exportImage error:', err) + return Promise.resolve('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=') + } } // Download JPEG, PNG exported Image diff --git a/eda-frontend/src/components/SchematicEditor/VersionComponent.js b/eda-frontend/src/components/SchematicEditor/VersionComponent.js index 7aab0277d..e7ee843cc 100644 --- a/eda-frontend/src/components/SchematicEditor/VersionComponent.js +++ b/eda-frontend/src/components/SchematicEditor/VersionComponent.js @@ -7,6 +7,7 @@ import api from '../../utils/Api' import Popover from '@material-ui/core/Popover' import Typography from '@material-ui/core/Typography' import { makeStyles } from '@material-ui/core/styles' +import queryString from 'query-string' const useStyles = makeStyles((theme) => ({ typography: { @@ -33,6 +34,14 @@ export default function VersionComponent ({ const [popoverOpen, setPopoverOpen] = React.useState(false) + const getQueryParams = () => { + const hashIndex = window.location.href.indexOf('#') + const searchString = hashIndex !== -1 + ? window.location.href.slice(hashIndex).split('?')[1] + : window.location.href.split('?')[1] + return searchString ? queryString.parse(searchString) : {} + } + const handleClickPopover = (e) => { setAnchorEl(e.currentTarget) setPopoverOpen(true) @@ -54,7 +63,10 @@ export default function VersionComponent ({ } const checkActiveVersionOrProject = (version, branch) => { - if (version === window.location.href.split('version=')[1].substr(0, 20) && branch === decodeURI(window.location.href.split('branch=')[1])) return false + const query = getQueryParams() + const currentVersion = query.version || '' + const currentBranch = query.branch ? decodeURI(query.branch) : 'master' + if (version === currentVersion && branch === currentBranch) return false if (version === projectVersion && branch === projectBranch) return false return true } @@ -70,10 +82,11 @@ export default function VersionComponent ({ config.headers.Authorization = `Token ${token}` } api.delete(`/save/${save_id}/${version}/${branch}`, config).then(resp => { + const query = getQueryParams() + const saveId = query.id || '' api .get( - 'save/versions/' + - window.location.href.split('?id=')[1].substring(0, 36), + 'save/versions/' + saveId, config ) .then((resp) => { @@ -91,9 +104,10 @@ export default function VersionComponent ({ }) setVersions(Object.entries(versionsAccordingFreq).reverse()) const temp = [] + const currentBranch = query.branch ? decodeURI(query.branch) : 'master' for (let i = 0; i < Object.entries(versionsAccordingFreq).length; i++) { console.log(Object.entries(versionsAccordingFreq)[0]) - if (decodeURI(window.location.href.split('branch=')[1]) === Object.entries(versionsAccordingFreq)[i][0]) { temp.push(true) } else { temp.push(false) } + if (currentBranch === Object.entries(versionsAccordingFreq)[i][0]) { temp.push(true) } else { temp.push(false) } } setBranchOpen(temp.reverse()) }) @@ -111,7 +125,12 @@ export default function VersionComponent ({ style={{ overflowX: 'hidden', width: '77%' }} size="small" color="primary" - disabled={((version === window.location.href.split('version=')[1].substr(0, 20)) && (branch === decodeURI(window.location.href.split('branch=')[1])))} + disabled={(() => { + const query = getQueryParams() + const currentVersion = query.version || '' + const currentBranch = query.branch ? decodeURI(query.branch) : 'master' + return version === currentVersion && branch === currentBranch + })()} onClick={handleClick} >

diff --git a/eda-frontend/src/components/Shared/Layout.js b/eda-frontend/src/components/Shared/Layout.js index 41e8d15f7..83154befb 100644 --- a/eda-frontend/src/components/Shared/Layout.js +++ b/eda-frontend/src/components/Shared/Layout.js @@ -42,25 +42,29 @@ function Layout ({ header, resToolbar, sidebar }) { {header} - - - + {sidebar && ( + + + + )} {resToolbar} {/* Left Sidebar for Layout */} - - {sidebar} - + {sidebar && ( + + {sidebar} + + )} ) } diff --git a/eda-frontend/src/components/Shared/Navbar.js b/eda-frontend/src/components/Shared/Navbar.js index 51f6c5f15..682562e6a 100644 --- a/eda-frontend/src/components/Shared/Navbar.js +++ b/eda-frontend/src/components/Shared/Navbar.js @@ -267,20 +267,7 @@ export function Header () { > - - My Profile - - - My Schematics - + {/* Schematic editor header and left side pane */} - } sidebar={} /> + } />

{/* Subroutes under dashboard section */} {ltiDetails !== null && - } /> - - } - /> + } /> + - {/* Submission page header and left side pane */} - } sidebar={} /> + } />
- - - + + diff --git a/eda-frontend/src/pages/SubmissionPage.js b/eda-frontend/src/pages/SubmissionPage.js index 5eeb782de..194b5f1c9 100644 --- a/eda-frontend/src/pages/SubmissionPage.js +++ b/eda-frontend/src/pages/SubmissionPage.js @@ -1,14 +1,12 @@ // Main Layout for Submission Page import React, { useEffect } from 'react' -import { Switch, Route } from 'react-router-dom' +import { Switch, Route, Redirect } from 'react-router-dom' import { CssBaseline } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' import { Header } from '../components/Shared/Navbar' import Layout from '../components/Shared/Layout' import LayoutMain from '../components/Shared/LayoutMain' -import DashboardSidebar from '../components/Dashboard/DashboardSidebar' -import DashboardHome from '../components/Dashboard/DashboardHome' import SchematicsList from '../components/Dashboard/SchematicsList' import SubmissionTable from '../components/LTI/SubmissionTable' @@ -35,19 +33,13 @@ export default function Submissions () {
- {/* Submission page header and left side pane */} - } sidebar={} /> + } />
- - - + + diff --git a/eda-frontend/src/pages/signUp.js b/eda-frontend/src/pages/signUp.js index c5e6e219e..6300248f9 100644 --- a/eda-frontend/src/pages/signUp.js +++ b/eda-frontend/src/pages/signUp.js @@ -20,7 +20,7 @@ import Visibility from '@material-ui/icons/Visibility' import VisibilityOff from '@material-ui/icons/VisibilityOff' import { Link as RouterLink, useHistory } from 'react-router-dom' import { useSelector, useDispatch } from 'react-redux' -import { signUp, authDefault, googleLogin } from '../redux/actions/index' +import { signUp, authDefault, googleLogin, verifyOtp } from '../redux/actions/index' import google from '../static/google.png' const useStyles = makeStyles((theme) => ({ @@ -62,11 +62,19 @@ export default function SignUp () { const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [email, setEmail] = useState('') + const [otp, setOtp] = useState('') + const [showOtpForm, setShowOtpForm] = useState(false) const [accept, setAccept] = useState(true) const [showPassword, setShowPassword] = useState(false) const handleClickShowPassword = () => setShowPassword(!showPassword) const handleMouseDownPassword = () => setShowPassword(!showPassword) + useEffect(() => { + if (auth.isRegistered && auth.regErrors && !auth.regErrors.includes('verified') && !auth.regErrors.includes('Redirecting')) { + setShowOtpForm(true) + } + }, [auth.isRegistered, auth.regErrors]) + // Function call for google oAuth sign up. const handleGoogleSignup = () => { var host = window.location.protocol + '//' + window.location.host @@ -78,6 +86,11 @@ export default function SignUp () { dispatch(signUp(email, username, password, history)) } + const handleVerifyOtp = (event) => { + event.preventDefault() + dispatch(verifyOtp(email, otp, history)) + } + return ( @@ -86,13 +99,13 @@ export default function SignUp () { - Register | Sign Up + {showOtpForm ? 'Verify Email' : 'Register | Sign Up'} {/* Display's error messages while signing in */} - + {auth.regErrors} - { auth.isRegistered && + { (auth.isRegistered && !showOtpForm) && <>
@@ -102,88 +115,122 @@ export default function SignUp () { }
-
- setUsername(e.target.value)} - autoFocus - /> - setEmail(e.target.value)} - autoFocus - /> - - - {showPassword ? : } {/* handle password visibility */} - - - ) - }} - type={showPassword ? 'text' : 'password'} - id="password" - value={password} - onChange={e => setPassword(e.target.value)} - autoComplete="current-password" - /> - setAccept(e.target.checked)} color="primary" />} - label="I accept the Terms of Use & Privacy Policy" - /> - - Or - - {/* Google oAuth Sign Up option */} - - + {!showOtpForm ? ( +
+ setUsername(e.target.value)} + autoFocus + /> + setEmail(e.target.value)} + autoFocus + /> + + + {showPassword ? : } {/* handle password visibility */} + + + ) + }} + type={showPassword ? 'text' : 'password'} + id="password" + value={password} + onChange={e => setPassword(e.target.value)} + autoComplete="current-password" + /> + setAccept(e.target.checked)} color="primary" />} + label="I accept the Terms of Use & Privacy Policy" + /> + + Or + + {/* Google oAuth Sign Up option */} + + + ) : ( +
+ setOtp(e.target.value)} + autoFocus + /> + + + + )} diff --git a/eda-frontend/src/redux/actions/authActions.js b/eda-frontend/src/redux/actions/authActions.js index 88535fe1c..9cc981d37 100644 --- a/eda-frontend/src/redux/actions/authActions.js +++ b/eda-frontend/src/redux/actions/authActions.js @@ -210,7 +210,7 @@ export const signUp = (email, username, password, history) => (dispatch) => { dispatch({ type: actions.SIGNUP_SUCCESSFUL, payload: { - data: 'Successfully Signed Up! A verification link has been sent to your email account.' + data: 'Successfully Signed Up! A 6-digit OTP code has been sent to your email account.' } }) // history.push('/login') @@ -448,3 +448,40 @@ export const fetchRole = () => (dispatch, getState) => { }) }).catch(() => { console.log('Error') }) } + +export const verifyOtp = (email, otp, history) => (dispatch) => { + const body = { + token: otp, + email: email + } + const config = { + headers: { + 'Content-Type': 'application/json' + } + } + + api.post('auth/users/activation/', body, config) + .then((res) => { + if (res.status === 200 || res.status === 204) { + dispatch({ + type: actions.SIGNUP_SUCCESSFUL, + payload: { + data: 'Account successfully verified! Redirecting to login...' + } + }) + setTimeout(() => { + history.push('/login') + }, 1500) + } + }) + .catch((err) => { + var res = err.response + const errMsg = res && res.data && res.data.token ? res.data.token[0] : 'Invalid OTP code.' + dispatch({ + type: actions.SIGNUP_FAILED, + payload: { + data: errMsg + } + }) + }) +} diff --git a/eda-frontend/src/redux/actions/dashboardActions.js b/eda-frontend/src/redux/actions/dashboardActions.js index 811ea3b02..4d831c323 100644 --- a/eda-frontend/src/redux/actions/dashboardActions.js +++ b/eda-frontend/src/redux/actions/dashboardActions.js @@ -155,3 +155,26 @@ export const togglePinSave = (saveId, version, branch, pinned) => (dispatch, get ) .catch((err) => { console.error('[togglePinSave] error:', err); throw err }) } + +export const removeFromProject = (saveId) => (dispatch, getState) => { + const token = getState().authReducer.token + const config = { + headers: { + 'Content-Type': 'application/json' + } + } + + if (token) { + config.headers.Authorization = `Token ${token}` + } + + return api.post('publish/remove_from_project/', { save_id: saveId }, config) + .then(() => { + dispatch(fetchSchematics()) + dispatch(fetchMyProjects()) + }) + .catch((err) => { + console.error(err) + throw err + }) +} diff --git a/eda-frontend/src/redux/actions/saveSchematicActions.js b/eda-frontend/src/redux/actions/saveSchematicActions.js index 732c3e9dd..49b1806c8 100644 --- a/eda-frontend/src/redux/actions/saveSchematicActions.js +++ b/eda-frontend/src/redux/actions/saveSchematicActions.js @@ -72,7 +72,12 @@ export const saveSchematic = (title, description, xml, base64, newBranch = false if (schSave.isSaved && !ltiExists) { // Updating saved schemaic body.save_id = schSave.details.save_id - body.branch = decodeURI(window.location.href.split('branch=')[1]) + const hashIndex = window.location.href.indexOf('#') + const searchString = hashIndex !== -1 + ? window.location.href.slice(hashIndex).split('?')[1] + : window.location.href.split('?')[1] + const query = searchString ? queryString.parse(searchString) : {} + body.branch = query.branch ? decodeURI(query.branch) : 'master' api .post('save', queryString.stringify(body), config) .then((res) => { diff --git a/esim-cloud-backend/authAPI/models.py b/esim-cloud-backend/authAPI/models.py index 4549012ea..9bf721801 100644 --- a/esim-cloud-backend/authAPI/models.py +++ b/esim-cloud-backend/authAPI/models.py @@ -5,3 +5,12 @@ class User(AbstractUser): email = models.EmailField(unique=True) + + +class PendingUser(models.Model): + email = models.EmailField(unique=True) + username = models.CharField(max_length=150, unique=True) + password = models.CharField(max_length=128) + token = models.CharField(max_length=100, unique=True) + created_at = models.DateTimeField(auto_now_add=True) + diff --git a/esim-cloud-backend/authAPI/templates/account_dashboard.html b/esim-cloud-backend/authAPI/templates/account_dashboard.html new file mode 100644 index 000000000..aa126385f --- /dev/null +++ b/esim-cloud-backend/authAPI/templates/account_dashboard.html @@ -0,0 +1,409 @@ + + + + + + Account Management Dashboard + + + + +
+
+
+

Account Management

+

Locally hosted panel to manage users and verify registrations

+
+
+ +
+ +
+ +
+

+ + Registered Users +

+
+ {% if users %} + + + + + + + + + + + + {% for u in users %} + + + + + + + + {% endfor %} + +
UsernameEmailStatusJoinedActions
{{ u.username }}{{ u.email }} + {% if u.is_active %} + Active + {% else %} + Inactive + {% endif %} + {{ u.date_joined|date:"Y-m-d H:i" }} +
+ {% csrf_token %} + + + +
+
+ {% else %} +
No registered users found.
+ {% endif %} +
+
+ + +
+

+ + Pending Email OTP Registrations +

+
+ {% if pending_users %} + + + + + + + + + + + + {% for p in pending_users %} + + + + + + + + {% endfor %} + +
UsernameEmailOTP CodeCreated AtActions
{{ p.username }}{{ p.email }}{{ p.token }}{{ p.created_at|date:"Y-m-d H:i" }} +
+
+ {% csrf_token %} + + + +
+
+ {% csrf_token %} + + + +
+
+
+ {% else %} +
No pending registrations.
+ {% endif %} +
+
+
+ + +
+

+ + Create User Directly +

+
+ {% csrf_token %} + + +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+
+ + diff --git a/esim-cloud-backend/authAPI/urls.py b/esim-cloud-backend/authAPI/urls.py index 62a0b3865..5beba1a45 100644 --- a/esim-cloud-backend/authAPI/urls.py +++ b/esim-cloud-backend/authAPI/urls.py @@ -10,5 +10,6 @@ url(r'^google-callback', authAPI_views.GoogleOAuth2), url(r'^users/activate/(?P[\w-]+)/(?P[\w-]+)/$', authAPI_views.activate_user), - url(r'user/token/', authAPI_views.CustomTokenCreateView.as_view()) + url(r'user/token/', authAPI_views.CustomTokenCreateView.as_view()), + url(r'^dashboard/$', authAPI_views.account_dashboard) ] diff --git a/esim-cloud-backend/authAPI/views.py b/esim-cloud-backend/authAPI/views.py index 7468cb366..846e21324 100644 --- a/esim-cloud-backend/authAPI/views.py +++ b/esim-cloud-backend/authAPI/views.py @@ -95,3 +95,172 @@ def _action(self, serializer): return Response( data=data, status=status.HTTP_200_OK ) + + +class CustomUserCreateView(generics.GenericAPIView): + permission_classes = [permissions.AllowAny] + + def post(self, request, *args, **kwargs): + username = request.data.get("username") + email = request.data.get("email") + password = request.data.get("password") + + # Basic validation + errors = {} + if not username: + errors["username"] = ["This field is required."] + if not email: + errors["email"] = ["This field is required."] + if not password: + errors["password"] = ["This field is required."] + + if errors: + return Response(errors, status=status.HTTP_400_BAD_REQUEST) + + # Check if username or email already exists in User + User = get_user_model() + if User.objects.filter(username=username).exists(): + errors["username"] = ["A user with that username already exists."] + if User.objects.filter(email=email).exists(): + errors["email"] = ["A user with that email already exists."] + + if errors: + return Response(errors, status=status.HTTP_400_BAD_REQUEST) + + # Encrypt password, generate OTP code, save PendingUser + from django.contrib.auth.hashers import make_password + import random + from django.core.mail import send_mail + from authAPI.models import PendingUser + + hashed_password = make_password(password) + token = f"{random.randint(100000, 999999)}" + + # Clear existing pending entries for this email or username + PendingUser.objects.filter(email=email).delete() + PendingUser.objects.filter(username=username).delete() + + pending_user = PendingUser.objects.create( + username=username, + email=email, + password=hashed_password, + token=token + ) + + # Send activation email + subject = "eSim Cloud Email Verification OTP" + message = ( + f"Hello {username},\n\n" + f"Thank you for registering at eSim Cloud.\n" + f"Your verification OTP code is: {token}\n\n" + f"Please enter this code on the registration page to verify your email and activate your account.\n\n" + f"Best regards,\neSim Cloud Team" + ) + from_email = settings.DEFAULT_FROM_EMAIL + try: + send_mail(subject, message, from_email, [email], fail_silently=False) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.warning(f"SMTP email sending failed: {str(e)}.") + + if settings.DEBUG: + print("\n" + "="*80) + print("EMAIL SENDING FAILED. FALLING BACK TO CONSOLE PRINT:") + print(f"To: {email}") + print(f"Subject: {subject}") + print(message) + print("="*80 + "\n") + + return Response({ + "username": username, + "email": email, + "id": pending_user.id + }, status=status.HTTP_201_CREATED) + + +class CustomUserActivationView(generics.GenericAPIView): + permission_classes = [permissions.AllowAny] + + def post(self, request, *args, **kwargs): + token = request.data.get("token") + email = request.data.get("email") + + if not token: + return Response({"token": ["This field is required."]}, status=status.HTTP_400_BAD_REQUEST) + if not email: + return Response({"email": ["This field is required."]}, status=status.HTTP_400_BAD_REQUEST) + + from authAPI.models import PendingUser + try: + pending_user = PendingUser.objects.get(token=token, email=email) + except PendingUser.DoesNotExist: + return Response({"token": ["Invalid OTP code."]}, status=status.HTTP_400_BAD_REQUEST) + + User = get_user_model() + if User.objects.filter(username=pending_user.username).exists() or User.objects.filter(email=pending_user.email).exists(): + pending_user.delete() + return Response({"detail": "User already registered."}, status=status.HTTP_400_BAD_REQUEST) + + User.objects.create( + username=pending_user.username, + email=pending_user.email, + password=pending_user.password, + is_active=True + ) + + pending_user.delete() + + return Response(status=status.HTTP_204_NO_CONTENT) + + +def account_dashboard(request): + from authAPI.models import PendingUser + from django.contrib.auth import get_user_model + User = get_user_model() + + if request.method == "POST": + action = request.POST.get("action") + user_id = request.POST.get("user_id") + pending_id = request.POST.get("pending_id") + + if action == "delete_user" and user_id: + User.objects.filter(id=user_id).delete() + elif action == "delete_pending" and pending_id: + PendingUser.objects.filter(id=pending_id).delete() + elif action == "activate_pending" and pending_id: + try: + pending = PendingUser.objects.get(id=pending_id) + # Check if username or email already exists in User to avoid IntegrityError + if not User.objects.filter(username=pending.username).exists() and not User.objects.filter(email=pending.email).exists(): + User.objects.create( + username=pending.username, + email=pending.email, + password=pending.password, + is_active=True + ) + pending.delete() + except PendingUser.DoesNotExist: + pass + elif action == "create_user": + username = request.POST.get("username") + email = request.POST.get("email") + password = request.POST.get("password") + if username and email and password: + from django.contrib.auth.hashers import make_password + if not User.objects.filter(username=username).exists() and not User.objects.filter(email=email).exists(): + User.objects.create( + username=username, + email=email, + password=make_password(password), + is_active=True + ) + + # Fetch data + users = User.objects.all().order_by("-date_joined") + pending_users = PendingUser.objects.all().order_by("-created_at") + + return render(request, "account_dashboard.html", { + "users": users, + "pending_users": pending_users + }) diff --git a/esim-cloud-backend/esimCloud/settings.py b/esim-cloud-backend/esimCloud/settings.py index aa0063aa3..17bf26e5c 100644 --- a/esim-cloud-backend/esimCloud/settings.py +++ b/esim-cloud-backend/esimCloud/settings.py @@ -125,17 +125,17 @@ ] # Mail server config - -# use this for console emails -EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' - -# Note SMTP is slow -# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -# EMAIL_HOST = os.environ.get("EMAIL_HOST", "smtp.gmail.com") -# EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "email@gmail.com") -# EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "gmailpassword") -# EMAIL_PORT = os.environ.get("EMAIL_PORT", 587) -# EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", True) +EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "") +if EMAIL_HOST_USER: + EMAIL_BACKEND = os.environ.get("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend") +else: + EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' + +EMAIL_HOST = os.environ.get("EMAIL_HOST", "smtp.gmail.com") +EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "") +EMAIL_PORT = int(os.environ.get("EMAIL_PORT", 587)) +EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", "True") == "True" +DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", EMAIL_HOST_USER) SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = os.environ.get( "SOCIAL_AUTH_GOOGLE_OAUTH2_KEY", "") diff --git a/esim-cloud-backend/esimCloud/urls.py b/esim-cloud-backend/esimCloud/urls.py index 73d3b66c4..5d12a769b 100644 --- a/esim-cloud-backend/esimCloud/urls.py +++ b/esim-cloud-backend/esimCloud/urls.py @@ -11,6 +11,7 @@ from saveAPI import urls as saveURLs from workflowAPI import urls as workURLs from publishAPI import urls as publishURLs +from authAPI import views as authAPI_views from authAPI import urls as authURLs from rest_framework import permissions from drf_yasg.views import get_schema_view @@ -62,6 +63,8 @@ path('api/chat/', include(chatbotURLs)), # Auth API Routes + path('api/auth/users/', authAPI_views.CustomUserCreateView.as_view()), + path('api/auth/users/activation/', authAPI_views.CustomUserActivationView.as_view()), url(r'^api/auth/', include('djoser.urls')), url(r'^api/auth/', include('djoser.urls.authtoken')), url(r'^api/auth/', include("djoser.social.urls")), diff --git a/esim-cloud-backend/publishAPI/serializers.py b/esim-cloud-backend/publishAPI/serializers.py index 9690d00a9..3636f9dfe 100644 --- a/esim-cloud-backend/publishAPI/serializers.py +++ b/esim-cloud-backend/publishAPI/serializers.py @@ -7,7 +7,7 @@ import six import uuid import imghdr -from saveAPI.serializers import StateSaveSerializer +from saveAPI.serializers import StateSaveSerializer, SaveListSerializer from workflowAPI.models import Transition @@ -100,6 +100,7 @@ class ProjectSerializer(serializers.ModelSerializer): status_name = serializers.CharField(read_only=True, source='state.name') author_name = serializers.CharField( read_only=True, source='author.username') + description = serializers.CharField(required=False, allow_blank=True, allow_null=True) fields = FieldSerializer(many=True) save_id = serializers.SerializerMethodField() active_save = serializers.SerializerMethodField() @@ -107,6 +108,7 @@ class ProjectSerializer(serializers.ModelSerializer): transient_analysis = TransientAnalysisSerializer() tf_analysis = TFAnalysisSerializer() ac_analysis = ACAnalysisSerializer() + schematics = serializers.SerializerMethodField() class Meta: model = Project @@ -125,16 +127,28 @@ class Meta: 'transient_analysis', 'tf_analysis', 'ac_analysis', + 'schematics', ) def get_save_id(self, obj): - return obj.statesave_set.first().save_id + first_save = obj.statesave_set.first() + return first_save.save_id if first_save else None def get_active_save(self, obj): - return StateSaveSerializer( - obj.statesave_set.get(save_id=obj.statesave_set.first().save_id, - branch=obj.active_branch, - version=obj.active_version)).data + first_save = obj.statesave_set.first() + if not first_save: + return None + try: + return StateSaveSerializer( + obj.statesave_set.get(save_id=first_save.save_id, + branch=obj.active_branch, + version=obj.active_version)).data + except Exception: + return StateSaveSerializer(first_save).data + + def get_schematics(self, obj): + saves = obj.statesave_set.order_by('save_id', '-save_time').distinct('save_id') + return SaveListSerializer(saves, many=True).data class ReportSerializer(serializers.ModelSerializer): diff --git a/esim-cloud-backend/publishAPI/urls.py b/esim-cloud-backend/publishAPI/urls.py index d3d645917..26044a5b6 100644 --- a/esim-cloud-backend/publishAPI/urls.py +++ b/esim-cloud-backend/publishAPI/urls.py @@ -22,4 +22,10 @@ url(r'^', include(router.urls)), path('publish/project/', publishAPI_views.ProjectViewSet.as_view(), name='create'), + path('publish/create_project/', + publishAPI_views.CreateBundleProjectView.as_view(), name='create_bundle'), + path('publish/remove_from_project/', + publishAPI_views.RemoveFromProjectView.as_view(), name='remove_from_project'), + path('publish/add_schematic/', + publishAPI_views.AddSchematicToProjectView.as_view(), name='add_schematic'), ] diff --git a/esim-cloud-backend/publishAPI/views.py b/esim-cloud-backend/publishAPI/views.py index d9486212f..65833b00b 100644 --- a/esim-cloud-backend/publishAPI/views.py +++ b/esim-cloud-backend/publishAPI/views.py @@ -273,13 +273,15 @@ class MyProjectViewSet(viewsets.ModelViewSet): parser_classes = (FormParser, JSONParser) permission_classes = (IsAuthenticated,) serializer_class = ProjectSerializer - queryset = Project.objects.none() + lookup_field = 'project_id' + + def get_queryset(self): + return Project.objects.filter(author=self.request.user) @swagger_auto_schema(response={200: ProjectSerializer}) def list(self, request): try: - queryset = Project.objects.filter( - author=self.request.user, is_arduino=False) + queryset = self.get_queryset().filter(is_arduino=False) except Project.DoesNotExist: return Response({'error': 'No circuit there'}, status=status.HTTP_404_NOT_FOUND) @@ -304,3 +306,91 @@ def list(self, request): return Response(status=status.HTTP_404_NOT_FOUND) serialized = ProjectSerializer(queryset, many=True) return Response(serialized.data) + + +class CreateBundleProjectView(APIView): + permission_classes = (permissions.IsAuthenticated,) + + def post(self, request): + title = request.data.get('title') + description = request.data.get('description', '') + save_ids = request.data.get('save_ids', []) + + if not title: + return Response({'error': 'Title is required'}, status=status.HTTP_400_BAD_REQUEST) + if not save_ids: + return Response({'error': 'Please select at least one circuit to bundle'}, status=status.HTTP_400_BAD_REQUEST) + + # Retrieve the state save objects + save_states = StateSave.objects.filter(save_id__in=save_ids, owner=request.user) + if not save_states.exists(): + return Response({'error': 'No matching circuits found'}, status=status.HTTP_404_NOT_FOUND) + + first_save = save_states.first() + project = Project( + title=title, + description=description, + author=request.user, + is_arduino=first_save.is_arduino, + active_branch=first_save.branch, + active_version=first_save.version + ) + project.save() + + # Update all selected saves to point to the new project + for save_state in save_states: + save_state.project = project + save_state.save(update_fields=['project']) + + return Response({ + 'project_id': str(project.project_id), + 'title': project.title, + 'description': project.description + }, status=status.HTTP_201_CREATED) + + +class RemoveFromProjectView(APIView): + permission_classes = (permissions.IsAuthenticated,) + + def post(self, request): + save_id = request.data.get('save_id') + if not save_id: + return Response({'error': 'save_id is required'}, status=status.HTTP_400_BAD_REQUEST) + + save_states = StateSave.objects.filter(save_id=save_id, owner=request.user) + if not save_states.exists(): + return Response({'error': 'No matching circuits found'}, status=status.HTTP_404_NOT_FOUND) + + for save_state in save_states: + save_state.project = None + save_state.save(update_fields=['project']) + + return Response({'success': True}, status=status.HTTP_200_OK) + + +class AddSchematicToProjectView(APIView): + permission_classes = (permissions.IsAuthenticated,) + + def post(self, request): + project_id = request.data.get('project_id') + save_ids = request.data.get('save_ids', []) + + if not project_id: + return Response({'error': 'project_id is required'}, status=status.HTTP_400_BAD_REQUEST) + if not save_ids: + return Response({'error': 'save_ids is required'}, status=status.HTTP_400_BAD_REQUEST) + + try: + project = Project.objects.get(project_id=project_id, author=request.user) + except Project.DoesNotExist: + return Response({'error': 'Project not found'}, status=status.HTTP_404_NOT_FOUND) + + save_states = StateSave.objects.filter(save_id__in=save_ids, owner=request.user) + for save_state in save_states: + save_state.project = project + save_state.save(update_fields=['project']) + + return Response({'success': True}, status=status.HTTP_200_OK) + + + diff --git a/esim-cloud-backend/saveAPI/views.py b/esim-cloud-backend/saveAPI/views.py index d86f74770..1301aa8ee 100644 --- a/esim-cloud-backend/saveAPI/views.py +++ b/esim-cloud-backend/saveAPI/views.py @@ -485,14 +485,14 @@ def delete(self, request, save_id): save_id=save_id, owner=self.request.user ) - if queryset[0].project is None: - queryset.delete() - return Response(data=None, status=status.HTTP_204_NO_CONTENT) - else: - return Response(data=None, status=status.HTTP_400_BAD_REQUEST) - except StateSave.DoesNotExist: - return Response({"error": "circuit not found"}, - status=status.HTTP_404_NOT_FOUND) + if not queryset.exists(): + return Response({"error": "circuit not found"}, + status=status.HTTP_404_NOT_FOUND) + queryset.delete() + return Response(data=None, status=status.HTTP_204_NO_CONTENT) + except Exception as e: + return Response({"error": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) class GalleryView(APIView): From 1c3773b429f45f8dfedb6dbcb31344461cbb91e3 Mon Sep 17 00:00:00 2001 From: Shreyash Neeraj Date: Tue, 16 Jun 2026 11:34:39 +0530 Subject: [PATCH 2/5] hotfix for .env --- .env | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ .gitignore | 2 +- 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 000000000..dce38feca --- /dev/null +++ b/.env @@ -0,0 +1,58 @@ +#Django Config Variables +PYTHONUNBUFFERED=True +DJANGO_DEBUG=True + +# Django DB_Engine Config +# Make sure settings correspond MYSQL or POSTGRES Config +SQL_ENGINE=django.db.backends.postgresql +SQL_PORT=5432 +SQL_HOST=db + +SQL_DATABASE=esimcloud_db +SQL_USER=user +SQL_PASSWORD=password + + +#POSTGRES Config ( Only if Postgres is being used as a backend ) +# Make sure SQL_ENGINE is set to django.db.backends.postgresql +# SQL_PORT is set to 5432 + +POSTGRES_DB=esimcloud_db +POSTGRES_USER=user +POSTGRES_PASSWORD=password + + +# MYSQL Config +# Make sure SQL_ENGINE is set to django.db.backends.mysql +# SQL_PORT is set to 3306 + +MYSQL_DATABASE=esimcloud_db +MYSQL_USER=user +MYSQL_PASSWORD=password +MYSQL_ROOT_PASSWORD=password + +#Docker Image Version Tags +TAG_MYSQL=8.0 +TAG_REDIS=alpine3.11 + +#Production Config +GUNICORN_WORKERS=5 +CELERY_WORKERS=5 +EDA_PUBLIC_URL=http://localhost/eda +ARDUINO_BASE_HREF=/arduino/ + +# Authentication +POST_ACTIVATE_REDIRECT_URL=http://localhost/ +SOCIAL_AUTH_GOOGLE_OAUTH2_KEY=yourkeyhere +SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET=yoursecrethere +GOOGLE_OAUTH_REDIRECT_URI=http://localhost/api/auth/google-callback + +# Email Service +EMAIL_HOST=smtp.gmail.com +EMAIL_HOST_USER=youremail@gmail.com +EMAIL_HOST_PASSWORD=yourpassword +EMAIL_PORT=587 +EMAIL_USE_TLS=True + +# AI Chatbot Configuration +GEMINI_API_KEY= diff --git a/.gitignore b/.gitignore index 3766b7c37..5a5bb52b0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ redis_data/ mysql_data/ .vscode venv -env + *.sqlite3 .env.prod mongo_data/ From 07eb50ae2ee1097b9c700a95ec965336373db06c Mon Sep 17 00:00:00 2001 From: Shreyash Neeraj Date: Tue, 16 Jun 2026 20:34:11 +0530 Subject: [PATCH 3/5] Add open Admin Dashboard button in user dropdown menu --- eda-frontend/src/components/SchematicEditor/Header.js | 8 ++++++++ eda-frontend/src/components/Shared/Navbar.js | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/eda-frontend/src/components/SchematicEditor/Header.js b/eda-frontend/src/components/SchematicEditor/Header.js index fbd6b82d5..aa9bb843a 100644 --- a/eda-frontend/src/components/SchematicEditor/Header.js +++ b/eda-frontend/src/components/SchematicEditor/Header.js @@ -607,6 +607,14 @@ function Header ({ gridRef }) { > Change password + + Admin Dashboard + { setLogoutConfirm(true) }}> diff --git a/eda-frontend/src/components/Shared/Navbar.js b/eda-frontend/src/components/Shared/Navbar.js index a9d6b06eb..008ecec37 100644 --- a/eda-frontend/src/components/Shared/Navbar.js +++ b/eda-frontend/src/components/Shared/Navbar.js @@ -275,6 +275,14 @@ export function Header () { > Change password + + Admin Dashboard + { store.dispatch(logout(history)) }}> From d5b2291d6c80a7c68631057034496e94055ea401 Mon Sep 17 00:00:00 2001 From: Shreyash Neeraj Date: Thu, 18 Jun 2026 19:02:00 +0530 Subject: [PATCH 4/5] auth + projects + dashboard --- ArduinoFrontend/package-lock.json | 6 +- docker-compose.dev.yml | 59 ++++++++++--------- .../Dashboard/DashboardOtherProjects.js | 4 +- .../components/Dashboard/SchematicsList.js | 8 +-- .../SchematicEditor/ToolbarExtension.js | 2 +- eda-frontend/src/redux/actions/authActions.js | 3 + esim-cloud-backend/authAPI/views.py | 9 ++- 7 files changed, 50 insertions(+), 41 deletions(-) diff --git a/ArduinoFrontend/package-lock.json b/ArduinoFrontend/package-lock.json index e293edf35..ffa3c20cb 100644 --- a/ArduinoFrontend/package-lock.json +++ b/ArduinoFrontend/package-lock.json @@ -9594,9 +9594,9 @@ "dev": true }, "ws": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", - "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.4.tgz", + "integrity": "sha512-PNIUUyLI5YpkJZj60YBzX1o0ByQ4ovvfmq9N/Kig/PAYbVlGyz4R6G0SEWrD0O9acc0sT2+IdMBVLFv8FSi0Nw==", "dev": true, "requires": { "async-limiter": "~1.0.0" diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 4e473f5cd..53aa125a0 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,4 +1,5 @@ version: '3' + services: nginx: @@ -6,12 +7,12 @@ services: ports: - 80:80 volumes: - - tmp_vol:/tmp - - ./Nginx/dev.conf.d:/etc/nginx/conf.d - - ./Nginx/static_pages:/usr/share/nginx/static_pages - - ./esim-cloud-backend/static:/usr/share/nginx/django_static - - ./esim-cloud-backend/file_storage:/usr/share/nginx/django_file_storage - - ./esim-cloud-backend/kicad-symbols/:/usr/share/nginx/kicad-libs + - tmp_vol:/tmp:z + - ./Nginx/dev.conf.d:/etc/nginx/conf.d:z + - ./Nginx/static_pages:/usr/share/nginx/static_pages:z + - ./esim-cloud-backend/static:/usr/share/nginx/django_static:z + - ./esim-cloud-backend/file_storage:/usr/share/nginx/django_file_storage:z + - ./esim-cloud-backend/kicad-symbols/:/usr/share/nginx/kicad-libs:z depends_on: - django - eda-frontend @@ -26,7 +27,7 @@ services: ports: - "3000:3000" volumes: - - ./eda-frontend:/code + - ./eda-frontend:/code:z environment: - NODE_ENV=development - "PUBLIC_URL=${EDA_PUBLIC_URL}" @@ -48,7 +49,7 @@ services: - "4200:4200" volumes: - - ./ArduinoFrontend:/code + - ./ArduinoFrontend:/code:z environment: - NODE_ENV=development depends_on: @@ -64,48 +65,48 @@ services: ports: - "8000:8000" volumes: - - ./esim-cloud-backend:/code + - ./esim-cloud-backend:/code:z - run_vol:/var/run - cache_vol:/var/cache - tmp_vol:/tmp depends_on: - - redis - - db - - celery + - redis + - db + - celery env_file: - .env links: - - "redis:redis_cache" - - "db:mysql" + - "redis:redis_cache" + - "db:mysql" celery: image: "docker.pkg.github.com/frg-fossee/esim-cloud/celery:dev" build: ./esim-cloud-backend/ command: celery -A esimCloud.celery worker -l info --concurrency=1 links: - - "redis:redis_cache" - - "db:postgres" + - "redis:redis_cache" + - "db:postgres" env_file: - - .env + - .env volumes: - - ./esim-cloud-backend:/code + - ./esim-cloud-backend:/code:z - run_vol:/var/run - cache_vol:/var/cache - tmp_vol:/tmp depends_on: - - redis - - db + - redis + - db redis: - image: "redis:${TAG_REDIS}" - environment: - - ALLOW_EMPTY_PASSWORD=yes - - REDIS_DISABLE_COMMANDS=FLUSHDB,FLUSHALL - volumes: - - ./redis_data:/data + image: "redis:${TAG_REDIS}" + environment: + - ALLOW_EMPTY_PASSWORD=yes + - REDIS_DISABLE_COMMANDS=FLUSHDB,FLUSHALL + volumes: + - ./redis_data:/data:z # Uncomment this and Change appropriate env variables to switch to mysql # db: @@ -117,9 +118,9 @@ services: # - ./mysql_data:/var/lib/mysql db: - image: postgres:15 + image: postgres:13 volumes: - - ./postgres_data:/var/lib/postgresql/data/ + - ./postgres_data:/var/lib/postgresql/data/:z env_file: - .env @@ -135,4 +136,4 @@ volumes: tmp_vol: driver_opts: type: tmpfs - device: tmpfs + device: tmpfs \ No newline at end of file diff --git a/eda-frontend/src/components/Dashboard/DashboardOtherProjects.js b/eda-frontend/src/components/Dashboard/DashboardOtherProjects.js index f173bcc44..8c3034669 100644 --- a/eda-frontend/src/components/Dashboard/DashboardOtherProjects.js +++ b/eda-frontend/src/components/Dashboard/DashboardOtherProjects.js @@ -194,7 +194,7 @@ function DashboardOtherProjects () { : - Hey {auth.user.username} , You don't have any projects to review... + Hey {auth.user?.username || ''} , You don't have any projects to review... @@ -216,7 +216,7 @@ function DashboardOtherProjects () { : - Hey {auth.user.username} , You dont have any reported projects to review... + Hey {auth.user?.username || ''} , You dont have any reported projects to review... diff --git a/eda-frontend/src/components/Dashboard/SchematicsList.js b/eda-frontend/src/components/Dashboard/SchematicsList.js index aeb3dba9e..c25b1af47 100644 --- a/eda-frontend/src/components/Dashboard/SchematicsList.js +++ b/eda-frontend/src/components/Dashboard/SchematicsList.js @@ -619,7 +619,7 @@ export default function SchematicsList ({ ltiDetails = null }) { x.lti_id == null && x.is_submission == null)} - emptyMessage={`Hey ${auth.user.username}, you don't have any saved schematics...`} + emptyMessage={`Hey ${auth.user?.username || ''}, you don't have any saved schematics...`} onRefresh={doFetch} classes={classes} /> @@ -691,7 +691,7 @@ export default function SchematicsList ({ ltiDetails = null }) { {myProjects.length === 0 ? ( - Hey {auth.user.username}, you don't have any saved projects... + Hey {auth.user?.username || ''}, you don't have any saved projects... ) : ( @@ -734,7 +734,7 @@ export default function SchematicsList ({ ltiDetails = null }) { x.lti_id != null)} - emptyMessage={`Hey ${auth.user.username}, you don't have any saved LTI apps...`} + emptyMessage={`Hey ${auth.user?.username || ''}, you don't have any saved LTI apps...`} onRefresh={doFetch} classes={classes} /> @@ -744,7 +744,7 @@ export default function SchematicsList ({ ltiDetails = null }) { x.is_submission != null)} - emptyMessage={`Hey ${auth.user.username}, you don't have any saved submissions...`} + emptyMessage={`Hey ${auth.user?.username || ''}, you don't have any saved submissions...`} onRefresh={doFetch} classes={classes} /> diff --git a/eda-frontend/src/components/SchematicEditor/ToolbarExtension.js b/eda-frontend/src/components/SchematicEditor/ToolbarExtension.js index 7a8e5d1fa..3fabadaa1 100644 --- a/eda-frontend/src/components/SchematicEditor/ToolbarExtension.js +++ b/eda-frontend/src/components/SchematicEditor/ToolbarExtension.js @@ -829,7 +829,7 @@ export function OpenSchDialog (props) { {/* Listing Saved Schematics */} {schematics.length === 0 ? - Hey {auth.user.username} , You dont have any saved schematics... + Hey {auth.user?.username || ''} , You dont have any saved schematics... : diff --git a/eda-frontend/src/redux/actions/authActions.js b/eda-frontend/src/redux/actions/authActions.js index b0a2b5f7e..7e5ec8164 100644 --- a/eda-frontend/src/redux/actions/authActions.js +++ b/eda-frontend/src/redux/actions/authActions.js @@ -208,6 +208,9 @@ export const signUp = (email, username, password, history) => (dispatch) => { api.post('auth/users/', body, config) .then((res) => { if (res.status === 200 || res.status === 201) { + if (res.data && res.data.otp) { + alert('OTP code is: ' + res.data.otp) + } dispatch({ type: actions.SIGNUP_SUCCESSFUL, payload: { diff --git a/esim-cloud-backend/authAPI/views.py b/esim-cloud-backend/authAPI/views.py index 9b48bd798..ed1a7ddf6 100644 --- a/esim-cloud-backend/authAPI/views.py +++ b/esim-cloud-backend/authAPI/views.py @@ -172,11 +172,16 @@ def post(self, request, *args, **kwargs): print(message) print("="*80 + "\n") - return Response({ + response_data = { "username": username, "email": email, "id": pending_user.id - }, status=status.HTTP_201_CREATED) + } + if (getattr(settings, 'EMAIL_HOST_USER', '') == 'youremail@gmail.com' and + getattr(settings, 'EMAIL_HOST_PASSWORD', '') == 'yourpassword'): + response_data["otp"] = token + + return Response(response_data, status=status.HTTP_201_CREATED) class CustomUserActivationView(generics.GenericAPIView): From 7db1b44407451098a5689e0880c2c2391781eb87 Mon Sep 17 00:00:00 2001 From: Shreyash Neeraj Date: Tue, 23 Jun 2026 16:42:43 +0530 Subject: [PATCH 5/5] optimised magnet snap from ON2 to O1 --- docker-compose.dev.yml | 2 +- .../SchematicEditor/Helper/ComponentDrag.js | 166 ++++++++++++++---- 2 files changed, 137 insertions(+), 31 deletions(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 53aa125a0..13a8ae29c 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -136,4 +136,4 @@ volumes: tmp_vol: driver_opts: type: tmpfs - device: tmpfs \ No newline at end of file + device: tmpfs diff --git a/eda-frontend/src/components/SchematicEditor/Helper/ComponentDrag.js b/eda-frontend/src/components/SchematicEditor/Helper/ComponentDrag.js index 6e1fa4c58..2701c51fd 100644 --- a/eda-frontend/src/components/SchematicEditor/Helper/ComponentDrag.js +++ b/eda-frontend/src/components/SchematicEditor/Helper/ComponentDrag.js @@ -14,6 +14,33 @@ import KeyboardShorcuts from './KeyboardShorcuts.js' import { SideBar, magneticSnap } from './SideBar.js' import KiCadFileUtils from './KiCadFileUtils' +const HEX_SIZE = 150; + +function getHexCoords(x, y) { + var q = (Math.sqrt(3) / 3 * x - 1 / 3 * y) / HEX_SIZE; + var r = (2 / 3 * y) / HEX_SIZE; + + var frac_q = q; + var frac_r = r; + var frac_s = -q - r; + + var round_q = Math.round(frac_q); + var round_r = Math.round(frac_r); + var round_s = Math.round(frac_s); + + var q_diff = Math.abs(round_q - frac_q); + var r_diff = Math.abs(round_r - frac_r); + var s_diff = Math.abs(round_s - frac_s); + + if (q_diff > r_diff && q_diff > s_diff) { + round_q = -round_r - round_s; + } else if (r_diff > s_diff) { + round_r = -round_q - round_s; + } + + return { q: round_q, r: round_r }; +} + var graph const { @@ -93,6 +120,15 @@ export default function LoadGrid(container, sidebar, outline, minimap) { var movedCell = this.cells[0] var model = this.graph.getModel() + // Reset index/caches if dragging a new/different component + if (this.lastMovedCell !== movedCell) { + this.lastMovedCell = movedCell + this.staticPinsByHex = null + this.currentHexKey = null + this.cachedTargetPins = null + this.lastCalcTime = 0 + } + // Collect pins of the moved component var movedPins = [] var movedChildren = model.getChildCount(movedCell) @@ -102,19 +138,72 @@ export default function LoadGrid(container, sidebar, outline, minimap) { } if (movedPins.length > 0) { - // Collect static pins - var staticPins = [] - var allCells = model.cells - Object.values(allCells).forEach(function (cell) { - if (cell && cell.Pin && cell.ParentComponent !== movedCell && cell.parent !== movedCell) { - staticPins.push(cell) - } - }) - var scale = this.graph.view.scale var graphDx = delta.x / scale var graphDy = delta.y / scale + // Get dragged component current center/top-left coordinate + var compX = movedCell.geometry.x + graphDx + var compY = movedCell.geometry.y + graphDy + var currentHexCoords = getHexCoords(compX, compY) + var currentHexKey = `${currentHexCoords.q},${currentHexCoords.r}` + + var now = Date.now() + var hexChanged = (this.currentHexKey !== currentHexKey) + var cooldownPassed = (!this.lastCalcTime || (now - this.lastCalcTime > 500)) + + if (this.currentHexKey === undefined || this.currentHexKey === null || (hexChanged && cooldownPassed)) { + this.currentHexKey = currentHexKey + this.lastCalcTime = now + + // Index all static pins on first run of the drag + if (!this.staticPinsByHex) { + this.staticPinsByHex = {} + var allCells = model.cells + Object.values(allCells).forEach(cell => { + if (cell && cell.Pin && cell.ParentComponent !== movedCell && cell.parent !== movedCell) { + var parent = cell.ParentComponent || cell.parent + if (parent && parent.geometry) { + var px = parent.geometry.x + cell.geometry.x + var py = parent.geometry.y + cell.geometry.y + var hc = getHexCoords(px, py) + var pKey = `${hc.q},${hc.r}` + if (!this.staticPinsByHex[pKey]) { + this.staticPinsByHex[pKey] = [] + } + this.staticPinsByHex[pKey].push(cell) + } + } + }) + } + + // Target neighboring hexagonal chunks (Priority 1) of the current chunk (Priority 2) + var qc = currentHexCoords.q + var rc = currentHexCoords.r + var neighbors = [ + { q: qc, r: rc }, // Priority 2 + { q: qc + 1, r: rc }, // Priority 1 neighbors + { q: qc - 1, r: rc }, + { q: qc, r: rc + 1 }, + { q: qc, r: rc - 1 }, + { q: qc + 1, r: rc - 1 }, + { q: qc - 1, r: rc + 1 } + ] + + var targetPins = [] + neighbors.forEach(n => { + var key = `${n.q},${n.r}` + var pinsInChunk = this.staticPinsByHex[key] + if (pinsInChunk) { + targetPins = targetPins.concat(pinsInChunk) + } + }) + + this.cachedTargetPins = targetPins + } + + var staticPins = this.cachedTargetPins || [] + var SNAP_TOLERANCE = 20 var bestDist = SNAP_TOLERANCE var bestDx = delta.x @@ -330,7 +419,8 @@ export default function LoadGrid(container, sidebar, outline, minimap) { return isLeftClickCanvas } - // Add Mouse Wheel Zooming (cursor-centric) + // Add Mouse Wheel Zooming (cursor-centric, throttled using requestAnimationFrame) + var pendingZoom = null mxEvent.addMouseWheelListener(function (evt, up) { if (mxEvent.isConsumed(evt)) return @@ -338,31 +428,47 @@ export default function LoadGrid(container, sidebar, outline, minimap) { var x = evt.clientX - rect.left var y = evt.clientY - rect.top - var prevScale = graph.view.scale - var prevTranslate = graph.view.translate - var zoomFactor = 1.05 // Smoother, slower zoom for scroll wheels + var factor = up ? zoomFactor : 1 / zoomFactor - var oldCenterZoom = graph.centerZoom - graph.centerZoom = false // Prevent mxGraph from centering the zoom - - if (up) { - graph.zoom(zoomFactor) + if (!pendingZoom) { + pendingZoom = { + factor: factor, + x: x, + y: y + } + + requestAnimationFrame(function () { + if (!pendingZoom) return + + var targetFactor = pendingZoom.factor + var targetX = pendingZoom.x + var targetY = pendingZoom.y + pendingZoom = null + + var prevScale = graph.view.scale + var prevTranslate = graph.view.translate + + var newScale = Math.max(0.1, Math.min(10, prevScale * targetFactor)) + if (newScale !== prevScale) { + var newTx = prevTranslate.x - targetX * (1 / prevScale - 1 / newScale) + var newTy = prevTranslate.y - targetY * (1 / prevScale - 1 / newScale) + + var oldCenterZoom = graph.centerZoom + graph.centerZoom = false + + graph.view.scaleAndTranslate(newScale, newTx, newTy) + + graph.centerZoom = oldCenterZoom + } + }) } else { - graph.zoom(1 / zoomFactor) + // Accumulate zoom factor and use latest cursor coordinates + pendingZoom.factor *= factor + pendingZoom.x = x + pendingZoom.y = y } - var newScale = graph.view.scale - if (newScale !== prevScale) { - var newTranslate = new mxPoint( - prevTranslate.x - x * (1 / prevScale - 1 / newScale), - prevTranslate.y - y * (1 / prevScale - 1 / newScale) - ) - graph.view.setTranslate(newTranslate.x, newTranslate.y) - } - - graph.centerZoom = oldCenterZoom - mxEvent.consume(evt) }, graph.container)