diff --git a/eda-frontend/src/components/Dashboard/UserProfile.js b/eda-frontend/src/components/Dashboard/UserProfile.js
new file mode 100644
index 00000000..9b19174c
--- /dev/null
+++ b/eda-frontend/src/components/Dashboard/UserProfile.js
@@ -0,0 +1,397 @@
+import React, { useEffect, useState } from 'react'
+import { useSelector } from 'react-redux'
+import { makeStyles } from '@material-ui/core/styles'
+import {
+ Avatar,
+ Button,
+ Card,
+ CardContent,
+ Chip,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ Divider,
+ Grid,
+ TextField,
+ Typography,
+ Snackbar
+} from '@material-ui/core'
+import MuiAlert from '@material-ui/lab/Alert'
+import api from '../../utils/Api'
+
+const useStyles = makeStyles((theme) => ({
+ root: {
+ padding: theme.spacing(3)
+ },
+ card: {
+ maxWidth: 620,
+ margin: '0 auto',
+ marginTop: theme.spacing(4)
+ },
+ avatar: {
+ width: theme.spacing(10),
+ height: theme.spacing(10),
+ margin: '0 auto',
+ marginBottom: theme.spacing(2),
+ backgroundColor: theme.palette.primary.main,
+ fontSize: '2rem'
+ },
+ center: {
+ textAlign: 'center'
+ },
+ chip: {
+ margin: theme.spacing(0.5)
+ },
+ statsRow: {
+ display: 'flex',
+ justifyContent: 'center',
+ gap: theme.spacing(3),
+ marginTop: theme.spacing(2),
+ marginBottom: theme.spacing(1)
+ },
+ statBox: {
+ textAlign: 'center',
+ padding: theme.spacing(1.5, 3),
+ backgroundColor: theme.palette.grey[100],
+ borderRadius: theme.shape.borderRadius
+ },
+ statNumber: {
+ fontSize: '1.8rem',
+ fontWeight: 'bold',
+ color: theme.palette.primary.main
+ },
+ sectionTitle: {
+ fontWeight: 600,
+ marginBottom: theme.spacing(2),
+ marginTop: theme.spacing(1)
+ },
+ field: {
+ marginBottom: theme.spacing(2)
+ },
+ btnRow: {
+ display: 'flex',
+ gap: theme.spacing(2),
+ marginTop: theme.spacing(2),
+ justifyContent: 'flex-end'
+ }
+}))
+
+function formatDate (dateStr) {
+ if (!dateStr) return 'Never'
+ const d = new Date(dateStr)
+ return d.toLocaleDateString('en-IN', {
+ day: 'numeric', month: 'short', year: 'numeric',
+ hour: '2-digit', minute: '2-digit'
+ })
+}
+
+export default function UserProfile () {
+ const classes = useStyles()
+ const auth = useSelector(state => state.authReducer)
+
+ const [profile, setProfile] = useState(null)
+ const [schematicCount, setSchematicCount] = useState(0)
+ const [simulationCount] = useState(0)
+ const [editMode, setEditMode] = useState(false)
+ const [formData, setFormData] = useState({
+ username: '', email: '', first_name: '', last_name: ''
+ })
+ const [snackbar, setSnackbar] = useState({ open: false, message: '', severity: 'success' })
+ const [passwordDialog, setPasswordDialog] = useState(false)
+ const [passwordForm, setPasswordForm] = useState({ current_password: '', new_password: '', re_new_password: '' })
+ const [passwordErrors, setPasswordErrors] = useState({})
+
+ const getConfig = () => {
+ const token = localStorage.getItem('esim_auth_token')
+ const config = { headers: { 'Content-Type': 'application/json' } }
+ if (token) config.headers.Authorization = `Token ${token}`
+ return config
+ }
+
+ useEffect(() => {
+ const config = getConfig()
+ api.get('auth/user/profile/', config)
+ .then(res => {
+ setProfile(prev => ({ ...prev, ...res.data }))
+ setFormData({
+ username: res.data.username || '',
+ email: res.data.email || '',
+ first_name: res.data.first_name || '',
+ last_name: res.data.last_name || ''
+ })
+ })
+ .catch(err => console.log(err))
+
+ api.get('save/list', config)
+ .then(res => setSchematicCount(res.data.length))
+ .catch(err => console.log(err))
+ }, [])
+
+ const handleSave = () => {
+ const config = getConfig()
+ api.patch('auth/user/profile/', formData, config)
+ .then(res => {
+ setProfile(prev => ({ ...prev, ...res.data }))
+ setEditMode(false)
+ setSnackbar({ open: true, message: 'Profile updated successfully', severity: 'success' })
+ })
+ .catch(() => {
+ setSnackbar({ open: true, message: 'Failed to update profile', severity: 'error' })
+ })
+ }
+
+ if (!auth.user || !profile) {
+ return (
+
+ Loading profile...
+
+ )
+ }
+
+ return (
+
+
+
+
+ {/* Avatar and name */}
+
+
+ {profile.username.charAt(0).toUpperCase()}
+
+
+ {profile.first_name || profile.last_name
+ ? `${profile.first_name} ${profile.last_name}`.trim()
+ : profile.username}
+
+
+ {profile.email || 'No email set'}
+
+ {auth.roles && (
+
+ {auth.roles.is_type_contributor && (
+
+ )}
+ {auth.roles.is_type_reviewer && (
+
+ )}
+
+ )}
+
+
+
+
+ {/* Stats */}
+
+
+
{schematicCount}
+
Schematics
+
+
+
{simulationCount}
+
Simulations
+
+
+
+
+
+ {/* Account info */}
+
+ Account Information
+
+
+ {!editMode ? (
+
+
+ Username
+ {profile.username}
+
+
+ Email
+ {profile.email || 'Not set'}
+
+
+ First Name
+ {profile.first_name || 'Not set'}
+
+
+ Last Name
+ {profile.last_name || 'Not set'}
+
+
+ Member Since
+ {formatDate(profile.date_joined)}
+
+
+ Last Login
+ {formatDate(profile.last_login)}
+
+
+ ) : (
+
+
+ setFormData({ ...formData, username: e.target.value })}
+ fullWidth size="small" variant="outlined"
+ className={classes.field}
+ />
+
+
+ setFormData({ ...formData, email: e.target.value })}
+ fullWidth size="small" variant="outlined"
+ className={classes.field}
+ />
+
+
+ setFormData({ ...formData, first_name: e.target.value })}
+ fullWidth size="small" variant="outlined"
+ className={classes.field}
+ />
+
+
+ setFormData({ ...formData, last_name: e.target.value })}
+ fullWidth size="small" variant="outlined"
+ className={classes.field}
+ />
+
+
+ )}
+
+
+ {!editMode ? (
+ <>
+
+
+ >
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
setSnackbar({ ...snackbar, open: false })}
+ >
+
+ {snackbar.message}
+
+
+
+ )
+}
diff --git a/eda-frontend/src/pages/Dashboard.js b/eda-frontend/src/pages/Dashboard.js
index 9bd75bb4..995279cd 100644
--- a/eda-frontend/src/pages/Dashboard.js
+++ b/eda-frontend/src/pages/Dashboard.js
@@ -11,6 +11,7 @@ import DashboardHome from '../components/Dashboard/DashboardHome'
import SchematicsList from '../components/Dashboard/SchematicsList'
import DashboardOtherProjects from '../components/Dashboard/DashboardOtherProjects'
import api from '../utils/Api'
+import UserProfile from '../components/Dashboard/UserProfile'
const useStyles = makeStyles((theme) => ({
root: {
@@ -62,7 +63,7 @@ export default function Dashboard () {
{/* Subroutes under dashboard section */}
{ltiDetails !== null &&
} />
-
+
[\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'^user/profile/$', authAPI_views.UserProfileView.as_view(),
+ name='user-profile'),
]
diff --git a/esim-cloud-backend/authAPI/views.py b/esim-cloud-backend/authAPI/views.py
index eccd536d..16ab79a3 100644
--- a/esim-cloud-backend/authAPI/views.py
+++ b/esim-cloud-backend/authAPI/views.py
@@ -88,6 +88,9 @@ class CustomTokenCreateView(utils.ActionViewMixin, generics.GenericAPIView):
def _action(self, serializer):
from rest_framework.authtoken.models import Token
token, created = Token.objects.get_or_create(user=serializer.user)
+ from django.utils import timezone
+ serializer.user.last_login = timezone.now()
+ serializer.user.save(update_fields=["last_login"])
data = {
'auth_token': token.key,
'user_id': serializer.user.id
@@ -95,3 +98,21 @@ def _action(self, serializer):
return Response(
data=data, status=status.HTTP_200_OK
)
+
+from authAPI.serializers import UserProfileSerializer, UserProfileUpdateSerializer
+
+
+class UserProfileView(generics.RetrieveUpdateAPIView):
+ """
+ GET: Returns full user profile including date_joined, last_login, name
+ PATCH: Update username, email, first_name, last_name
+ """
+ permission_classes = [permissions.IsAuthenticated]
+
+ def get_serializer_class(self):
+ if self.request.method in ['PUT', 'PATCH']:
+ return UserProfileUpdateSerializer
+ return UserProfileSerializer
+
+ def get_object(self):
+ return self.request.user