Merge pull request #254 from OktopUSP/dev

Kubernetes Improvements + Frontend Features
This commit is contained in:
Leandro Antônio Farias Machado 2024-04-30 18:01:48 -03:00 committed by GitHub
commit c48a44f6ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 771 additions and 406 deletions

View File

@ -4,11 +4,13 @@ import (
"encoding/json"
"log"
"net/http"
"net/mail"
"github.com/gorilla/mux"
"github.com/leandrofars/oktopus/internal/api/auth"
"github.com/leandrofars/oktopus/internal/db"
"github.com/leandrofars/oktopus/internal/utils"
"go.mongodb.org/mongo-driver/bson/primitive"
)
func (a *Api) retrieveUsers(w http.ResponseWriter, r *http.Request) {
@ -20,6 +22,11 @@ func (a *Api) retrieveUsers(w http.ResponseWriter, r *http.Request) {
}
for _, x := range users {
objectID, ok := x["_id"].(primitive.ObjectID)
if ok {
creationTime := objectID.Timestamp()
x["createdAt"] = creationTime.Format("02/01/2006")
}
delete(x, "password")
}
@ -27,7 +34,6 @@ func (a *Api) retrieveUsers(w http.ResponseWriter, r *http.Request) {
if err != nil {
log.Println(err)
}
return
}
func (a *Api) registerUser(w http.ResponseWriter, r *http.Request) {
@ -64,12 +70,27 @@ func (a *Api) registerUser(w http.ResponseWriter, r *http.Request) {
return
}
if user.Email == "" || user.Password == "" || !valid(user.Email) {
w.WriteHeader(http.StatusBadRequest)
return
}
if err := a.db.RegisterUser(user); err != nil {
if err == db.ErrorUserExists {
w.WriteHeader(http.StatusConflict)
w.Write([]byte("User with this email already exists"))
return
}
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func valid(email string) bool {
_, err := mail.ParseAddress(email)
return err == nil
}
func (a *Api) deleteUser(w http.ResponseWriter, r *http.Request) {
tokenString := r.Header.Get("Authorization")
if tokenString == "" {
@ -84,22 +105,22 @@ func (a *Api) deleteUser(w http.ResponseWriter, r *http.Request) {
//Check if user which is requesting deletion has the necessary privileges
rUser, err := a.db.FindUser(email)
if rUser.Level != AdminUser {
w.WriteHeader(http.StatusForbidden)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
userEmail := mux.Vars(r)["user"]
if userEmail == email {
w.WriteHeader(http.StatusBadRequest)
return
}
if rUser.Email == userEmail || (rUser.Level == AdminUser && rUser.Email != userEmail) { //Admin can delete any account, but admin account can never be deleted
if err := a.db.DeleteUser(userEmail); err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(err)
return
}
} else {
w.WriteHeader(http.StatusForbidden)
}
}
func (a *Api) changePassword(w http.ResponseWriter, r *http.Request) {

View File

@ -4,6 +4,7 @@ import (
"context"
"log"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
@ -33,6 +34,15 @@ func NewDatabase(ctx context.Context, mongoUri string) Database {
log.Println("Connected to MongoDB-->", mongoUri)
db.users = client.Database("account-mngr").Collection("users")
indexField := bson.M{"email": 1}
_, err = db.users.Indexes().CreateOne(ctx, mongo.IndexModel{
Keys: indexField,
Options: options.Index().SetUnique(true),
})
if err != nil {
log.Fatalln(err)
}
db.ctx = ctx
return db

View File

@ -1,6 +1,7 @@
package db
import (
"errors"
"log"
"go.mongodb.org/mongo-driver/bson"
@ -13,8 +14,11 @@ type User struct {
Name string `json:"name"`
Password string `json:"password,omitempty"`
Level int `json:"level"`
Phone string `json:"phone"`
}
var ErrorUserExists = errors.New("User already exists")
func (d *Database) RegisterUser(user User) error {
err := d.users.FindOne(d.ctx, bson.D{{"email", user.Email}}).Err()
if err != nil {
@ -23,8 +27,10 @@ func (d *Database) RegisterUser(user User) error {
return err
}
log.Println(err)
}
return err
} else {
return ErrorUserExists
}
}
func (d *Database) UpdatePassword(user User) error {

View File

@ -19,6 +19,15 @@ git clone https://github.com/OktopUSP/oktopus
export DEPLOYMENT_PATH=oktopus/deploy/kubernetes
```
## HAProxy Ingress Controller
```shell
helm install haproxy-kubernetes-ingress haproxytech/kubernetes-ingress \
--create-namespace \
--namespace haproxy-controller \
--set controller.kind=DaemonSet \
--set controller.daemonset.useHostPort=true
```
## MongoBD
@ -49,27 +58,6 @@ helm install nats nats/nats --set config.jetstream.enabled=true
## Oktopus
<b>Node Ports</b>
For this deployment, we are not using a load balancer and kubernetes is deployed on-premises so we are using Nodeports to insource the client traffic into cluster. below the ports set on deployment files:
1. MQTT broker service (mqtt-svc): 30000
2. Frontend (frontend-svc): 30001
3. SocketIO: (socketio-svc): 30002
4. Controller (controller-svc): 30003
5. WebSocket (ws-svc): 30005
Before deploying the files, edit the frontend.yaml file to set the correct enviroment variables:
```yaml
env:
- name: NEXT_PUBLIC_REST_ENDPOINT
value: "<FRONTEND_IP>:30003"
- name: NEXT_PUBLIC_WS_ENDPOINT
value: "<FRONTEND_IP>:30005"
```
```shell
kubectl apply -f $DEPLOYMENT_PATH/mqtt.yaml
kubectl apply -f $DEPLOYMENT_PATH/mqtt-adapter.yaml

View File

@ -33,7 +33,7 @@ spec:
- name: NATS_VERIFY_CERTIFICATES
value: "false"
- name: MONGO_URI
value: "mongodb://oktopusp:oktopusp@mongodb-0.mongodb-svc.mongodb.svc.cluster.local:27017,mongodb-1.mongodb-svc.mongodb.svc.cluster.local:27017,mongodb-2.mongodb-svc.mongodb.svc.cluster.local:27017/adapter?replicaSet=mongodb&ssl=false"
value: "mongodb://oktopusp:oktopusp@mongodb-0.mongodb-svc.mongodb.svc.cluster.local:27017,mongodb-1.mongodb-svc.mongodb.svc.cluster.local:27017,mongodb-2.mongodb-svc.mongodb.svc.cluster.local:27017/?replicaSet=mongodb&ssl=false"
- name: CONTROLLER_ID
value: "oktopusController"

View File

@ -48,6 +48,5 @@ spec:
- protocol: TCP
port: 8000
targetPort: 8000
nodePort: 30003
type: NodePort
type: ClusterIP

View File

@ -29,9 +29,7 @@ spec:
imagePullPolicy: IfNotPresent
env:
- name: NEXT_PUBLIC_REST_ENDPOINT
value: "192.168.1.130:30003"
- name: NEXT_PUBLIC_WS_ENDPOINT
value: "192.168.1.130:30005"
value: "/api"
---
apiVersion: v1
kind: Service
@ -44,5 +42,5 @@ spec:
- protocol: TCP
port: 3000
targetPort: 3000
nodePort: 30001
type: NodePort
#externalTrafficPolicy: Local
type: ClusterIP

View File

@ -1,7 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: haproxy-kubernetes-ingress
namespace: haproxy-controller
data:
syslog-server: "address:stdout, format: raw, facility:daemon"

View File

@ -1,8 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: haproxy-tcp
namespace: default
data:
1883: # Port where the frontend is going to listen to.
default/mqtt-svc:1883 # Kubernetes service in the format NS/ServiceName:ServicePort

View File

@ -1,8 +0,0 @@
controller:
service:
tcpPorts:
- name: mqtt
port: 1883
targetPort: 1883
extraArgs:
- --configmap-tcp-services=default/haproxy-tcp

View File

@ -0,0 +1,34 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-ingress
namespace: default
annotations:
spec:
ingressClassName: "haproxy"
rules:
- host: oktopus.rdss.cloud
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend-svc
port:
number: 3000
- path: /api
pathType: Prefix
backend:
service:
name: controller-svc
port:
number: 8000
- path: /socket.io
pathType: Prefix
backend:
service:
name: socketio-svc
port:
number: 5000

View File

@ -31,7 +31,7 @@ spec:
- name: NATS_VERIFY_CERTIFICATES
value: "false"
- name: MQTT_URL
value: "tcp://mqtt:1883"
value: "tcp://mqtt-svc:1883"
- name: MQTT_CLIENT_ID
value: "mqtt-adapter"
- name: MQTT_USERNAME

View File

@ -9,8 +9,8 @@ spec:
- protocol: TCP
port: 1883
targetPort: 1883
nodePort: 30000
type: NodePort
externalTrafficPolicy: Local
type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
@ -46,3 +46,6 @@ spec:
value: "false"
- name: LOG_LEVEL
value: "0" # 0 - DEBUG
- name: REDIS_ENABLE
value: "false"

View File

@ -19,6 +19,8 @@ spec:
env:
- name: NATS_URL
value: "nats:4222"
- name: CORS_ALLOWED_ORIGINS
value: ""
---
apiVersion: v1
kind: Service
@ -31,5 +33,3 @@ spec:
- protocol: TCP
port: 5000
targetPort: 5000
nodePort: 30002
type: NodePort

View File

@ -82,18 +82,22 @@ export const AuthProvider = (props) => {
console.error(err);
}
console.log("isAuthenticated: ", isAuthenticated)
if (isAuthenticated) {
const user = {
id: '5e86809283e28b96d2d38537',
avatar: '/assets/avatars/default-avatar.png',
name: 'Oktopus',
email: 'anika.visser@devias.io',
token: localStorage.getItem("token")
};
dispatch({
type: HANDLERS.INITIALIZE,
payload: user
});
console.log("AUTH CONTEXT --> auth.user.token:", user.token)
} else {
dispatch({
type: HANDLERS.INITIALIZE

View File

@ -2,6 +2,8 @@ import ChartBarIcon from '@heroicons/react/24/solid/ChartBarIcon';
import CogIcon from '@heroicons/react/24/solid/CogIcon';
import ChatBubbleLeftRightIcon from '@heroicons/react/24/solid/ChatBubbleLeftRightIcon'
import MapIcon from '@heroicons/react/24/solid/MapIcon'
import UserGroupIcon from '@heroicons/react/24/solid/UserGroupIcon'
import KeyIcon from '@heroicons/react/24/solid/KeyIcon'
import CpuChip from '@heroicons/react/24/solid/CpuChipIcon';
import { SvgIcon } from '@mui/material';
@ -42,6 +44,24 @@ export const items = [
// </SvgIcon>
// )
// },
// {
// title: 'Credentials',
// path: '/credentials',
// icon: (
// <SvgIcon fontSize="small">
// <KeyIcon/>
// </SvgIcon>
// )
// },
{
title: 'Users',
path: '/users',
icon: (
<SvgIcon fontSize="small">
<UserGroupIcon/>
</SvgIcon>
)
},
{
title: 'Settings',
path: '/settings',

79
frontend/src/pages/403.js Normal file
View File

@ -0,0 +1,79 @@
import Head from 'next/head';
import NextLink from 'next/link';
import ArrowLeftIcon from '@heroicons/react/24/solid/ArrowLeftIcon';
import { Box, Button, Container, SvgIcon, Typography } from '@mui/material';
const Page = () => (
<>
<Head>
<title>
403 | Oktopus TR-369
</title>
</Head>
<Box
component="main"
sx={{
alignItems: 'center',
display: 'flex',
flexGrow: 1,
minHeight: '100%'
}}
>
<Container maxWidth="md">
<Box
sx={{
alignItems: 'center',
display: 'flex',
flexDirection: 'column'
}}
>
<Box
sx={{
mb: 3,
textAlign: 'center'
}}
>
<img
alt="Under development"
src="/assets/errors/error-401.png"
style={{
display: 'inline-block',
maxWidth: '100%',
width: 400
}}
/>
</Box>
<Typography
align="center"
sx={{ mb: 3 }}
variant="h3"
>
403: You're not allowed to perform this action
</Typography>
<Typography
align="center"
color="text.secondary"
variant="body1"
>
You either tried to perform an action you're not authorized to do or you came here by mistake.
</Typography>
<Button
component={NextLink}
href="/"
startIcon={(
<SvgIcon fontSize="small">
<ArrowLeftIcon />
</SvgIcon>
)}
sx={{ mt: 3 }}
variant="contained"
>
Go back to dashboard
</Button>
</Box>
</Container>
</Box>
</>
);
export default Page;

View File

@ -1,290 +0,0 @@
import { useCallback, useMemo, useState } from 'react';
import Head from 'next/head';
import { subDays, subHours } from 'date-fns';
import ArrowDownOnSquareIcon from '@heroicons/react/24/solid/ArrowDownOnSquareIcon';
import ArrowUpOnSquareIcon from '@heroicons/react/24/solid/ArrowUpOnSquareIcon';
import PlusIcon from '@heroicons/react/24/solid/PlusIcon';
import { Box, Button, Container, Stack, SvgIcon, Typography } from '@mui/material';
import { useSelection } from 'src/hooks/use-selection';
import { Layout as DashboardLayout } from 'src/layouts/dashboard/layout';
import { CustomersTable } from 'src/sections/customer/customers-table';
import { CustomersSearch } from 'src/sections/customer/customers-search';
import { applyPagination } from 'src/utils/apply-pagination';
const now = new Date();
const data = [
{
id: '5e887ac47eed253091be10cb',
address: {
city: 'Cleveland',
country: 'USA',
state: 'Ohio',
street: '2849 Fulton Street'
},
avatar: '/assets/avatars/avatar-carson-darrin.png',
createdAt: subDays(subHours(now, 7), 1).getTime(),
email: 'carson.darrin@devias.io',
name: 'Carson Darrin',
phone: '304-428-3097'
},
{
id: '5e887b209c28ac3dd97f6db5',
address: {
city: 'Atlanta',
country: 'USA',
state: 'Georgia',
street: '1865 Pleasant Hill Road'
},
avatar: '/assets/avatars/avatar-fran-perez.png',
createdAt: subDays(subHours(now, 1), 2).getTime(),
email: 'fran.perez@devias.io',
name: 'Fran Perez',
phone: '712-351-5711'
},
{
id: '5e887b7602bdbc4dbb234b27',
address: {
city: 'North Canton',
country: 'USA',
state: 'Ohio',
street: '4894 Lakeland Park Drive'
},
avatar: '/assets/avatars/avatar-jie-yan-song.png',
createdAt: subDays(subHours(now, 4), 2).getTime(),
email: 'jie.yan.song@devias.io',
name: 'Jie Yan Song',
phone: '770-635-2682'
},
{
id: '5e86809283e28b96d2d38537',
address: {
city: 'Madrid',
country: 'Spain',
name: 'Anika Visser',
street: '4158 Hedge Street'
},
avatar: '/assets/avatars/avatar-anika-visser.png',
createdAt: subDays(subHours(now, 11), 2).getTime(),
email: 'anika.visser@devias.io',
name: 'Anika Visser',
phone: '908-691-3242'
},
{
id: '5e86805e2bafd54f66cc95c3',
address: {
city: 'San Diego',
country: 'USA',
state: 'California',
street: '75247'
},
avatar: '/assets/avatars/avatar-miron-vitold.png',
createdAt: subDays(subHours(now, 7), 3).getTime(),
email: 'miron.vitold@devias.io',
name: 'Miron Vitold',
phone: '972-333-4106'
},
{
id: '5e887a1fbefd7938eea9c981',
address: {
city: 'Berkeley',
country: 'USA',
state: 'California',
street: '317 Angus Road'
},
avatar: '/assets/avatars/avatar-penjani-inyene.png',
createdAt: subDays(subHours(now, 5), 4).getTime(),
email: 'penjani.inyene@devias.io',
name: 'Penjani Inyene',
phone: '858-602-3409'
},
{
id: '5e887d0b3d090c1b8f162003',
address: {
city: 'Carson City',
country: 'USA',
state: 'Nevada',
street: '2188 Armbrester Drive'
},
avatar: '/assets/avatars/avatar-omar-darboe.png',
createdAt: subDays(subHours(now, 15), 4).getTime(),
email: 'omar.darobe@devias.io',
name: 'Omar Darobe',
phone: '415-907-2647'
},
{
id: '5e88792be2d4cfb4bf0971d9',
address: {
city: 'Los Angeles',
country: 'USA',
state: 'California',
street: '1798 Hickory Ridge Drive'
},
avatar: '/assets/avatars/avatar-siegbert-gottfried.png',
createdAt: subDays(subHours(now, 2), 5).getTime(),
email: 'siegbert.gottfried@devias.io',
name: 'Siegbert Gottfried',
phone: '702-661-1654'
},
{
id: '5e8877da9a65442b11551975',
address: {
city: 'Murray',
country: 'USA',
state: 'Utah',
street: '3934 Wildrose Lane'
},
avatar: '/assets/avatars/avatar-iulia-albu.png',
createdAt: subDays(subHours(now, 8), 6).getTime(),
email: 'iulia.albu@devias.io',
name: 'Iulia Albu',
phone: '313-812-8947'
},
{
id: '5e8680e60cba5019c5ca6fda',
address: {
city: 'Salt Lake City',
country: 'USA',
state: 'Utah',
street: '368 Lamberts Branch Road'
},
avatar: '/assets/avatars/avatar-nasimiyu-danai.png',
createdAt: subDays(subHours(now, 1), 9).getTime(),
email: 'nasimiyu.danai@devias.io',
name: 'Nasimiyu Danai',
phone: '801-301-7894'
}
];
const useCustomers = (page, rowsPerPage) => {
return useMemo(
() => {
return applyPagination(data, page, rowsPerPage);
},
[page, rowsPerPage]
);
};
const useCustomerIds = (customers) => {
return useMemo(
() => {
return customers.map((customer) => customer.id);
},
[customers]
);
};
const Page = () => {
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
const customers = useCustomers(page, rowsPerPage);
const customersIds = useCustomerIds(customers);
const customersSelection = useSelection(customersIds);
const handlePageChange = useCallback(
(event, value) => {
setPage(value);
},
[]
);
const handleRowsPerPageChange = useCallback(
(event) => {
setRowsPerPage(event.target.value);
},
[]
);
return (
<>
<Head>
<title>
Customers | Devias Kit
</title>
</Head>
<Box
component="main"
sx={{
flexGrow: 1,
py: 8
}}
>
<Container maxWidth="xl">
<Stack spacing={3}>
<Stack
direction="row"
justifyContent="space-between"
spacing={4}
>
<Stack spacing={1}>
<Typography variant="h4">
Customers
</Typography>
<Stack
alignItems="center"
direction="row"
spacing={1}
>
<Button
color="inherit"
startIcon={(
<SvgIcon fontSize="small">
<ArrowUpOnSquareIcon />
</SvgIcon>
)}
>
Import
</Button>
<Button
color="inherit"
startIcon={(
<SvgIcon fontSize="small">
<ArrowDownOnSquareIcon />
</SvgIcon>
)}
>
Export
</Button>
</Stack>
</Stack>
<div>
<Button
startIcon={(
<SvgIcon fontSize="small">
<PlusIcon />
</SvgIcon>
)}
variant="contained"
>
Add
</Button>
</div>
</Stack>
<CustomersSearch />
<CustomersTable
count={data.length}
items={customers}
onDeselectAll={customersSelection.handleDeselectAll}
onDeselectOne={customersSelection.handleDeselectOne}
onPageChange={handlePageChange}
onRowsPerPageChange={handleRowsPerPageChange}
onSelectAll={customersSelection.handleSelectAll}
onSelectOne={customersSelection.handleSelectOne}
page={page}
rowsPerPage={rowsPerPage}
selected={customersSelection.selected}
/>
</Stack>
</Container>
</Box>
</>
);
};
Page.getLayout = (page) => (
<DashboardLayout>
{page}
</DashboardLayout>
);
export default Page;

View File

@ -22,7 +22,7 @@ const Page = () => {
const router = useRouter()
const auth = useAuth();
const [devices, setDevices] = useState([]);
const [deviceFound, setDeviceFound] = useState(false)
const [deviceFound, setDeviceFound] = useState(true)
const [pages, setPages] = useState(0);
const [page, setPage] = useState(null);
const [Loading, setLoading] = useState(true);
@ -104,6 +104,7 @@ const Page = () => {
const fetchDevicePerId = async (id) => {
setLoading(true)
setDeviceFound(true)
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("Authorization", auth.user.token);

449
frontend/src/pages/users.js Normal file
View File

@ -0,0 +1,449 @@
import { useCallback, useMemo, useState, useEffect } from 'react';
import Head from 'next/head';
import { subDays, subHours } from 'date-fns';
import ArrowDownOnSquareIcon from '@heroicons/react/24/solid/ArrowDownOnSquareIcon';
import ArrowUpOnSquareIcon from '@heroicons/react/24/solid/ArrowUpOnSquareIcon';
import PlusIcon from '@heroicons/react/24/solid/PlusIcon';
import { Box, Button, CircularProgress, Container, Dialog, DialogContent, DialogTitle, Stack, SvgIcon, Typography,
DialogActions,
TextField,
Backdrop,
} from '@mui/material';
import { useSelection } from 'src/hooks/use-selection';
import { Layout as DashboardLayout } from 'src/layouts/dashboard/layout';
import { CustomersTable } from 'src/sections/customer/customers-table';
import { CustomersSearch } from 'src/sections/customer/customers-search';
import { applyPagination } from 'src/utils/apply-pagination';
import { useAuth } from 'src/hooks/use-auth';
import { useRouter } from 'next/router';
import { is } from 'date-fns/locale';
import { set } from 'nprogress';
const Page = () => {
const auth = useAuth();
const router = useRouter();
const validateEmail = (email) => {
return email.match(
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
);
};
//const [page, setPage] = useState(0);
//const [rowsPerPage, setRowsPerPage] = useState(5);
const [loading, setLoading] = useState(true);
const [creatingNewUser, setCreatingNewUser] = useState(false);
const [users, setUsers] = useState([]);
const [selected, setSelected] = useState([]);
const [addDeviceDialogOpen, setAddDeviceDialogOpen] = useState(false);
const [newUserData, setNewUserData] = useState({});
const [isPasswordEmpty, setIsPasswordEmpty] = useState(false);
const [isEmailEmpty, setIsEmailEmpty] = useState(false);
const [isEmailExistent, setIsEmailExistent] = useState(false);
const deleteUser = (id) => {
console.log("request to delete user: ", id)
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("Authorization", auth.user.token);
var requestOptions = {
method: 'DELETE',
headers: myHeaders,
redirect: 'follow'
}
return fetch(process.env.NEXT_PUBLIC_REST_ENDPOINT + '/auth/delete/' + id, requestOptions)
.then(response => {
if (response.status === 401) {
router.push("/auth/login")
} else if (response.status === 403) {
return router.push("/403")
}
setUsers(users.filter(user => user.email !== id))
})
.catch(error => {
return console.error('Error:', error)
});
}
const fetchUsers = async () => {
console.log("fetching users data...")
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("Authorization", auth.user.token);
var requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
}
return fetch(process.env.NEXT_PUBLIC_REST_ENDPOINT + '/users', requestOptions)
.then(response => {
if (response.status === 401) {
router.push("/auth/login")
} else if (response.status === 403) {
return router.push("/403")
}
return response.json()
})
.then(json => {
console.log("users: ", json)
setUsers(json)
// setPages(json.pages + 1)
// setPage(json.page +1)
// setDevices(json.devices)
setLoading(false)
})
.catch(error => {
return console.error('Error:', error)
});
}
useEffect(() => {
// if (auth.user.token) {
// console.log("auth.user.token =", auth.user.token)
// }else{
// auth.user.token = localStorage.getItem("token")
// }
//console.log("auth.user.token =", auth.user.token)
fetchUsers()
}, []);
// const handlePageChange = useCallback(
// (event, value) => {
// setPage(value);
// },
// []
// );
// const handleRowsPerPageChange = useCallback(
// (event) => {
// setRowsPerPage(event.target.value);
// },
// []
// );
const createUser = async (data) => {
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("Authorization", auth.user.token);
var raw = JSON.stringify(data);
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: raw,
redirect: 'follow'
};
let result = await fetch(process.env.NEXT_PUBLIC_REST_ENDPOINT+"/auth/register", requestOptions)
if (result.status == 200) {
console.log("user created: deu boa raça !!")
}else if (result.status == 403) {
console.log("num tenx permissão, seu boca de sandália")
setCreatingNewUser(false)
return router.push("/403")
}else if (result.status == 401){
console.log("taix nem autenticado, sai fora oh")
setCreatingNewUser(false)
return router.push("/auth/login")
}else if (result.status == 409){
console.log("usuário já existe, seu boca de bagre")
setIsEmailExistent(true)
setCreatingNewUser(false)
return
}else if (result.status == 400){
console.log("faltou mandar dados jow")
setAddDeviceDialogOpen(false)
setNewUserData({})
setIsPasswordEmpty(false)
setIsEmailEmpty(false)
setIsEmailExistent(false)
setCreatingNewUser(false)
return
}else {
console.log("agora quebrasse ux córno mô quiridu")
const content = await result.json()
setCreatingNewUser(false)
throw new Error(content);
}
setAddDeviceDialogOpen(false)
data["_id"] = data.email
data["createdAt"] = new Date().toLocaleDateString('es-pa')
data["level"] = 0
setUsers([...users, data])
setNewUserData({})
setIsPasswordEmpty(false)
setIsEmailEmpty(false)
setIsEmailExistent(false)
setCreatingNewUser(false)
}
return (
<>
<Head>
<title>
Oktopus | Users
</title>
</Head>
<Box
component="main"
sx={{
flexGrow: 1,
py: 8
}}
>
<Container maxWidth="xl">
<Stack spacing={3}>
<Stack
direction="row"
justifyContent="space-between"
spacing={4}
>
<Stack spacing={1}>
<Typography variant="h4">
Users
</Typography>
<Stack
alignItems="center"
direction="row"
spacing={1}
>
{/* <Button
color="inherit"
startIcon={(
<SvgIcon fontSize="small">
<ArrowUpOnSquareIcon />
</SvgIcon>
)}
>
Import
</Button> */}
{/* <Button
color="inherit"
startIcon={(
<SvgIcon fontSize="small">
<ArrowDownOnSquareIcon />
</SvgIcon>
)}
>
Export
</Button> */}
</Stack>
</Stack>
<div>
<Button
startIcon={(
<SvgIcon fontSize="small">
<PlusIcon />
</SvgIcon>
)}
variant="contained"
onClick={() => {
setAddDeviceDialogOpen(true)
}}
>
Add
</Button>
</div>
</Stack>
{/* <CustomersSearch /> */}
{users && !loading ?
<CustomersTable
count={users.length}
items={users}
//onDeselectAll={customersSelection.handleDeselectAll}
onDeselectOne={(id) => {
setSelected(selected.filter((item) => item !== id))
}}
//onPageChange={handlePageChange}
//onRowsPerPageChange={handleRowsPerPageChange}
//onSelectAll={customersSelection.handleSelectAll}
onSelectOne={(id) => {
setSelected([...selected, id])
console.log("added user " + id + " to selected array")
}}
//page={page}
//rowsPerPage={rowsPerPage}
deleteUser={deleteUser}
selected={selected}
/> :
<CircularProgress></CircularProgress>
}
</Stack>
</Container>
</Box>
<Dialog
open={addDeviceDialogOpen}
onClose={() => {
setAddDeviceDialogOpen(false)
setIsEmailEmpty(false)
setIsEmailExistent(false)
setIsPasswordEmpty(false)
setNewUserData({})
}}
>
<DialogTitle>Create User</DialogTitle>
<DialogContent>
<Stack
alignItems="center"
direction="row"
spacing={2}
>
<TextField
inputProps={{
form: {
autocomplete: 'new-password',
},
}}
// focused={isEmailEmpty}
// color={isEmailEmpty ? "error" : "primary"}
helperText={isEmailEmpty ? "Email error" : (isEmailExistent ? "Email already exists" : "")}
autoFocus
required
margin="dense"
id="email"
name="email"
label="Email Address"
type="email"
fullWidth
onChange={
(event) => {
setNewUserData({...newUserData, email: event.target.value})
}
}
variant="standard">
</TextField>
<TextField
// focused={isPasswordEmpty}
//color={isPasswordEmpty ? "error" : "primary"}
helperText={isPasswordEmpty ? "Password cannot be empty" : ""}
autoFocus
required
margin="dense"
id="password"
name="password"
label="Password"
type="password"
autoComplete='new-password'
fullWidth
onChange={
(event) => {
setNewUserData({...newUserData, password: event.target.value})
}
}
variant="standard">
</TextField>
</Stack>
<Stack
alignItems="center"
direction="row"
spacing={2}
>
<TextField
inputProps={{
form: {
autocomplete: 'off',
},
}}
autoFocus
margin="dense"
id="name"
name="name"
label="Full Name"
type="name"
fullWidth
variant="standard"
onChange={
(event) => {
setNewUserData({...newUserData, name: event.target.value})
}
}
>
</TextField>
<TextField
inputProps={{
form: {
autocomplete: 'off',
},
}}
autoComplete="off"
autoFocus
margin="dense"
id="phone"
name="phone"
label="Phone Number"
type="phone"
fullWidth
variant="standard"
onChange={
(event) => {
setNewUserData({...newUserData, phone: event.target.value})
}
}
>
</TextField>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={
() => {
setAddDeviceDialogOpen(false)
setIsEmailEmpty(false)
setIsEmailExistent(false)
setIsPasswordEmpty(false)
setNewUserData({})
}
}>Cancel</Button>
<Button onClick={()=>{
console.log("new user data: ", newUserData)
if (newUserData.password === undefined || newUserData.password === "") {
setIsPasswordEmpty(true)
return
} else{
setIsPasswordEmpty(false)
}
if (newUserData.email === undefined || newUserData.email === "") {
setIsEmailEmpty(true)
return
} else if(!validateEmail(newUserData.email)){
setIsEmailEmpty(true)
return
}else{
setIsEmailEmpty(false)
}
setIsEmailExistent(false)
setCreatingNewUser(true)
createUser(newUserData)
}}>Confirm</Button>
</DialogActions>
{
<Backdrop
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
open={creatingNewUser}
>
<CircularProgress color="inherit" />
</Backdrop>
}
</Dialog>
</>
);
};
Page.getLayout = (page) => (
<DashboardLayout>
{page}
</DashboardLayout>
);
export default Page;

View File

@ -1,21 +1,31 @@
import PropTypes from 'prop-types';
import { format } from 'date-fns';
import {
Avatar,
Box,
Card,
Checkbox,
Icon,
Stack,
Tab,
Table,
TableBody,
TableCell,
TableHead,
TablePagination,
//TablePagination,
TableRow,
Typography
Typography,
SvgIcon,
Dialog,
DialogActions,
DialogTitle,
DialogContent,
DialogContentText,
Button
} from '@mui/material';
import { Scrollbar } from 'src/components/scrollbar';
import { getInitials } from 'src/utils/get-initials';
import TrashIcon from '@heroicons/react/24/outline/TrashIcon';
import { useState } from 'react';
export const CustomersTable = (props) => {
const {
@ -27,13 +37,17 @@ export const CustomersTable = (props) => {
onRowsPerPageChange,
onSelectAll,
onSelectOne,
deleteUser,
page = 0,
rowsPerPage = 0,
selected = []
} = props;
const selectedSome = (selected.length > 0) && (selected.length < items.length);
const selectedAll = (items.length > 0) && (selected.length === items.length);
// const selectedSome = (selected.length > 0) && (selected.length < items.length);
// const selectedAll = (items.length > 0) && (selected.length === items.length);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [userToDelete, setUserToDelete] = useState("")
return (
<Card>
@ -42,8 +56,8 @@ export const CustomersTable = (props) => {
<Table>
<TableHead>
<TableRow>
<TableCell padding="checkbox">
<Checkbox
{/* <TableCell padding="checkbox"> */}
{/* <Checkbox
checked={selectedAll}
indeterminate={selectedSome}
onChange={(event) => {
@ -53,55 +67,60 @@ export const CustomersTable = (props) => {
onDeselectAll?.();
}
}}
/>
</TableCell>
<TableCell>
/> */}
{/* </TableCell> */}
<TableCell sx={{marginLeft:"30px"}}>
Name
</TableCell>
<TableCell>
Email
</TableCell>
<TableCell>
{/* <TableCell>
Location
</TableCell>
</TableCell> */}
<TableCell>
Phone
</TableCell>
<TableCell>
Signed Up
Created At
</TableCell>
<TableCell>
Level
</TableCell>
<TableCell>
Actions
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{items.map((customer) => {
const isSelected = selected.includes(customer.id);
const createdAt = format(customer.createdAt, 'dd/MM/yyyy');
const isSelected = selected.includes(customer._id);
return (
<TableRow
hover
key={customer.id}
key={customer._id}
selected={isSelected}
>
<TableCell padding="checkbox">
<Checkbox
{/* <TableCell padding="checkbox"> */}
{/* <Checkbox
checked={isSelected}
onChange={(event) => {
if (event.target.checked) {
onSelectOne?.(customer.id);
console.log(customer._id+" is selected");
onSelectOne(customer._id);
} else {
onDeselectOne?.(customer.id);
onDeselectOne(customer._id);
}
}}
/>
</TableCell>
<TableCell>
/> */}
{/* </TableCell> */}
<TableCell align="center" sx={{margin: 'auto', textAlign: 'center'}}>
<Stack
alignItems="center"
direction="row"
spacing={2}
>
<Avatar src={customer.avatar}>
<Avatar src={customer.avatar ? customer.avatar : "/assets/avatars/default-avatar.png"}>
{getInitials(customer.name)}
</Avatar>
<Typography variant="subtitle2">
@ -112,14 +131,33 @@ export const CustomersTable = (props) => {
<TableCell>
{customer.email}
</TableCell>
<TableCell>
{customer.address.city}, {customer.address.state}, {customer.address.country}
</TableCell>
{/* <TableCell>
{customer.address}
</TableCell> */}
<TableCell>
{customer.phone}
</TableCell>
<TableCell>
{createdAt}
{customer.createdAt}
</TableCell>
<TableCell>
{customer.level == 1 ? "Admin" : "User"}
</TableCell>
<TableCell>
{ customer.level == 0 ? <Button
onClick={() => {
console.log("delete user: ", customer._id)
setUserToDelete(customer.email);
setShowDeleteDialog(true);
}}
><SvgIcon
color="action"
fontSize="small"
sx={{ cursor: 'pointer'}}
>
<TrashIcon
></TrashIcon>
</SvgIcon></Button>: <span></span>}
</TableCell>
</TableRow>
);
@ -128,15 +166,43 @@ export const CustomersTable = (props) => {
</Table>
</Box>
</Scrollbar>
<TablePagination
{/* <TablePagination
component="div"
count={count}
onPageChange={onPageChange}
onRowsPerPageChange={onRowsPerPageChange}
page={page}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={[5, 10, 25]}
/>
//onPageChange={onPageChange}
//onRowsPerPageChange={onRowsPerPageChange}
//page={page}
//rowsPerPage={rowsPerPage}
//rowsPerPageOptions={[5, 10, 25]}
/> */}
<Dialog
open={showDeleteDialog}
onClose={() => setShowDeleteDialog(false)}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{"Delete User"}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete this user?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => {
setShowDeleteDialog(false)
setUserToDelete("")
}} color="primary">
Cancel
</Button>
<Button onClick={() => {
deleteUser(userToDelete);
setShowDeleteDialog(false);
setUserToDelete("")
}} color="primary" autoFocus>
Delete
</Button>
</DialogActions>
</Dialog>
</Card>
);
};
@ -147,10 +213,11 @@ CustomersTable.propTypes = {
onDeselectAll: PropTypes.func,
onDeselectOne: PropTypes.func,
onPageChange: PropTypes.func,
onRowsPerPageChange: PropTypes.func,
//onRowsPerPageChange: PropTypes.func,
onSelectAll: PropTypes.func,
onSelectOne: PropTypes.func,
page: PropTypes.number,
rowsPerPage: PropTypes.number,
deleteUser: PropTypes.func,
//page: PropTypes.number,
//rowsPerPage: PropTypes.number,
selected: PropTypes.array
};

View File

@ -79,7 +79,7 @@ export const OverviewLatestOrders = (props) => {
hover
key={order.SN}
>
<TableCell TableCell align="center">
<TableCell align="center">
{order.SN}
</TableCell>
<TableCell>
@ -97,18 +97,17 @@ export const OverviewLatestOrders = (props) => {
</SeverityPill>
</TableCell>
<TableCell>
{ order.Status == 2 && (order.Mqtt == 0 && order.Websockets == 0 && order.Stomp == 0) ? <span></span>: <SvgIcon
{ order.Mqtt == 0 && order.Websockets == 0 && order.Stomp == 0 ? <span></span>: <Button>
<SvgIcon
fontSize="small"
sx={{cursor: order.Status == 2 && 'pointer'}}
onClick={()=>{
if (order.Status == 2){
router.push("devices/"+order.SN+"/discovery")
}
}
}
>
<ArrowTopRightOnSquareIcon />
</SvgIcon>}
</SvgIcon></Button>}
</TableCell>
</TableRow>
);