mirror of
https://github.com/semaphoreui/semaphore.git
synced 2025-01-20 23:39:56 +01:00
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:
parent
9a292f0f82
commit
0b417594a4
@ -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
31
main.go
@ -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)
|
||||
|
@ -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%);
|
||||
}
|
||||
|
@ -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 }}
|
@ -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")
|
||||
|
@ -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 }}
|
||||
|
@ -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
|
||||
|
@ -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
|
27
public/html/users/user.jade
Normal file
27
public/html/users/user.jade
Normal 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
|
@ -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();
|
||||
|
43
public/js/controllers/user.js
Normal file
43
public/js/controllers/user.js
Normal 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');
|
||||
});
|
||||
}
|
||||
}]);
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
}]);
|
||||
});
|
||||
|
@ -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');
|
||||
}]
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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(),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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"))
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user