move sessions component to security

This commit is contained in:
SrGooglo 2023-10-10 12:16:01 +00:00
parent ca90786e92
commit eec2f4b1f7
10 changed files with 491 additions and 239 deletions

View File

@ -0,0 +1,26 @@
export default () => <svg viewBox="0 0 48 48">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-400.000000, -1043.000000)">
<g transform="translate(400.000000, 1043.000000)">
<path d="M5.7954035,8.36130434 C16.9522782,-4.62351526 37.639151,-2.06037988 45.3727574,13.1072081 C39.9288251,13.1091897 31.4040328,13.1055761 26.786937,13.1072081 C23.4382318,13.1083738 21.2761308,13.0322537 18.9347285,14.2648621 C16.1820632,15.7138239 14.1051274,18.3997073 13.3801164,21.5544341 L5.7954035,8.36130434 L5.7954035,8.36130434 Z"
fill="currentColor"
/>
<path
d="M16.015461,23.9991346 C16.015461,28.3998753 19.5936811,31.9800817 23.9919804,31.9800817 C28.3901632,31.9800817 31.9683834,28.3998753 31.9683834,23.9991346 C31.9683834,19.5985104 28.3901632,16.0181875 23.9919804,16.0181875 C19.5936811,16.0181875 16.015461,19.5985104 16.015461,23.9991346 L16.015461,23.9991346 Z"
fill="currentColor"
/>
<path
d="M27.0876366,34.4456482 C22.6105798,35.7761751 17.371347,34.3006354 14.5014777,29.3468879 C12.3108329,25.5655987 6.52286114,15.4823164 3.89206021,10.8973955 C-5.32185953,25.0194695 2.61924235,44.2642006 19.3464574,47.5489026 L27.0876366,34.4456482 L27.0876366,34.4456482 Z"
fill="currentColor"
/>
<path
d="M31.4014697,16.0181875 C35.1303309,19.4863704 35.9427207,25.102234 33.4168909,29.4566966 C31.5138971,32.7374352 25.4402549,42.9884614 22.4966379,47.9523505 C39.730883,49.0147671 52.2944399,32.1238121 46.6195946,16.0181875 L31.4014697,16.0181875 L31.4014697,16.0181875 Z"
fill="currentColor"
/>
</g>
</g>
</g>
</svg >

View File

@ -0,0 +1,3 @@
export default () => <svg fill="currentColor" viewBox="0 0 24 24" role="img">
<path d="M20.452 3.445a11.002 11.002 0 0 0-2.482-1.908C16.944.997 15.098.093 12.477.032c-.734-.017-1.457.03-2.174.144-.72.114-1.398.292-2.118.56-1.017.377-1.996.975-2.574 1.554.583-.349 1.476-.733 2.55-.992a10.083 10.083 0 0 1 3.729-.167c2.341.34 4.178 1.381 5.48 2.625a8.066 8.066 0 0 1 1.298 1.587c1.468 2.382 1.33 5.376.184 7.142-.85 1.312-2.67 2.544-4.37 2.53-.583-.023-1.438-.152-2.25-.566-2.629-1.343-3.021-4.688-1.118-6.306-.632-.136-1.82.13-2.646 1.363-.742 1.107-.7 2.816-.242 4.028a6.473 6.473 0 0 1-.59-1.895 7.695 7.695 0 0 1 .416-3.845A8.212 8.212 0 0 1 9.45 5.399c.896-1.069 1.908-1.72 2.75-2.005-.54-.471-1.411-.738-2.421-.767C8.31 2.583 6.327 3.061 4.7 4.41a8.148 8.148 0 0 0-1.976 2.414c-.455.836-.691 1.659-.697 1.678.122-1.445.704-2.994 1.248-4.055-.79.413-1.827 1.668-2.41 3.042C.095 9.37-.2 11.608.14 13.989c.966 5.668 5.9 9.982 11.843 9.982C18.62 23.971 24 18.591 24 11.956a11.93 11.93 0 0 0-3.548-8.511z" />
</svg>

View File

