122 |
123 |
124 |
125 |
126 | IAMOVPN
127 |
128 |
129 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 | );
156 | }
157 | }
158 |
159 |
160 | const mapStateToProps = (state) => {
161 | return {
162 | me: state.user.get('me')
163 | }
164 | };
165 | export default connect(mapStateToProps)(withStyles(styles)(Dashboard));
--------------------------------------------------------------------------------
/iamovpn/views/front/src/components/Login.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import {
5 | Avatar,
6 | Button,
7 | CssBaseline,
8 | TextField,
9 | Box,
10 | Typography,
11 | Container
12 | } from '@material-ui/core';
13 | import LockOutlinedIcon from '@material-ui/icons/LockOutlined';
14 | import { withStyles } from '@material-ui/core/styles';
15 | import { withSnackbar } from 'notistack';
16 |
17 | import Copyright from "./Copyright";
18 |
19 | import history from "../history";
20 | import * as userActions from '../modules/user';
21 |
22 |
23 | const styles = theme => ({
24 | paper: {
25 | marginTop: theme.spacing(8),
26 | display: 'flex',
27 | flexDirection: 'column',
28 | alignItems: 'center',
29 | },
30 | avatar: {
31 | margin: theme.spacing(1),
32 | backgroundColor: theme.palette.secondary.main,
33 | },
34 | form: {
35 | width: '100%', // Fix IE 11 issue.
36 | marginTop: theme.spacing(1),
37 | },
38 | submit: {
39 | margin: theme.spacing(3, 0, 2),
40 | },
41 | });
42 |
43 |
44 | class Login extends Component {
45 | constructor(props) {
46 | super(props);
47 | this.state = {
48 | id: {
49 | value: '',
50 | error: false
51 | },
52 | password: {
53 | value: '',
54 | error: false
55 | }
56 | };
57 | }
58 |
59 | handleChange(e) {
60 | let value = e.target.value;
61 | let error = false;
62 | if (value.length < 4) {
63 | error = true
64 | }
65 |
66 | this.setState({
67 | [e.target.name]: {
68 | value: value,
69 | error: error
70 | }
71 | });
72 | }
73 |
74 | handleSubmit(e) {
75 | const { dispatch, enqueueSnackbar } = this.props;
76 | let state = this.state;
77 | let error = false;
78 |
79 | ['id', 'password'].forEach(k => {
80 | if (state[k].value < 4) {
81 | error = state[k].error = true;
82 | }
83 | });
84 | if (error) {
85 | this.setState(state);
86 | } else {
87 | dispatch(
88 | userActions.login(
89 | state.id.value,
90 | state.password.value
91 | )
92 | ).then(response => {
93 | const user = response.payload.data.user;
94 | if (user.admin === true) {
95 | history.push('/dashboard');
96 | } else {
97 | history.push('/my');
98 | }
99 | })
100 | .catch(response => {
101 | const {id, password} = this.state;
102 | const {status, data} = response.error.response;
103 | if (data.hasOwnProperty('errors')) {
104 | Object.values(data.errors).forEach(msg => {
105 | enqueueSnackbar(msg, {variant: "error"});
106 | });
107 | } else {
108 | enqueueSnackbar(`${status}: ${data.message}`, {variant: "error"});
109 | }
110 |
111 | id.error = password.error = true;
112 | this.setState({
113 | id: id,
114 | password: password
115 | });
116 | });
117 | }
118 |
119 | e.preventDefault();
120 | }
121 |
122 | render() {
123 | const { classes } = this.props;
124 |
125 | return (
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | Sign in
134 |
135 |
178 |
179 |
180 |
181 |
182 |
183 | );
184 | }
185 | }
186 |
187 | export default connect()(withSnackbar(withStyles(styles)(Login)));
188 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/node,python,jetbrains+all
3 | # Edit at https://www.gitignore.io/?templates=node,python,jetbrains+all
4 |
5 | ### JetBrains+all ###
6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
8 |
9 | # User-specific stuff
10 | .idea/**/workspace.xml
11 | .idea/**/tasks.xml
12 | .idea/**/usage.statistics.xml
13 | .idea/**/dictionaries
14 | .idea/**/shelf
15 |
16 | # Generated files
17 | .idea/**/contentModel.xml
18 |
19 | # Sensitive or high-churn files
20 | .idea/**/dataSources/
21 | .idea/**/dataSources.ids
22 | .idea/**/dataSources.local.xml
23 | .idea/**/sqlDataSources.xml
24 | .idea/**/dynamic.xml
25 | .idea/**/uiDesigner.xml
26 | .idea/**/dbnavigator.xml
27 |
28 | # Gradle
29 | .idea/**/gradle.xml
30 | .idea/**/libraries
31 |
32 | # Gradle and Maven with auto-import
33 | # When using Gradle or Maven with auto-import, you should exclude module files,
34 | # since they will be recreated, and may cause churn. Uncomment if using
35 | # auto-import.
36 | # .idea/modules.xml
37 | # .idea/*.iml
38 | # .idea/modules
39 | # *.iml
40 | # *.ipr
41 |
42 | # CMake
43 | cmake-build-*/
44 |
45 | # Mongo Explorer plugin
46 | .idea/**/mongoSettings.xml
47 |
48 | # File-based project format
49 | *.iws
50 |
51 | # IntelliJ
52 | out/
53 |
54 | # mpeltonen/sbt-idea plugin
55 | .idea_modules/
56 |
57 | # JIRA plugin
58 | atlassian-ide-plugin.xml
59 |
60 | # Cursive Clojure plugin
61 | .idea/replstate.xml
62 |
63 | # Crashlytics plugin (for Android Studio and IntelliJ)
64 | com_crashlytics_export_strings.xml
65 | crashlytics.properties
66 | crashlytics-build.properties
67 | fabric.properties
68 |
69 | # Editor-based Rest Client
70 | .idea/httpRequests
71 |
72 | # Android studio 3.1+ serialized cache file
73 | .idea/caches/build_file_checksums.ser
74 |
75 | ### JetBrains+all Patch ###
76 | # Ignores the whole .idea folder and all .iml files
77 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
78 |
79 | .idea/
80 |
81 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
82 |
83 | *.iml
84 | modules.xml
85 | .idea/misc.xml
86 | *.ipr
87 |
88 | # Sonarlint plugin
89 | .idea/sonarlint
90 |
91 | ### Node ###
92 | # Logs
93 | logs
94 | *.log
95 | npm-debug.log*
96 | yarn-debug.log*
97 | yarn-error.log*
98 | lerna-debug.log*
99 |
100 | # Diagnostic reports (https://nodejs.org/api/report.html)
101 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
102 |
103 | # Runtime data
104 | pids
105 | *.pid
106 | *.seed
107 | *.pid.lock
108 |
109 | # Directory for instrumented libs generated by jscoverage/JSCover
110 | lib-cov
111 |
112 | # Coverage directory used by tools like istanbul
113 | coverage
114 | *.lcov
115 |
116 | # nyc test coverage
117 | .nyc_output
118 |
119 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
120 | .grunt
121 |
122 | # Bower dependency directory (https://bower.io/)
123 | bower_components
124 |
125 | # node-waf configuration
126 | .lock-wscript
127 |
128 | # Compiled binary addons (https://nodejs.org/api/addons.html)
129 | build/Release
130 |
131 | # Dependency directories
132 | node_modules/
133 | jspm_packages/
134 |
135 | # TypeScript v1 declaration files
136 | typings/
137 |
138 | # TypeScript cache
139 | *.tsbuildinfo
140 |
141 | # Optional npm cache directory
142 | .npm
143 |
144 | # Optional eslint cache
145 | .eslintcache
146 |
147 | # Optional REPL history
148 | .node_repl_history
149 |
150 | # Output of 'npm pack'
151 | *.tgz
152 |
153 | # Yarn Integrity file
154 | .yarn-integrity
155 |
156 | # dotenv environment variables file
157 | .env
158 | .env.test
159 |
160 | # parcel-bundler cache (https://parceljs.org/)
161 | .cache
162 |
163 | # next.js build output
164 | .next
165 |
166 | # nuxt.js build output
167 | .nuxt
168 |
169 | # react / gatsby
170 | public/
171 |
172 | # vuepress build output
173 | .vuepress/dist
174 |
175 | # Serverless directories
176 | .serverless/
177 |
178 | # FuseBox cache
179 | .fusebox/
180 |
181 | # DynamoDB Local files
182 | .dynamodb/
183 |
184 | ### Python ###
185 | # Byte-compiled / optimized / DLL files
186 | __pycache__/
187 | *.py[cod]
188 | *$py.class
189 |
190 | # C extensions
191 | *.so
192 |
193 | # Distribution / packaging
194 | .Python
195 | build/
196 | develop-eggs/
197 | dist/
198 | downloads/
199 | eggs/
200 | .eggs/
201 | lib/
202 | lib64/
203 | parts/
204 | sdist/
205 | var/
206 | wheels/
207 | pip-wheel-metadata/
208 | share/python-wheels/
209 | *.egg-info/
210 | .installed.cfg
211 | *.egg
212 | MANIFEST
213 |
214 | # PyInstaller
215 | # Usually these files are written by a python script from a template
216 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
217 | *.manifest
218 | *.spec
219 |
220 | # Installer logs
221 | pip-log.txt
222 | pip-delete-this-directory.txt
223 |
224 | # Unit test / coverage reports
225 | htmlcov/
226 | .tox/
227 | .nox/
228 | .coverage
229 | .coverage.*
230 | nosetests.xml
231 | coverage.xml
232 | *.cover
233 | .hypothesis/
234 | .pytest_cache/
235 |
236 | # Translations
237 | *.mo
238 | *.pot
239 |
240 | # Scrapy stuff:
241 | .scrapy
242 |
243 | # Sphinx documentation
244 | docs/_build/
245 |
246 | # PyBuilder
247 | target/
248 |
249 | # pyenv
250 | .python-version
251 |
252 | # pipenv
253 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
254 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
255 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
256 | # install all needed dependencies.
257 | #Pipfile.lock
258 |
259 | # celery beat schedule file
260 | celerybeat-schedule
261 |
262 | # SageMath parsed files
263 | *.sage.py
264 |
265 | # Spyder project settings
266 | .spyderproject
267 | .spyproject
268 |
269 | # Rope project settings
270 | .ropeproject
271 |
272 | # Mr Developer
273 | .mr.developer.cfg
274 | .project
275 | .pydevproject
276 |
277 | # mkdocs documentation
278 | /site
279 |
280 | # mypy
281 | .mypy_cache/
282 | .dmypy.json
283 | dmypy.json
284 |
285 | # Pyre type checker
286 | .pyre/
287 |
288 |
289 | ### VirtualEnv ###
290 | # Virtualenv
291 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
292 | .Python
293 | [Bb]in
294 | [Ii]nclude
295 | [Ll]ib
296 | [Ll]ib64
297 | [Ll]ocal
298 | [Ss]cripts
299 | pyvenv.cfg
300 | .env
301 | .venv
302 | env/
303 | venv/
304 | ENV/
305 | env.bak/
306 | venv.bak/
307 | pip-selfcheck.json
308 |
309 |
310 | # End of https://www.gitignore.io/api/node,python,jetbrains+all
311 |
312 |
313 | ## zsh auto env
314 | .autoenv*
315 |
316 | ## config file
317 | config.json
318 |
319 | # OS X
320 | .DS_Store
--------------------------------------------------------------------------------
/iamovpn/views/front/src/components/LogList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import {
5 | Paper,
6 | Table,
7 | TableBody,
8 | TableCell,
9 | TableHead,
10 | TableRow,
11 | TableFooter,
12 | TablePagination,
13 | TextField,
14 | IconButton, Typography
15 | } from '@material-ui/core';
16 | import SearchIcon from '@material-ui/icons/Search';
17 |
18 | import * as logActions from "../modules/log";
19 |
20 |
21 | class LogList extends Component {
22 | constructor(props) {
23 | super(props);
24 | const {size} = this.props;
25 |
26 | this.state = {
27 | tableSize: size,
28 | page: 0,
29 | keyword: ''
30 | };
31 | }
32 |
33 | componentDidMount() {
34 | const { dispatch } = this.props;
35 | dispatch(logActions.getList(
36 | this.state.keyword,
37 | this.state.page * this.state.tableSize,
38 | this.state.tableSize,
39 | ))
40 | };
41 |
42 | handleFindKeyword(e) {
43 | const { dispatch } = this.props;
44 | dispatch(logActions.getList(
45 | this.state.keyword,
46 | this.state.page * this.state.tableSize,
47 | this.state.tableSize,
48 | ));
49 |
50 | e.preventDefault();
51 | };
52 |
53 | handleChangePage(event, newPage) {
54 | const { dispatch } = this.props;
55 |
56 | this.setState({
57 | page: newPage
58 | });
59 |
60 | dispatch(logActions.getList(
61 | this.state.keyword,
62 | newPage * this.state.tableSize,
63 | this.state.tableSize,
64 | ))
65 | };
66 |
67 | handleChangeRowsPerPage(event) {
68 | const { dispatch } = this.props;
69 | const newTableSize = parseInt(event.target.value, 10);
70 |
71 | this.setState({
72 | tableSize: newTableSize,
73 | page: 0
74 | });
75 |
76 | dispatch(logActions.getList(
77 | this.state.keyword,
78 | 0,
79 | newTableSize,
80 | ))
81 | };
82 |
83 | handleChangeKeyword(e) {
84 | this.setState({
85 | keyword: e.target.value
86 | })
87 | };
88 |
89 | renderLogs() {
90 | const {logs} = this.props;
91 | let result = [];
92 |
93 | if (logs.length === 0) {
94 | result = [
95 |
96 | Empty
97 |
98 | ]
99 | } else {
100 | logs.forEach(log => {
101 | result.push(
102 |
103 | {log.log_type}
104 | {log.user_id}
105 | {log.authorized? "True" : "False"}
106 | {log.updated.format("YYYY-MM-DD HH:mm")}
107 | {log.remote_ip}
108 | {log.remote_port}
109 | {log.local_ip}
110 | {log.in_bytes? `${log.in_bytes} Bytes` :""}
111 | {log.out_bytes? `${log.out_bytes} Bytes` :""}
112 | {log.duration}
113 |
114 | )
115 | })
116 | }
117 |
118 | return result;
119 | };
120 |
121 | render() {
122 | return
123 |
124 |
125 |
126 |
127 |
131 | Logs
132 |
133 |
134 |
135 |
145 |
146 |
147 |
148 | Type
149 | User ID
150 | Authorized
151 | Last Seen
152 | Remote IP
153 | Remote Port
154 | Local IP
155 | In Bytes
156 | Out Bytes
157 | Duration seconds
158 |
159 |
160 |
161 | {this.renderLogs()}
162 |
163 |
164 |
165 |
166 |
167 |
175 |
176 |
177 |
178 |
179 | }
180 | }
181 |
182 |
183 | const mapStateToProps = (state) => {
184 | return {
185 | logs: state.log.get('logs'),
186 | log_count: state.log.get('log_count')
187 | }
188 | };
189 | export default connect(mapStateToProps)(LogList)
--------------------------------------------------------------------------------
/iamovpn/views/front/src/components/UserForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import {
5 | FormControlLabel,
6 | FormGroup,
7 | Checkbox,
8 | TextField,
9 | Button,
10 | ButtonGroup,
11 | Typography
12 | } from '@material-ui/core';
13 | import { withSnackbar } from 'notistack';
14 |
15 | import * as userActions from "../modules/user";
16 |
17 | const defaultUser = {
18 | uid: null,
19 | id: '',
20 | name: '',
21 | password: '',
22 | admin: false,
23 | active: true
24 | };
25 |
26 |
27 | class UserForm extends Component {
28 | constructor(props) {
29 | super(props);
30 | const callback = this.props.callback || (() => {});
31 | const user = this.props.user || defaultUser;
32 |
33 | this.state = {
34 | callback: callback,
35 | user: this.marshalUser(user),
36 | error: {
37 | id: false,
38 | name: false,
39 | password: false,
40 | admin: false,
41 | active: false
42 | }
43 | };
44 | };
45 |
46 | marshalUser(user) {
47 | const marshaled = defaultUser;
48 | Object.keys(marshaled).forEach(key => {
49 | if (user[key] || (typeof user[key] === 'boolean')) {
50 | marshaled[key] = user[key];
51 | }
52 | });
53 | if (user.uid !== null) {
54 | delete marshaled['password'];
55 | }
56 | return marshaled;
57 | }
58 |
59 | componentDidMount() {
60 | const {dispatch, enqueueSnackbar, user} = this.props;
61 |
62 | if ((user || null) !== null && (user.uid || null) !== null) {
63 | dispatch(userActions.get(user.uid))
64 | .then(response => {
65 | this.setState({
66 | user: this.marshalUser(response.payload.data.user)
67 | });
68 | }).catch(response => {
69 | const {status, data} = response.error.response;
70 | enqueueSnackbar(`${status}: ${data.message}`, {variant: "error"});
71 | })
72 | } else {
73 | this.setState({
74 | user: defaultUser
75 | });
76 | }
77 | };
78 |
79 | componentDidUpdate(prevProps, prevState, snapshot) {
80 | const prevUser = prevProps.user || {uid: ''};
81 | const currentUser = this.props.user || {uid: ''};
82 | if (prevUser.uid !== currentUser.uid) {
83 | this.setState({
84 | user: this.marshalUser(this.props.user || defaultUser)
85 | });
86 | }
87 | };
88 |
89 | handleUserChange(e) {
90 | const {user} = this.state;
91 | user[e.target.name] = (typeof user[e.target.name] === "boolean")? e.target.checked : e.target.value;
92 |
93 | this.setState({user: user})
94 | };
95 |
96 | saveUser() {
97 | const {dispatch, enqueueSnackbar} = this.props;
98 | const user = this.state.user;
99 |
100 | if ((user.uid || null) !== null) {
101 | dispatch(userActions.modify(user.uid, user))
102 | .then(response => {
103 | enqueueSnackbar("Saved", {variant: "success"});
104 | this.state.callback(true);
105 | })
106 | .catch(response => {
107 | const {status, data} = response.error.response;
108 | if (data.hasOwnProperty('errors')) {
109 | Object.values(data.errors).forEach(msg => {
110 | enqueueSnackbar(msg, {variant: "error"});
111 | });
112 | } else {
113 | enqueueSnackbar(`${status}: ${data.message}`, {variant: "error"});
114 | }
115 | })
116 | } else {
117 | dispatch(userActions.create(user))
118 | .then(response => {
119 | enqueueSnackbar("Saved", {variant: "success"});
120 | this.state.callback(true);
121 | })
122 | .catch(response => {
123 | const {status, data} = response.error.response;
124 | if (data.hasOwnProperty('errors')) {
125 | Object.values(data.errors).forEach(msg => {
126 | enqueueSnackbar(msg, {variant: "error"});
127 | });
128 | } else {
129 | enqueueSnackbar(`${status}: ${data.message}`, {variant: "error"});
130 | }
131 | })
132 | }
133 | };
134 |
135 | renderForm() {
136 | return Object.keys(this.state.user).map(attr => {
137 | if (attr !== 'uid') {
138 | if (typeof defaultUser[attr] === 'boolean') {
139 | return
140 | }
146 | label={`is ${attr}`}
147 | />
148 |
149 | } else {
150 | return
161 | }
162 | }
163 | return null;
164 | });
165 | }
166 |
167 | render() {
168 | const {title} = this.props;
169 | return
170 | {title? {title}:null}
171 |
182 |
183 | }
184 | }
185 |
186 | export default connect()(withSnackbar(UserForm));
--------------------------------------------------------------------------------
/iamovpn/apis/user.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 | from copy import deepcopy
4 | from flask import g, request
5 | from flask_restplus import Resource, abort, fields
6 | import database as db
7 | from datetime import datetime
8 | from models import User
9 | from .secure import need_session
10 | from flask_restplus import Namespace
11 | import config
12 |
13 | api = Namespace('user')
14 |
15 | user_model = api.model(
16 | 'User',
17 | {
18 | 'id': fields.String(description="User id"),
19 | 'name': fields.String(description="User name"),
20 | 'admin': fields.Boolean(description="Is admin"),
21 | 'active': fields.Boolean(description="Is not blocked")
22 | }
23 | )
24 |
25 | user_create_model = api.model(
26 | 'UserCreate',
27 | dict({
28 | 'password': fields.String(description="User password")
29 | }, **deepcopy(user_model))
30 | )
31 | for field in user_create_model.values():
32 | field.required = True
33 |
34 |
35 | @api.route('')
36 | @api.response(401, 'Unauthorized')
37 | @api.response(403, 'Forbidden')
38 | class UsersApi(Resource):
39 | user_find_parser = api.parser()
40 | user_find_parser.add_argument('keyword', type=str, help='Keyword: ID, name', location='args')
41 | user_find_parser.add_argument('offset', type=int, help='Page offset', location='args', default=0)
42 | user_find_parser.add_argument('length', type=int, help='Page length', location='args', default=20)
43 |
44 | @need_session(admin=True)
45 | @api.expect(user_find_parser)
46 | @api.doc(security='cookieAuth')
47 | def get(self):
48 | args = self.user_find_parser.parse_args()
49 | if args['length'] > 100:
50 | abort(400, '"length" must be <= 100')
51 |
52 | users, count = User.find(
53 | keyword=args.get('keyword'),
54 | offset=args['offset'],
55 | length=args['length']
56 | )
57 |
58 | return {
59 | 'state': 'success',
60 | 'users': [
61 | user.dict
62 | for user in users
63 | ],
64 | 'count': count
65 | }
66 |
67 | @need_session(admin=True)
68 | @api.response(406, 'Invalid data')
69 | @api.response(409, 'ID exists')
70 | @api.expect(user_create_model)
71 | @api.doc(security='cookieAuth')
72 | def post(self):
73 | data = request.json
74 | user_create_model.validate(data)
75 | data = {
76 | k: data[k]
77 | for k in user_create_model.keys()
78 | }
79 |
80 | user = User.query.filter(User.id == data['id']).first()
81 | if user:
82 | abort(409, 'ID exists')
83 |
84 | data['id'] = data['id'].strip()
85 | if ' ' in data['id'] or '\t' in data['id']:
86 | abort(406, 'ID can not contain spaces')
87 |
88 | data['password'] = data['password'].strip()
89 | if len(data['password']) < 6:
90 | abort(406, 'Password too short. It must be longer than 5.')
91 |
92 | user = User(**data)
93 | db.session.add(user)
94 | db.session.commit()
95 |
96 | return {
97 | 'state': 'success',
98 | 'user': user.dict
99 | }
100 |
101 |
102 | @api.route('/')
103 | @api.response(401, 'Unauthorized')
104 | @api.response(403, 'Forbidden')
105 | @api.response(404, 'Not Found')
106 | class UserApi(Resource):
107 | @need_session(admin=False)
108 | @api.doc(security='cookieAuth')
109 | def get(self, uid):
110 | if uid.lower() == 'me':
111 | return {
112 | 'state': 'success',
113 | 'user': g.user.dict
114 | }
115 |
116 | if g.user.uid != uid and not g.user.admin:
117 | abort(403)
118 |
119 | query = User.query.filter(User.uid == uid)
120 | if not g.user.admin:
121 | query = query.filter(User.active.is_(True))
122 |
123 | user = query.first()
124 | if user is None:
125 | abort(404, 'User not found')
126 |
127 | return {
128 | 'state': 'success',
129 | 'user': user.dict
130 | }
131 |
132 | @need_session(admin=False)
133 | @api.expect(user_model)
134 | @api.doc(security='cookieAuth')
135 | def put(self, uid):
136 | if uid.lower() == 'me':
137 | uid = g.user.uid
138 |
139 | if g.user.uid != uid and not g.user.admin:
140 | abort(403)
141 |
142 | query = User.query.filter(User.uid == uid)
143 | if not g.user.admin:
144 | query = query.filter(User.active.is_(True))
145 |
146 | user = query.first()
147 | if user is None:
148 | abort(404, 'User not found')
149 |
150 | data = request.json
151 | user_model.validate(data)
152 | data = {
153 | k: data[k]
154 | for k in data if k not in ('id', 'uid') and k in user_model
155 | }
156 |
157 | if data.get('admin') is not None and not g.user.admin:
158 | abort(403)
159 |
160 | if 'name' in data:
161 | data['name'] = data['name'].strip()
162 |
163 | for k, v in data.items():
164 | setattr(user, k, v)
165 |
166 | user.updated = datetime.now()
167 | db.session.commit()
168 | return {
169 | 'state': 'success',
170 | 'user': user.dict
171 | }
172 |
173 |
174 | @api.route('//password')
175 | @api.response(401, 'Unauthorized')
176 | @api.response(406, 'New password is not valid')
177 | class UserPasswordChangeAPI(Resource):
178 | password_model = api.model(
179 | 'Password',
180 | {
181 | 'password': fields.String(description="New password", required=True)
182 | }
183 | )
184 |
185 | @need_session(admin=True)
186 | @api.expect(password_model)
187 | @api.doc(security='cookieAuth')
188 | def put(self, uid):
189 | data = request.json
190 | self.password_model.validate(data)
191 |
192 | if len(data['password']) < 6:
193 | abort(406, 'Password too short. It must be longer than 5.')
194 |
195 | user = User.query.filter(User.uid == uid).first()
196 | if user is None:
197 | abort(404, 'User not found')
198 |
199 | user.password = data['password']
200 | user.updated = datetime.now()
201 | db.session.commit()
202 |
203 | return {'state': 'success'}
204 |
205 |
206 | @api.route('/config')
207 | @api.response(401, 'Unauthorized')
208 | class UserConfigAPI(Resource):
209 |
210 | @need_session(admin=False)
211 | @api.doc(security='cookieAuth')
212 | def get(self):
213 | ovpn_path = os.path.join(config['openvpn']['path'], 'client.ovpn')
214 | with open(ovpn_path, 'r') as f:
215 | return {
216 | 'state': 'success',
217 | 'config': f.read()
218 | }
219 |
220 |
--------------------------------------------------------------------------------
/iamovpn/install.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 | import shutil
4 | import wget
5 | import subprocess
6 | import netifaces
7 | from IPy import IP
8 | import database as db
9 | from app import create_app
10 | from models import User
11 | from flask import render_template
12 | from copy import deepcopy
13 |
14 |
15 | def init_db():
16 | print('## Init database ##')
17 | db.connect()
18 | db.create_all()
19 | print('Done')
20 |
21 |
22 | def install_ovpn_scripts(config, context):
23 | print('## Install openvpn callback script ##')
24 | context['api_host'] = '127.0.0.1' if '0.0.0.0' in config['host'] else config['host']
25 | context['api_port'] = config['port']
26 | context['api_key'] = config['api_keys'][0]
27 |
28 | os.makedirs(os.path.join(config['openvpn']['path'], 'scripts'), mode=0o755, exist_ok=True)
29 | for script_name in ['login.py', 'connect.py', 'disconnect.py', 'util.py']:
30 | script = render_template(os.path.join('ovpn_scripts', script_name), **context)
31 | script_path = os.path.join(config['openvpn']['path'], 'scripts', script_name)
32 | with open(script_path, 'w') as f:
33 | f.write(script)
34 | os.chmod(script_path, 0o755)
35 | print('Done\n')
36 |
37 |
38 | def install_ovpn_config(config, context):
39 | print('## Install openvpn configuration files ##')
40 | ovpn_path = config['openvpn']['path']
41 | context = deepcopy(context)
42 | context.update(config['openvpn'])
43 |
44 | with open(os.path.join(ovpn_path, 'ca.crt')) as f:
45 | context['ca'] = f.read()
46 | with open(os.path.join(ovpn_path, 'ta.key')) as f:
47 | context['ta'] = f.read()
48 |
49 | server_conf = render_template('configurations/server.conf', **context)
50 | server_conf_path = os.path.join(ovpn_path, 'server.conf')
51 | with open(server_conf_path, 'w') as f:
52 | f.write(server_conf)
53 | os.chmod(server_conf_path, 0o600)
54 | print('Server configuration has saved as', server_conf_path)
55 |
56 | client_conf = render_template('configurations/client.ovpn', **context)
57 | client_conf_path = os.path.join(ovpn_path, 'client.ovpn')
58 | with open(client_conf_path, 'w') as f:
59 | f.write(client_conf)
60 | os.chmod(client_conf_path, 0o644)
61 | print('Client configuration has saved as', client_conf_path)
62 | print('Done\n')
63 |
64 |
65 | def install_rsa_keys(config):
66 | print('## Generate and install rsa keys ##')
67 | ovpn_path = config['openvpn']['path']
68 | ez_rsa_tgz = os.path.join(ovpn_path, 'easy-rsa.tgz')
69 | ez_rsa_path = os.path.join(ovpn_path, 'easy-rsa')
70 |
71 | if os.path.exists(ez_rsa_path):
72 | print('easy-rsa found. skip download.')
73 | else:
74 | print('Download easy-rsa')
75 | wget.download(
76 | url='https://github.com/OpenVPN/easy-rsa/releases/download/v3.1.1/EasyRSA-3.1.1.tgz',
77 | out=os.path.join(ovpn_path, 'easy-rsa.tgz')
78 | )
79 | shutil.unpack_archive(ez_rsa_tgz, ovpn_path)
80 | shutil.move(
81 | os.path.join(ovpn_path, 'EasyRSA-3.1.1'),
82 | ez_rsa_path
83 | )
84 |
85 | print('\nGenerate rsa keys')
86 | subprocess.call(['./easyrsa', 'init-pki'], cwd=ez_rsa_path)
87 | subprocess.call(['./easyrsa', 'build-ca', 'nopass'], cwd=ez_rsa_path)
88 | subprocess.call(['./easyrsa', 'gen-dh'], cwd=ez_rsa_path)
89 | subprocess.call(['./easyrsa', 'build-server-full', 'server', 'nopass'], cwd=ez_rsa_path)
90 | subprocess.call(['openvpn', '--genkey', '--secret', 'pki/ta.key'], cwd=ez_rsa_path)
91 |
92 | print('Install rsa keys')
93 | for fname in ['ca.crt', 'ta.key', 'issued/server.crt', 'private/server.key', 'dh.pem']:
94 | shutil.copy(
95 | os.path.join(ez_rsa_path, 'pki', fname),
96 | ovpn_path
97 | )
98 | print('Done\n')
99 |
100 |
101 | def update_system_config(config):
102 | print('## Update system config')
103 | gws = netifaces.gateways()
104 | gw_dev = gws['default'][netifaces.AF_INET][1]
105 | vpn_net = str(IP('/'.join([config['openvpn']['network'], config['openvpn']['netmask']])))
106 |
107 | print('Set iptables')
108 | subprocess.call([
109 | 'iptables',
110 | '-I', 'FORWARD',
111 | '-i', 'tun0',
112 | '-j', 'ACCEPT'])
113 | subprocess.call([
114 | 'iptables',
115 | '-I', 'FORWARD',
116 | '-o', 'tun0',
117 | '-j', 'ACCEPT'])
118 | subprocess.call([
119 | 'iptables',
120 | '-I', 'OUTPUT',
121 | '-o', 'tun0',
122 | '-j', 'ACCEPT'])
123 | subprocess.call([
124 | 'iptables',
125 | '-A', 'FORWARD',
126 | '-i', 'tun0',
127 | '-o', gw_dev,
128 | '-j', 'ACCEPT'])
129 | subprocess.call([
130 | 'iptables',
131 | '-t', 'nat',
132 | '-A', 'POSTROUTING',
133 | '-o', gw_dev,
134 | '-j', 'MASQUERADE'])
135 | subprocess.call([
136 | 'iptables',
137 | '-t', 'nat',
138 | '-A', 'POSTROUTING',
139 | '-s', vpn_net,
140 | '-o', gw_dev,
141 | '-j', 'MASQUERADE'])
142 | subprocess.call([
143 | 'iptables',
144 | '-t', 'nat',
145 | '-A', 'POSTROUTING',
146 | '-s', vpn_net,
147 | '-o', gw_dev,
148 | '-j', 'MASQUERADE'])
149 |
150 | print('Set ipv4forward')
151 | with open('/proc/sys/net/ipv4/ip_forward', 'w') as f:
152 | f.write('1')
153 |
154 | print('Set ipv4forward persistent')
155 | with open('/etc/sysctl.conf', 'a') as f:
156 | f.write('\nnet.ipv4.ip_forward = 1\n')
157 |
158 | print('Done\n')
159 |
160 |
161 | def build_front(context):
162 | print('## Build web console')
163 | front_path = os.path.join(context['project_path'], 'iamovpn/views/front')
164 | subprocess.call(['npm', 'install'], cwd=front_path)
165 | subprocess.call(['npm', 'run', 'build'], cwd=front_path)
166 | print('Done\n')
167 |
168 |
169 | def run(config):
170 | app = create_app(config)
171 |
172 | init_db()
173 |
174 | # Insert default admin
175 | if User.query.filter(User.id == 'admin').first() is None:
176 | db.session.add(
177 | User(
178 | 'admin',
179 | 'need2change',
180 | 'Administrator',
181 | admin=True
182 | )
183 | )
184 | db.session.commit()
185 |
186 | install_rsa_keys(config)
187 | update_system_config(config)
188 |
189 | with app.app_context():
190 | context = {
191 | 'venv': os.environ.get('VIRTUAL_ENV'),
192 | 'config_path': config.path,
193 | 'project_path': os.path.abspath(
194 | os.path.join(
195 | os.path.dirname(os.path.abspath(__file__)),
196 | '../'
197 | )
198 | )
199 | }
200 | install_ovpn_scripts(config, context)
201 | install_ovpn_config(config, context)
202 | build_front(context)
203 |
204 | print('Installation has been done.')
205 | print('Please enter below command when you start service')
206 | print('# systemctl restart openvpn@server')
207 | print('$ python3 iamovpn run standalone --config {}'.format(config.path))
208 |
--------------------------------------------------------------------------------
/iamovpn/views/front/src/modules/user.js:
--------------------------------------------------------------------------------
1 | import { createAction, handleActions } from "redux-actions";
2 | import { Map } from "immutable";
3 | import moment from "moment";
4 |
5 | const CHECK_SESSION = "iamovpn/user/CHECK_SESSION";
6 | const CHECK_SESSION_SUCCESS = "iamovpn/user/CHECK_SESSION_SUCCESS";
7 | const CHECK_SESSION_FAIL = "iamovpn/user/CHECK_SESSION_FAIL";
8 | const LOGIN = "iamovpn/user/LOGIN";
9 | const LOGIN_SUCCESS = "iamovpn/user/LOGIN_SUCCESS";
10 | const LOGOUT = "iamovpn/user/LOGOUT";
11 | const GET = "iamovpn/user/GET";
12 | const GET_SUCCESS = "iamovpn/user/GET_SUCCESS";
13 | const GET_FAIL = "iamovpn/user/GET_FAIL";
14 | const CREATE = "iamovpn/user/CREATE";
15 | const CREATE_SUCCESS = "iamovpn/user/CREATE_SUCCESS";
16 | const CREATE_FAIL = "iamovpn/user/CREATE_FAIL";
17 | const MODIFY = "iamovpn/user/MODIFY";
18 | const MODIFY_SUCCESS = "iamovpn/user/MODIFY_SUCCESS";
19 | const MODIFY_FAIL = "iamovpn/user/MODIFY_FAIL";
20 | const MODIFY_PASSWORD = "iamovpn/user/MODIFY_PASSWORD";
21 | const MODIFY_PASSWORD_SUCCESS = "iamovpn/user/MODIFY_PASSWORD_SUCCESS";
22 | const MODIFY_PASSWORD_FAIL = "iamovpn/user/MODIFY_PASSWORD_FAIL";
23 | const MODIFY_MY_PASSWORD = "iamovpn/user/MODIFY_MY_PASSWORD";
24 | const MODIFY_MY_PASSWORD_SUCCESS = "iamovpn/user/MODIFY_MY_PASSWORD_SUCCESS";
25 | const MODIFY_MY_PASSWORD_FAIL = "iamovpn/user/MODIFY_MY_PASSWORD_FAIL";
26 | const GET_LIST = "iamovpn/user/GET_LIST";
27 | const GET_LIST_SUCCESS = "iamovpn/user/GET_LIST_SUCCESS";
28 | const GET_LIST_FAIL = "iamovpn/user/GET_LIST_FAIL";
29 | const GET_CONFIG = "iamovpn/user/GET_CONFIG";
30 | const GET_CONFIG_SUCCESS = "iamovpn/user/GET_CONFIG_SUCCESS";
31 | const GET_CONFIG_FAIL = "iamovpn/user/GET_CONFIG_FAIL";
32 |
33 |
34 | export const checkSession = createAction(
35 | CHECK_SESSION,
36 | () => {
37 | return {
38 | request: {
39 | method: "GET",
40 | url: "/api/user/me"
41 | }
42 | }
43 | }
44 | );
45 | export const login = createAction(
46 | LOGIN,
47 | (id, pwd) => {
48 | return {
49 | request: {
50 | method: "POST",
51 | url: "/api/secure/login",
52 | data: {
53 | id: id,
54 | password: pwd
55 | }
56 | }
57 | }
58 | }
59 | );
60 | export const logout = createAction(
61 | LOGOUT,
62 | () => {
63 | return {
64 | request: {
65 | method: "DELETE",
66 | url: "/api/secure/login"
67 | }
68 | }
69 | }
70 | );
71 | export const get = createAction(
72 | GET,
73 | (uid) => {
74 | return {
75 | request: {
76 | method: "GET",
77 | url: `/api/user/${uid}`
78 | }
79 | }
80 | }
81 | );
82 | export const create = createAction(
83 | CREATE,
84 | (user) => {
85 | console.log(user);
86 | return {
87 | request: {
88 | method: "POST",
89 | url: "/api/user",
90 | data: user
91 | }
92 | }
93 | }
94 | );
95 | export const modify = createAction(
96 | MODIFY,
97 | (user_uid, user) => {
98 | return {
99 | request: {
100 | method: "PUT",
101 | url: `/api/user/${user_uid}`,
102 | data: user
103 | }
104 | }
105 | }
106 | );
107 | export const modifyPassword = createAction(
108 | MODIFY_PASSWORD,
109 | (user_uid, password) => {
110 | return {
111 | request: {
112 | method: "PUT",
113 | url: `/api/user/${user_uid}/password`,
114 | data: {
115 | password: password
116 | }
117 | }
118 | }
119 | }
120 | );
121 | export const modifyMyPassword = createAction(
122 | MODIFY_MY_PASSWORD,
123 | (old_pwd, new_pwd) => {
124 | return {
125 | request: {
126 | method: "PUT",
127 | url: `/api/secure/password`,
128 | data: {
129 | old: old_pwd,
130 | new: new_pwd
131 | }
132 | }
133 | }
134 | }
135 | );
136 | export const getList = createAction(
137 | GET_LIST,
138 | (keyword, offset, length) => {
139 | return {
140 | request: {
141 | method: "GET",
142 | url: "/api/user",
143 | params: {
144 | keyword: keyword,
145 | offset: offset,
146 | length: length
147 | }
148 | }
149 | }
150 | }
151 | );
152 | export const getConfig = createAction(
153 | GET_CONFIG,
154 | () => {
155 | return {
156 | request: {
157 | method: "GET",
158 | url: "/api/user/config"
159 | }
160 | }
161 | }
162 | );
163 |
164 | const initialState = Map({
165 | me: null,
166 | user: null,
167 | users: [],
168 | user_count: 0,
169 | config: null
170 | });
171 | const noUpdate = (state, action) => {
172 | return state
173 | };
174 | const user = handleActions({
175 | [CHECK_SESSION]: noUpdate,
176 | [CHECK_SESSION_SUCCESS]: (state, action) => {
177 | return state.set("me", action.payload.data.user);
178 | },
179 | [CHECK_SESSION_FAIL]: (state, action) => {
180 | return state.set("me", null);
181 | },
182 | [LOGIN]: noUpdate,
183 | [LOGIN_SUCCESS]: (state, action) => {
184 | return state.set("me", action.payload.data.user);
185 | },
186 | [LOGOUT]: (state, action) => {
187 | return state.set("me", null);
188 | },
189 | [GET]: noUpdate,
190 | [GET_SUCCESS]: (state, action) => {
191 | return state.set("user", action.payload.data.user);
192 | },
193 | [GET_FAIL]: (state, action) => {
194 | return state.set("user", null);
195 | },
196 | [CREATE]: noUpdate,
197 | [CREATE_SUCCESS]: noUpdate,
198 | [CREATE_FAIL]: noUpdate,
199 | [MODIFY]: noUpdate,
200 | [MODIFY_SUCCESS]: (state, action) => {
201 | const user = action.payload.data.user;
202 | user.created = new moment(user.created);
203 | user.updated = new moment(user.updated);
204 | return state.set("user", user);
205 | },
206 | [MODIFY_FAIL]: noUpdate,
207 | [MODIFY_PASSWORD]: noUpdate,
208 | [MODIFY_PASSWORD_SUCCESS]: noUpdate,
209 | [MODIFY_PASSWORD_FAIL]: noUpdate,
210 | [MODIFY_MY_PASSWORD]: noUpdate,
211 | [MODIFY_MY_PASSWORD_SUCCESS]: noUpdate,
212 | [MODIFY_MY_PASSWORD_FAIL]: noUpdate,
213 | [GET_LIST]: noUpdate,
214 | [GET_LIST_SUCCESS]: (state, action) => {
215 | const users = action.payload.data.users;
216 | users.forEach(user => {
217 | user.created = new moment(user.created);
218 | user.updated = new moment(user.updated);
219 | });
220 | return state.set("users", users)
221 | .set("user_count", action.payload.data.count);
222 | },
223 | [GET_LIST_FAIL]: (state, action) => {
224 | return state.set("users", []).set("user_count", 0)
225 | },
226 | [GET_CONFIG]: noUpdate,
227 | [GET_CONFIG_SUCCESS]: (state, action) => {
228 | return state.set("config", action.payload.data.config);
229 | },
230 | [GET_CONFIG_FAIL]: noUpdate,
231 | }, initialState);
232 | export default user;
--------------------------------------------------------------------------------
/iamovpn/views/front/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/iamovpn/views/front/src/components/MyPage.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import {
5 | CssBaseline,
6 | Box,
7 | AppBar,
8 | Toolbar,
9 | Typography,
10 | Divider,
11 | IconButton,
12 | Container,
13 | Grid,
14 | ButtonGroup,
15 | Button,
16 | Modal,
17 | Paper,
18 | TextareaAutosize
19 | } from '@material-ui/core';
20 | import { withStyles } from '@material-ui/core/styles';
21 | import PowerSettingsNewIcon from '@material-ui/icons/PowerSettingsNew';
22 |
23 |
24 |
25 | import PasswordForm from "./PasswordForm";
26 | import Copyright from "./Copyright";
27 | import * as userActions from "../modules/user";
28 | import history from "../history";
29 |
30 |
31 | const drawerWidth = 240;
32 |
33 | const styles = theme => ({
34 | root: {
35 | display: 'flex',
36 | },
37 | toolbar: {
38 | paddingRight: 24, // keep right padding when drawer closed
39 | },
40 | toolbarIcon: {
41 | display: 'flex',
42 | alignItems: 'center',
43 | justifyContent: 'flex-end',
44 | padding: '0 8px',
45 | ...theme.mixins.toolbar,
46 | },
47 | appBar: {
48 | zIndex: theme.zIndex.drawer + 1,
49 | transition: theme.transitions.create(['width', 'margin'], {
50 | easing: theme.transitions.easing.sharp,
51 | duration: theme.transitions.duration.leavingScreen,
52 | }),
53 | },
54 | appBarShift: {
55 | marginLeft: drawerWidth,
56 | width: `calc(100% - ${drawerWidth}px)`,
57 | transition: theme.transitions.create(['width', 'margin'], {
58 | easing: theme.transitions.easing.sharp,
59 | duration: theme.transitions.duration.enteringScreen,
60 | }),
61 | },
62 | menuButton: {
63 | marginRight: 36,
64 | },
65 | menuButtonHidden: {
66 | display: 'none',
67 | },
68 | title: {
69 | flexGrow: 1,
70 | },
71 | drawerPaper: {
72 | position: 'relative',
73 | whiteSpace: 'nowrap',
74 | width: drawerWidth,
75 | transition: theme.transitions.create('width', {
76 | easing: theme.transitions.easing.sharp,
77 | duration: theme.transitions.duration.enteringScreen,
78 | }),
79 | },
80 | drawerPaperClose: {
81 | overflowX: 'hidden',
82 | transition: theme.transitions.create('width', {
83 | easing: theme.transitions.easing.sharp,
84 | duration: theme.transitions.duration.leavingScreen,
85 | }),
86 | width: theme.spacing(7),
87 | [theme.breakpoints.up('sm')]: {
88 | width: theme.spacing(9),
89 | },
90 | },
91 | appBarSpacer: theme.mixins.toolbar,
92 | content: {
93 | flexGrow: 1,
94 | height: '100vh',
95 | overflow: 'auto',
96 | },
97 | container: {
98 | paddingTop: theme.spacing(4),
99 | paddingBottom: theme.spacing(4),
100 | },
101 | paper: {
102 | padding: theme.spacing(2),
103 | display: 'flex',
104 | overflow: 'auto',
105 | flexDirection: 'column',
106 | },
107 | fixedHeight: {
108 | height: 240,
109 | },
110 | modal: {
111 | display: 'flex',
112 | alignItems: 'center',
113 | justifyContent: 'center',
114 | },
115 | configArea: {
116 | width: '100%'
117 | }
118 | });
119 |
120 | class MyPage extends Component {
121 | constructor(props) {
122 | super(props);
123 | this.state = {
124 | changePassword: false
125 | };
126 | }
127 |
128 | logout() {
129 | const { dispatch } = this.props;
130 |
131 | dispatch(userActions.logout())
132 | .then((response) => {
133 | history.push('/')
134 | });
135 | }
136 |
137 | getConfig() {
138 | const { dispatch } = this.props;
139 | dispatch(userActions.getConfig());
140 | }
141 |
142 | handlePasswordForm(success) {
143 | if (success) {
144 | this.setState({
145 | changePassword: false
146 | });
147 | }
148 | }
149 |
150 | render() {
151 | const { classes, config } = this.props;
152 |
153 | return (
154 |
155 |
156 |
157 |
158 |
159 | IAMOVPN
160 |
161 |
162 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 | {/* Recent Orders */}
173 |
174 | this.setState({changePassword: false})}
178 | >
179 |
180 |
181 |
182 |
183 |
184 |
187 |
188 |
189 |
190 |
191 |
194 |
195 |
196 | {config?
197 |
198 |
199 | Copy and save below text as "iamovpn.ovpn".
200 |
201 |
206 |
207 | :null}
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 | );
217 | }
218 | }
219 |
220 |
221 | const mapStateToProps = (state) => {
222 | return {
223 | me: state.user.get('me'),
224 | config: state.user.get('config')
225 | }
226 | };
227 | export default connect(mapStateToProps)(withStyles(styles)(MyPage));
--------------------------------------------------------------------------------
/iamovpn/views/front/src/components/UserList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import {
5 | Paper,
6 | Table,
7 | TableBody,
8 | TableCell,
9 | TableHead,
10 | TableRow,
11 | TableFooter,
12 | TablePagination,
13 | TextField,
14 | IconButton,
15 | Typography,
16 | Tooltip,
17 | Modal
18 | } from '@material-ui/core';
19 | import SearchIcon from '@material-ui/icons/Search';
20 | import AddBox from '@material-ui/icons/AddBox';
21 | import Edit from '@material-ui/icons/Edit';
22 | import LockOpen from '@material-ui/icons/LockOpen';
23 | import Lock from '@material-ui/icons/Lock';
24 | import VpnKey from '@material-ui/icons/VpnKey';
25 | import { withStyles } from '@material-ui/core/styles';
26 |
27 | import UserForm from "./UserForm";
28 | import PasswordForm from "./PasswordForm";
29 | import * as userActions from "../modules/user";
30 |
31 |
32 | const styles = theme => ({
33 | modal: {
34 | display: 'flex',
35 | alignItems: 'center',
36 | justifyContent: 'center',
37 | },
38 | paper: {
39 | backgroundColor: theme.palette.background.paper,
40 | border: '2px solid #000',
41 | boxShadow: theme.shadows[5],
42 | padding: theme.spacing(2, 4, 3),
43 | }
44 | });
45 |
46 |
47 | class UserList extends Component {
48 | constructor(props) {
49 | super(props);
50 | const {size} = this.props;
51 |
52 | this.state = {
53 | tableSize: size,
54 | page: 0,
55 | keyword: '',
56 | selectedUser: null,
57 | createUser: false,
58 | passwordChange: null
59 | };
60 | }
61 |
62 | componentDidMount() {
63 | const { dispatch } = this.props;
64 | dispatch(userActions.getList(
65 | this.state.keyword,
66 | this.state.page * this.state.tableSize,
67 | this.state.tableSize,
68 | ))
69 | };
70 |
71 | handleFindKeyword(e) {
72 | const { dispatch } = this.props;
73 | dispatch(userActions.getList(
74 | this.state.keyword,
75 | this.state.page * this.state.tableSize,
76 | this.state.tableSize,
77 | ));
78 |
79 | e.preventDefault();
80 | };
81 |
82 | handleChangePage(event, newPage) {
83 | const { dispatch } = this.props;
84 |
85 | this.setState({
86 | page: newPage
87 | });
88 |
89 | dispatch(userActions.getList(
90 | this.state.keyword,
91 | newPage * this.state.tableSize,
92 | this.state.tableSize,
93 | ))
94 | };
95 |
96 | handleChangeRowsPerPage(event) {
97 | const { dispatch } = this.props;
98 | const newTableSize = parseInt(event.target.value, 10);
99 |
100 | this.setState({
101 | tableSize: newTableSize,
102 | page: 0
103 | });
104 |
105 | dispatch(userActions.getList(
106 | this.state.keyword,
107 | 0,
108 | newTableSize,
109 | ))
110 | };
111 |
112 | handleChangeKeyword(e) {
113 | this.setState({
114 | keyword: e.target.value
115 | })
116 | };
117 |
118 | handleUserForm(success) {
119 | const { dispatch } = this.props;
120 | const newState = {
121 | createUser: false,
122 | selectedUser: null,
123 | passwordChange: null
124 | };
125 |
126 | if (success) {
127 | newState.page = 0;
128 | dispatch(userActions.getList(
129 | this.state.keyword,
130 | 0,
131 | this.state.tableSize
132 | ))
133 | }
134 |
135 | this.setState(newState);
136 | };
137 |
138 | renderUsers() {
139 | const {users} = this.props;
140 | let result = [];
141 |
142 | if (users.length === 0) {
143 | result = [
144 |
145 | Empty
146 |
147 | ]
148 | } else {
149 | users.forEach(user => {
150 | result.push(
151 |
152 | {user.id}
153 | {user.name}
154 | {user.admin? "True" : "False"}
155 |
156 | {user.active?
157 |
158 |
159 |
160 |
161 |
162 | :
163 |
164 |
165 |
166 |
167 |
168 | }
169 |
170 | {user.created.format("YYYY-MM-DD HH:mm")}
171 | {user.updated.format("YYYY-MM-DD HH:mm")}
172 |
173 |
174 | this.setState({selectedUser: user})}
176 | >
177 |
178 |
179 |
180 |
181 |
182 | this.setState({passwordChange: user})}
184 | >
185 |
186 |
187 |
188 |
189 |
190 | )
191 | })
192 | }
193 |
194 | return result;
195 | };
196 |
197 | render() {
198 | const {classes} = this.props;
199 | return
200 | this.setState({createUser: false})}
204 | >
205 |
207 |
211 |
212 |
213 | this.setState({selectedUser: null})}
217 | >
218 |
220 |
225 |
226 |
227 | this.setState({passwordChange: null})}
231 | >
232 |
234 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
249 | Users
250 |
251 | this.setState({createUser: true})}
253 | color="primary"
254 | >
255 |
256 |
257 |
258 |
259 |
260 |
261 |
271 |
272 |
273 |
274 | ID
275 | Name
276 | Admin
277 | Block
278 | Created
279 | Updated
280 | Action
281 |
282 |
283 |
284 | {this.renderUsers()}
285 |
286 |
287 |
288 |
289 |
290 |
298 |
299 |
300 |
301 |
302 | }
303 | }
304 |
305 |
306 | const mapStateToProps = (state) => {
307 | return {
308 | users: state.user.get('users'),
309 | user_count: state.user.get('user_count')
310 | }
311 | };
312 | export default connect(mapStateToProps)(withStyles(styles)(UserList));
--------------------------------------------------------------------------------