Improve setup, upgrade, new API

- Improve upgrade process (fixes #106)
- Improve upgrade UI
- Delete Users API
- Get user API
- User update API
- Security improvement (does not spill secret over api)
- Improve setup (fixes #100)
This commit is contained in:
Matej Kramny 2016-05-23 20:29:38 +01:00
parent 9a292f0f82
commit 0b417594a4
18 changed files with 207 additions and 60 deletions

View File

@ -851,6 +851,15 @@ paths:
/users/{user_id}:
parameters:
- $ref: "#/parameters/user_id"
get:
tags:
- user
summary: Fetches a user profile
responses:
200:
description: User profile
schema:
$ref: "#/definitions/User"
put:
tags:
- user

31
main.go
View File

@ -87,7 +87,7 @@ func doSetup() int {
var b []byte
setup := util.NewConfig()
for true {
for {
setup.Scan()
setup.GenerateCookieSecrets()
@ -136,21 +136,30 @@ func doSetup() int {
stdin := bufio.NewReader(os.Stdin)
var user models.User
user.Name = readNewline("\n\n > Your name: ", stdin)
user.Username = readNewline(" > Username: ", stdin)
user.Email = readNewline(" > Email: ", stdin)
user.Password = readNewline(" > Password: ", stdin)
pwdHash, _ := bcrypt.GenerateFromPassword([]byte(user.Password), 11)
user.Username = readNewline("\n\n > Username: ", stdin)
user.Username = strings.ToLower(user.Username)
user.Email = readNewline(" > Email: ", stdin)
user.Email = strings.ToLower(user.Email)
if _, err := database.Mysql.Exec("insert into user set name=?, username=?, email=?, password=?, created=NOW()", user.Name, user.Username, user.Email, pwdHash); err != nil {
fmt.Printf(" Inserting user failed. If you already have a user, you can disregard this error.\n %v\n", err.Error())
os.Exit(1)
var existingUser models.User
database.Mysql.SelectOne(&existingUser, "select * from user where email=? or username=?", user.Email, user.Username)
if existingUser.ID > 0 {
// user already exists
fmt.Printf("\n Welcome back, %v! (a user with this username/email is already set up..)\n\n", existingUser.Name)
} else {
user.Name = readNewline(" > Your name: ", stdin)
user.Password = readNewline(" > Password: ", stdin)
pwdHash, _ := bcrypt.GenerateFromPassword([]byte(user.Password), 11)
if _, err := database.Mysql.Exec("insert into user set name=?, username=?, email=?, password=?, created=NOW()", user.Name, user.Username, user.Email, pwdHash); err != nil {
fmt.Printf(" Inserting user failed. If you already have a user, you can disregard this error.\n %v\n", err.Error())
os.Exit(1)
}
fmt.Printf("\n You are all setup %v!\n", user.Name)
}
fmt.Printf("\n You are all setup %v!\n", user.Name)
fmt.Printf(" Re-launch this program pointing to the configuration file\n\n./semaphore -config %v\n\n", configPath)
fmt.Printf(" To run as daemon:\n\nnohup ./semaphore -config %v &\n\n", configPath)
fmt.Printf(" You can login with %v or %v.\n", user.Email, user.Username)

View File

@ -76,14 +76,26 @@ input, button.btn {
font-family: @font-family-monospace;
}
ul.nav.navbar-nav .dropdown-menu li > a {
color: black !important;
}
ul.nav.navbar-nav .dropdown-menu li > a, ul.nav.navbar-nav .dropdown-menu li.dropdown-header {
padding-left: 15px;
padding-right: 15px;
ul.nav.navbar-nav {
li > a {
margin-right: 2px;
cursor: pointer;
transition: background 0.1s linear;
}
.dropdown-menu {
li > a {
color: black !important;
}
li > a, li.dropdown-header {
padding-left: 15px;
padding-right: 15px;
}
}
}
.navbar-brand:hover, .navbar-brand:focus, ul.nav.navbar-nav li > a:hover, ul.nav.navbar-nav li > a:active, ul.nav.navbar-nav li > a:focus {
color: darken(white, 5%);
}

View File

@ -11,7 +11,11 @@
li.list-group-item: a(href="https://ansible-semaphore.github.io/semaphore/" target="_blank") API Documentation
.col-sm-4
div(ng-if="upgrade.updateBody")
button.btn.btn-primary.btn-block(ng-click="doUpgrade()") upgrade to {{ upgrade.update.tag_name }}
button.btn.btn-primary.btn-block(ng-click="doUpgrade()" ng-disabled="upgrade.config.cmdPath.length == 0") upgrade to {{ upgrade.update.tag_name }}
p.text-center(ng-if="upgrade.config.cmdPath.length == 0")
a(href="https://github.com/ansible-semaphore/semaphore/wiki/Troubleshooting#upgrades-failing" target="_blank") Upgrading isn't possible!
br
| You should fix this error or upgrade manually.
div(ng-bind-html="upgrade.updateBody")
.col-sm-4
dl
@ -20,4 +24,11 @@
dt Playbook Path
dd: code {{ upgrade.config.path }}
dt semaphore location
dd: code {{ upgrade.config.cmdPath }}
dd
span(ng-if="upgrade.config.cmdPath.length == 0")
code semaphore
|  not found in 
code $PATH
| . Upgrading 
a(href="https://github.com/ansible-semaphore/semaphore/wiki/Installation#install-instructions" target="_blank") will not work
code(ng-if="upgrade.config.cmdPath.length > 0") {{ upgrade.config.cmdPath }}

View File

@ -20,9 +20,13 @@ html(lang="en" ng-app="semaphore")
//- li(ng-class="{ active: $state.includes('keys') }")
//- a(ui-sref="keys") Key Store
li(ng-class="{ active: $state.includes('users') }")
a(ui-sref="users") Users
a(ui-sref="users.list") Users
ul.nav.navbar-nav.navbar-right(style="margin-right: 0;")
li(ng-class="{ active: $state.includes('user') }")
a(ui-sref="user")
i.fa.fa-fw.fa-user
|  {{ user.name }}
li(uib-dropdown)
a(uib-dropdown-toggle)
span(ng-if="semaphore.update")

View File

@ -8,7 +8,7 @@ table.table
th Email
th Admin
th  
tbody: tr(ng-repeat="u in users" ng-class="{ info: u.id == user.data.id }")
tbody: tr(ng-repeat="u in users" ng-class="{ info: u.id == user.id }")
td {{ u.name }}
td {{ u.username }}
td {{ u.email }}

View File

@ -8,10 +8,12 @@
th Username
th Email
th  
tr(ng-repeat="u in users" ng-class="{ info: u.id == user.data.id }")
tr(ng-repeat="u in users" ng-class="{ info: u.id == user.id }")
td {{ u.name }}
td {{ u.username }}
td {{ u.email }}
td: button.btn.btn-default.btn-xs.pull-right(ng-click="changePassword(u)") Change Password
td
.btn-group.pull-right
button.btn.btn-info.btn-xs(ui-sref="users.user({ user_id: u.id })") Update User
p(ng-show="users.length == 0") No Users

View File

@ -1,11 +0,0 @@
.modal-header
h4.modal-title Change user password
.modal-body
form.form-horizontal
.form-group
label.control-label.col-sm-4 Password
.col-sm-6
input.form-control(type="password" placeholder="Password" ng-model="password" required)
.modal-footer
button.btn.btn-default.pull-left(ng-click="$dismiss()") Dismiss
button.btn.btn-success(ng-click="$close(password)") Change

View File

@ -0,0 +1,27 @@
.container-fluid: .row: .col-sm-4.col-sm-offset-4
h3.text-center {{ user.name }}
hr
form.form-horizontal
.form-group
label.control-label.col-sm-4 Name
.col-sm-8: input.form-control(type="text" placeholder="Your name" ng-model="user.name")
.form-group
label.control-label.col-sm-4 Username
.col-sm-8: input.form-control(type="text" placeholder="Username" ng-model="user.username")
.form-group
label.control-label.col-sm-4 Email
.col-sm-8: input.form-control(type="email" placeholder="Email address" ng-model="user.email")
.form-group
label.control-label.col-sm-4 Password
.col-sm-8: input.form-control(type="password" placeholder="Enter new password" ng-model="user.password")
.form-group: .col-sm-8.col-sm-offset-4
button.btn.btn-success(ng-click="updateUser()") Update Profile
button.btn.btn-default(ng-if="$state.includes('users.user')" ui-sref="users.list") back
hr(ng-if="!is_self")
.row(ng-if="!is_self")
.col-sm-8.col-sm-offset-4
button.btn.btn-danger(ng-click="deleteUser()") Delete user

View File

@ -54,7 +54,7 @@ app.run(['$rootScope', '$window', '$couchPotato', '$injector', '$state', '$http'
$http.get('/user')
.then(function (user) {
$rootScope.user = user;
$rootScope.user = user.data;
$rootScope.loggedIn = true;
$rootScope.refreshInfo();

View File

@ -0,0 +1,43 @@
define(function () {
app.registerController('UserCtrl', ['$scope', '$http', '$uibModal', '$rootScope', 'user', '$state', function ($scope, $http, $modal, $rootScope, user, $state) {
$scope.user = user.data;
$scope.is_self = $scope.user.id == $rootScope.user.id;
$scope.updatePassword = function (pwd) {
$http.post('/users/' + $scope.user.id + '/password', {
password: pwd
}).success(function () {
swal('OK', 'User profile & password were updated.');
}).error(function (_, status) {
swal('Error', 'Setting password failed, API responded with HTTP ' + status, 'error');
});
}
$scope.updateUser = function () {
var pwd = $scope.user.password;
$http.put('/users/' + $scope.user.id, $scope.user).success(function () {
if ($rootScope.user.id == $scope.user.id) {
$rootScope.user = $scope.user;
}
if (pwd && pwd.length > 0) {
$scope.updatePassword(pwd);
return;
}
swal('OK', 'User has been updated!');
}).error(function (_, status) {
swal('Error', 'User profile could not be updated: ' + status, 'error');
});
}
$scope.deleteUser = function () {
$http.delete('/users/' + $scope.user.id).success(function () {
$state.go('users.list');
}).error(function (_, status) {
swal('Error', 'User could not be deleted! ' + status, 'error');
});
}
}]);
});

View File

@ -21,17 +21,5 @@ define(function () {
});
});
}
$scope.changePassword = function (user) {
$modal.open({
templateUrl: '/tpl/users/password.html'
}).result.then(function (password) {
$http.post('/users/' + user.id + '/password', {
password: password
}).error(function (_, status) {
swal('Error', 'Setting password failed, API responded with HTTP ' + status, 'error');
});
});
}
}]);
});

View File

@ -18,6 +18,11 @@ app.config(function ($stateProvider, $urlRouterProvider, $locationProvider, $cou
})
.state('users', {
url: '/users',
abstract: true,
templateUrl: '/tpl/abstract.html'
})
.state('users.list', {
url: '',
pageTitle: 'Users',
templateUrl: '/tpl/users/list.html',
controller: 'UsersCtrl',
@ -25,6 +30,18 @@ app.config(function ($stateProvider, $urlRouterProvider, $locationProvider, $cou
$d: $couchPotatoProvider.resolve(['controllers/users'])
}
})
.state('users.user', {
url: '/:user_id',
pageTitle: 'User',
templateUrl: '/tpl/users/user.html',
controller: 'UserCtrl',
resolve: {
$d: $couchPotatoProvider.resolve(['controllers/user']),
user: ['$http', '$stateParams', function ($http, $stateParams) {
return $http.get('/users/' + $stateParams.user_id);
}]
}
})
.state('admin', {
url: '/admin',
pageTitle: 'System Info',
@ -33,6 +50,18 @@ app.config(function ($stateProvider, $urlRouterProvider, $locationProvider, $cou
resolve: {
$d: $couchPotatoProvider.resolve(['controllers/admin'])
}
})
.state('user', {
url: '/user',
pageTitle: 'User',
templateUrl: '/tpl/users/user.html',
controller: 'UserCtrl',
resolve: {
$d: $couchPotatoProvider.resolve(['controllers/user']),
user: ['$http', function ($http) {
return $http.get('/user');
}]
}
});
});

View File

@ -35,7 +35,7 @@ func GetKeys(c *gin.Context) {
project := c.MustGet("project").(models.Project)
var keys []models.AccessKey
q := squirrel.Select("*").
q := squirrel.Select("id, name, type, project_id, `key`").
From("access_key").
Where("project_id=?", project.ID)

View File

@ -1,7 +1,6 @@
package routes
import (
"os/exec"
"strings"
"github.com/russross/blackfriday"
@ -53,8 +52,10 @@ func Route(r *gin.Engine) {
api.GET("/users", getUsers)
api.POST("/users", addUser)
api.GET("/users/:user_id", getUserMiddleware, getUser)
api.PUT("/users/:user_id", getUserMiddleware, updateUser)
api.POST("/users/:user_id/password", getUserMiddleware, updateUserPassword)
api.DELETE("/users/:user_id", getUserMiddleware, deleteUser)
func(api *gin.RouterGroup) {
api.Use(projects.ProjectMiddleware)
@ -153,7 +154,6 @@ func servePublic(c *gin.Context) {
}
func getSystemInfo(c *gin.Context) {
cmdPath, _ := exec.LookPath("semaphore")
body := map[string]interface{}{
"version": util.Version,
"update": upgrade.UpdateAvailable,
@ -162,7 +162,7 @@ func getSystemInfo(c *gin.Context) {
"dbName": util.Config.MySQL.DbName,
"dbUser": util.Config.MySQL.Username,
"path": util.Config.TmpPath,
"cmdPath": cmdPath,
"cmdPath": upgrade.FindSemaphore(),
},
}

View File

@ -13,6 +13,11 @@ import (
)
func getUser(c *gin.Context) {
if u, exists := c.Get("_user"); exists {
c.JSON(200, u)
return
}
c.JSON(200, c.MustGet("user"))
}

View File

@ -71,21 +71,30 @@ func updateUser(c *gin.Context) {
}
func updateUserPassword(c *gin.Context) {
user := c.MustGet("_user").(models.User)
var pwd struct {
Pwd string `json:"password"`
}
userID, err := util.GetIntParam("user_id", c)
if err != nil {
return
}
if err := c.Bind(&pwd); err != nil {
return
}
password, _ := bcrypt.GenerateFromPassword([]byte(pwd.Pwd), 11)
if _, err := database.Mysql.Exec("update user set password=? where id=?", string(password), userID); err != nil {
if _, err := database.Mysql.Exec("update user set password=? where id=?", string(password), user.ID); err != nil {
panic(err)
}
c.AbortWithStatus(204)
}
func deleteUser(c *gin.Context) {
user := c.MustGet("_user").(models.User)
if _, err := database.Mysql.Exec("delete from project__user where user_id=?", user.ID); err != nil {
panic(err)
}
if _, err := database.Mysql.Exec("delete from user where id=?", user.ID); err != nil {
panic(err)
}

View File

@ -52,9 +52,9 @@ func Upgrade(version string) error {
}
// replace it
cmdPath, err := exec.LookPath("semaphore")
if err != nil {
return err
cmdPath := FindSemaphore()
if len(cmdPath) == 0 {
return errors.New("Cannot find semaphore binary")
}
fmt.Printf("replacing %s\n", cmdPath)
@ -72,6 +72,16 @@ func Upgrade(version string) error {
return nil
}
func FindSemaphore() string {
cmdPath, _ := exec.LookPath("semaphore")
if len(cmdPath) == 0 {
cmdPath, _ = filepath.Abs(os.Args[0])
}
return cmdPath
}
// findAsset returns the binary for this platform.
func findAsset(release *github.RepositoryRelease) *github.ReleaseAsset {
for _, asset := range release.Assets {