mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
initialize basic scatfold
This commit is contained in:
parent
f6ce583c52
commit
7b3b16e8ab
48
.gitignore
vendored
Executable file → Normal file
48
.gitignore
vendored
Executable file → Normal file
@ -1,26 +1,28 @@
|
|||||||
# dependencies
|
# Secrets
|
||||||
/node_modules
|
/**/**/.env
|
||||||
/npm-debug.log*
|
/**/**/origin.server
|
||||||
/yarn-error.log
|
/**/**/server.manifest
|
||||||
/yarn.lock
|
/**/**/server.registry
|
||||||
|
|
||||||
# production
|
# Trash
|
||||||
/build
|
/**/**/.crash.log
|
||||||
/dist
|
/**/**/.tmp
|
||||||
/out
|
/**/**/.cache
|
||||||
|
/**/**/out
|
||||||
|
/**/**/.out
|
||||||
|
/**/**/dist
|
||||||
|
/**/**/node_modules
|
||||||
|
/**/**/corenode_modules
|
||||||
|
/**/**/.DS_Store
|
||||||
|
/**/**/package-lock.json
|
||||||
|
/**/**/yarn.lock
|
||||||
|
/**/**/.evite
|
||||||
|
|
||||||
# umi
|
# Logs
|
||||||
/packages/**/src/.umi
|
/**/**/npm-debug.log*
|
||||||
/packages/**/src/.umi-production
|
/**/**/yarn-error.log
|
||||||
/packages/**/src/.umi-test
|
/**/**/dumps.log
|
||||||
/packages/**/.env.local
|
/**/**/corenode.log
|
||||||
|
|
||||||
/packages/*/src/.umi
|
# Temporal configurations
|
||||||
/packages/*/src/.umi-production
|
/**/**/.aliaser
|
||||||
/packages/*/src/.umi-test
|
|
||||||
/packages/*/.env.local
|
|
||||||
|
|
||||||
/.env.local
|
|
||||||
|
|
||||||
/packages/*/node_modules
|
|
||||||
/packages/**/node_modules
|
|
@ -145,9 +145,6 @@ class App {
|
|||||||
RenderError: (props) => {
|
RenderError: (props) => {
|
||||||
return <RenderError {...props} />
|
return <RenderError {...props} />
|
||||||
},
|
},
|
||||||
initialization: () => {
|
|
||||||
return <h1>Initializing...</h1>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import config from 'config'
|
import config from 'config'
|
||||||
import { Bridge } from "linebridge/client"
|
import { Bridge } from "linebridge/client"
|
||||||
import { Session } from "models"
|
import { Session } from "models"
|
||||||
import io from "socket.io-client"
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
key: "apiBridge",
|
key: "apiBridge",
|
||||||
@ -10,15 +9,6 @@ export default {
|
|||||||
mutateContext: {
|
mutateContext: {
|
||||||
async initializeDefaultBridge() {
|
async initializeDefaultBridge() {
|
||||||
this.apiBridge = await this.createBridge()
|
this.apiBridge = await this.createBridge()
|
||||||
this.ws = io("http://localhost:9001/main", { transports: ["websocket"] })
|
|
||||||
|
|
||||||
this.ws.on("connect", () => {
|
|
||||||
console.log(this.ws.id)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.ws.on("connect_error", (...context) => {
|
|
||||||
console.log(...context)
|
|
||||||
})
|
|
||||||
|
|
||||||
window.app.apiBridge = this.apiBridge
|
window.app.apiBridge = this.apiBridge
|
||||||
},
|
},
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import styles from './404.less'
|
|
||||||
|
|
||||||
const Error404 = () => (
|
|
||||||
<div className={styles.error}>
|
|
||||||
|
|
||||||
<h1>OBA BLYAT</h1>
|
|
||||||
<p>
|
|
||||||
<strong>ERROR 404</strong>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default Error404
|
|
@ -1,55 +0,0 @@
|
|||||||
/* devanagari */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Poppins';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: local('Poppins Regular'), local('Poppins-Regular'), url(https://fonts.gstatic.com/s/poppins/v6/pxiEyp8kv8JHgFVrJJbecmNE.woff2) format('woff2');
|
|
||||||
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Poppins';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: local('Poppins Regular'), local('Poppins-Regular'), url(https://fonts.gstatic.com/s/poppins/v6/pxiEyp8kv8JHgFVrJJnecmNE.woff2) format('woff2');
|
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Poppins';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: local('Poppins Regular'), local('Poppins-Regular'), url(https://fonts.gstatic.com/s/poppins/v6/pxiEyp8kv8JHgFVrJJfecg.woff2) format('woff2');
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
background-color: transparent;
|
|
||||||
color: black;
|
|
||||||
text-align: center;
|
|
||||||
position: absolute;
|
|
||||||
top: 30%;
|
|
||||||
margin-top: -50px;
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -100px;
|
|
||||||
width: 200px;
|
|
||||||
|
|
||||||
:global .anticon {
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-family: 'Poppins', sans-serif;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-family: 'Poppins', sans-serif;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,237 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import * as antd from 'antd'
|
|
||||||
import { verbosity, objectToArrayMap } from '@nodecorejs/utils'
|
|
||||||
import * as Icons from 'components/Icons'
|
|
||||||
import { connect } from 'umi'
|
|
||||||
|
|
||||||
import { pathMatchRegexp, booleanFix } from 'core'
|
|
||||||
import HandleError from 'core/libs/errorhandler'
|
|
||||||
import GlobalBadges from 'schemas/badges_list.json'
|
|
||||||
import { Invalid, PostsFeed} from 'components'
|
|
||||||
import FollowButton from './components/follow'
|
|
||||||
|
|
||||||
import styles from './index.less'
|
|
||||||
|
|
||||||
export class UserLayout extends React.Component {
|
|
||||||
state = {
|
|
||||||
styleComponent: "UserLayout",
|
|
||||||
userString: pathMatchRegexp('/@/:id', location.pathname)[1],
|
|
||||||
layoutData: {
|
|
||||||
avatar: null,
|
|
||||||
cover: null,
|
|
||||||
about: null,
|
|
||||||
followed: null,
|
|
||||||
followers: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClickFollow(user_id) {
|
|
||||||
if (typeof (this.props.onFollow) !== "undefined") {
|
|
||||||
this.updateFollow(!booleanFix(this.state.layoutData.is_following))
|
|
||||||
|
|
||||||
this.props.onFollow(user_id, (callback) => {
|
|
||||||
this.updateFollow(callback)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFollow(to) {
|
|
||||||
let updated = this.state.layoutData
|
|
||||||
updated.is_following = to
|
|
||||||
this.setState({ layoutData: updated })
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { layoutData } = this.props
|
|
||||||
if (layoutData) {
|
|
||||||
this.setState({ layoutData: { ...this.state.layoutData, ...layoutData } })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderUserBadges() {
|
|
||||||
let { layoutData } = this.state
|
|
||||||
if (typeof(layoutData.user_tags) == "undefined") {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
let userTags = objectToArrayMap(layoutData.user_tags)
|
|
||||||
let renderTags = []
|
|
||||||
|
|
||||||
if (!userTags) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
userTags = JSON.parse(userTags[0].value.badges)
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!userTags) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
objectToArrayMap(GlobalBadges).forEach(e => {
|
|
||||||
if(userTags.includes(e.value.id)) {
|
|
||||||
renderTags.push(e.value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (Array.isArray(userTags)) {
|
|
||||||
return renderTags.map((element) => {
|
|
||||||
return(
|
|
||||||
<antd.Tooltip key={element.key ?? Math.random()} title={element.tip} >
|
|
||||||
<antd.Tag icon={React.createElement(Icons[element.icon]) ?? null} key={element.key ?? Math.random()} color={element.color ?? "default"} >
|
|
||||||
{element.title ?? "maybe"}
|
|
||||||
</antd.Tag>
|
|
||||||
</antd.Tooltip>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { styleComponent } = this.state
|
|
||||||
const toStyles = e => styles[`${styleComponent}_${e}`]
|
|
||||||
const { followers_count } = this.state.layoutData.details ?? {}
|
|
||||||
const isFollowed = booleanFix(this.state.layoutData.is_following)
|
|
||||||
|
|
||||||
if (!this.state.layoutData) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className={toStyles("wrapper")} >
|
|
||||||
<div className={toStyles("cover")}>
|
|
||||||
<img src={this.state.layoutData.cover} />
|
|
||||||
</div>
|
|
||||||
<div className={toStyles("header")}>
|
|
||||||
|
|
||||||
<div className={toStyles("avatar")}>
|
|
||||||
<antd.Avatar shape="square" src={this.state.layoutData.avatar} />
|
|
||||||
</div>
|
|
||||||
<div className={toStyles("title")}>
|
|
||||||
<div style={{ display: "flex", alignItems: "center", marginBottom: "7px" }} >
|
|
||||||
{/* {this.renderUserBadges()} */}
|
|
||||||
</div>
|
|
||||||
<antd.Tooltip title={`${followers_count ?? "Non-existent"} Followers`}>
|
|
||||||
<h1>{this.state.userString}</h1>
|
|
||||||
</antd.Tooltip>
|
|
||||||
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: '14px',
|
|
||||||
fontWeight: '100',
|
|
||||||
lineHeight: '0',
|
|
||||||
marginBottom: '5px',
|
|
||||||
}}
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: typeof(this.state.layoutData.about) == "string"? this.state.layoutData.about : null,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={toStyles("options")}>
|
|
||||||
<div><FollowButton onClick={() => { this.handleClickFollow(this.state.layoutData.user_id) }} followed={isFollowed} /></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={toStyles("content")}>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@connect(({ app }) => ({ app }))
|
|
||||||
export default class UserIndexer extends React.Component {
|
|
||||||
state = {
|
|
||||||
ErrorCode: null,
|
|
||||||
loading: true,
|
|
||||||
response: null,
|
|
||||||
layoutData: null
|
|
||||||
}
|
|
||||||
|
|
||||||
promiseState = async state => new Promise(resolve => this.setState(state, resolve));
|
|
||||||
|
|
||||||
handleClickFollow(user_id, callback) {
|
|
||||||
if (this.props.app.session_valid) {
|
|
||||||
const requestCallback = (callbackResponse) => {
|
|
||||||
if (callbackResponse.code == 200) {
|
|
||||||
const response = callbackResponse.response
|
|
||||||
const result =( response.follow ?? false) == "followed" ? true : false
|
|
||||||
if (typeof(response) !== "undefined") {
|
|
||||||
return callback(result)
|
|
||||||
}else{
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
return callback(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.dispatch({
|
|
||||||
type: "socket/use",
|
|
||||||
scope: "users",
|
|
||||||
invoke: "actions",
|
|
||||||
query: {
|
|
||||||
payload: {
|
|
||||||
userToken: this.props.app.session_token,
|
|
||||||
action: "follow",
|
|
||||||
user_id,
|
|
||||||
},
|
|
||||||
callback: requestCallback
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}else{
|
|
||||||
verbosity.log(`Needs auth to dispatch 'actions' event`)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const matchRegexp = pathMatchRegexp('/@/:id', location.pathname)
|
|
||||||
|
|
||||||
if (matchRegexp && this.props.app.session_valid) {
|
|
||||||
this.props.dispatch({
|
|
||||||
type: "user/get",
|
|
||||||
payload: {
|
|
||||||
from: "profileData",
|
|
||||||
username: matchRegexp[1]
|
|
||||||
},
|
|
||||||
callback: (callbackResponse) => {
|
|
||||||
if (callbackResponse.code == 200) {
|
|
||||||
this.setState({ loading: false, layoutData: callbackResponse.response })
|
|
||||||
} else {
|
|
||||||
this.setState({ ErrorCode: callbackResponse.code })
|
|
||||||
return HandleError({ code: callbackResponse.code, msg: "no message provided" })
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.setState({ ErrorCode: 140 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
if (this.state.ErrorCode) {
|
|
||||||
return <Invalid typeByCode={this.state.ErrorCode} messageProp1={location.pathname} />
|
|
||||||
}
|
|
||||||
if (this.state.loading) {
|
|
||||||
return <div style={{ display: "flex", width: "100%", justifyContent: "center", alignContent: "center" }}><antd.Card style={{ width: "100%" }} ><antd.Skeleton active /></antd.Card></div>
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<UserLayout onFollow={(...context) => {this.handleClickFollow(...context)}} layoutData={this.state.layoutData} />
|
|
||||||
<PostsFeed from="user" fromID={this.state.layoutData.user_id} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import styles from './index.less'
|
|
||||||
import classnames from 'classnames'
|
|
||||||
|
|
||||||
const FollowButton = (props) => {
|
|
||||||
return (
|
|
||||||
<a onClick={props.onClick} className={classnames(styles.button, {[styles.disabled]: !props.followed })}>
|
|
||||||
<span>{props.followed ? 'Following' : 'Follow'}</span>
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FollowButton
|
|
@ -1,32 +0,0 @@
|
|||||||
.button {
|
|
||||||
user-select: none;
|
|
||||||
width: 100px;
|
|
||||||
height: 30px;
|
|
||||||
padding: 5px 15px;
|
|
||||||
text-decoration: none;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #7e7e7e;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled{
|
|
||||||
&:hover {
|
|
||||||
border: none;
|
|
||||||
content: '';
|
|
||||||
color: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: linear-gradient(120deg, #6559ae, #ff7159, #6559ae);
|
|
||||||
background-size: 400% 400%;
|
|
||||||
animation: gradient 15s ease-in-out infinite, border 1s forwards ease-in-out reverse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes gradient {
|
|
||||||
0% { background-position: 14% 0%; }
|
|
||||||
50% { background-position: 87% 100%; }
|
|
||||||
100% { background-position: 14% 0%; }
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import * as antd from 'antd'
|
|
||||||
import { MoreOutlined } from 'components/Icons'
|
|
||||||
|
|
||||||
const moreMenu = (
|
|
||||||
<antd.Menu>
|
|
||||||
<antd.Menu.Item>__</antd.Menu.Item>
|
|
||||||
<antd.Menu.Item>__set2</antd.Menu.Item>
|
|
||||||
</antd.Menu>
|
|
||||||
)
|
|
||||||
|
|
||||||
const Menu = (props) => {
|
|
||||||
return (
|
|
||||||
<antd.Dropdown overlay={moreMenu}>
|
|
||||||
<MoreOutlined />
|
|
||||||
</antd.Dropdown>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Menu
|
|
@ -1,111 +0,0 @@
|
|||||||
.UserLayout_wrapper{
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
margin: auto;
|
|
||||||
overflow: overlay !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.UserLayout_header{
|
|
||||||
font-family: "Poppins", sans-serif;
|
|
||||||
color: #242424;
|
|
||||||
|
|
||||||
vertical-align: top;
|
|
||||||
display: flex;
|
|
||||||
padding: 20px;
|
|
||||||
|
|
||||||
height: auto;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
max-width: 600px;
|
|
||||||
max-height: 200px;
|
|
||||||
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-radius: 0 0 20px 20px;
|
|
||||||
transform: translate(0, -5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.UserLayout_cover{
|
|
||||||
display: flex;
|
|
||||||
align-content: center;
|
|
||||||
|
|
||||||
max-height: 400px;
|
|
||||||
max-width: 600px;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 15px 15px 0 0;
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.UserLayout_title{
|
|
||||||
color: #2d2d2d;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 20px;
|
|
||||||
line-height: 28px;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
word-break: break-all;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
span{
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.UserLayout_avatar {
|
|
||||||
transform: translate(-35px, -45px);
|
|
||||||
|
|
||||||
&>span {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mobile{
|
|
||||||
transform: translate(0,-90px);
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global {
|
|
||||||
.ant-avatar {
|
|
||||||
box-shadow: 13px 13px 17px 4px rgba(69, 69, 69, 0.151);
|
|
||||||
border-radius: 7px;
|
|
||||||
height: 120px;
|
|
||||||
width: 120px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.UserLayout_options{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
text-align: left;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.UserLayout_content{
|
|
||||||
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import Error404 from './404.js'
|
|
||||||
|
|
||||||
export default class PageIndexer extends React.Component {
|
|
||||||
render() {
|
|
||||||
const { location } = this.props
|
|
||||||
|
|
||||||
// By default return Error 404
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Error404 />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
181
packages/app/src/pages/account/components/editor/index.jsx
Normal file
181
packages/app/src/pages/account/components/editor/index.jsx
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import React from "react"
|
||||||
|
import debounce from "lodash/debounce"
|
||||||
|
import { Icons } from "components/Icons"
|
||||||
|
|
||||||
|
import classnames from "classnames"
|
||||||
|
import * as antd from "antd"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
export const EditAccountField = ({ id, component, props, header, handleChange, delay, defaultValue, allowEmpty }) => {
|
||||||
|
const [currentValue, setCurrentValue] = React.useState(defaultValue)
|
||||||
|
const [emittedValue, setEmittedValue] = React.useState(null)
|
||||||
|
|
||||||
|
const debouncedHandleChange = React.useCallback(
|
||||||
|
debounce((value) => handleChange({ id, value }), delay ?? 300),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleDiscard = (event) => {
|
||||||
|
if (typeof event !== "undefined") {
|
||||||
|
event.target.blur()
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentValue(defaultValue)
|
||||||
|
handleChange({ id, value: defaultValue })
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
debouncedHandleChange(currentValue)
|
||||||
|
}, [emittedValue])
|
||||||
|
|
||||||
|
const onChange = (event) => {
|
||||||
|
event.persist()
|
||||||
|
let { value } = event.target
|
||||||
|
|
||||||
|
if (typeof value === "string") {
|
||||||
|
if (value.length === 0) {
|
||||||
|
// if is not allowed to be empty, discard modifications
|
||||||
|
if (!allowEmpty) {
|
||||||
|
return handleDiscard(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentValue(value)
|
||||||
|
setEmittedValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (event) => {
|
||||||
|
if (event.keyCode === 27) {
|
||||||
|
// "escape" pressed, reset to default value
|
||||||
|
handleDiscard(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.app.eventBus.on("discardAllChanges", () => {
|
||||||
|
setCurrentValue(defaultValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
const RenderComponent = component
|
||||||
|
return (
|
||||||
|
<div key={id} className="edit_account_field">
|
||||||
|
{header ? header : null}
|
||||||
|
<RenderComponent {...props} value={currentValue} id={id} onChange={onChange} onKeyDown={handleKeyDown} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class EditAccount extends React.Component {
|
||||||
|
state = {
|
||||||
|
values: this.props.user ?? {},
|
||||||
|
changes: [],
|
||||||
|
loading: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
toogleLoading = (to) => {
|
||||||
|
this.setState({ loading: to ?? !this.state.loading })
|
||||||
|
}
|
||||||
|
|
||||||
|
onSaveDone = (error, data) => {
|
||||||
|
this.setState({ changes: [] })
|
||||||
|
this.toogleLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave = () => {
|
||||||
|
this.props.onSave(this.state.changes, this.onSaveDone)
|
||||||
|
}
|
||||||
|
|
||||||
|
discardAll = () => {
|
||||||
|
window.app.eventBus.emit("discardAllChanges")
|
||||||
|
this.setState({ changes: [] }) // clean changes after emit, cause controller wont handle changes
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange = (event) => {
|
||||||
|
const { id, value } = event
|
||||||
|
let changes = [...this.state.changes]
|
||||||
|
|
||||||
|
changes = changes.filter((change) => change.id !== id)
|
||||||
|
|
||||||
|
if (this.state.values[id] !== value) {
|
||||||
|
// changes detected
|
||||||
|
changes.push({ id, value })
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ changes })
|
||||||
|
}
|
||||||
|
|
||||||
|
renderActions = () => {
|
||||||
|
return (
|
||||||
|
<div className={classnames("edit_account_actions", { ["show"]: this.state.changes.length > 0 })}>
|
||||||
|
<div className="edit_account_actions_indicator">
|
||||||
|
{this.state.loading && <Icons.LoadingOutlined style={{ marginRight: "20px" }} />}
|
||||||
|
{this.state.changes.length} Changes
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<antd.Button disabled={this.state.loading} type="primary" onClick={this.onSave}>
|
||||||
|
Save
|
||||||
|
</antd.Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<antd.Button disabled={this.state.loading} onClick={this.discardAll}>
|
||||||
|
Discard all
|
||||||
|
</antd.Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { username, fullName, email } = this.state.values
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="edit_account">
|
||||||
|
{this.renderActions()}
|
||||||
|
<div className="edit_account_wrapper">
|
||||||
|
<div className="edit_account_category">
|
||||||
|
<h2>
|
||||||
|
<Icons.User /> Account information
|
||||||
|
</h2>
|
||||||
|
<EditAccountField
|
||||||
|
id="username"
|
||||||
|
defaultValue={username}
|
||||||
|
header={
|
||||||
|
<div>
|
||||||
|
<Icons.Tag /> Username
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
component={antd.Input}
|
||||||
|
props={{ placeholder: "Username", disabled: true }}
|
||||||
|
handleChange={this.handleChange}
|
||||||
|
/>
|
||||||
|
<EditAccountField
|
||||||
|
id="fullName"
|
||||||
|
defaultValue={fullName}
|
||||||
|
header={
|
||||||
|
<div>
|
||||||
|
<Icons.User /> Name
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
component={antd.Input}
|
||||||
|
props={{ placeholder: "Your full name" }}
|
||||||
|
handleChange={this.handleChange}
|
||||||
|
/>
|
||||||
|
<EditAccountField
|
||||||
|
id="email"
|
||||||
|
defaultValue={email}
|
||||||
|
header={
|
||||||
|
<div>
|
||||||
|
<Icons.Mail /> Email
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
component={antd.Input}
|
||||||
|
props={{ placeholder: "Your email address", type: "email" }}
|
||||||
|
handleChange={this.handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
60
packages/app/src/pages/account/components/editor/index.less
Normal file
60
packages/app/src/pages/account/components/editor/index.less
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
.edit_account{
|
||||||
|
overflow: hidden!important;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit_account_wrapper{
|
||||||
|
overflow: scroll;
|
||||||
|
> div {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit_account_actions {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px 0 20px 0;
|
||||||
|
background-color: rgba(221, 221, 221, 0.5);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
border-radius: 18px 18px 0 0;
|
||||||
|
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
display: flex;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit_account_actions_indicator{
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit_account_category{
|
||||||
|
> div {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit_account_field {
|
||||||
|
input:not(:focus){
|
||||||
|
color:rgba(105, 105, 105, 0.5)
|
||||||
|
}
|
||||||
|
}
|
3
packages/app/src/pages/account/components/index.js
Normal file
3
packages/app/src/pages/account/components/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { default as AccountEditor } from './editor'
|
||||||
|
export { default as SessionsView } from './sessionsView'
|
||||||
|
export { default as StatisticsView } from './statisticsView'
|
@ -0,0 +1,37 @@
|
|||||||
|
import React from "react"
|
||||||
|
import * as antd from "antd"
|
||||||
|
import { Icons } from "components/Icons"
|
||||||
|
import { Sessions } from "components"
|
||||||
|
|
||||||
|
export default class SessionsView extends React.Component {
|
||||||
|
signOutAll = () => {
|
||||||
|
antd.Modal.warning({
|
||||||
|
title: "Caution",
|
||||||
|
content: "This action will cause all sessions to be closed, you will have to log in again.",
|
||||||
|
onOk: () => {
|
||||||
|
//this.setState({ sessions: null })
|
||||||
|
window.app.eventBus.emit("destroyAllSessions")
|
||||||
|
},
|
||||||
|
okCancel: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { sessions, decodedToken } = this.props
|
||||||
|
|
||||||
|
if (!sessions) {
|
||||||
|
return <antd.Skeleton active />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="session_wrapper">
|
||||||
|
<Sessions current={decodedToken?.uuid} sessions={this.props.sessions} />
|
||||||
|
{sessions && (
|
||||||
|
<antd.Button onClick={this.signOutAll} type="danger">
|
||||||
|
Destroy all sessions
|
||||||
|
</antd.Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
export default class StatisticsView extends React.Component {
|
||||||
|
state = {
|
||||||
|
statistics: null
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount = async () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
181
packages/app/src/pages/account/index.jsx
Normal file
181
packages/app/src/pages/account/index.jsx
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import React from "react"
|
||||||
|
import * as antd from "antd"
|
||||||
|
import { Icons } from "components/Icons"
|
||||||
|
|
||||||
|
import { Roles } from "components"
|
||||||
|
import { AccountEditor, SessionsView, StatisticsView } from "./components"
|
||||||
|
|
||||||
|
import { Session } from "models"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
const api = window.app.apiBridge
|
||||||
|
|
||||||
|
const SelfViewComponents = {
|
||||||
|
sessionsView: SessionsView,
|
||||||
|
statisticsView: StatisticsView,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelfViewTabDecorators = {
|
||||||
|
sessionsView: (
|
||||||
|
<div>
|
||||||
|
<Icons.Key /> Sessions
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
statisticsView: (
|
||||||
|
<div>
|
||||||
|
<Icons.PieChart /> Statistics
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelfView extends React.Component {
|
||||||
|
renderComponents = () => {
|
||||||
|
const renderTagDecorator = (key) => {
|
||||||
|
if (typeof this.props.decorators[key] !== "undefined") {
|
||||||
|
return this.props.decorators[key]
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(this.props.components).map((key, index) => {
|
||||||
|
const Component = this.props.components[key]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<antd.Tabs.TabPane tab={renderTagDecorator(key)} key={index}>
|
||||||
|
<div key={key}>
|
||||||
|
<Component {...this.props.componentProps} />
|
||||||
|
</div>
|
||||||
|
</antd.Tabs.TabPane>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<antd.Tabs defaultActiveKey="0" centered>
|
||||||
|
{this.renderComponents()}
|
||||||
|
</antd.Tabs>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Account extends React.Component {
|
||||||
|
static bindApp = ["userController", "sessionController"]
|
||||||
|
|
||||||
|
state = {
|
||||||
|
isSelf: false,
|
||||||
|
user: null,
|
||||||
|
sessions: null
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount = async () => {
|
||||||
|
const token = Session.decodedToken
|
||||||
|
const location = window.app.history.location
|
||||||
|
const query = new URLSearchParams(location.search)
|
||||||
|
|
||||||
|
const requestedUser = location.state?.username ?? query.get("username") ?? token?.username
|
||||||
|
let state = this.state
|
||||||
|
|
||||||
|
if (requestedUser != null) {
|
||||||
|
if (token.username === requestedUser) {
|
||||||
|
state.isSelf = true
|
||||||
|
state.sessions = await this.props.contexts.app.sessionController.getAllSessions()
|
||||||
|
}
|
||||||
|
|
||||||
|
state.user = await this.props.contexts.app.userController.getData({ username: requestedUser })
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUpdateUserData = async (changes, callback) => {
|
||||||
|
const update = {}
|
||||||
|
if (Array.isArray(changes)) {
|
||||||
|
changes.forEach((change) => {
|
||||||
|
update[change.id] = change.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.put
|
||||||
|
.selfUser(update)
|
||||||
|
.then((data) => {
|
||||||
|
callback(false, data)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
callback(true, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
window.app.eventBus.emit("forceReloadUser")
|
||||||
|
}
|
||||||
|
|
||||||
|
openUserEdit = () => {
|
||||||
|
window.app.DrawerController.open("editAccount", AccountEditor, {
|
||||||
|
props: {
|
||||||
|
keyboard: false,
|
||||||
|
width: "45%",
|
||||||
|
bodyStyle: {
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
onSave: this.handleUpdateUserData,
|
||||||
|
user: this.state.user,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSelfActions = () => {
|
||||||
|
if (this.state.isSelf) {
|
||||||
|
return (
|
||||||
|
<div onClick={this.openUserEdit}>
|
||||||
|
<antd.Button>Edit</antd.Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const user = this.state.user
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return <antd.Skeleton active />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="account_wrapper">
|
||||||
|
<div className="account_card">
|
||||||
|
<img src={user.avatar} />
|
||||||
|
<div style={{ margin: "0 15px" }}>
|
||||||
|
{Boolean(user.fullName) ?
|
||||||
|
<>
|
||||||
|
<h1>{user.fullName}</h1>
|
||||||
|
<span>@{user.username}#{user._id}</span>
|
||||||
|
</> :
|
||||||
|
<>
|
||||||
|
<h1>@{user.username}</h1>
|
||||||
|
<span>#{user._id}</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<Roles roles={user.roles} />
|
||||||
|
{this.state.isSelf && this.renderSelfActions()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{this.state.isSelf && (
|
||||||
|
<SelfView
|
||||||
|
components={SelfViewComponents}
|
||||||
|
decorators={SelfViewTabDecorators}
|
||||||
|
componentProps={{
|
||||||
|
sessions: this.state.sessions,
|
||||||
|
user: this.state.user,
|
||||||
|
decodedToken: Session.decodedToken,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
30
packages/app/src/pages/account/index.less
Normal file
30
packages/app/src/pages/account/index.less
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
.account_wrapper{
|
||||||
|
> div {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.account_card{
|
||||||
|
display: flex;
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding: 12px;
|
||||||
|
color: #333;
|
||||||
|
border: 1px solid #33333396;
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 35px;
|
||||||
|
span {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +0,0 @@
|
|||||||
export default () => {
|
|
||||||
return <div></div>
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import * as antd from 'antd'
|
|
||||||
import { connect } from 'umi'
|
|
||||||
|
|
||||||
@connect(({ app }) => ({ app }))
|
|
||||||
export default class GuestSession extends React.PureComponent {
|
|
||||||
state = {
|
|
||||||
accept: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div style={{}}>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<h6><antd.Checkbox onChange={(e) => this.setState({accept: e.target.checked})} /> You have read and accept the TOS</h6>
|
|
||||||
<antd.Button disabled={!this.state.accept} onClick={() => { this.props.dispatch({ type: "guestLogin" }) }} > Continue </antd.Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,132 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { iatToString } from 'core'
|
|
||||||
import { router, ui } from 'core/libs'
|
|
||||||
|
|
||||||
import styles from './index.less'
|
|
||||||
import classnames from 'classnames'
|
|
||||||
|
|
||||||
import * as antd from 'antd'
|
|
||||||
import * as Icons from 'components/Icons'
|
|
||||||
|
|
||||||
import RegistrationForm from './register.js'
|
|
||||||
import NormalLoginForm from './login.js'
|
|
||||||
import GuestSession from './guest.js'
|
|
||||||
|
|
||||||
import config from 'config'
|
|
||||||
import { connect } from 'umi'
|
|
||||||
|
|
||||||
const types = [
|
|
||||||
{
|
|
||||||
id: "login",
|
|
||||||
key: 0,
|
|
||||||
renderText: `Sign in ${config.app.siteName}`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "register",
|
|
||||||
key: 1,
|
|
||||||
renderText: "Register"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "guest",
|
|
||||||
key: 2,
|
|
||||||
renderText: "Use guest session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "forgot",
|
|
||||||
key: 3,
|
|
||||||
renderText: "Forgotten password"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const typesRenderMap = {
|
|
||||||
0: <NormalLoginForm />,
|
|
||||||
1: <RegistrationForm />,
|
|
||||||
2: <GuestSession />
|
|
||||||
}
|
|
||||||
|
|
||||||
@connect(({ app }) => ({ app }))
|
|
||||||
class Login extends React.Component {
|
|
||||||
state = {
|
|
||||||
transition: false,
|
|
||||||
key: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
renderHelperButtons() {
|
|
||||||
return types.map((e) => {
|
|
||||||
return (
|
|
||||||
<antd.Button key={e.key} type="link" onClick={() => this.setState({ key: e.key })}>
|
|
||||||
{e.renderText || "Invalid"}
|
|
||||||
</antd.Button>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (this.props.app.session_valid) {
|
|
||||||
ui.notify.info('You have already logged into an account, you can change your account by logging in again')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
antd.Modal.destroyAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
renderAccountModal() {
|
|
||||||
const dispatchLogout = () => this.props.dispatch({ type: "app/logout" })
|
|
||||||
antd.Modal.confirm({
|
|
||||||
title: this.props.app.session_data.username,
|
|
||||||
icon: <antd.Avatar src={this.props.app.session_data.avatar} />,
|
|
||||||
onOk() {
|
|
||||||
router.push('/')
|
|
||||||
},
|
|
||||||
onCancel() {
|
|
||||||
dispatchLogout()
|
|
||||||
},
|
|
||||||
okText: <><Icons.Home />Resume</>,
|
|
||||||
cancelText: <><Icons.Trash />Logout</>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
renderAccountCard() {
|
|
||||||
if (this.props.app.session_authframe) {
|
|
||||||
return (
|
|
||||||
<div className={styles.third_body}>
|
|
||||||
<div className={styles.last_auth} onClick={() => this.renderAccountModal()}>
|
|
||||||
<h4><antd.Avatar size="small" src={this.props.app.session_data.avatar} /> @{this.props.app.session_data.username}</h4>
|
|
||||||
<h5><Icons.Clock />Last login <antd.Tag>{iatToString(this.props.app.session_authframe.iat || 0)}</antd.Tag></h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTitle() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h6><img className={styles.yid_logo} src={'https://api.ragestudio.net/id.svg'} /> YulioID™</h6>
|
|
||||||
<h2>{types[this.state.key].renderText || "Auth"}</h2>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className={styles.login_wrapper}>
|
|
||||||
<div className={styles.auth_box}>
|
|
||||||
<div className={styles.left_body}>
|
|
||||||
{this.renderTitle()}
|
|
||||||
</div>
|
|
||||||
<div className={styles.right_body}>
|
|
||||||
{typesRenderMap[this.state.key]}
|
|
||||||
<div className={styles.login_helper_footer}>
|
|
||||||
{this.renderHelperButtons()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{this.renderAccountCard()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default Login
|
|
145
packages/app/src/pages/login/index.jsx
Normal file
145
packages/app/src/pages/login/index.jsx
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import config from "config"
|
||||||
|
import * as antd from "antd"
|
||||||
|
import { FormGenerator } from 'components'
|
||||||
|
import { Icons } from 'components/Icons'
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
const formInstance = [
|
||||||
|
{
|
||||||
|
id: "username",
|
||||||
|
element: {
|
||||||
|
component: "Input",
|
||||||
|
icon: "User",
|
||||||
|
placeholder: "Username",
|
||||||
|
props: null
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
hasFeedback: true,
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Please input your Username!',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
props: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "password",
|
||||||
|
element: {
|
||||||
|
component: "Input",
|
||||||
|
icon: "Lock",
|
||||||
|
placeholder: "Password",
|
||||||
|
props: {
|
||||||
|
type: "password"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
hasFeedback: true,
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Please input your Password!',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "login_btn",
|
||||||
|
withValidation: true,
|
||||||
|
element: {
|
||||||
|
component: "Button",
|
||||||
|
props: {
|
||||||
|
icon: "User",
|
||||||
|
children: "Login",
|
||||||
|
type: "primary",
|
||||||
|
htmlType: "submit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "allowRegenerate",
|
||||||
|
withValidation: false,
|
||||||
|
element: {
|
||||||
|
component: "Checkbox",
|
||||||
|
props: {
|
||||||
|
children: "Not expire",
|
||||||
|
defaultChecked: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export default class Login extends React.Component {
|
||||||
|
static bindApp = ["sessionController"]
|
||||||
|
|
||||||
|
handleFinish = async (values, ctx) => {
|
||||||
|
ctx.toogleValidation(true)
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
username: values.username,
|
||||||
|
password: values.password,
|
||||||
|
allowRegenerate: values.allowRegenerate,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.contexts.app.sessionController.login(payload, (error, response) => {
|
||||||
|
ctx.toogleValidation(false)
|
||||||
|
ctx.clearErrors()
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
ctx.shake("all")
|
||||||
|
return ctx.error("result", error)
|
||||||
|
} else {
|
||||||
|
if (response.status === 200) {
|
||||||
|
this.onDone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onDone = () => {
|
||||||
|
if (typeof this.props.onDone === "function") {
|
||||||
|
this.props.onDone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.app.SidebarController.toogleVisible(true)
|
||||||
|
window.app.HeaderController.toogleVisible(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (window.app.SidebarController.isVisible()) {
|
||||||
|
window.app.SidebarController.toogleVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.app.HeaderController.isVisible()) {
|
||||||
|
window.app.HeaderController.toogleVisible(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="app_login">
|
||||||
|
{this.props.session?.valid && <div className="session_available">
|
||||||
|
<h3><Icons.AlertCircle /> You already have a valid session.</h3>
|
||||||
|
<div className="session_card">
|
||||||
|
@{this.props.session.username}
|
||||||
|
</div>
|
||||||
|
<antd.Button type="primary" onClick={() => window.app.setLocation(config.app?.mainPath ?? "/main")} >Go to main</antd.Button>
|
||||||
|
</div>}
|
||||||
|
<div>
|
||||||
|
<FormGenerator
|
||||||
|
name="normal_login"
|
||||||
|
renderLoadingIcon
|
||||||
|
className="login-form"
|
||||||
|
items={formInstance}
|
||||||
|
onFinish={this.handleFinish}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,244 +1,46 @@
|
|||||||
@import '~theme/index.less';
|
.app_login {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
|
||||||
.login_wrapper{
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
font-family: "Poppins", sans-serif!important;
|
|
||||||
h1,h2,h3,h4,h5,h6{color: #333;}
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
display: flex;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 20px;
|
|
||||||
margin: auto;
|
|
||||||
|
|
||||||
overflow-y: scroll;
|
|
||||||
overflow-x: hidden;
|
|
||||||
&.goOut{
|
|
||||||
.auth_box{
|
|
||||||
-webkit-animation-name: fadeOutLeft;
|
|
||||||
animation-name: fadeOutLeft;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
transition: all 300ms ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.auth_box{
|
|
||||||
display: flex;
|
|
||||||
transition: all 300ms ease-in-out;
|
|
||||||
|
|
||||||
.yid_logo {
|
|
||||||
vertical-align: middle;
|
|
||||||
height: 17px;
|
|
||||||
}
|
|
||||||
:global{
|
|
||||||
input:-webkit-autofill,
|
|
||||||
input:-webkit-autofill:hover,
|
|
||||||
input:-webkit-autofill:focus,
|
|
||||||
textarea:-webkit-autofill,
|
|
||||||
textarea:-webkit-autofill:hover,
|
|
||||||
textarea:-webkit-autofill:focus,
|
|
||||||
select:-webkit-autofill,
|
|
||||||
select:-webkit-autofill:hover,
|
|
||||||
select:-webkit-autofill:focus {
|
|
||||||
border: 0;
|
|
||||||
-webkit-text-fill-color: #333;
|
|
||||||
-webkit-box-shadow: 0 0 0 1000px #ffffff98 inset;
|
|
||||||
box-shadow: #fff 0 0 0 1000px inset;
|
|
||||||
transition: background-color 5000s ease-in-out 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-input-affix-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
height: 40px;
|
|
||||||
padding: 4px 11px;
|
|
||||||
color: #333;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5715;
|
|
||||||
background-color: #fff; //rgba(255, 255, 255, 0.596);
|
|
||||||
border: 1.5px #2F66DF solid;
|
|
||||||
border-radius: 7px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.centering_wrapper{
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left_body{
|
|
||||||
z-index: 50;
|
|
||||||
transform: translate(12px,0);
|
|
||||||
float: left;
|
|
||||||
width: 30%;
|
|
||||||
color: #333;
|
|
||||||
background-color: #fff;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 12px 0 0 12px;
|
|
||||||
box-shadow: 0 10px 20px 0 rgba(51,51,51,0.52);
|
|
||||||
transition: all 300ms ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right_body{
|
|
||||||
z-index: 51;
|
|
||||||
float: right;
|
|
||||||
width: 70%;
|
|
||||||
max-height: -webkit-fill-available;
|
|
||||||
height: 150px;
|
|
||||||
padding: 20px 50px;
|
|
||||||
color: #333;
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 10px 20px 0 rgba(51,51,51,0.52);
|
|
||||||
transition: all 300ms ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.third_body{
|
|
||||||
z-index: 50;
|
|
||||||
transform: translate(0, -24px);
|
|
||||||
float: right;
|
|
||||||
width: 100%;
|
|
||||||
max-height: -webkit-fill-available;
|
|
||||||
height: 120px;
|
|
||||||
padding: 45px 50px;
|
|
||||||
color: #333;
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 10px 20px 0 rgba(51, 51, 51, 0.52);
|
|
||||||
transition: all 300ms ease-in-out;
|
|
||||||
|
|
||||||
.last_auth{
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.third_body:hover{
|
|
||||||
background-color: rgba(255, 255, 255, 0.3);
|
|
||||||
box-shadow: 0 10px 20px 0 rgba(51, 51, 51, 0.8);
|
|
||||||
transform: translate(0, -16px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.helper_login_btn{
|
|
||||||
transform: translate(-20px, -14px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login_helper_footer{
|
|
||||||
width: 100%;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
display: flex;
|
|
||||||
margin: auto;
|
|
||||||
:global{
|
|
||||||
.ant-btn{
|
|
||||||
margin: auto;
|
|
||||||
padding: 0 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transform: translate(0, -10px);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@keyframes go-out {
|
|
||||||
0% {
|
|
||||||
filter: blur(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
filter: blur(15px)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Medium format max-width: 830px
|
|
||||||
@media (min-width: 486px){
|
|
||||||
.auth_box {
|
|
||||||
padding: 0 16px 40px;
|
|
||||||
width: 500px;
|
|
||||||
min-height: 500px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
justify-content: center;
|
||||||
.left_body{
|
|
||||||
width: 100%;
|
> div {
|
||||||
float: none;
|
margin-bottom: 20px;
|
||||||
border-radius: 12px 12px 0 0;
|
|
||||||
padding: 20px;
|
|
||||||
transform: translate(0, 22px);
|
|
||||||
}
|
|
||||||
.right_body{
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
float: none;
|
|
||||||
padding: 20px 60px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mobile format
|
.login-form {
|
||||||
@media (max-width: 485px){
|
|
||||||
.auth_box {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
padding: 70px 16px 40px;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 500px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 0;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.left_body{
|
|
||||||
box-shadow: none;
|
.session_available {
|
||||||
width: 100%;
|
width: fit-content;
|
||||||
float: none;
|
height: fit-content;
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
margin-top: 30px;
|
|
||||||
transform: translate(0, 0);
|
|
||||||
}
|
|
||||||
.right_body{
|
|
||||||
box-shadow: none;
|
|
||||||
width: 100%;
|
|
||||||
float: none;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes fadeOutLeft {
|
display: flex;
|
||||||
from {
|
flex-direction: column;
|
||||||
opacity: 1;
|
align-items: center;
|
||||||
}
|
justify-content: center;
|
||||||
|
|
||||||
to {
|
border: 1px solid #e5e5e5;
|
||||||
opacity: 0;
|
border-radius: 8px;
|
||||||
-webkit-transform: translate3d(-100%, 0, 0);
|
|
||||||
transform: translate3d(-100%, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeOutLeft {
|
.session_card {
|
||||||
from {
|
width: fit-content;
|
||||||
opacity: 1;
|
height: fit-content;
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
margin: 10px;
|
||||||
opacity: 0;
|
padding: 5px 10px;
|
||||||
-webkit-transform: translate3d(-100%, 0, 0);
|
|
||||||
transform: translate3d(-100%, 0, 0);
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,229 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import styles from './index.less'
|
|
||||||
|
|
||||||
import Fade from 'react-reveal/Fade'
|
|
||||||
import HeadShake from 'react-reveal/HeadShake';
|
|
||||||
|
|
||||||
import * as antd from 'antd'
|
|
||||||
|
|
||||||
import { Form, Input, Button, Checkbox } from 'antd'
|
|
||||||
import {
|
|
||||||
UserOutlined,
|
|
||||||
LockOutlined,
|
|
||||||
BulbOutlined,
|
|
||||||
SwapLeftOutlined
|
|
||||||
} from 'components/Icons'
|
|
||||||
import { connect } from 'umi'
|
|
||||||
|
|
||||||
@connect(({ app, socket }) => ({ app, socket }))
|
|
||||||
export default class NormalLoginForm extends React.Component {
|
|
||||||
state = {
|
|
||||||
usernameRaw: null,
|
|
||||||
passwordRaw: null,
|
|
||||||
|
|
||||||
step: 1,
|
|
||||||
validating: false,
|
|
||||||
error_count: 0,
|
|
||||||
step_error: false,
|
|
||||||
step_show: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
next() {
|
|
||||||
let step = this.state.step
|
|
||||||
this.setState({ validating: true, step_error: false })
|
|
||||||
switch (step) {
|
|
||||||
case 1: {
|
|
||||||
if (this.state.usernameRaw) {
|
|
||||||
const payload = { username: this.state.usernameRaw }
|
|
||||||
user.get.basicData(payload, (err, res) => {
|
|
||||||
if (res.code == 200) {
|
|
||||||
step++
|
|
||||||
this.anim_transition(50)
|
|
||||||
return this.setState({
|
|
||||||
validating: false,
|
|
||||||
early_data: res.response,
|
|
||||||
step: step,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (res.code == 210) {
|
|
||||||
return this.anim_error()
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return this.anim_error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 2: {
|
|
||||||
return this.auth()
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
back() {
|
|
||||||
if (this.state.step > 1) {
|
|
||||||
this.state.step--
|
|
||||||
this.anim_transition(150)
|
|
||||||
}
|
|
||||||
this.setState({ step: this.state.step })
|
|
||||||
}
|
|
||||||
|
|
||||||
anim_transition(duration) {
|
|
||||||
this.setState({ step_show: false })
|
|
||||||
setTimeout(() => {
|
|
||||||
this.setState({ step_show: true })
|
|
||||||
}, duration || 150)
|
|
||||||
}
|
|
||||||
|
|
||||||
anim_error() {
|
|
||||||
this.setState({ step_error: true, error_count: (this.state.error_count + 1), validating: false })
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeField(event) {
|
|
||||||
if(!this.state) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
let updated = this.state
|
|
||||||
updated[event.target.id] = event.target.value
|
|
||||||
this.setState(updated)
|
|
||||||
}
|
|
||||||
|
|
||||||
auth() {
|
|
||||||
const { usernameRaw, passwordRaw } = this.state
|
|
||||||
if (!usernameRaw || !passwordRaw) return false
|
|
||||||
this.setState({ step_error: false, validating: true })
|
|
||||||
|
|
||||||
this.props.dispatch({
|
|
||||||
type: 'app/login',
|
|
||||||
payload: { username: usernameRaw, password: passwordRaw },
|
|
||||||
callback: (callbackResponse) => {
|
|
||||||
console.log(callbackResponse)
|
|
||||||
this.setState({ validating: false })
|
|
||||||
switch (callbackResponse) {
|
|
||||||
case 100: {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
case 400: {
|
|
||||||
console.log('Credentials error')
|
|
||||||
return this.anim_error()
|
|
||||||
}
|
|
||||||
case 500: {
|
|
||||||
console.log('Server error')
|
|
||||||
return this.back()
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
console.log('Unknown error')
|
|
||||||
return this.back()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
renderFormItems = {
|
|
||||||
username: () => {
|
|
||||||
return (
|
|
||||||
<Form.Item
|
|
||||||
name="username"
|
|
||||||
hasFeedback
|
|
||||||
help={this.state.step_error ? "It seems that this user does not exist" : null}
|
|
||||||
validateStatus={this.state.step_error ? 'error' : this.state.validating ? 'validating' : null}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'Please use your Username or Email!',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
autoFocus
|
|
||||||
disabled={this.state.validating}
|
|
||||||
onPressEnter={() => this.next()}
|
|
||||||
id="usernameRaw"
|
|
||||||
onChange={(e) => this.onChangeField(e)}
|
|
||||||
prefix={<UserOutlined className="site-form-item-icon" />}
|
|
||||||
placeholder="Username or Email"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
password: () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h4><antd.Avatar shape='square' src={this.state.early_data.avatar} /> Welcome Back @{this.state.early_data.username}</h4>
|
|
||||||
<Form.Item
|
|
||||||
name="password"
|
|
||||||
hasFeedback
|
|
||||||
help={this.state.step_error ? "Incorrect password" : null}
|
|
||||||
validateStatus={this.state.step_error ? 'error' : this.state.validating ? 'validating' : null}
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: 'Please input your Password!' },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input.Password
|
|
||||||
autoFocus
|
|
||||||
onPressEnter={() => this.next()}
|
|
||||||
disabled={this.state.validating}
|
|
||||||
id="passwordRaw"
|
|
||||||
prefix={<LockOutlined className="site-form-item-icon" />}
|
|
||||||
onChange={(e) => this.onChangeField(e)}
|
|
||||||
placeholder="Password"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderButtons() {
|
|
||||||
const PrimaryButton = () => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
style={{ marginRight: "5px" }}
|
|
||||||
type="primary"
|
|
||||||
className="login-form-button"
|
|
||||||
onClick={() => this.next()}
|
|
||||||
>
|
|
||||||
{this.state.step == 1 ? "Next" : "Login"}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const SecondaryButton = () => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
style={{ marginRight: "5px" }}
|
|
||||||
className="login-form-button"
|
|
||||||
onClick={() => this.back()}
|
|
||||||
>
|
|
||||||
<SwapLeftOutlined />
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (this.state.step > 1) {
|
|
||||||
return <div><SecondaryButton /><PrimaryButton /></div>
|
|
||||||
}
|
|
||||||
return <PrimaryButton />
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className={styles.login_form}>
|
|
||||||
<Fade left opposite when={this.state.step_show}>
|
|
||||||
<Form
|
|
||||||
name="signin"
|
|
||||||
className="login-form"
|
|
||||||
>
|
|
||||||
<HeadShake spy={this.state.error_count}>
|
|
||||||
{this.renderFormItems[this.state.step == 1 ? "username" : "password"]()}
|
|
||||||
</HeadShake>
|
|
||||||
</Form>
|
|
||||||
</Fade>
|
|
||||||
{this.renderButtons()}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,151 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import * as Icons from 'components/Icons'
|
|
||||||
import styles from './index.less'
|
|
||||||
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
Tooltip,
|
|
||||||
Cascader,
|
|
||||||
Select,
|
|
||||||
Row,
|
|
||||||
Col,
|
|
||||||
Checkbox,
|
|
||||||
Button,
|
|
||||||
AutoComplete,
|
|
||||||
} from 'antd'
|
|
||||||
|
|
||||||
import ReCAPTCHA from 'react-google-recaptcha'
|
|
||||||
import { g_recaptcha_key } from 'config/keys'
|
|
||||||
|
|
||||||
export default class RegistrationForm extends React.Component {
|
|
||||||
state = {
|
|
||||||
usernameRaw: null,
|
|
||||||
passwordRaw: null,
|
|
||||||
emailRaw: null,
|
|
||||||
|
|
||||||
captchaValue: null
|
|
||||||
}
|
|
||||||
|
|
||||||
handleRegister() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
onFinish(values) {
|
|
||||||
console.log('Received values of form: ', values)
|
|
||||||
}
|
|
||||||
|
|
||||||
onCaptcha(values) {
|
|
||||||
this.setState({ captchaValue: values })
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeField(event) {
|
|
||||||
if(!this.state) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
let updated = this.state
|
|
||||||
updated[event.target.id] = event.target.value
|
|
||||||
this.setState(updated)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderForm() {
|
|
||||||
return (
|
|
||||||
<Form
|
|
||||||
name="register"
|
|
||||||
className={styles.register_form}
|
|
||||||
onFinish={this.onFinish}
|
|
||||||
scrollToFirstError
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
name="username"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'Please input your username!',
|
|
||||||
whitespace: true,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input id="usernameRaw" onChange={(e) => this.onChangeField(e)} placeholder="randomuser" prefix={<Icons.TagOutlined />} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name="email"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
type: 'email',
|
|
||||||
message: 'The input is not valid E-mail!',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'Please input your E-mail!',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input id="emailRaw" onChange={(e) => this.onChangeField(e)} placeholder="example@no-real.com" prefix={<Icons.Mail />} />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="password"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'Please input your password!',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
hasFeedback
|
|
||||||
>
|
|
||||||
<Input.Password id="passwordRaw" onChange={(e) => this.onChangeField(e)} placeholder="mysupersecretpassword" prefix={<Icons.Lock />} />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item extra="We must make sure that your are a human.">
|
|
||||||
<Row gutter={8}>
|
|
||||||
<Col span={12}>
|
|
||||||
<Form.Item
|
|
||||||
name="captcha"
|
|
||||||
noStyle
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'Please complete the captcha!',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<ReCAPTCHA sitekey={g_recaptcha_key} onChange={this.onCaptcha} />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}></Col>
|
|
||||||
</Row>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="agreement"
|
|
||||||
valuePropName="checked"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
validator: (_, value) =>
|
|
||||||
value
|
|
||||||
? Promise.resolve()
|
|
||||||
: Promise.reject('Should accept agreement'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Checkbox>
|
|
||||||
I have read the <a>agreement</a>
|
|
||||||
</Checkbox>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item >
|
|
||||||
<Button type="primary"> Register </Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className={styles.centering_wrapper}>
|
|
||||||
{ this.renderForm()}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import * as antd from 'antd'
|
|
||||||
import { connect } from 'umi'
|
|
||||||
import { router } from 'core/libs';
|
|
||||||
import { Home, Trash } from 'components/Icons'
|
|
||||||
@connect(({ app }) => ({ app }))
|
|
||||||
export default class Logout extends React.Component{
|
|
||||||
|
|
||||||
componentDidMount(){
|
|
||||||
if (!this.props.app.session_valid) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const dispatchLogout = () => this.props.dispatch({ type: "app/logout" })
|
|
||||||
|
|
||||||
antd.Modal.confirm({
|
|
||||||
title: this.props.app.session_data.username,
|
|
||||||
icon: <antd.Avatar src={this.props.app.session_data.avatar} />,
|
|
||||||
content: 'Are you sure you want to log out',
|
|
||||||
onOk() {
|
|
||||||
router.push('/')
|
|
||||||
},
|
|
||||||
onCancel() {
|
|
||||||
dispatchLogout()
|
|
||||||
},
|
|
||||||
okText: <><Home/>Resume</>,
|
|
||||||
cancelText: <><Trash/>Logout</>
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount(){
|
|
||||||
antd.Modal.destroyAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
render(){
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
86
packages/app/src/pages/main/index.jsx
Normal file
86
packages/app/src/pages/main/index.jsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import React from "react"
|
||||||
|
import * as antd from "antd"
|
||||||
|
import { AppSearcher } from "components"
|
||||||
|
import * as charts from "react-chartjs-2"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
labels: ["Red", "Orange", "Blue"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "Popularity of colours",
|
||||||
|
data: [55, 23, 96],
|
||||||
|
borderWidth: 5,
|
||||||
|
fill: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartKeys = Object.fromEntries(Object.keys(charts).map((key) => {
|
||||||
|
return [String(key).toLowerCase(), key]
|
||||||
|
}))
|
||||||
|
|
||||||
|
class ChartGenerator extends React.Component {
|
||||||
|
constructor(payload) {
|
||||||
|
super(payload)
|
||||||
|
this.payload = payload
|
||||||
|
|
||||||
|
this.type = this.payload.type
|
||||||
|
this.Chart = charts[this.type] ?? charts[chartKeys[this.type]]
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
labels: [],
|
||||||
|
datasets: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.Chart) {
|
||||||
|
console.error("Chart type is not valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { Chart } = this
|
||||||
|
|
||||||
|
if (React.isValidElement(Chart)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Chart data={this.state} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Main extends React.Component {
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (!window.app?.HeaderController?.isVisible()) {
|
||||||
|
window.app.HeaderController.toogleVisible(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (window.app?.HeaderController?.isVisible()) {
|
||||||
|
window.app.HeaderController.toogleVisible(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const user = this.props.user ?? {}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dashboard">
|
||||||
|
<div className="top">
|
||||||
|
<div>
|
||||||
|
<h1>Welcome back, {user.fullName ?? user.username ?? "Guest"}</h1>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<AppSearcher />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="content">
|
||||||
|
<h2>Statistics</h2>
|
||||||
|
<div><ChartGenerator type="line" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
30
packages/app/src/pages/main/index.less
Normal file
30
packages/app/src/pages/main/index.less
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
.dashboard {
|
||||||
|
padding-top: 20px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
h1{
|
||||||
|
font-size: 28px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
> div {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import * as antd from 'antd'
|
|
||||||
import { connect } from 'umi'
|
|
||||||
|
|
||||||
@connect(({ app }) => ({ app }))
|
|
||||||
export default class NewStreaming extends React.Component{
|
|
||||||
render(){
|
|
||||||
return(
|
|
||||||
<div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { connect } from 'umi'
|
|
||||||
import { PostsFeed } from 'components'
|
|
||||||
|
|
||||||
@connect(({ app }) => ({ app }))
|
|
||||||
export default class Post extends React.Component{
|
|
||||||
state = {
|
|
||||||
postID: null
|
|
||||||
}
|
|
||||||
componentDidMount(){
|
|
||||||
this.setState({ postID: new URLSearchParams(location.search).get('key') })
|
|
||||||
}
|
|
||||||
render(){
|
|
||||||
if (!this.state.postID) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return <PostsFeed from="post" fromID={this.state.postID} />
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { connect } from 'umi'
|
|
||||||
import { PostsFeed } from 'components'
|
|
||||||
|
|
||||||
@connect(({ app }) => ({ app }))
|
|
||||||
export default class Saves extends React.Component{
|
|
||||||
render(){
|
|
||||||
return <PostsFeed from="saved" />
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { About } from 'components'
|
|
||||||
import * as Icons from 'components/Icons'
|
|
||||||
import * as antd from 'antd'
|
|
||||||
import l from 'schemas/links'
|
|
||||||
|
|
||||||
export default class AppAbout extends React.Component {
|
|
||||||
render() {
|
|
||||||
const handleClickLinks = (e) => {
|
|
||||||
const link = l[e]
|
|
||||||
link ? window.openLink(link) : console.log("Link not available")
|
|
||||||
}
|
|
||||||
return <>
|
|
||||||
<About />
|
|
||||||
<antd.Card>
|
|
||||||
<div>
|
|
||||||
<h4>🎉✨ It's completely free and open source !</h4>
|
|
||||||
<h5>It is an impressive amount of work and effort, help us to continue offering quality services, you can support us from our patreon campaign.</h5>
|
|
||||||
<a onClick={() => handleClickLinks("patreon")}><Icons.Patreon /> Support us with Patreon!</a>
|
|
||||||
</div>
|
|
||||||
<antd.Divider dashed />
|
|
||||||
<div>
|
|
||||||
<h4>👨💻 You are developer? You can help us by joining our team!</h4>
|
|
||||||
<a onClick={() => handleClickLinks("github")}><Icons.GitHub />Official Repository</a><br />
|
|
||||||
<a onClick={() => handleClickLinks("trello")}><Icons.Trello />Join our Trello</a>
|
|
||||||
</div>
|
|
||||||
</antd.Card>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import * as antd from 'antd'
|
|
||||||
import * as Icons from 'components/Icons'
|
|
||||||
|
|
||||||
|
|
||||||
export default class Earnings extends React.Component {
|
|
||||||
state = {
|
|
||||||
loading: true,
|
|
||||||
pro_points: 0
|
|
||||||
}
|
|
||||||
componentDidMount(){
|
|
||||||
// app.comty_data.get_user_data((err,res)=>{
|
|
||||||
// if (err) return false
|
|
||||||
// try {
|
|
||||||
// const a = JSON.parse(res)['user_data']
|
|
||||||
// this.setState({ loading: false, pro_points: a.points })
|
|
||||||
// } catch (error) {
|
|
||||||
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<h1><Icons.PaperClipOutlined /> Redeem an Code</h1>
|
|
||||||
<antd.Input placeholder="XXXX-XXXX-XXXX-XXXX" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1> Your Pro Points </h1>
|
|
||||||
{this.state.pro_points}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import * as antd from 'antd'
|
|
||||||
import * as Icons from 'components/Icons'
|
|
||||||
import styles from './index.less'
|
|
||||||
import { connect } from 'umi';
|
|
||||||
import { package_json } from 'core'
|
|
||||||
import { objectToArrayMap } from '@nodecorejs/utils'
|
|
||||||
|
|
||||||
const AppTech = (info) => {
|
|
||||||
if (!info) return null
|
|
||||||
return(
|
|
||||||
<div className={styles.versions}>
|
|
||||||
<div>umi<antd.Tag>{info.g_umi.version}</antd.Tag></div>
|
|
||||||
<div>react<antd.Tag>{info.react_version}</antd.Tag></div>
|
|
||||||
<div><Icons.V8/><antd.Tag>{info.process.versions.v8}</antd.Tag></div>
|
|
||||||
<div><Icons.NodeDotJs /><antd.Tag>{info.process.version}</antd.Tag></div>
|
|
||||||
<div><Icons.Electron /><antd.Tag>{info.process.versions.electron}</antd.Tag></div>
|
|
||||||
<div><Icons.Webpack /> Webpack </div>
|
|
||||||
<div><Icons.SocketDotIo /> Socket.io </div>
|
|
||||||
<div><Icons.Javascript /> JS </div>
|
|
||||||
<div><Icons.Typescript /> TS </div>
|
|
||||||
<div><Icons.Webassembly /> WebAssembly </div>
|
|
||||||
<div><Icons.Openai /> OpenAI </div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@connect(({ app }) => ({ app }))
|
|
||||||
export default class ElectronSettings extends React.PureComponent{
|
|
||||||
state = {
|
|
||||||
loading: true,
|
|
||||||
info: []
|
|
||||||
}
|
|
||||||
|
|
||||||
getInfo(){
|
|
||||||
this.setState({ loading: true })
|
|
||||||
this.setState({
|
|
||||||
loading: false,
|
|
||||||
info: {
|
|
||||||
g_umi: window.g_umi,
|
|
||||||
process: window.process,
|
|
||||||
react_version: React.version,
|
|
||||||
deps: objectToArrayMap(package_json.dependencies)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount(){
|
|
||||||
this.getInfo()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
render(){
|
|
||||||
const showAppTech = () => {
|
|
||||||
antd.Modal.info({
|
|
||||||
title: package_json.title,
|
|
||||||
content: AppTech(this.state.info),
|
|
||||||
width: 550
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const showThirdParty = () => {
|
|
||||||
const generateList = () => {
|
|
||||||
return this.state.info.deps.map((e) => {
|
|
||||||
return(
|
|
||||||
<div key={e.key}>
|
|
||||||
-> {e.key} <antd.Tag onClick={null} >{e.value.slice(1,e.value.length)}</antd.Tag>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
antd.Modal.info({
|
|
||||||
title: package_json.title,
|
|
||||||
content: generateList(),
|
|
||||||
width: 550
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.loading){
|
|
||||||
return <antd.Skeleton active />
|
|
||||||
}
|
|
||||||
return(
|
|
||||||
<div className={styles.main}>
|
|
||||||
<antd.Button onClick={() => showAppTech()}> App Technologies </antd.Button>
|
|
||||||
<antd.Button onClick={() => showThirdParty()}> Third-Party </antd.Button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
.versions{
|
|
||||||
overflow: scroll;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
grid-template-rows: repeat(4, 1fr);
|
|
||||||
grid-column-gap: 15px;
|
|
||||||
grid-row-gap: 15px;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { List, Button, Switch, Checkbox, InputNumber, Input } from 'antd'
|
|
||||||
import * as Icons from 'components/Icons'
|
|
||||||
|
|
||||||
import { verbosity } from '@nodecorejs/utils'
|
|
||||||
import { settings, newSetting } from 'core/libs/settings'
|
|
||||||
import listSettings from 'schemas/settings_general.json'
|
|
||||||
|
|
||||||
const AntdComponents = { Button, Switch, Checkbox, InputNumber, Input }
|
|
||||||
export default class GeneralSettings extends React.Component {
|
|
||||||
state = {
|
|
||||||
list: listSettings,
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSetting = (item) => {
|
|
||||||
if (!item.type || !item.id) {
|
|
||||||
verbosity.log("Invalid component >", item)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (typeof(AntdComponents[item.type]) == "undefined") {
|
|
||||||
verbosity.log(`Invalid component, '${item.type}' not exists >`, item)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
let itemProps = {
|
|
||||||
onChange: (e) => this.onChange(item, e),
|
|
||||||
checked: settings.get(item.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (item.type) {
|
|
||||||
case 'Switch': {
|
|
||||||
itemProps = { ...itemProps } // checkedChildren: "Enabled", unCheckedChildren: "Disabled"
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return React.createElement(AntdComponents[item.type], itemProps)
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(item, event) {
|
|
||||||
try {
|
|
||||||
let to = event
|
|
||||||
|
|
||||||
verbosity.colors({ log: { textColor: "blue" } }).log(`Updating setting (${item.id}) > ${to}`)
|
|
||||||
settings.set(item.id, to)
|
|
||||||
|
|
||||||
const updatedValues = this.state.list.map(element =>
|
|
||||||
element.id === item.id ? Object.assign(element, { value: to }) : element
|
|
||||||
)
|
|
||||||
this.setState({ list: updatedValues })
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderIcon(icon, props) {
|
|
||||||
if (!Icons[icon]) {
|
|
||||||
verbosity.log(`${icon} not exist!`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return React.createElement(Icons[icon], props ?? null)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<List
|
|
||||||
itemLayout="horizontal"
|
|
||||||
dataSource={this.state.list}
|
|
||||||
renderItem={(item) => (
|
|
||||||
<List.Item actions={item.actions} key={item.id}>
|
|
||||||
<List.Item.Meta
|
|
||||||
title={<>{this.renderIcon(item.icon)}{item.title}</>}
|
|
||||||
description={item.description}
|
|
||||||
/>
|
|
||||||
{this.renderSetting(item)}
|
|
||||||
</List.Item>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
export default class Help extends React.Component{
|
|
||||||
render(){
|
|
||||||
return <>
|
|
||||||
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { connect } from 'umi'
|
|
||||||
|
|
||||||
@connect(({ app }) => ({ app }))
|
|
||||||
export default class Keybinds extends React.Component {
|
|
||||||
render() {
|
|
||||||
return <></>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import * as Icons from 'components/Icons'
|
|
||||||
import styles from './index.less'
|
|
||||||
|
|
||||||
export default class NotificationView extends React.Component {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className={styles.main}>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
|||||||
@import '~theme/index.less';
|
|
||||||
|
|
||||||
.main {
|
|
||||||
|
|
||||||
:global {
|
|
||||||
h2 {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.anticon {
|
|
||||||
color: #2d2d2d;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {connect} from 'umi'
|
|
||||||
@connect(({ app }) => ({ app }))
|
|
||||||
export default class Plugins extends React.Component {
|
|
||||||
render() {
|
|
||||||
return <>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import * as Icons from 'components/Icons'
|
|
||||||
import * as antd from 'antd'
|
|
||||||
import styles from './index.less'
|
|
||||||
|
|
||||||
import Sessions_Manager from './sessions.js'
|
|
||||||
|
|
||||||
const { Menu } = antd
|
|
||||||
export default class SecurityView extends React.Component {
|
|
||||||
state = {
|
|
||||||
current: 'privacy',
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick = e => {
|
|
||||||
this.setState({
|
|
||||||
current: e.key,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
renderChildren = () => {
|
|
||||||
const { current } = this.state
|
|
||||||
switch (current) {
|
|
||||||
case 'privacy':
|
|
||||||
return null
|
|
||||||
case 'credentials':
|
|
||||||
return null
|
|
||||||
case 'sessions':
|
|
||||||
return <Sessions_Manager />
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className={styles.main}>
|
|
||||||
<Menu
|
|
||||||
onClick={this.handleClick}
|
|
||||||
selectedKeys={[this.state.current]}
|
|
||||||
mode="horizontal"
|
|
||||||
>
|
|
||||||
<Menu.Item key="privacy">
|
|
||||||
<Icons.AimOutlined />
|
|
||||||
Privacy
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="credentials">
|
|
||||||
<Icons.KeyOutlined />
|
|
||||||
Credentials
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="sessions">
|
|
||||||
<Icons.NumberOutlined />
|
|
||||||
Sessions
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu>
|
|
||||||
<div>{this.renderChildren()}</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
|||||||
@import '~theme/index.less';
|
|
||||||
|
|
||||||
.main {
|
|
||||||
|
|
||||||
:global {
|
|
||||||
h2 {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.anticon {
|
|
||||||
color: #2d2d2d;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import * as antd from 'antd'
|
|
||||||
|
|
||||||
export default class Sessions_Manager extends React.Component {
|
|
||||||
state = {
|
|
||||||
sessions_data: '',
|
|
||||||
}
|
|
||||||
componentDidMount() {
|
|
||||||
// app.comty_data.session_id((err, res) => {
|
|
||||||
// this.setState({ sid: res })
|
|
||||||
// })
|
|
||||||
// app.comty_data.sessions((err, res) => {
|
|
||||||
// const a = JSON.parse(res)['data']
|
|
||||||
// this.setState({ sessions_data: a })
|
|
||||||
// })
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<antd.List
|
|
||||||
dataSource={this.state.sessions_data}
|
|
||||||
renderItem={item => (
|
|
||||||
<antd.List.Item>
|
|
||||||
<antd.Card grid={{ gutter: 16, column: 4 }} hoverable>
|
|
||||||
<h3>Session #{item.id}</h3>
|
|
||||||
{this.state.sid == item.session_id ? 'This Session' : null}
|
|
||||||
<hr />
|
|
||||||
<p>{item.platform}</p>
|
|
||||||
<p>{item.ip_address} </p>
|
|
||||||
<p>{item.time}</p>
|
|
||||||
</antd.Card>
|
|
||||||
</antd.List.Item>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,158 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import * as Icons from 'components/Icons'
|
|
||||||
import * as antd from 'antd'
|
|
||||||
import { connect } from 'umi'
|
|
||||||
import styles from '../../index.less'
|
|
||||||
|
|
||||||
import { theme, getOptimalOpacityFromIMG, get_style_rule_value } from 'core/libs/style'
|
|
||||||
import { urlToBase64, imageToBase64, arrayToObject } from 'core'
|
|
||||||
import ThemeConfigurator from '../../configurator'
|
|
||||||
|
|
||||||
@connect(({ app }) => ({ app }))
|
|
||||||
export default class BackgroundImage extends ThemeConfigurator {
|
|
||||||
state = {
|
|
||||||
configKey: "backgroundImage",
|
|
||||||
model: { active: false, opacity: null, src: null },
|
|
||||||
|
|
||||||
textColor: this.rgbToScheme(getComputedStyle(document.getElementById("appWrapper")).color),
|
|
||||||
overlayColor: this.rgbToScheme(getComputedStyle(document.getElementById("appWrapper")).backgroundColor),
|
|
||||||
|
|
||||||
processing: null,
|
|
||||||
fileURL: null,
|
|
||||||
customURL: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFileUpload = info => {
|
|
||||||
if (info.file.status === 'uploading') {
|
|
||||||
return this.setState({ processing: false })
|
|
||||||
}
|
|
||||||
if (info.file.status === 'done') {
|
|
||||||
this.setState({ processing: true })
|
|
||||||
imageToBase64(info.file.originFileObj, fileURL => {
|
|
||||||
this.setState({ fileURL: fileURL })
|
|
||||||
this.proccessBackground(fileURL)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCustomURL(url) {
|
|
||||||
this.setState({ processing: true, fileURL: url })
|
|
||||||
urlToBase64(url, fileURL => {
|
|
||||||
this.proccessBackground(fileURL)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
proccessBackground(data) {
|
|
||||||
getOptimalOpacityFromIMG({ textColor: this.state.textColor, overlayColor: this.state.overlayColor, img: data }, (res) => {
|
|
||||||
this.handleUpdate({ active: true, src: this.state.fileURL, opacity: res })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
schemeToRGB(values) {
|
|
||||||
const scheme = values ? values : { r: '0', g: '0', b: '0' }
|
|
||||||
const r = scheme.r || '0'
|
|
||||||
const g = scheme.g || '0'
|
|
||||||
const b = scheme.b || '0'
|
|
||||||
return `rgb(${r}, ${g}, ${b})`
|
|
||||||
}
|
|
||||||
|
|
||||||
rgbToScheme(rgb) {
|
|
||||||
const values = rgb.replace(/[^\d,]/g, '').split(',');
|
|
||||||
return { r: values[0], g: values[1], b: values[2] }
|
|
||||||
}
|
|
||||||
|
|
||||||
renderPreviewModel() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h3><Icons.Layout /> Preview</h3>
|
|
||||||
{ this.state.model.src ? <div className={styles.background_image_preview} style={{ backgroundColor: this.schemeToRGB(this.state.overlayColor) }}>
|
|
||||||
<div style={{ color: `${this.schemeToRGB(this.state.textColor)}!important` }} className={styles.text_wrapper}>
|
|
||||||
<h1 style={{ color: this.schemeToRGB(this.state.textColor) }}>Sample text</h1>
|
|
||||||
<h2 style={{ color: this.schemeToRGB(this.state.textColor) }}>Sample text</h2>
|
|
||||||
<h3 style={{ color: this.schemeToRGB(this.state.accentColor) }}>Sample text</h3>
|
|
||||||
<h4 style={{ color: this.schemeToRGB(this.state.accentColor) }}>Sample text</h4>
|
|
||||||
<p style={{ color: this.schemeToRGB(this.state.textColor) }}>Some text here</p>
|
|
||||||
<p style={{ color: this.schemeToRGB(this.state.accentColor) }}>Some text here</p>
|
|
||||||
</div>
|
|
||||||
<img style={{ opacity: this.state.model.opacity }} src={this.state.model.src} />
|
|
||||||
</div> : <h3 style={{ textAlign: 'center' }} > No Background </h3>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderUploader() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h3><Icons.Upload /> Upload </h3>
|
|
||||||
<div className={styles.background_image_uploader} >
|
|
||||||
<div>
|
|
||||||
Upload from your files <br />
|
|
||||||
<antd.Upload onChange={this.handleFileUpload}>
|
|
||||||
<antd.Button icon={<Icons.Upload type="primary" style={{ margin: '5px 0 0 0' }} />} />
|
|
||||||
</antd.Upload>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3>Or</h3>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Upload from URL
|
|
||||||
<antd.Input onPressEnter={() => this.handleCustomURL(this.state.customURL)} onChange={e => this.setState({ customURL: e.target.value })} value={this.state.customURL} placeholder="http://example.com/my_coolest_image.jpg" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.state.processing ? <h4><Icons.LoadingOutlined spin /> Processing image ... </h4> : null}
|
|
||||||
{this.state.params ? JSON.stringify(this.state.params) : null}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderControls() {
|
|
||||||
return (
|
|
||||||
<div className={styles.background_image_controls} >
|
|
||||||
<div>
|
|
||||||
<h4><Icons.Eye />Enabled</h4>
|
|
||||||
<antd.Switch
|
|
||||||
checkedChildren="Enabled"
|
|
||||||
unCheckedChildren="Disabled"
|
|
||||||
loading={this.state.processing}
|
|
||||||
onChange={(e) => { this.promiseState(prevState => ({ model: { ...prevState.model, active: e } })).then(() => this.handleUpdate()) }}
|
|
||||||
checked={this.state.model.active}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<h4><Icons.Layers />Opacity</h4>
|
|
||||||
<antd.Slider disabled={!this.state.model.src} onChange={(e) => { this.setState(prevState => ({ model: { ...prevState.model, opacity: e / 100 } })) }} onAfterChange={() => this.handleUpdate()} value={this.state.model.opacity * 100} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4><Icons.Code />Export Code</h4>
|
|
||||||
<antd.Button disabled={!this.state.model.src} size="small" onClick={() => this.handleExport()}> Export </antd.Button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4><Icons.Copy />Import Code</h4>
|
|
||||||
<antd.Button size="small" onClick={() => null}> Import </antd.Button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4><Icons.Trash />Erase</h4>
|
|
||||||
<antd.Popconfirm disabled={!this.state.model.src} placement="topLeft" title="Are you sure?" onConfirm={() => this.handleErase()} okText="Yes" cancelText="No">
|
|
||||||
<antd.Button disabled={!this.state.model.src} size="small" type="primary" danger > Delete </antd.Button>
|
|
||||||
</antd.Popconfirm>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{this.renderControls()}
|
|
||||||
<antd.Divider type="horizontal" dashed />
|
|
||||||
{this.renderPreviewModel()}
|
|
||||||
<antd.Divider type="horizontal" dashed />
|
|
||||||
{this.renderUploader()}
|
|
||||||
<antd.Divider type="horizontal" dashed />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import * as Icons from 'components/Icons'
|
|
||||||
import * as antd from 'antd'
|
|
||||||
import styles from '../../index.less'
|
|
||||||
import ThemeConfigurator from '../../configurator'
|
|
||||||
|
|
||||||
export default class Colors extends ThemeConfigurator {
|
|
||||||
state = {
|
|
||||||
configKey: "colors",
|
|
||||||
model: { active: false }
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className={styles.background_image_controls} >
|
|
||||||
<div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import * as Icons from 'components/Icons'
|
|
||||||
import * as antd from 'antd'
|
|
||||||
import styles from '../../index.less'
|
|
||||||
import ThemeConfigurator from '../../configurator'
|
|
||||||
|
|
||||||
export default class DarkMode extends ThemeConfigurator {
|
|
||||||
state = {
|
|
||||||
configKey: "darkmode",
|
|
||||||
model: { active: false }
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className={styles.background_image_controls} >
|
|
||||||
<div>
|
|
||||||
<h4><Icons.Eye />Enabled</h4>
|
|
||||||
<antd.Switch
|
|
||||||
checkedChildren="Enabled"
|
|
||||||
unCheckedChildren="Disabled"
|
|
||||||
onChange={(e) => { this.promiseState(prevState => ({ model: { ...prevState.model, active: e } })).then(() => this.handleUpdate()) }}
|
|
||||||
checked={this.state.model.active}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import ErrorHandler from 'core/libs/errorhandler'
|
|
||||||
import { theme } from 'core/libs/style'
|
|
||||||
import exportDataAsFile from 'core/libs/ui/export_data'
|
|
||||||
import { verbosity } from '@nodecorejs/utils'
|
|
||||||
|
|
||||||
export default class ThemeConfigurator extends React.Component {
|
|
||||||
componentDidMount() {
|
|
||||||
this.applyStoraged()
|
|
||||||
}
|
|
||||||
|
|
||||||
applyStoraged() {
|
|
||||||
const storaged = theme.get()
|
|
||||||
if (storaged && this.state) {
|
|
||||||
if (storaged[this.state.configKey]) {
|
|
||||||
return this.setState({ model: storaged[this.state.configKey] })
|
|
||||||
} else {
|
|
||||||
return verbosity.log(`cannot get storagedSetting for ${this.state.configKey}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
promiseState = async state => new Promise(resolve => this.setState(state, resolve));
|
|
||||||
|
|
||||||
handleUpdate(payload) {
|
|
||||||
if (!this.state.configKey) {
|
|
||||||
return ErrorHandler({ msg: `cannot update without 'configKey', is missing`, code: 140 })
|
|
||||||
}
|
|
||||||
if (!payload) {
|
|
||||||
payload = this.state.model
|
|
||||||
}
|
|
||||||
this.setState({ model: payload, processing: false })
|
|
||||||
window.dispatcher({
|
|
||||||
type: 'app/updateTheme',
|
|
||||||
payload: {
|
|
||||||
key: this.state.configKey,
|
|
||||||
value: payload
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleErase() {
|
|
||||||
this.handleUpdate({})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleExport() {
|
|
||||||
exportDataAsFile({ data: JSON.stringify(this.state.model), type: 'text/json' })
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,75 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import * as Icons from 'components/Icons'
|
|
||||||
import * as antd from 'antd'
|
|
||||||
import { connect } from 'umi'
|
|
||||||
import { arrayToObject } from 'core'
|
|
||||||
import ThemeSettingsList from 'schemas/theme_settings.json'
|
|
||||||
|
|
||||||
import BackgroundSetting from './components/background'
|
|
||||||
import DarkmodeSetting from './components/darkmode'
|
|
||||||
import ColorSetting from './components/color'
|
|
||||||
|
|
||||||
const componentsMap = {
|
|
||||||
backgroundImage: <BackgroundSetting />,
|
|
||||||
darkmode: <DarkmodeSetting />,
|
|
||||||
color: <ColorSetting />,
|
|
||||||
}
|
|
||||||
|
|
||||||
@connect(({ app }) => ({ app }))
|
|
||||||
export default class ThemeSettings extends React.Component {
|
|
||||||
state = {
|
|
||||||
selectedKey: null,
|
|
||||||
keys: []
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
let mix = []
|
|
||||||
ThemeSettingsList.forEach(e => {
|
|
||||||
mix[e.id] = e
|
|
||||||
})
|
|
||||||
this.setState({ keys: mix })
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSelectedKey() {
|
|
||||||
const selectedKeyItem = this.state.keys[this.state.selectedKey] ?? { icon: null, title: null }
|
|
||||||
return (
|
|
||||||
<antd.Drawer
|
|
||||||
placement="right"
|
|
||||||
width="50%"
|
|
||||||
closable
|
|
||||||
onClose={() => this.setState({ selectedKey: null })}
|
|
||||||
visible={this.state.selectedKey ? true : false}
|
|
||||||
>
|
|
||||||
<React.Fragment>
|
|
||||||
<div>
|
|
||||||
<h2>{selectedKeyItem.icon} {selectedKeyItem.title}</h2>
|
|
||||||
</div>
|
|
||||||
<antd.Divider type="horizontal" dashed />
|
|
||||||
{componentsMap[this.state.selectedKey]}
|
|
||||||
</React.Fragment>
|
|
||||||
</antd.Drawer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const handleClick = (key) => this.setState({ selectedKey: key })
|
|
||||||
const isActive = (key) => { return key ? key.active : false }
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<antd.List
|
|
||||||
itemLayout="horizontal"
|
|
||||||
dataSource={ThemeSettingsList}
|
|
||||||
renderItem={item => (
|
|
||||||
<div style={{ margin: '10px 0 10px 0' }} >
|
|
||||||
<antd.Card size="small" bodyStyle={{ width: '100%' }} style={{ display: "flex", flexDirection: "row", margin: 'auto', borderRadius: '12px' }} hoverable onClick={() => handleClick(item.id)}>
|
|
||||||
<h3>{React.createElement(Icons[item.icon])}{item.title} <div style={{ float: "right" }}><antd.Tag color={isActive(arrayToObject(this.props.app.app_theme)[item.id]) ? "green" : "default"} > {isActive(arrayToObject(this.props.app.app_theme)[item.id]) ? "Enabled" : "Disabled"} </antd.Tag></div></h3>
|
|
||||||
<p>{item.description}</p>
|
|
||||||
</antd.Card>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{this.renderSelectedKey()}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
.background_image_controls{
|
|
||||||
width: 100%;
|
|
||||||
margin: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.background_image_uploader{
|
|
||||||
width: 100%;
|
|
||||||
margin: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.background_image_preview{
|
|
||||||
position: relative;
|
|
||||||
height: 50%;
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
img{
|
|
||||||
z-index: 9;
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text_wrapper{
|
|
||||||
position: absolute;
|
|
||||||
z-index: 10;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
h1{
|
|
||||||
font-size: 68px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import * as Icons from 'components/Icons'
|
|
||||||
|
|
||||||
import { ListedMenu } from 'components'
|
|
||||||
|
|
||||||
import NotificationView from './components/notification'
|
|
||||||
import SecurityView from './components/security'
|
|
||||||
import Base from './components/general'
|
|
||||||
import AppAbout from './components/about'
|
|
||||||
import Theme from './components/theme'
|
|
||||||
import ElectronApp from './components/electron'
|
|
||||||
import Keybinds from './components/keybinds'
|
|
||||||
import Plugins from './components/plugins'
|
|
||||||
|
|
||||||
const Settings = {
|
|
||||||
base: <Base />,
|
|
||||||
about: <AppAbout />,
|
|
||||||
keybinds: <Keybinds />,
|
|
||||||
theme: <Theme />,
|
|
||||||
plugins: <Plugins />,
|
|
||||||
security: <SecurityView />,
|
|
||||||
notification: <NotificationView />,
|
|
||||||
app: <ElectronApp />
|
|
||||||
}
|
|
||||||
|
|
||||||
const menuList = [
|
|
||||||
{
|
|
||||||
key: "base",
|
|
||||||
title: "General",
|
|
||||||
icon: <Icons.template />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "app",
|
|
||||||
title: "Application",
|
|
||||||
icon: <Icons.Command />,
|
|
||||||
require: "embedded"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "keybinds",
|
|
||||||
title: "Keybinds",
|
|
||||||
icon: <Icons.lightningBolt />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "theme",
|
|
||||||
title: "Customization",
|
|
||||||
icon: <Icons.sparkles />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "plugins",
|
|
||||||
title: "Plugins",
|
|
||||||
icon: <Icons.cubeTransparent />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "security",
|
|
||||||
title: "Security & Privacity",
|
|
||||||
icon: <Icons.keyRound />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "notification",
|
|
||||||
title: "Notification",
|
|
||||||
icon: <Icons.Bell />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "help",
|
|
||||||
title: "Help",
|
|
||||||
icon: <Icons.LifeBuoy />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "about",
|
|
||||||
title: "About",
|
|
||||||
icon: <Icons.Info />,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
class GeneralSettings extends React.Component {
|
|
||||||
render() {
|
|
||||||
return <ListedMenu defaultKey="base" icon={<Icons.SettingOutlined />} title="Settings" childrens={Settings} menuArray={menuList} />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default GeneralSettings
|
|
@ -1,52 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import * as antd from 'antd'
|
|
||||||
import * as Icons from 'components/Icons'
|
|
||||||
import { connect } from 'umi'
|
|
||||||
import { verbosity } from '@nodecorejs/utils'
|
|
||||||
|
|
||||||
import { goLive } from 'core/models/helpers'
|
|
||||||
|
|
||||||
@connect(({ app }) => ({ app }))
|
|
||||||
export default class Streams extends React.Component {
|
|
||||||
|
|
||||||
state = {
|
|
||||||
list: []
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
try {
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
verbosity.log(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
availableList = () => {
|
|
||||||
if (!Array.isArray(this.state.list)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (this.state.list.length == 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
if (!this.availableList()) {
|
|
||||||
return <div style={{ display: "flex", textAlign: "center", justifyContent: "center" }}>
|
|
||||||
<antd.Result status="404" title="Its seems like nothing is on streaming" >
|
|
||||||
<antd.Button onClick={() => {goLive()}} type="primary" >
|
|
||||||
<Icons.Cast /> Start Streaming
|
|
||||||
</antd.Button>
|
|
||||||
</antd.Result>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
2
packages/server/.gitignore
vendored
Normal file
2
packages/server/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
.env
|
@ -7,14 +7,15 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"corenode": "^0.28.26",
|
|
||||||
"linebridge": "^0.8.4",
|
|
||||||
"mongoose": "^6.0.13",
|
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.24.0",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
"connect-mongo": "^4.6.0",
|
"connect-mongo": "^4.6.0",
|
||||||
|
"corenode": "^0.28.26",
|
||||||
|
"dicebar_lib": "^1.0.1",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"linebridge": "^0.8.4",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
|
"mongoose": "^6.0.13",
|
||||||
"passport": "^0.5.0",
|
"passport": "^0.5.0",
|
||||||
"passport-jwt": "^4.0.0",
|
"passport-jwt": "^4.0.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
|
36
packages/server/src/controllers/RolesController/index.js
Normal file
36
packages/server/src/controllers/RolesController/index.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Role, User } from '../../models'
|
||||||
|
import { selectValues } from "../../lib"
|
||||||
|
|
||||||
|
export const RolesController = {
|
||||||
|
get: selectValues(["user_id", "username"], async (req, res) => {
|
||||||
|
const { user_id, username } = req.selectedValues
|
||||||
|
|
||||||
|
if (typeof user_id !== "undefined" || typeof username !== "undefined") {
|
||||||
|
const user = await User.findOne(req.selectedValues)
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ error: "No user founded" })
|
||||||
|
}
|
||||||
|
return res.json(user.roles)
|
||||||
|
}
|
||||||
|
|
||||||
|
const roles = await Role.find({})
|
||||||
|
|
||||||
|
return res.json(roles)
|
||||||
|
}),
|
||||||
|
set: (req, res, next) => {
|
||||||
|
const { name, description } = req.body
|
||||||
|
Role.findOne({ name }).then((data) => {
|
||||||
|
if (data) {
|
||||||
|
return res.status(409).json("This role is already created")
|
||||||
|
}
|
||||||
|
let document = new Role({
|
||||||
|
name,
|
||||||
|
description
|
||||||
|
})
|
||||||
|
document.save()
|
||||||
|
return res.json(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RolesController
|
104
packages/server/src/controllers/SessionController/index.js
Normal file
104
packages/server/src/controllers/SessionController/index.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { Session } from '../../models'
|
||||||
|
import jwt from 'jsonwebtoken'
|
||||||
|
import { Token } from '../../lib'
|
||||||
|
|
||||||
|
export const SessionController = {
|
||||||
|
regenerate: async (req, res) => {
|
||||||
|
jwt.verify(req.jwtToken, req.jwtStrategy.secretOrKey, async (err, decoded) => {
|
||||||
|
if (err && !decoded?.allowRegenerate) {
|
||||||
|
return res.status(403).send("This token is invalid and is not allowed to be regenerated")
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionToken = await Session.findOneAndDelete({ token: req.jwtToken, user_id: decoded.user_id })
|
||||||
|
|
||||||
|
if (sessionToken) {
|
||||||
|
delete decoded["iat"]
|
||||||
|
delete decoded["exp"]
|
||||||
|
delete decoded["date"]
|
||||||
|
|
||||||
|
const token = await Token.signNew({
|
||||||
|
...decoded,
|
||||||
|
}, req.jwtStrategy)
|
||||||
|
|
||||||
|
return res.json({ token })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteAll: async (req, res) => {
|
||||||
|
const { user_id } = req.body
|
||||||
|
|
||||||
|
if (typeof user_id === "undefined") {
|
||||||
|
return res.status(400).send("No user_id provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
const allSessions = await Session.deleteMany({ user_id })
|
||||||
|
if (allSessions) {
|
||||||
|
return res.send("done")
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(404).send("not found")
|
||||||
|
},
|
||||||
|
delete: async (req, res) => {
|
||||||
|
const { token, user_id } = req.body
|
||||||
|
|
||||||
|
if (typeof user_id === "undefined") {
|
||||||
|
return res.status(400).send("No user_id provided")
|
||||||
|
}
|
||||||
|
if (typeof token === "undefined") {
|
||||||
|
return res.status(400).send("No token provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await Session.findOneAndDelete({ user_id, token })
|
||||||
|
if (session) {
|
||||||
|
return res.send("done")
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(404).send("not found")
|
||||||
|
},
|
||||||
|
validate: async (req, res) => {
|
||||||
|
const token = req.body.session
|
||||||
|
let result = {
|
||||||
|
expired: false,
|
||||||
|
valid: true
|
||||||
|
}
|
||||||
|
|
||||||
|
await jwt.verify(token, req.jwtStrategy.secretOrKey, async (err, decoded) => {
|
||||||
|
if (err) {
|
||||||
|
result.valid = false
|
||||||
|
result.error = err.message
|
||||||
|
|
||||||
|
if (err.message === "jwt expired") {
|
||||||
|
result.expired = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result = { ...result, ...decoded }
|
||||||
|
|
||||||
|
const sessions = await Session.find({ user_id: result.user_id })
|
||||||
|
const sessionsTokens = sessions.map((session) => {
|
||||||
|
if (session.user_id === result.user_id) {
|
||||||
|
return session.token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!sessionsTokens.includes(token)) {
|
||||||
|
result.valid = false
|
||||||
|
result.error = "Session token not found"
|
||||||
|
} else {
|
||||||
|
result.valid = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.json(result)
|
||||||
|
},
|
||||||
|
get: async (req, res) => {
|
||||||
|
// get current session _id
|
||||||
|
const { _id } = req.user
|
||||||
|
const sessions = await Session.find({ user_id: _id }, { token: 0 })
|
||||||
|
|
||||||
|
return res.json(sessions)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SessionController
|
209
packages/server/src/controllers/UserController/index.js
Normal file
209
packages/server/src/controllers/UserController/index.js
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
import passport from 'passport'
|
||||||
|
import bcrypt from 'bcrypt'
|
||||||
|
|
||||||
|
import { User } from '../../models'
|
||||||
|
import SessionController from '../SessionController'
|
||||||
|
import { Token, selectValues } from '../../lib'
|
||||||
|
import AvatarController from 'dicebar_lib'
|
||||||
|
|
||||||
|
export const UserController = {
|
||||||
|
isAuth: (req, res) => {
|
||||||
|
return res.json(`You look nice today 😎`)
|
||||||
|
},
|
||||||
|
getSelf: (req, res) => {
|
||||||
|
return res.json(req.user)
|
||||||
|
},
|
||||||
|
get: selectValues(["_id", "username"], async (req, res) => {
|
||||||
|
const user = await User.find(req.selectedValues, { username: 1, fullName: 1, _id: 1, roles: 1, avatar: 1 })
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ error: "User not exists" })
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json(user)
|
||||||
|
}),
|
||||||
|
getOne: selectValues(["_id", "username"], async (req, res) => {
|
||||||
|
const user = await User.findOne(req.selectedValues)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ error: "User not exists" })
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json(user)
|
||||||
|
}),
|
||||||
|
register: (req, res, next) => {
|
||||||
|
User.findOne({ username: req.body.username })
|
||||||
|
.then((data) => {
|
||||||
|
if (data) {
|
||||||
|
return res.status(409).json("Username is already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
const avatar = AvatarController.generate({ seed: req.body.username, type: "initials" })
|
||||||
|
const hash = bcrypt.hashSync(req.body.password, parseInt(process.env.BCRYPT_ROUNDS))
|
||||||
|
|
||||||
|
let document = new User({
|
||||||
|
username: req.body.username,
|
||||||
|
fullName: req.body.fullName,
|
||||||
|
avatar: avatar.uri,
|
||||||
|
email: req.body.email,
|
||||||
|
roles: ["registered"],
|
||||||
|
password: hash
|
||||||
|
})
|
||||||
|
|
||||||
|
return document.save()
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
return res.send(data)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
return next(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
denyRole: async (req, res) => {
|
||||||
|
// check if issuer user is admin
|
||||||
|
if (!req.isAdmin()) {
|
||||||
|
return res.status(403).send("You do not have administrator permission")
|
||||||
|
}
|
||||||
|
|
||||||
|
let { user_id, username, roles } = req.body
|
||||||
|
const userQuery = {
|
||||||
|
username: username,
|
||||||
|
user_id: user_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse requested roles
|
||||||
|
if (typeof roles === "string") {
|
||||||
|
roles = roles.split(",").map(role => role.trim())
|
||||||
|
} else {
|
||||||
|
return res.send("No effect")
|
||||||
|
}
|
||||||
|
|
||||||
|
// get current user roles
|
||||||
|
const user = await User.findOne({ ...userQuery })
|
||||||
|
if (typeof user === "undefined") {
|
||||||
|
return res.status(404).send(`[${username}] User not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// query all roles mutation
|
||||||
|
let queryRoles = []
|
||||||
|
if (Array.isArray(roles)) {
|
||||||
|
queryRoles = roles
|
||||||
|
} else if (typeof roles === 'string') {
|
||||||
|
queryRoles.push(roles)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mutate all roles
|
||||||
|
if (queryRoles.length > 0 && Array.isArray(user.roles)) {
|
||||||
|
queryRoles.forEach(role => {
|
||||||
|
user.roles = user.roles.filter(_role => _role !== role)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// update user roles
|
||||||
|
await user.save()
|
||||||
|
return res.send("done")
|
||||||
|
},
|
||||||
|
grantRole: async (req, res) => {
|
||||||
|
// check if issuer user is admin
|
||||||
|
if (!req.isAdmin()) {
|
||||||
|
return res.status(403).send("You do not have administrator permission")
|
||||||
|
}
|
||||||
|
|
||||||
|
let { user_id, username, roles } = req.body
|
||||||
|
const userQuery = {
|
||||||
|
username: username,
|
||||||
|
user_id: user_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse requested roles
|
||||||
|
if (typeof roles === "string") {
|
||||||
|
roles = roles.split(",").map(role => role.trim())
|
||||||
|
} else {
|
||||||
|
return res.send("No effect")
|
||||||
|
}
|
||||||
|
|
||||||
|
// get current user roles
|
||||||
|
const user = await User.findOne({ ...userQuery })
|
||||||
|
if (typeof user === "undefined") {
|
||||||
|
return res.status(404).send(`[${username}] User not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// query all roles mutation
|
||||||
|
let queryRoles = []
|
||||||
|
if (Array.isArray(roles)) {
|
||||||
|
queryRoles = roles
|
||||||
|
} else if (typeof roles === 'string') {
|
||||||
|
queryRoles.push(roles)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// mutate all roles
|
||||||
|
if (queryRoles.length > 0 && Array.isArray(user.roles)) {
|
||||||
|
queryRoles.forEach(role => {
|
||||||
|
if (!user.roles.includes(role)) {
|
||||||
|
user.roles.push(role)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// update user roles
|
||||||
|
await user.save()
|
||||||
|
return res.send("done")
|
||||||
|
},
|
||||||
|
updatePassword: async (req, res) => {
|
||||||
|
//TODO
|
||||||
|
},
|
||||||
|
updateSelf: async (req, res) => {
|
||||||
|
Object.keys(req.body).forEach(key => {
|
||||||
|
req.user[key] = req.body[key]
|
||||||
|
})
|
||||||
|
|
||||||
|
User.findOneAndUpdate({ _id: req.user._id }, req.user)
|
||||||
|
.then(() => {
|
||||||
|
return res.send(req.user)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
return res.send(500).send(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
update: async (req, res) => {
|
||||||
|
// TODO
|
||||||
|
},
|
||||||
|
login: async (req, res) => {
|
||||||
|
passport.authenticate("local", { session: false }, async (error, user, options) => {
|
||||||
|
if (error) {
|
||||||
|
return res.status(500).json(`Error validating user > ${error.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json("Invalid credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
user_id: user._id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.body.allowRegenerate) {
|
||||||
|
payload.allowRegenerate = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate token
|
||||||
|
const token = Token.signNew(payload, options)
|
||||||
|
|
||||||
|
// send result
|
||||||
|
res.json({ token: token })
|
||||||
|
})(req, res)
|
||||||
|
},
|
||||||
|
logout: async (req, res, next) => {
|
||||||
|
req.body = {
|
||||||
|
user_id: req.decodedToken.user_id,
|
||||||
|
token: req.jwtToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return SessionController.delete(req, res, next)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserController
|
3
packages/server/src/controllers/index.js
Normal file
3
packages/server/src/controllers/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { default as RolesController } from './RolesController'
|
||||||
|
export { default as SessionController } from './SessionController'
|
||||||
|
export { default as UserController } from './UserController'
|
109
packages/server/src/endpoints/index.js
Normal file
109
packages/server/src/endpoints/index.js
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
route: "/regenerate",
|
||||||
|
method: "POST",
|
||||||
|
middleware: ["ensureAuthenticated", "useJwtStrategy"],
|
||||||
|
fn: "SessionController.regenerate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/role",
|
||||||
|
method: 'PUT',
|
||||||
|
middleware: ["ensureAuthenticated", "roles"],
|
||||||
|
fn: "UserController.grantRole"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/role",
|
||||||
|
method: 'DELETE',
|
||||||
|
middleware: ["ensureAuthenticated", "roles"],
|
||||||
|
fn: "UserController.denyRole"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/roles",
|
||||||
|
method: "GET",
|
||||||
|
fn: "RolesController.get",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/session",
|
||||||
|
method: 'DELETE',
|
||||||
|
middleware: "ensureAuthenticated",
|
||||||
|
fn: "SessionController.delete",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/sessions",
|
||||||
|
method: 'DELETE',
|
||||||
|
middleware: "ensureAuthenticated",
|
||||||
|
fn: "SessionController.deleteAll",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/validate_session",
|
||||||
|
method: "POST",
|
||||||
|
middleware: "useJwtStrategy",
|
||||||
|
fn: "SessionController.validate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/sessions",
|
||||||
|
method: "GET",
|
||||||
|
middleware: "ensureAuthenticated",
|
||||||
|
fn: "SessionController.get",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/has_permissions",
|
||||||
|
method: "POST",
|
||||||
|
middleware: [
|
||||||
|
"ensureAuthenticated",
|
||||||
|
"hasPermissions"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/self",
|
||||||
|
method: "GET",
|
||||||
|
middleware: "ensureAuthenticated",
|
||||||
|
fn: "UserController.getSelf",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/users",
|
||||||
|
method: "GET",
|
||||||
|
middleware: "ensureAuthenticated",
|
||||||
|
fn: "UserController.get",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/user",
|
||||||
|
method: "GET",
|
||||||
|
middleware: "ensureAuthenticated",
|
||||||
|
fn: "UserController.getOne",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/self_user",
|
||||||
|
method: "PUT",
|
||||||
|
middleware: "ensureAuthenticated",
|
||||||
|
fn: "UserController.updateSelf",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/user",
|
||||||
|
method: "PUT",
|
||||||
|
middleware: ["ensureAuthenticated", "privileged"],
|
||||||
|
fn: "UserController.update",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/login",
|
||||||
|
method: "POST",
|
||||||
|
fn: "UserController.login",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/logout",
|
||||||
|
method: "POST",
|
||||||
|
middleware: ["ensureAuthenticated"],
|
||||||
|
fn: "UserController.logout",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/register",
|
||||||
|
method: "POST",
|
||||||
|
fn: "UserController.register",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/is_auth",
|
||||||
|
method: "POST",
|
||||||
|
middleware: "ensureAuthenticated",
|
||||||
|
fn: "UserController.isAuth",
|
||||||
|
}
|
||||||
|
]
|
@ -92,7 +92,7 @@ class Server {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
console.log("🌐 Trying to connect to DB...")
|
console.log("🌐 Trying to connect to DB...")
|
||||||
mongoose.connect(this.getDBConnectionString(), { useNewUrlParser: true, useFindAndModify: false })
|
mongoose.connect(this.getDBConnectionString(), { useNewUrlParser: true, useUnifiedTopology: true })
|
||||||
.then((res) => { return resolve(true) })
|
.then((res) => { return resolve(true) })
|
||||||
.catch((err) => { return reject(err) })
|
.catch((err) => { return reject(err) })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
3
packages/server/src/lib/index.js
Normal file
3
packages/server/src/lib/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * as Token from './token'
|
||||||
|
export { default as Schematized } from './schematized'
|
||||||
|
export { default as selectValues } from './selectValues'
|
20
packages/server/src/lib/schematized/index.js
Normal file
20
packages/server/src/lib/schematized/index.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export default (schema, fn) => {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
const missingKeys = []
|
||||||
|
const requiredKeys = Array.isArray(schema) ? schema : []
|
||||||
|
|
||||||
|
for await (let key of requiredKeys) {
|
||||||
|
if (typeof req.body[key] === "undefined") {
|
||||||
|
missingKeys.push(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingKeys.length > 0) {
|
||||||
|
return res.status(400).json({ error: `Missing required keys > ${missingKeys}` })
|
||||||
|
} else {
|
||||||
|
if (typeof fn === "function") {
|
||||||
|
return await fn(req, res, next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
packages/server/src/lib/selectValues/index.js
Normal file
27
packages/server/src/lib/selectValues/index.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
export default (query = [], fn) => {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
if (typeof fn === "function") {
|
||||||
|
const obj = {}
|
||||||
|
|
||||||
|
if (!req.body) {
|
||||||
|
req.body = {}
|
||||||
|
}
|
||||||
|
if (!req.query) {
|
||||||
|
req.query = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(query)) {
|
||||||
|
query.forEach(key => {
|
||||||
|
const value = req.body[key] ?? req.query[key]
|
||||||
|
if (typeof value !== "undefined") {
|
||||||
|
obj[key] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
req.selectedValues = obj
|
||||||
|
|
||||||
|
return await fn(req, res, next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
packages/server/src/lib/token/index.js
Normal file
29
packages/server/src/lib/token/index.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import jwt from 'jsonwebtoken'
|
||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
import { Session } from '../../models'
|
||||||
|
|
||||||
|
export function signNew(payload, options) {
|
||||||
|
const data = {
|
||||||
|
uuid: nanoid(),
|
||||||
|
allowRegenerate: false,
|
||||||
|
...payload
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = jwt.sign(data, options.secretOrKey, {
|
||||||
|
expiresIn: options.expiresIn ?? "1h",
|
||||||
|
algorithm: options.algorithm ?? "HS256"
|
||||||
|
})
|
||||||
|
|
||||||
|
let newSession = new Session({
|
||||||
|
uuid: data.uuid,
|
||||||
|
user_id: data.user_id,
|
||||||
|
allowRegenerate: data.allowRegenerate,
|
||||||
|
token: token,
|
||||||
|
date: new Date().getTime(),
|
||||||
|
location: options.sessionLocationSign
|
||||||
|
})
|
||||||
|
|
||||||
|
newSession.save()
|
||||||
|
|
||||||
|
return token
|
||||||
|
}
|
40
packages/server/src/middlewares/ensureAuthenticated/index.js
Normal file
40
packages/server/src/middlewares/ensureAuthenticated/index.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import passport from 'passport'
|
||||||
|
import { Session } from '../../models'
|
||||||
|
|
||||||
|
export default (req, res, next) => {
|
||||||
|
function unauthorized() {
|
||||||
|
console.log("Returning failed session")
|
||||||
|
return res.status(401).send({ error: 'Invalid session', })
|
||||||
|
}
|
||||||
|
|
||||||
|
const authHeader = req.headers?.authorization?.split(' ')
|
||||||
|
|
||||||
|
if (authHeader && authHeader[0] === 'Bearer') {
|
||||||
|
const token = authHeader[1]
|
||||||
|
|
||||||
|
passport.authenticate('jwt', { session: false }, async (err, user, decodedToken) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).send({ error: err.message })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).send({ error: "No user data found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessions = await Session.find({ user_id: decodedToken.user_id })
|
||||||
|
const sessionsTokens = sessions.map(session => session.token)
|
||||||
|
|
||||||
|
if (!sessionsTokens.includes(token)) {
|
||||||
|
return unauthorized()
|
||||||
|
}
|
||||||
|
|
||||||
|
req.user = user
|
||||||
|
req.jwtToken = token
|
||||||
|
req.decodedToken = decodedToken
|
||||||
|
|
||||||
|
return next()
|
||||||
|
})(req, res, next)
|
||||||
|
} else {
|
||||||
|
return unauthorized()
|
||||||
|
}
|
||||||
|
}
|
5
packages/server/src/middlewares/errorHandler/index.js
Normal file
5
packages/server/src/middlewares/errorHandler/index.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const errorHandler = (error, req, res, next) => {
|
||||||
|
res.json({ error: error.message })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default errorHandler
|
30
packages/server/src/middlewares/hasPermissions/index.js
Normal file
30
packages/server/src/middlewares/hasPermissions/index.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
export const hasPermissions = (req, res, next) => {
|
||||||
|
if (typeof (req.userData) == "undefined") {
|
||||||
|
return res.status(403).json(`User data is not available, please ensure if you are authenticated`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { _id, username, roles } = req.userData
|
||||||
|
const { permissions } = req.body
|
||||||
|
|
||||||
|
req.userPermissions = roles
|
||||||
|
|
||||||
|
let check = []
|
||||||
|
|
||||||
|
if (Array.isArray(permissions)) {
|
||||||
|
check = permissions
|
||||||
|
} else {
|
||||||
|
check.push(permissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (check.length > 0) {
|
||||||
|
check.forEach((role) => {
|
||||||
|
if (!roles.includes(role)) {
|
||||||
|
return res.status(403).json(`${username} not have permissions ${permissions}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hasPermissions
|
4
packages/server/src/middlewares/index.js
Normal file
4
packages/server/src/middlewares/index.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { default as ensureAuthenticated } from './ensureAuthenticated'
|
||||||
|
export { default as errorHandler } from './errorHandler'
|
||||||
|
export { default as hasPermissions } from './hasPermissions'
|
||||||
|
export { default as roles } from './roles'
|
7
packages/server/src/middlewares/privileged/index.js
Normal file
7
packages/server/src/middlewares/privileged/index.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export default (req, res, next) => {
|
||||||
|
if (!req.user.roles.includes("admin")) {
|
||||||
|
return res.status(401).send({ error: "To make this request it is necessary to have administrator permissions" })
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
11
packages/server/src/middlewares/roles/index.js
Normal file
11
packages/server/src/middlewares/roles/index.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export default (req, res, next) => {
|
||||||
|
req.isAdmin = () => {
|
||||||
|
if (req.user.roles.includes("admin")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
28
packages/server/src/models/index.js
Normal file
28
packages/server/src/models/index.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import mongoose from 'mongoose'
|
||||||
|
import { Schema } from 'mongoose'
|
||||||
|
|
||||||
|
function getSchemas() {
|
||||||
|
const obj = Object()
|
||||||
|
|
||||||
|
const _schemas = require("../schemas")
|
||||||
|
Object.keys(_schemas).forEach(key => {
|
||||||
|
obj[key] = Schema(_schemas[key])
|
||||||
|
})
|
||||||
|
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemas = getSchemas()
|
||||||
|
|
||||||
|
export const FabricObject = mongoose.model('FabricObject', schemas.FabricObject, "fabricObjects")
|
||||||
|
export const Workload = mongoose.model('Workload', schemas.Workload, "workload")
|
||||||
|
export const Workshifts = mongoose.model('Workshifts', schemas.Workshift, "workshifts")
|
||||||
|
export const Role = mongoose.model('Role', schemas.Role, 'roles')
|
||||||
|
export const Vault = mongoose.model('Vault', schemas.VaultItem, "vault")
|
||||||
|
export const GeoRegion = mongoose.model('GeoRegion', schemas.Region, "regions")
|
||||||
|
|
||||||
|
export const Contract = mongoose.model('Contract', schemas.Contract, "contracts")
|
||||||
|
export const User = mongoose.model('User', schemas.User, "accounts")
|
||||||
|
export const Session = mongoose.model('Session', schemas.Session, "sessions")
|
||||||
|
export const Workshift = mongoose.model("Workshift", schemas.Workshift, "workshifts")
|
||||||
|
export const Report = mongoose.model("Report", schemas.Report, "reports")
|
3
packages/server/src/schemas/index.js
Normal file
3
packages/server/src/schemas/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { default as User } from './user'
|
||||||
|
export { default as Role } from './role'
|
||||||
|
export { default as Session } from './session'
|
5
packages/server/src/schemas/role/index.js
Normal file
5
packages/server/src/schemas/role/index.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
apply: Object
|
||||||
|
}
|
9
packages/server/src/schemas/session/index.js
Normal file
9
packages/server/src/schemas/session/index.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export default {
|
||||||
|
allowRegenerate: { type: Boolean, default: false },
|
||||||
|
uuid: { type: String, required: true },
|
||||||
|
token: { type: String, required: true },
|
||||||
|
user_id: { type: String, required: true },
|
||||||
|
date: { type: Number, default: 0 },
|
||||||
|
location: { type: String, default: "Unknown" },
|
||||||
|
geo: { type: String, default: "Unknown" },
|
||||||
|
}
|
10
packages/server/src/schemas/user/index.js
Normal file
10
packages/server/src/schemas/user/index.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export default {
|
||||||
|
username: { type: String, required: true },
|
||||||
|
password: { type: String, required: true, select: false },
|
||||||
|
fullName: String,
|
||||||
|
avatar: { type: String },
|
||||||
|
email: String,
|
||||||
|
roles: [],
|
||||||
|
legal_id: Object,
|
||||||
|
phone: Number,
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user