@ -0,0 +1,34 @@
export default () => {
return <svg
viewBox="0 0 308.1161 511.4894"
>
<rect
x="10"
y="10"
width="288.1161"
height="491.4894"
rx="34"
ry="34"
style={{
fill: "none",
stroke: "currentColor",
strokeLinecap: "round",
strokeLinejoin: "round",
strokeWidth: "2rem",
}}
/>
<line
x1="85.8071"
y1="449.5339"
x2="222.3091"
y2="449.5339"
style={{
fill: "none",
stroke: "currentColor",
strokeLinecap: "round",
strokeLinejoin: "round",
strokeWidth: "2rem",
}}
/>
</svg>
}

View File

@ -0,0 +1,145 @@
import React from "react"
import * as antd from "antd"
import SessionModel from "models/session"
import classnames from "classnames"
import moment from "moment"
import UAParser from "ua-parser-js"
import { Icons } from "components/Icons"
import ChromeIcon from "./icons/chrome"
import MobileIcon from "./icons/mobile"
import FirefoxIcon from "./icons/firefox"
import "./index.less"
const DeviceIcon = (props) => {
if (!props.ua) {
return null
}
if (props.ua.ua === "capacitor") {
return <MobileIcon />
}
switch (props.ua.browser.name) {
case "Chrome": {
return <ChromeIcon />
}
case "Firefox": {
return <FirefoxIcon />
}
default: {
return <Icons.Globe />
}
}
}
const SessionItem = (props) => {
const { session } = props
const [collapsed, setCollapsed] = React.useState(true)
const onClickCollapse = () => {
setCollapsed((prev) => {
return !prev
})
}
const onClickRevoke = () => {
// if (typeof props.onClickRevoke === "function") {
// props.onClickRevoke(session)
// }
}
const isCurrentSession = React.useMemo(() => {
const currentUUID = SessionModel.session_uuid
return session.session_uuid === currentUUID
})
const ua = React.useMemo(() => {
return UAParser(session.client)
})
console.log(session, ua)
return <div
className={classnames(
"security_sessions_list_item_wrapper",
{
["collapsed"]: collapsed
}
)}
>
<div
id={session._id}
key={props.key}
className="security_sessions_list_item"
onClick={onClickCollapse}
>
<div className="security_sessions_list_item_icon">
<DeviceIcon
ua={ua}
/>
</div>
<antd.Badge dot={isCurrentSession}>
<div className="security_sessions_list_item_info">
<div className="security_sessions_list_item_title">
<h3><Icons.Tag /> {session.session_uuid}</h3>
</div>
<div className="security_sessions_list_item_info_details">
<div className="security_sessions_list_item_info_details_item">
<Icons.Clock />
<span>
{moment(session.date).format("DD/MM/YYYY HH:mm")}
</span>
</div>
<div className="security_sessions_list_item_info_details_item">
<Icons.IoMdLocate />
<span>
{session.ip_address}
</span>
</div>
</div>
</div>
</antd.Badge>
</div>
<div className="security_sessions_list_item_extra-body">
<div className="security_sessions_list_item_actions">
<antd.Button
onClick={onClickRevoke}
danger
size="small"
>
Revoke
</antd.Button>
</div>
<div className="security_sessions_list_item_info_details_item">
<Icons.MdDns />
<span>
{session.location}
</span>
</div>
{
ua.device.vendor && <div className="security_sessions_list_item_info_details_item">
<Icons.Cpu />
<span>
{ua.device.vendor} | {ua.device.model}
</span>
</div>
}
</div>
</div>
}
export default SessionItem

View File

