diff --git a/Backend/database/api/cam_groups.py b/Backend/database/api/cam_groups.py
index 9fc1903f299706476d8d42ce97c6c7b8374db71d..04735b4e1cc2d858b1468884fea1109410c58d53 100644
--- a/Backend/database/api/cam_groups.py
+++ b/Backend/database/api/cam_groups.py
@@ -6,6 +6,7 @@ from database import db_connection as db
 from database.repositories.cam_groups import GroupRepository
 from database.repositories.users import UserRepository
 from database.schemas.cam_groups import GroupFull, GroupBase
+from database.schemas.cams import CamOut
 
 db_cam_groups_router = APIRouter()
 
@@ -23,7 +24,7 @@ async def list_groups(
 
 
 @db_cam_groups_router.get("/{group_id}", response_model=GroupFull)
-async def get_groups(
+async def get_group(
         group_id: int,
         group_repo: GroupRepository = Depends(db.get_repository(GroupRepository))
 ):
@@ -76,6 +77,19 @@ async def delete_group(
     return group
 
 
+@db_cam_groups_router.get("/{group_id}/cams", response_model=List[CamOut], tags=["Cams"])
+async def list_cams_in_group(
+        group_id: int,
+        group_repo: GroupRepository = Depends(db.get_repository(GroupRepository))
+):
+    group = await group_repo.get(group_id)
+    if group is None:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND
+        )
+    return await group_repo.list_cams_in_group(group)
+
+
 @db_cam_groups_router.get("/user/{user_id}", response_model=List[GroupFull], tags=["Users"])
 async def list_user_groups(
         user_id: int,
diff --git a/Backend/database/api/cams.py b/Backend/database/api/cams.py
index 344c345b2b918b903d3c81e802133c591a6a0bbf..a233a2abde33fed60abc051b55cfaf91023c2c58 100644
--- a/Backend/database/api/cams.py
+++ b/Backend/database/api/cams.py
@@ -5,6 +5,7 @@ from fastapi import APIRouter, HTTPException, status, Depends
 from database import db_connection as db
 from database.repositories.cam_groups import GroupRepository
 from database.repositories.cams import CamRepository
+from database.schemas.cam_groups import GroupFull
 from database.schemas.cams import CamOut, CamCreate, CamUpdate
 
 db_cams_router = APIRouter()
@@ -76,7 +77,7 @@ async def delete_cam(
     return cam
 
 
-@db_cams_router.get("/{cam_id}/groups", response_model=List[CamOut], tags=["Cam groups"])
+@db_cams_router.get("/{cam_id}/groups", response_model=List[GroupFull], tags=["Cam groups"])
 async def list_cam_groups(
         cam_id: int,
         cam_repo: CamRepository = Depends(db.get_repository(CamRepository))
@@ -90,7 +91,7 @@ async def list_cam_groups(
     return groups
 
 
-@db_cams_router.post("/{cam_id}/groups", response_model=CamOut, tags=["Cam groups"])
+@db_cams_router.post("/{cam_id}/groups", response_model=GroupFull, tags=["Cam groups"])
 async def add_cam_group(
         cam_id: int,
         group_id: int,
@@ -103,7 +104,7 @@ async def add_cam_group(
     return group
 
 
-@db_cams_router.delete("/{cam_id}/groups", response_model=CamOut, tags=["Cam groups"])
+@db_cams_router.delete("/{cam_id}/groups", response_model=GroupFull, tags=["Cam groups"])
 async def delete_cam_group(
         cam_id: int,
         group_id: int,
diff --git a/Backend/database/api/roles.py b/Backend/database/api/roles.py
index dbaf61e3ca1a76fba825ad46f0ef18e04a5be704..0d7d1754f6fc7433a32ecbe7aea36c9559da608c 100644
--- a/Backend/database/api/roles.py
+++ b/Backend/database/api/roles.py
@@ -22,7 +22,7 @@ async def list_roles(
 
 
 @db_roles_router.get("/{role_id}", response_model=RoleFull)
-async def get_roles(
+async def get_role(
         role_id: int,
         role_repo: RoleRepository = Depends(db.get_repository(RoleRepository))
 ):
diff --git a/Backend/database/models/cams.py b/Backend/database/models/cams.py
index 9c53e426d0b5ecdf05c36ec3b1127d2548e9555f..11110b322dd919bc247b81b76b0f43eaadc416e7 100644
--- a/Backend/database/models/cams.py
+++ b/Backend/database/models/cams.py
@@ -8,7 +8,7 @@ class Cam(Base):
     id: Column = Column(Integer, primary_key=True)  # Will make SERIAL
     name: Column = Column(String, unique=True, nullable=False)
     host: Column = Column(String, nullable=False)
-    port: Column = Column(Integer, unique=True, nullable=False)
+    port: Column = Column(Integer, unique=False, nullable=False)
     username: Column = Column(String, nullable=False)
     password: Column = Column(String, nullable=False)
 
diff --git a/Backend/database/repositories/cam_groups.py b/Backend/database/repositories/cam_groups.py
index 1e3c63ac0121093e1a9eaa8865c4039f0b494f3c..6a77c77ed5843a33837d76ee48d584102b0da00c 100644
--- a/Backend/database/repositories/cam_groups.py
+++ b/Backend/database/repositories/cam_groups.py
@@ -4,7 +4,7 @@ from sqlalchemy import select
 from sqlalchemy.ext.asyncio import AsyncSession
 
 from database.models.auth import User
-from database.models.cams import Group, UserInGroup
+from database.models.cams import Group, UserInGroup, Cam, CamInGroup
 from database.repositories.base import Repository
 from database.schemas.cam_groups import GroupBase
 
@@ -45,6 +45,12 @@ class GroupRepository(Repository):
         await self._session.commit()
         return group_db_obj
 
+    async def list_cams_in_group(self, group: Group) -> List[Cam]:
+        query = select(Cam).join(CamInGroup, CamInGroup.cam_id == Cam.id)
+        query = query.filter(CamInGroup.group_id == group.id)
+        cams_db_obj = await self._session.execute(query)
+        return [cam for cam, in cams_db_obj]
+
     async def get_user_groups(self, user: User) -> List[Group]:
         query = select(Group).join(UserInGroup, UserInGroup.group_id == Group.id)
         query = query.filter(UserInGroup.user_id == user.id)
diff --git a/Backend/database/repositories/cams.py b/Backend/database/repositories/cams.py
index aea012584414815a4d8a5614192c28442869e9ec..a92387c84f7c0559afe273e75c668e392bafa391 100644
--- a/Backend/database/repositories/cams.py
+++ b/Backend/database/repositories/cams.py
@@ -45,7 +45,7 @@ class CamRepository(Repository):
         return cam_db_obj
 
     async def get_cam_groups(self, cam: Cam) -> List[Group]:
-        query = select(Group).join(CamInGroup, CamInGroup.cam_id == Cam.id)
+        query = select(Group).join(CamInGroup, CamInGroup.cam_id == Group.id)
         query = query.filter(CamInGroup.cam_id == cam.id)
         groups_db_obj = await self._session.execute(query)
         return [group for group, in groups_db_obj]
diff --git a/Backend/main.py b/Backend/main.py
index 94f5da54728dfb4e4eee1193eb39db62d4367d1f..afcb42e5b590c61fc3be4db10dde02b01aa43048 100644
--- a/Backend/main.py
+++ b/Backend/main.py
@@ -6,6 +6,7 @@ from fastapi.middleware.cors import CORSMiddleware
 from fastapi.staticfiles import StaticFiles
 from starlette.responses import FileResponse, RedirectResponse
 
+from database import db_connection
 from database.api import db_router
 from onvif_proxy import cams_connection
 from onvif_proxy.onvif_proxy import onvif_router
@@ -15,10 +16,10 @@ from proxy_switch.proxy_switch import switch_router
 
 app = FastAPI()
 
-app.add_middleware(
-    CORSMiddleware,
-    allow_origins=['*']
-)
+# app.add_middleware(
+#     CORSMiddleware,
+#     allow_origins=['*']
+# )
 
 app.include_router(
     onvif_router,
@@ -55,6 +56,7 @@ rtsp_task = None
 
 @app.on_event("startup")
 async def startup_event():
+    await db_connection.create_models()
     global rtsp_task
     rtsp_task = asyncio.create_task(start_rtsp_server())
 
diff --git a/Backend/requirements.txt b/Backend/requirements.txt
index 672745860583903cf4f6ff8d9c9a58070e7c1d9a..5c56635490c649c8a78d51db524ff7a299bbcde4 100644
--- a/Backend/requirements.txt
+++ b/Backend/requirements.txt
@@ -1,6 +1,7 @@
 aiohttp
 alembic
 asyncpg
+bcrypt
 fastapi
 lxml
 onvif-zeep-async
diff --git a/Frontend/package.json b/Frontend/package.json
index 66ea8f8440556192ab60d40223821dc96bc00f80..3a9509f8ebf3873ba117b292c85f608a9b3e2f18 100644
--- a/Frontend/package.json
+++ b/Frontend/package.json
@@ -8,19 +8,20 @@
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
-    "@fortawesome/fontawesome-free": "^6.4.2",
+    "@fortawesome/fontawesome-free": "^6.5.1",
     "bootstrap": "^5.3.2",
-    "sass": "^1.69.3",
+    "sass": "^1.69.5",
     "sass-loader": "^13.3.2",
-    "vue": "^3.3.4"
+    "vue": "^3.3.11",
+    "axios": "^1.6.2"
   },
   "devDependencies": {
-    "@babel/core": "^7.23.3",
+    "@babel/core": "^7.23.6",
     "@babel/eslint-parser": "^7.23.3",
     "@vue/cli-plugin-babel": "^5.0.8",
     "@vue/cli-service": "^5.0.8",
-    "eslint": "^8.54.0",
-    "eslint-plugin-vue": "^9.18.1"
+    "eslint": "^8.55.0",
+    "eslint-plugin-vue": "^9.19.2"
   },
   "eslintConfig": {
     "root": true,
diff --git a/Frontend/src/App.vue b/Frontend/src/App.vue
index 35c3b8ff971b2683c9e77796947123d416558bb4..8a7d2b85560f495502f7bb984323a47ecad10838 100644
--- a/Frontend/src/App.vue
+++ b/Frontend/src/App.vue
@@ -3,7 +3,8 @@
     <NavBar id="navbar"/>
     <div class="content-wrapper">
 <!--      <component :is="currentComponent"/>-->
-      <Gallery/>
+<!--      <CamsList/>-->
+      <GroupsList/>
     </div>
     <Footer/>
   </div>
@@ -11,13 +12,15 @@
 
 <script>
 import NavBar from './components/NavBar.vue'
-import Gallery from './components/Gallery.vue'
+import CamsList from './components/CamsList.vue'
 import Footer from './components/Footer.vue'
+import GroupsList from "@/components/GroupsList.vue";
 
 export default {
   name: 'App',
   components: {
-    Gallery,
+    GroupsList,
+    CamsList,
     NavBar,
     Footer
   },
diff --git a/Frontend/src/components/CamGroup.vue b/Frontend/src/components/CamGroup.vue
new file mode 100644
index 0000000000000000000000000000000000000000..5fab73fdb680e12819fb62657f4ac491cf464194
--- /dev/null
+++ b/Frontend/src/components/CamGroup.vue
@@ -0,0 +1,72 @@
+<template>
+  <div class="group-container">
+    <h2>{{group_name}}</h2>
+    <Gallery class=""
+             :cams="cams"
+             :active_cam_id="active_id"/>
+  </div>
+</template>
+
+<script>
+import axios from "axios";
+import Gallery from "@/components/Gallery.vue";
+
+export default {
+  /* eslint-disable */
+  name: 'CamGroup',
+  components: {Gallery},
+  inject: ['Gallery'],
+  props: {
+    group_id: {
+      required: true
+    },
+    group_name: {
+      required: false
+    },
+    active_id: {
+      required: false
+    }
+  },
+  mounted() {
+    setInterval(this.fetchCams, 3000)
+  },
+  data() {
+    return {
+      cams: []
+    };
+  },
+  created() {
+    this.fetchCams();
+  },
+  methods: {
+    fetchCams() {
+      axios.get(`/db/cam_groups/${this.group_id}/cams`, {
+        headers: {
+          'Content-Type': 'application/json',
+        }
+      })
+          .then(response => {
+            this.cams = response.data;
+          })
+          .catch(error => {
+            console.error('Cams loading error:', error);
+          });
+    }
+  }
+}
+</script>
+
+<style scoped>
+.group-container {
+  border-bottom: 1px solid var(--light-color);
+}
+
+h2 {
+  margin: 0.5rem;
+  padding: 0.5rem 0;
+  color: var(--white-color);
+  background-color: var(--light-color);
+  border: none;
+  border-radius: var(--bs-border-radius);
+}
+</style>
diff --git a/Frontend/src/components/CameraPreview.vue b/Frontend/src/components/CameraPreview.vue
index 825a59b128f7fad13952b13c143838fbdb20126a..560d2d3c4068adff2402c6df2a5d86c1fec51c8a 100644
--- a/Frontend/src/components/CameraPreview.vue
+++ b/Frontend/src/components/CameraPreview.vue
@@ -60,7 +60,8 @@ export default {
     chooseCamera() {
       fetch(`/switch/choose_camera/${this.camera_id}`,
           { method: 'POST', mode: 'no-cors' })
-      this.Gallery.update_active_cam()
+      // TODO
+      // this.Gallery.update_active_cam()
     }
   }
 }
diff --git a/Frontend/src/components/CamsList.vue b/Frontend/src/components/CamsList.vue
new file mode 100644
index 0000000000000000000000000000000000000000..caf79739e7f64f1e2a75f6686f1a1885989c848b
--- /dev/null
+++ b/Frontend/src/components/CamsList.vue
@@ -0,0 +1,61 @@
+<template>
+  <Gallery class=""
+           :cams="cams"
+           :active_cam_id="active_cam"/>
+</template>
+
+<script>
+import Gallery from "@/components/Gallery.vue";
+
+export default {
+  /* eslint-disable */
+  name: 'CamsList',
+  components: {
+    Gallery
+  },
+  data() {
+    return {
+      cams: [],
+      active_cam: -1
+    }
+  },
+  methods: {
+    update_active_cam() {
+      fetch("/switch/active_cam", { method: 'GET' })
+          .then(
+              response => {
+                if (response.ok) {
+                  response.json().then(data => {
+                    this.active_cam = data
+                  })
+                }
+                else if (response.status === 503) {
+                  this.active_cam = -1
+                }
+              },
+              error => {
+                console.error("There was an error!", error);
+              });
+    }
+  },
+  mounted() {
+    fetch("/switch/cams", { method: 'GET' })
+        .then(
+            response => {
+              if (response.ok) {
+                response.json().then(json => {
+                  this.cams = json
+                })
+              }
+            },
+            error => {
+              console.error("There was an error!", error);
+            });
+    setInterval(this.update_active_cam, 1000)
+  }
+}
+</script>
+
+<style scoped>
+
+</style>
diff --git a/Frontend/src/components/Gallery.vue b/Frontend/src/components/Gallery.vue
index 4c8601ac209b37c238afb56c8d3db43d6122762a..863c055c08d69dc3be859bcfec424e9c94bb557f 100644
--- a/Frontend/src/components/Gallery.vue
+++ b/Frontend/src/components/Gallery.vue
@@ -5,7 +5,7 @@
                      :camera_id="index"
                      :camera_name="camera.name"
                      :host="camera.host"
-                     :active_id="active_cam"/>
+                     :active_id="active_cam_id"/>
     </div>
   </div>
 </template>
@@ -19,50 +19,18 @@ export default {
   components: {
     CameraPreview
   },
-  data() {
-    return {
-      cams: [],
-      active_cam: -1
+  props: {
+    cams: {
+      required: true
+    },
+    active_cam_id: {
+      required: false
     }
   },
   provide() {
     return {
       Gallery: this
     }
-  },
-  methods: {
-    update_active_cam() {
-      fetch("/switch/active_cam", { method: 'GET' })
-          .then(
-              response => {
-                if (response.ok) {
-                  response.json().then(data => {
-                    this.active_cam = data
-                  })
-                }
-                else if (response.status === 503) {
-                  this.active_cam = -1
-                }
-              },
-              error => {
-                console.error("There was an error!", error);
-              });
-    }
-  },
-  mounted() {
-    fetch("/switch/cams", { method: 'GET' })
-        .then(
-            response => {
-              if (response.ok) {
-                response.json().then(json => {
-                  this.cams = json
-                })
-              }
-            },
-            error => {
-              console.error("There was an error!", error);
-            });
-    setInterval(this.update_active_cam, 1000)
   }
 }
 </script>
@@ -71,7 +39,8 @@ export default {
 
 .cams-container {
   display: grid;
-  padding: 3rem;
+  padding: 0;
+  margin: 0;
   justify-items: center;
   grid-template-columns: repeat(auto-fit, minmax(24rem, 1fr));
   grid-auto-rows: 19rem;
diff --git a/Frontend/src/components/GroupsList.vue b/Frontend/src/components/GroupsList.vue
new file mode 100644
index 0000000000000000000000000000000000000000..9770e5465b7962a31373c2fcfca8407c523fe0dd
--- /dev/null
+++ b/Frontend/src/components/GroupsList.vue
@@ -0,0 +1,52 @@
+<template>
+  <div class="groups-container">
+    <div v-for="group in groups">
+      <CamGroup :group_id="group.id"
+                :group_name="group.name"/>
+    </div>
+  </div>
+</template>
+
+<script>
+import CamGroup from "@/components/CamGroup.vue";
+
+export default {
+  /* eslint-disable */
+  name: 'GroupsList',
+  components: {
+    CamGroup
+  },
+  data() {
+    return {
+      groups: [],
+      active_group: -1
+    }
+  },
+  methods: {
+    update_groups() {
+      fetch("/db/cam_groups/list", { method: 'GET' })
+          .then(
+              response => {
+                if (response.ok) {
+                  response.json().then(json => {
+                    this.groups = json
+                  })
+                }
+              },
+              error => {
+                console.error("There was an error!", error);
+              });
+    }
+  },
+  mounted() {
+    this.update_groups()
+    setInterval(this.update_groups, 1000)
+  }
+}
+</script>
+
+<style scoped>
+.groups-container {
+  margin: 1rem 3rem;
+}
+</style>
diff --git a/Frontend/vue.config.js b/Frontend/vue.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..cbc1709bb8f757a4d9b3c3c59387d9efff76900f
--- /dev/null
+++ b/Frontend/vue.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+  pluginOptions: {
+    i18n: {
+      locale: 'ru',
+      fallbackLocale: 'en',
+      localeDir: 'locales',
+      enableInSFC: true
+    }
+  },
+  devServer: {
+    proxy: {
+      '^/assets': {
+        target: `http://${process.env.HOST || 'localhost'}:${process.env.PORT || 8080}`,
+        changeOrigin: true
+      },
+      '^/(?!ws)': {
+        target: 'http://localhost:9000',
+        changeOrigin: true
+      }
+    }
+  }
+}
\ No newline at end of file