Dashboard updates

pull/144/head
Henrique Dias 2017-07-07 20:00:32 +01:00
parent ba7c3d4234
commit af369cdea1
No known key found for this signature in database
GPG Key ID: 936F5EB68D786730
10 changed files with 536 additions and 127 deletions

53
api.go
View File

@ -9,6 +9,7 @@ import (
"net/http"
"net/url"
"os"
"sort"
"strconv"
"strings"
@ -361,6 +362,10 @@ func usersGetHandler(c *requestContext, w http.ResponseWriter, r *http.Request)
users = append(users, u)
}
sort.Slice(users, func(i, j int) bool {
return users[i].ID < users[j].ID
})
return renderJSON(w, users)
}
@ -449,8 +454,8 @@ func usersPostHandler(c *requestContext, w http.ResponseWriter, r *http.Request)
c.fm.Users[u.Username] = &u
// Set the Location header and return.
w.Header().Set("Location", "/users/"+strconv.Itoa(u.ID))
w.WriteHeader(http.StatusCreated)
w.Header().Set("Location", c.fm.RootURL()+"/api/users/"+strconv.Itoa(u.ID))
return 0, nil
}
@ -516,22 +521,32 @@ func usersPutHandler(c *requestContext, w http.ResponseWriter, r *http.Request)
}
if sid == "self" {
if u.Password == "" {
return http.StatusBadRequest, errors.New("Password missing")
if u.Password != "" {
pw, err := hashPassword(u.Password)
if err != nil {
return http.StatusInternalServerError, err
}
c.us.Password = pw
err = c.fm.db.UpdateField(&User{ID: c.us.ID}, "Password", pw)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
pw, err := hashPassword(u.Password)
if err != nil {
return http.StatusInternalServerError, err
if u.CSS != "" {
c.us.CSS = u.CSS
err = c.fm.db.UpdateField(&User{ID: c.us.ID}, "CSS", u.CSS)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
c.us.Password = pw
err = c.fm.db.UpdateField(&User{ID: c.us.ID}, "Password", pw)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
return http.StatusBadRequest, errors.New("Password or CSS is missing")
}
// The username and the filesystem cannot be empty.
@ -555,7 +570,17 @@ func usersPutHandler(c *requestContext, w http.ResponseWriter, r *http.Request)
}
u.ID = id
u.Password = ouser.Password
if u.Password == "" {
u.Password = ouser.Password
} else {
pw, err := hashPassword(u.Password)
if err != nil {
return http.StatusInternalServerError, err
}
u.Password = pw
}
// Updates the whole User struct because we always are supposed
// to send a new entire object.

View File

@ -1,11 +1,79 @@
<template>
<div>
<div class="dashboard">
<h1>Settings</h1>
<router-link v-if="user.admin" to="/users">Go to User Management</router-link>
<form @submit="changePassword">
<h2>Change Password</h2>
<p><input :class="passwordClass" type="password" placeholder="Your new password" v-model="password" name="password"></p>
<p><input :class="passwordClass" type="password" placeholder="Confirm your new password" v-model="passwordConf" name="password"></p>
<p><input type="submit" value="Change Password"></p>
</form>
<form @submit="updateCSS">
<h2>Costum Stylesheet</h2>
<textarea v-model="css" name="css"></textarea>
<p><input type="submit" value="Update"></p>
</form>
</div>
</template>
<script>
import { mapState } from 'vuex'
import api from '@/utils/api'
export default {
name: 'settings'
name: 'settings',
data: function () {
return {
password: '',
passwordConf: '',
css: ''
}
},
computed: {
...mapState([ 'user' ]),
passwordClass () {
if (this.password === '' && this.passwordConf === '') {
return ''
}
if (this.password === this.passwordConf) {
return 'green'
}
return 'red'
}
},
created () {
this.css = this.user.css
},
methods: {
changePassword (event) {
event.preventDefault()
if (this.password !== this.passwordConf) {
return
}
api.updatePassword(this.password).then(() => {
console.log('Success')
// TODO: show success
}).catch(e => {
this.$store.commit('showError', e)
})
},
updateCSS (event) {
event.preventDefault()
api.updateCSS(this.css).then(() => {
console.log('Success')
// TODO: show success
}).catch(e => {
this.$store.commit('showError', e)
})
}
}
}
</script>

View File

@ -1,12 +1,11 @@
<template>
<div class="dashboard">
<h1>User</h1>
<form @submit="save" class="dashboard">
<h1 v-if="id === 0">New User</h1>
<h1 v-else>User {{ username }}</h1>
<p><label for="username">Username</label><input type="text" v-model="username" name="username"></p>
<p><label for="password">Password</label><input type="password" :disabled="passwordBlock" v-model="password" name="password"></p>
<p><label for="scope">Scope</label><input type="text" v-model="scope" name="scope"></p>
<hr>
<p><label for="username">Username</label><input type="text" v-model="username" id="username"></p>
<p><label for="password">Password</label><input type="password" :placeholder="passwordPlaceholder" v-model="password" id="password"></p>
<p><label for="scope">Scope</label><input type="text" v-model="filesystem" id="scope"></p>
<h2>Permissions</h2>
@ -23,9 +22,7 @@
<p class="small">A space separated list with the available commands for this user. Example: <i>git svn hg</i>.</p>
<input type="text" v-model="commands">
<hr>
<input type="text" v-model.trim="commands">
<h2>Rules</h2>
@ -43,16 +40,14 @@
<li><code>disallow /Caddyfile</code> - blocks the access to the file named <i>Caddyfile</i> on the root of the scope</li>
</ul>
<textarea v-model="rules"></textarea>
<textarea v-model.trim="rules"></textarea>
<hr>
<h2>CSS</h2>
<p class="small">Costum user CSS</p>
<h2>Costum Stylesheet</h2>
<textarea name="css"></textarea>
</div>
<p><input type="submit" value="Save"></p>
</form>
</template>
<script>
@ -62,116 +57,153 @@ export default {
name: 'user',
data: () => {
return {
id: 0,
admin: false,
allowNew: false,
allowEdit: false,
allowCommands: false,
passwordBlock: true,
password: '',
username: '',
scope: '',
filesystem: '',
rules: '',
css: '',
commands: ''
}
},
computed: {
passwordPlaceholder () {
if (this.$route.path === '/users/new') return ''
return '(leave blank to avoid changes)'
}
},
created () {
if (this.$route.path === '/users/new') return
api.getUser(this.$route.params[0]).then(user => {
this.admin = user.admin
this.allowCommands = user.allowCommands
this.allowNew = user.allowNew
this.allowEdit = user.allowEdit
this.scope = user.filesystem
this.username = user.username
this.commands = user.commands.join(' ')
this.css = user.css
for (let rule of user.rules) {
if (rule.allow) {
this.rules += 'allow '
} else {
this.rules += 'disallow '
}
if (rule.regex) {
this.rules += 'regex ' + rule.regexp.raw
} else {
this.rules += rule.path
}
this.rules += '\n'
}
}).catch(error => {
console.log(error)
})
this.fetchData()
},
watch: {
'$route': 'fetchData',
admin: function () {
if (!this.admin) return
this.allowCommands = true
this.allowEdit = true
this.allowNew = true
}
},
methods: {
fetchData () {
if (this.$route.path === '/users/new') return
api.getUser(this.$route.params[0]).then(user => {
this.id = user.ID
this.admin = user.admin
this.allowCommands = user.allowCommands
this.allowNew = user.allowNew
this.allowEdit = user.allowEdit
this.filesystem = user.filesystem
this.username = user.username
this.commands = user.commands.join(' ')
this.css = user.css
for (let rule of user.rules) {
if (rule.allow) {
this.rules += 'allow '
} else {
this.rules += 'disallow '
}
if (rule.regex) {
this.rules += 'regex ' + rule.regexp.raw
} else {
this.rules += rule.path
}
this.rules += '\n'
}
this.rules = this.rules.trim()
}).catch(error => {
console.log(error)
this.$router.push({ path: '/users/new' })
})
},
save (event) {
event.preventDefault()
let user = this.parseForm()
if (this.$route.path === '/users/new') {
api.newUser(user).then(location => {
this.$router.push({ path: location })
}).catch(e => {
this.$store.commit('showError', e)
})
return
}
api.updateUser(user).then(location => {
this.$router.push({ path: location })
}).catch(e => {
this.$store.commit('showError', e)
})
},
parseForm () {
let user = {
ID: this.id,
username: this.username,
password: this.password,
filesystem: this.filesystem,
admin: this.admin,
allowCommands: this.allowCommands,
allowNew: this.allowNew,
allowEdit: this.allowEdit,
css: this.css,
commands: this.commands.split(' '),
rules: []
}
let rules = this.rules.split('\n')
for (let rawRule of rules) {
let rule = {
allow: true,
path: '',
regex: false,
regexp: {
raw: ''
}
}
rawRule = rawRule.split(' ')
// Skip a malformed rule
if (rawRule.length < 2) {
continue
}
// Skip a malformed rule
if (rawRule[0] !== 'allow' && rawRule[0] !== 'disallow') {
continue
}
rule.allow = (rawRule[0] === 'allow')
rawRule.shift()
if (rawRule[0] === 'regex') {
rule.regex = true
rawRule.shift()
rule.regexp.raw = rawRule.join(' ')
} else {
rule.path = rawRule.join(' ')
}
user.rules.push(rule)
}
return user
}
}
}
</script>
<style>
.dashboard {
max-width: 600px;
}
.dashboard textarea,
.dashboard input[type="text"],
.dashboard input[type="password"] {
padding: .5em 1em;
display: block;
border: 1px solid #e9e9e9;
transition: .2s ease border;
color: #333;
width: 100%;
}
.dashboard textarea:focus,
.dashboard textarea:hover,
.dashboard input[type="text"]:focus,
.dashboard input[type="password"]:focus,
.dashboard input[type="text"]:hover,
.dashboard input[type="password"]:hover {
border-color: #9f9f9f;
}
.dashboard textarea {
font-family: monospace;
min-height: 10em;
resize: vertical;
}
.dashboard p label {
margin-bottom: .2em;
display: block;
font-size: .8em
}
hr {
border-bottom: 2px solid rgba(181, 181, 181, 0.5);
border-top: 0;
border-right: 0;
border-left: 0;
margin: 1em 0;
}
li code,
p code {
background: rgba(0, 0, 0, 0.05);
padding: .1em;
border-radius: .2em;
}
.small {
font-size: .8em;
line-height: 1.5;
}
</style>

View File

@ -1,11 +1,43 @@
<template>
<div>
<h1>Users</h1>
<div class="dashboard">
<h1>Users <router-link to="/users/new"><button>New</button></router-link></h1>
<table>
<tr>
<th>Username</th>
<th>Admin</th>
<th>Scope</th>
<th></th>
</tr>
<tr v-for="user in users">
<td>{{ user.username }}</td>
<td><i v-if="user.admin" class="material-icons">done</i><i v-else class="material-icons">close</i></td>
<td>{{ user.filesystem }}</td>
<td><router-link :to="'/users/' + user.ID"><i class="material-icons">mode_edit</i></router-link></td>
</tr>
</table>
</div>
</template>
<script>
import api from '@/utils/api'
export default {
name: 'users'
name: 'users',
data: function () {
return {
users: []
}
},
created () {
api.getUsers().then(users => {
this.users = users
console.log(users)
}).catch(error => {
console.log(error)
})
}
}
</script>

View File

@ -3,7 +3,7 @@ body {
padding-top: 4em;
background-color: #f8f8f8;
user-select: none;
color: #333;
color: #212121;
}
* {

View File

@ -0,0 +1,121 @@
.dashboard {
max-width: 600px;
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
border-radius: .5em;
background: #fff;
padding: 1em;
margin: 1em 0;
}
.dashboard a {
color: inherit
}
.dashboard h1 button {
font-size: 0.5em;
float: right;
}
.dashboard table {
width: 100%;
}
.dashboard table tr {
}
.dashboard table th {
font-weight: 500;
color: #757575;
text-align: left;
}
.dashboard table th,
.dashboard table td {
padding: .5em 0;
}
.dashboard table td {
}
.dashboard table td:last-child {
width: 1em
}
.dashboard > *:first-child {
margin-top: 0;
}
.dashboard > *:last-child {
margin-bottom: 0;
}
form.dashboard input[type="submit"],
.dashboard form input[type="submit"] {
margin-left: auto;
display: block;
}
.dashboard textarea,
.dashboard input[type="text"],
.dashboard input[type="password"] {
padding: 0;
line-height: 1.7;
display: block;
border: 0;
border-bottom: 1px solid #dddddd;
transition: .2s ease border;
width: 100%;
}
.dashboard #username,
.dashboard #password,
.dashboard #scope {
max-width: 18em;
}
.dashboard textarea:focus,
.dashboard textarea:hover,
.dashboard input[type="text"]:focus,
.dashboard input[type="password"]:focus,
.dashboard input[type="text"]:hover,
.dashboard input[type="password"]:hover {
border-color: #2979ff;
}
.dashboard input.red {
border-color: red;
}
.dashboard input.green {
border-color: green;
}
.dashboard textarea {
line-height: 1.15;
padding: .5em;
border: 1px solid #ddd;
font-family: monospace;
min-height: 10em;
resize: vertical;
}
.dashboard p label {
margin-bottom: .2em;
display: block;
font-size: .8em;
font-weight: bold;
}
li code,
p code {
background: rgba(0, 0, 0, 0.05);
padding: .1em;
border-radius: .2em;
}
.small {
font-size: .8em;
line-height: 1.5;
}

View File

@ -6,7 +6,7 @@
}
#editor .CodeMirror {
border: 1px solid #dddddd;
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
margin: 2em 0;
border-radius: .5em;
}

View File

@ -5,6 +5,7 @@
@import "./prompts.css";
@import "./listing.css";
@import "./editor.css";
@import "./dashboard.css";
/* * * * * * * * * * * * * * * *
* ACTION *

View File

@ -7,6 +7,7 @@ import Users from '@/components/Users'
import User from '@/components/User'
import Settings from '@/components/Settings'
import auth from '@/utils/auth.js'
import store from '@/store'
Vue.use(Router)
@ -55,7 +56,10 @@ const router = new Router({
{
path: '/users',
name: 'Users',
component: Users
component: Users,
meta: {
requiresAdmin: true
}
},
{
path: '/users/',
@ -66,7 +70,10 @@ const router = new Router({
{
path: '/users/*',
name: 'User',
component: User
component: User,
meta: {
requiresAdmin: true
}
},
{
path: '/*',
@ -85,6 +92,19 @@ router.beforeEach((to, from, next) => {
// if not, redirect to login page.
auth.loggedIn()
.then(() => {
if (to.matched.some(record => record.meta.requiresAdmin)) {
if (store.state.user.admin) {
next()
return
}
next({
path: '/403'
})
return
}
next()
})
.catch(e => {

View File

@ -190,6 +190,27 @@ function download (format, ...files) {
window.open(url)
}
function getUsers () {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/users/`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
case 200:
resolve(JSON.parse(request.responseText))
break
default:
reject(request.responseText)
break
}
}
request.onerror = (error) => reject(error)
request.send()
})
}
function getUser (id) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
@ -211,6 +232,90 @@ function getUser (id) {
})
}
function newUser (user) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', `${store.state.baseURL}/api/users/`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
case 201:
resolve(request.getResponseHeader('Location'))
break
default:
reject(request.responseText)
break
}
}
request.onerror = (error) => reject(error)
request.send(JSON.stringify(user))
})
}
function updateUser (user) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('PUT', `${store.state.baseURL}/api/users/${user.ID}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
case 200:
resolve(request.getResponseHeader('Location'))
break
default:
reject(request.responseText)
break
}
}
request.onerror = (error) => reject(error)
request.send(JSON.stringify(user))
})
}
function updatePassword (password) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('PUT', `${store.state.baseURL}/api/users/self`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
case 200:
resolve()
break
default:
reject(request.responseText)
break
}
}
request.onerror = (error) => reject(error)
request.send(JSON.stringify({ 'password': password }))
})
}
function updateCSS (css) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('PUT', `${store.state.baseURL}/api/users/self`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
case 200:
resolve()
break
default:
reject(request.responseText)
break
}
}
request.onerror = (error) => reject(error)
request.send(JSON.stringify({ 'css': css }))
})
}
export default {
delete: rm,
fetch,
@ -221,5 +326,10 @@ export default {
command,
search,
download,
getUser
getUser,
newUser,
updateUser,
getUsers,
updatePassword,
updateCSS
}