@ -0,0 +1,167 @@
@item_border_radius: 12px;
.security_sessions_list_item_wrapper {
display: flex;
flex-direction: column;
&.collapsed {
.security_sessions_list_item_extra-body {
height: 0;
transform: translateY(-100%);
padding-top: 0;
padding-bottom: 0;
}
.security_sessions_list_item {
border-radius: @item_border_radius;
border-bottom-color: transparent;
}
}
}
.security_sessions_list_item {
position: relative;
z-index: 100;
display: flex;
flex-direction: row;
align-items: center;
border-radius: @item_border_radius @item_border_radius 0 0;
border-bottom: 1px var(--border-color) solid;
background-color: var(--background-color-primary);
transition: all 150ms ease-in-out;
padding: 5px;
width: 100%;
overflow: hidden;
h1,
h2,
h3,
h4,
h5,
span {
margin: 0 !important;
}
svg {
margin: 0 !important;
}
gap: 10px;
.security_sessions_list_item_icon {
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
padding: 10px;
background-color: var(--background-color-accent);
border-radius: 12px;
svg {
width: 30px;
height: 30px;
}
}
.security_sessions_list_item_info {
display: flex;
flex-direction: column;
gap: 5px;
span {
user-select: text;
}
.security_sessions_list_item_title {
display: inline-flex;
align-items: center;
justify-content: space-between;
font-size: 0.7rem;
font-weight: 600;
gap: 20px;
}
.security_sessions_list_item_info_details {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 7px;
}
}
.security_sessions_list_item_actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
gap: 10px;
}
}
.security_sessions_list_item_extra-body {
position: relative;
z-index: 99;
display: inline-flex;
flex-direction: row;
align-items: center;
width: 100%;
gap: 10px;
background-color: var(--background-color-primary);
transform: translateY(calc(-1 * calc(@item_border_radius / 2)));
padding: 5px 10px;
padding-top: calc(calc(@item_border_radius / 2) + 5px);
transition: all 150ms ease-in-out;
border-radius: 0 0 @item_border_radius @item_border_radius;
overflow: hidden;
}
.security_sessions_list_item_info_details_item {
display: flex;
flex-direction: row;
align-items: center;
gap: 5px;
font-size: 0.8rem;
font-weight: 400;
svg {
margin: 0 !important;
}
}

View File

@ -1,13 +1,79 @@
import React from "react" import React from "react"
import { Button } from "antd" import * as antd from "antd"
import SessionModel from "models/session"
export default (props) => { import SessionItem from "../sessionItem"
return <Button
onClick={() => { import "./index.less"
app.location.push("/security/sessions")
props.ctx.close() export default () => {
}} const [loading, setLoading] = React.useState(true)
> const [sessions, setSessions] = React.useState([])
Check activity const [sessionsPage, setSessionsPage] = React.useState(1)
</Button> const [itemsPerPage, setItemsPerPage] = React.useState(3)
const loadSessions = async () => {
setLoading(true)
const response = await SessionModel.getAllSessions().catch((err) => {
console.error(err)
app.message.error("Failed to load sessions")
return null
})
console.log(response)
if (response) {
setSessions(response)
}
setLoading(false)
}
const onClickRevoke = async (session) => {
console.log(session)
app.message.warning("Not implemented yet")
}
const onClickRevokeAll = async () => {
app.message.warning("Not implemented yet")
}
React.useEffect(() => {
loadSessions()
}, [])
if (loading) {
return <antd.Skeleton active />
}
const offset = (sessionsPage - 1) * itemsPerPage
const slicedItems = sessions.slice(offset, offset + itemsPerPage)
return <div className="security_sessions">
<div className="security_sessions_list">
{
slicedItems.map((session) => {
return <SessionItem
key={session._id}
session={session}
onClickRevoke={onClickRevoke}
/>
})
}
<antd.Pagination
onChange={(page) => {
setSessionsPage(page)
}}
total={sessions.length}
showTotal={(total) => {
return `${total} Sessions`
}}
simple
/>
</div>
</div>
} }

View File

