diff --git a/gns3server/db/models/roles.py b/gns3server/db/models/roles.py
index 2cc54f37..6cba0cc1 100644
--- a/gns3server/db/models/roles.py
+++ b/gns3server/db/models/roles.py
@@ -38,7 +38,7 @@ class Role(BaseTable):
__tablename__ = "roles"
role_id = Column(GUID, primary_key=True, default=generate_uuid)
- name = Column(String)
+ name = Column(String, unique=True)
description = Column(String)
is_builtin = Column(Boolean, default=False)
permissions = relationship("Permission", secondary=permission_role_link, back_populates="roles")
diff --git a/gns3server/db/models/users.py b/gns3server/db/models/users.py
index 8cf3d503..e6bc53f4 100644
--- a/gns3server/db/models/users.py
+++ b/gns3server/db/models/users.py
@@ -15,7 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from sqlalchemy import Table, Boolean, Column, String, ForeignKey, event
+from sqlalchemy import Table, Boolean, Column, String, DateTime, ForeignKey, event
from sqlalchemy.orm import relationship
from .base import Base, BaseTable, generate_uuid, GUID
@@ -45,6 +45,7 @@ class User(BaseTable):
email = Column(String, unique=True, index=True)
full_name = Column(String)
hashed_password = Column(String)
+ last_login = Column(DateTime)
is_active = Column(Boolean, default=True)
is_superadmin = Column(Boolean, default=False)
groups = relationship("UserGroup", secondary=user_group_link, back_populates="users")
diff --git a/gns3server/db/repositories/users.py b/gns3server/db/repositories/users.py
index 68d12f78..2db1516f 100644
--- a/gns3server/db/repositories/users.py
+++ b/gns3server/db/repositories/users.py
@@ -17,7 +17,7 @@
from uuid import UUID
from typing import Optional, List, Union
-from sqlalchemy import select, update, delete
+from sqlalchemy import select, update, delete, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -140,6 +140,15 @@ class UsersRepository(BaseRepository):
return user
if not self._auth_service.verify_password(password, user.hashed_password):
return None
+
+ # Backup the updated_at value
+ updated_at = user.updated_at
+ user.last_login = func.current_timestamp()
+ await self._db_session.commit()
+ # Restore the original updated_at value
+ # so it is not affected by the last login update
+ user.updated_at = updated_at
+ await self._db_session.commit()
return user
async def get_user_memberships(self, user_id: UUID) -> List[models.UserGroup]:
diff --git a/gns3server/schemas/controller/users.py b/gns3server/schemas/controller/users.py
index fc60568c..89effbb3 100644
--- a/gns3server/schemas/controller/users.py
+++ b/gns3server/schemas/controller/users.py
@@ -14,6 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+from datetime import datetime
from typing import Optional
from pydantic import EmailStr, BaseModel, Field, SecretStr
from uuid import UUID
@@ -51,6 +52,7 @@ class UserUpdate(UserBase):
class User(DateTimeModelMixin, UserBase):
user_id: UUID
+ last_login: Optional[datetime] = None
is_active: bool = True
is_superadmin: bool = False