@ -0,0 +1,40 @@
.security_sessions {
display: flex;
flex-direction: column;
width: 100%;
.security_sessions_list {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
gap: 5px;
.ant-pagination {
display: flex;
flex-direction: row;
align-items: center;
li {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
button {
width: 24px!important;
svg {
margin: 0 !important;
}
}
}
}
}

View File

@ -1,8 +1,4 @@
import React from "react"
import loadable from "@loadable/component" import loadable from "@loadable/component"
import AuthModel from "models/auth"
// TODO: Make logout button require a valid session to be not disabled
export default { export default {
id: "security", id: "security",
@ -33,7 +29,6 @@ export default {
"description": "Manage your active sessions", "description": "Manage your active sessions",
"icon": "Monitor", "icon": "Monitor",
"component": loadable(() => import("../components/sessions")), "component": loadable(() => import("../components/sessions")),
"storaged": false
} }
] ]
} }

View File

@ -1,128 +0,0 @@
import React from "react"
import * as antd from "antd"
import SessionModel from "models/session"
import moment from "moment"
import { Icons } from "components/Icons"
import "./index.less"
const SessionItem = (props) => {
const { session } = props
const [isCurrent, setIsCurrent] = React.useState(false)
const onClickRevoke = () => {
if (typeof props.onClickRevoke === "function") {
props.onClickRevoke(session)
}
}
React.useEffect(() => {
const currentUUID = SessionModel.session_uuid
if (currentUUID === session.session_uuid) {
setIsCurrent(true)
}
}, [])
return <div
id={session._id}
key={props.key}
className="sessionItem"
>
<div className="sessionItem_info">
<div className="sessionItem_info_title">
<h3><Icons.Tag /> {session.session_uuid}</h3>
{
isCurrent && <antd.Badge dot>
Current
</antd.Badge>
}
</div>
<div className="sessionItem_info_details">
<div className="sessionItem_info_details_detail">
<Icons.Navigation /> <span>{session.location}</span>
</div>
<div className="sessionItem_info_details_detail">
<Icons.Clock />
<span>
{moment(session.date).format("DD/MM/YYYY HH:mm")}
</span>
</div>
</div>
</div>
<div className="sessionItem_actions">
<antd.Button
onClick={onClickRevoke}
danger
>
Revoke
</antd.Button>
</div>
</div>
}
export default () => {
const [loading, setLoading] = React.useState(true)
const [sessions, setSessions] = React.useState([])
const loadSessions = async () => {
setLoading(true)
const response = await SessionModel.getAllSessions().catch((err) => {
console.error(err)
app.message.error("Failed to load sessions")
return null
})
console.log(response)
if (response) {
setSessions(response)
}
setLoading(false)
}
const onClickRevoke = async (session) => {
console.log(session)
app.message.warning("Not implemented yet")
}
const onClickRevokeAll = async () => {
app.message.warning("Not implemented yet")
}
React.useEffect(() => {
loadSessions()
}, [])
if (loading) {
return <antd.Skeleton active />
}
return <div className="sessions">
<div className="sessions_header">
<h1>Generated Sessions</h1>
<antd.Button
onClick={onClickRevokeAll}
danger
>
Revoke all sessions
</antd.Button>
</div>
<div className="sessions_list">
{sessions.map((session, index) => {
return <SessionItem
key={index}
session={session}
onClickRevoke={onClickRevoke}
/>
})}
</div>
</div>
}

View File

@ -1,96 +0,0 @@
.sessions {
display: flex;
flex-direction: column;
.sessions_header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h1 {
font-size: 1.5rem;
}
}
.sessions_list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
.sessionItem {
display: flex;
flex-direction: column;
padding: 10px;
border-radius: 12px;
background-color: var(--background-color-accent);
margin-bottom: 10px;
width: 35vw;
h1,
h2,
h3,
h4,
h5,
span {
margin: 0 !important;
}
.sessionItem_info {
display: flex;
flex-direction: column;
gap: 10px;
.sessionItem_info_title {
display: inline-flex;
align-items: center;
justify-content: space-between;
font-size: 1rem;
font-weight: 600;
}
.sessionItem_info_details {
display: inline-flex;
flex-direction: row;
align-items: center;
font-size: 0.8rem;
font-weight: 400;
.sessionItem_info_details_detail {
display: flex;
flex-direction: row;
align-items: center;
margin-right: 10px;
}
}
}
.sessionItem_actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
gap: 10px;
}
}
}
}