feat(global): add authentication support with single admin account

pull/387/head
Anthony Lapenna 2016-12-15 16:33:47 +13:00 committed by GitHub
parent 1e5207517d
commit 4e77c72fa2
35 changed files with 1475 additions and 220 deletions

View File

@ -2,6 +2,8 @@ package main
import (
"crypto/tls"
"errors"
"github.com/gorilla/securecookie"
"log"
"net/http"
"net/url"
@ -15,6 +17,8 @@ type (
dataPath string
tlsConfig *tls.Config
templatesURL string
dataStore *dataStore
secret []byte
}
apiConfig struct {
@ -31,7 +35,21 @@ type (
}
)
const (
datastoreFileName = "portainer.db"
)
var (
errSecretKeyGeneration = errors.New("Unable to generate secret key to sign JWT")
)
func (a *api) run(settings *Settings) {
err := a.initDatabase()
if err != nil {
log.Fatal(err)
}
defer a.cleanUp()
handler := a.newHandler(settings)
log.Printf("Starting portainer on %s", a.bindAddress)
if err := http.ListenAndServe(a.bindAddress, handler); err != nil {
@ -39,12 +57,34 @@ func (a *api) run(settings *Settings) {
}
}
func (a *api) cleanUp() {
a.dataStore.cleanUp()
}
func (a *api) initDatabase() error {
dataStore, err := newDataStore(a.dataPath + "/" + datastoreFileName)
if err != nil {
return err
}
err = dataStore.initDataStore()
if err != nil {
return err
}
a.dataStore = dataStore
return nil
}
func newAPI(apiConfig apiConfig) *api {
endpointURL, err := url.Parse(apiConfig.Endpoint)
if err != nil {
log.Fatal(err)
}
secret := securecookie.GenerateRandomKey(32)
if secret == nil {
log.Fatal(errSecretKeyGeneration)
}
var tlsConfig *tls.Config
if apiConfig.TLSEnabled {
tlsConfig = newTLSConfig(apiConfig.TLSCACertPath, apiConfig.TLSCertPath, apiConfig.TLSKeyPath)
@ -57,5 +97,6 @@ func newAPI(apiConfig apiConfig) *api {
dataPath: apiConfig.DataPath,
tlsConfig: tlsConfig,
templatesURL: apiConfig.TemplatesURL,
secret: secret,
}
}

88
api/auth.go Normal file
View File

@ -0,0 +1,88 @@
package main
import (
"encoding/json"
"github.com/asaskevich/govalidator"
"golang.org/x/crypto/bcrypt"
"io/ioutil"
"log"
"net/http"
)
type (
credentials struct {
Username string `valid:"alphanum,required"`
Password string `valid:"length(8)"`
}
authResponse struct {
JWT string `json:"jwt"`
}
)
func hashPassword(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", nil
}
return string(hash), nil
}
func checkPasswordValidity(password string, hash string) error {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
}
// authHandler defines a handler function used to authenticate users
func (api *api) authHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.Header().Set("Allow", "POST")
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Unable to parse request body", http.StatusBadRequest)
return
}
var credentials credentials
err = json.Unmarshal(body, &credentials)
if err != nil {
http.Error(w, "Unable to parse credentials", http.StatusBadRequest)
return
}
_, err = govalidator.ValidateStruct(credentials)
if err != nil {
http.Error(w, "Invalid credentials format", http.StatusBadRequest)
return
}
var username = credentials.Username
var password = credentials.Password
u, err := api.dataStore.getUserByUsername(username)
if err != nil {
log.Printf("User not found: %s", username)
http.Error(w, "User not found", http.StatusNotFound)
return
}
err = checkPasswordValidity(password, u.Password)
if err != nil {
log.Printf("Invalid credentials for user: %s", username)
http.Error(w, "Invalid credentials", http.StatusUnprocessableEntity)
return
}
token, err := api.generateJWTToken(username)
if err != nil {
log.Printf("Unable to generate JWT token: %s", err.Error())
http.Error(w, "Unable to generate JWT token", http.StatusInternalServerError)
return
}
response := authResponse{
JWT: token,
}
json.NewEncoder(w).Encode(response)
}

98
api/datastore.go Normal file
View File

@ -0,0 +1,98 @@
package main
import (
"encoding/json"
"errors"
"github.com/boltdb/bolt"
)
const (
userBucketName = "users"
)
type (
dataStore struct {
db *bolt.DB
}
userItem struct {
Username string `json:"username"`
Password string `json:"password,omitempty"`
}
)
var (
errUserNotFound = errors.New("User not found")
)
func (dataStore *dataStore) initDataStore() error {
return dataStore.db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(userBucketName))
if err != nil {
return err
}
return nil
})
}
func (dataStore *dataStore) cleanUp() {
dataStore.db.Close()
}
func newDataStore(databasePath string) (*dataStore, error) {
db, err := bolt.Open(databasePath, 0600, nil)
if err != nil {
return nil, err
}
return &dataStore{
db: db,
}, nil
}
func (dataStore *dataStore) getUserByUsername(username string) (*userItem, error) {
var data []byte
err := dataStore.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
value := bucket.Get([]byte(username))
if value == nil {
return errUserNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var user userItem
err = json.Unmarshal(data, &user)
if err != nil {
return nil, err
}
return &user, nil
}
func (dataStore *dataStore) updateUser(user userItem) error {
buffer, err := json.Marshal(user)
if err != nil {
return err
}
err = dataStore.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
err = bucket.Put([]byte(user.Username), buffer)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
return nil
}

View File

@ -1,6 +1,7 @@
package main
import (
"github.com/gorilla/mux"
"golang.org/x/net/websocket"
"log"
"net/http"
@ -12,21 +13,35 @@ import (
// newHandler creates a new http.Handler with CSRF protection
func (a *api) newHandler(settings *Settings) http.Handler {
var (
mux = http.NewServeMux()
mux = mux.NewRouter()
fileHandler = http.FileServer(http.Dir(a.assetPath))
)
handler := a.newAPIHandler()
mux.Handle("/", fileHandler)
mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", handler))
mux.Handle("/ws/exec", websocket.Handler(a.execContainer))
mux.HandleFunc("/auth", a.authHandler)
mux.Handle("/users", addMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
a.usersHandler(w, r)
}), a.authenticate, secureHeaders))
mux.Handle("/users/{username}", addMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
a.userHandler(w, r)
}), a.authenticate, secureHeaders))
mux.Handle("/users/{username}/passwd", addMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
a.userPasswordHandler(w, r)
}), a.authenticate, secureHeaders))
mux.HandleFunc("/users/admin/check", a.checkAdminHandler)
mux.HandleFunc("/users/admin/init", a.initAdminHandler)
mux.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
settingsHandler(w, r, settings)
})
mux.HandleFunc("/templates", func(w http.ResponseWriter, r *http.Request) {
templatesHandler(w, r, a.templatesURL)
})
// mux.PathPrefix("/dockerapi/").Handler(http.StripPrefix("/dockerapi", handler))
mux.PathPrefix("/dockerapi/").Handler(http.StripPrefix("/dockerapi", addMiddleware(handler, a.authenticate, secureHeaders)))
mux.PathPrefix("/").Handler(http.StripPrefix("/", fileHandler))
// CSRF protection is disabled for the moment
// CSRFHandler := newCSRFHandler(a.dataPath)
// return CSRFHandler(newCSRFWrapper(mux))

29
api/jwt.go Normal file
View File

@ -0,0 +1,29 @@
package main
import (
"github.com/dgrijalva/jwt-go"
"time"
)
type claims struct {
Username string `json:"username"`
jwt.StandardClaims
}
func (api *api) generateJWTToken(username string) (string, error) {
expireToken := time.Now().Add(time.Hour * 8).Unix()
claims := claims{
username,
jwt.StandardClaims{
ExpiresAt: expireToken,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signedToken, err := token.SignedString(api.secret)
if err != nil {
return "", err
}
return signedToken, nil
}

View File

@ -4,14 +4,19 @@ import (
"gopkg.in/alecthomas/kingpin.v2"
)
const (
// Version number of portainer API
Version = "1.10.2"
)
// main is the entry point of the program
func main() {
kingpin.Version("1.10.2")
kingpin.Version(Version)
var (
endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String()
addr = kingpin.Flag("bind", "Address and port to serve Portainer").Default(":9000").Short('p').String()
assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String()
data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').String()
data = kingpin.Flag("data", "Path to the folder where the data is stored").Default("/data").Short('d').String()
tlsverify = kingpin.Flag("tlsverify", "TLS support").Default("false").Bool()
tlscacert = kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String()
tlscert = kingpin.Flag("tlscert", "Path to the TLS certificate file").Default("/certs/cert.pem").String()

65
api/middleware.go Normal file
View File

@ -0,0 +1,65 @@
package main
import (
"fmt"
"github.com/dgrijalva/jwt-go"
"net/http"
"strings"
)
func addMiddleware(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler {
for _, mw := range middleware {
h = mw(h)
}
return h
}
// authenticate provides Authentication middleware for handlers
func (api *api) authenticate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var token string
// Get token from the Authorization header
// format: Authorization: Bearer
tokens, ok := r.Header["Authorization"]
if ok && len(tokens) >= 1 {
token = tokens[0]
token = strings.TrimPrefix(token, "Bearer ")
}
if token == "" {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
msg := fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
return nil, msg
}
return api.secret, nil
})
if err != nil {
http.Error(w, "Invalid JWT token", http.StatusUnauthorized)
return
}
if parsedToken == nil || !parsedToken.Valid {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
// context.Set(r, "user", parsedToken)
next.ServeHTTP(w, r)
return
})
}
// SecureHeaders adds secure headers to the API
func secureHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("X-Content-Type-Options", "nosniff")
w.Header().Add("X-Frame-Options", "DENY")
next.ServeHTTP(w, r)
})
}

219
api/users.go Normal file
View File

@ -0,0 +1,219 @@
package main
import (
"encoding/json"
"github.com/gorilla/mux"
"io/ioutil"
"log"
"net/http"
)
type (
passwordCheckRequest struct {
Password string `json:"password"`
}
passwordCheckResponse struct {
Valid bool `json:"valid"`
}
initAdminRequest struct {
Password string `json:"password"`
}
)
// handle /users
// Allowed methods: POST
func (api *api) usersHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.Header().Set("Allow", "POST")
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Unable to parse request body", http.StatusBadRequest)
return
}
var user userItem
err = json.Unmarshal(body, &user)
if err != nil {
http.Error(w, "Unable to parse user data", http.StatusBadRequest)
return
}
user.Password, err = hashPassword(user.Password)
if err != nil {
http.Error(w, "Unable to hash user password", http.StatusInternalServerError)
return
}
err = api.dataStore.updateUser(user)
if err != nil {
log.Printf("Unable to persist user: %s", err.Error())
http.Error(w, "Unable to persist user", http.StatusInternalServerError)
return
}
}
// handle /users/admin/check
// Allowed methods: POST
func (api *api) checkAdminHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
w.Header().Set("Allow", "GET")
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
user, err := api.dataStore.getUserByUsername("admin")
if err == errUserNotFound {
log.Printf("User not found: %s", "admin")
http.Error(w, "User not found", http.StatusNotFound)
return
}
if err != nil {
log.Printf("Unable to retrieve user: %s", err.Error())
http.Error(w, "Unable to retrieve user", http.StatusInternalServerError)
return
}
user.Password = ""
json.NewEncoder(w).Encode(user)
}
// handle /users/admin/init
// Allowed methods: POST
func (api *api) initAdminHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.Header().Set("Allow", "POST")
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Unable to parse request body", http.StatusBadRequest)
return
}
var requestData initAdminRequest
err = json.Unmarshal(body, &requestData)
if err != nil {
http.Error(w, "Unable to parse user data", http.StatusBadRequest)
return
}
user := userItem{
Username: "admin",
}
user.Password, err = hashPassword(requestData.Password)
if err != nil {
http.Error(w, "Unable to hash user password", http.StatusInternalServerError)
return
}
err = api.dataStore.updateUser(user)
if err != nil {
log.Printf("Unable to persist user: %s", err.Error())
http.Error(w, "Unable to persist user", http.StatusInternalServerError)
return
}
}
// handle /users/{username}
// Allowed methods: PUT, GET
func (api *api) userHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "PUT" {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Unable to parse request body", http.StatusBadRequest)
return
}
var user userItem
err = json.Unmarshal(body, &user)
if err != nil {
http.Error(w, "Unable to parse user data", http.StatusBadRequest)
return
}
user.Password, err = hashPassword(user.Password)
if err != nil {
http.Error(w, "Unable to hash user password", http.StatusInternalServerError)
return
}
err = api.dataStore.updateUser(user)
if err != nil {
log.Printf("Unable to persist user: %s", err.Error())
http.Error(w, "Unable to persist user", http.StatusInternalServerError)
return
}
} else if r.Method == "GET" {
vars := mux.Vars(r)
username := vars["username"]
user, err := api.dataStore.getUserByUsername(username)
if err == errUserNotFound {
log.Printf("User not found: %s", username)
http.Error(w, "User not found", http.StatusNotFound)
return
}
if err != nil {
log.Printf("Unable to retrieve user: %s", err.Error())
http.Error(w, "Unable to retrieve user", http.StatusInternalServerError)
return
}
user.Password = ""
json.NewEncoder(w).Encode(user)
} else {
w.Header().Set("Allow", "PUT, GET")
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
}
// handle /users/{username}/passwd
// Allowed methods: POST
func (api *api) userPasswordHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.Header().Set("Allow", "POST")
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
vars := mux.Vars(r)
username := vars["username"]
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Unable to parse request body", http.StatusBadRequest)
return
}
var data passwordCheckRequest
err = json.Unmarshal(body, &data)
if err != nil {
http.Error(w, "Unable to parse user data", http.StatusBadRequest)
return
}
user, err := api.dataStore.getUserByUsername(username)
if err != nil {
log.Printf("Unable to retrieve user: %s", err.Error())
http.Error(w, "Unable to retrieve user", http.StatusInternalServerError)
return
}
valid := true
err = checkPasswordValidity(data.Password, user.Password)
if err != nil {
valid = false
}
response := passwordCheckResponse{
Valid: valid,
}
json.NewEncoder(w).Encode(response)
}

View File

@ -6,9 +6,12 @@ angular.module('portainer', [
'ngCookies',
'ngSanitize',
'angularUtils.directives.dirPagination',
'LocalStorageModule',
'angular-jwt',
'portainer.services',
'portainer.helpers',
'portainer.filters',
'auth',
'dashboard',
'container',
'containerConsole',
@ -19,8 +22,11 @@ angular.module('portainer', [
'events',
'images',
'image',
'main',
'service',
'services',
'settings',
'sidebar',
'createService',
'stats',
'swarm',
@ -31,131 +37,430 @@ angular.module('portainer', [
'templates',
'volumes',
'createVolume'])
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', function ($stateProvider, $urlRouterProvider, $httpProvider) {
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider) {
'use strict';
$urlRouterProvider.otherwise('/');
localStorageServiceProvider
.setStorageType('sessionStorage')
.setPrefix('portainer');
jwtOptionsProvider.config({
tokenGetter: ['localStorageService', function(localStorageService) {
return localStorageService.get('JWT');
}],
unauthenticatedRedirector: ['$state', function($state) {
$state.go('auth', {error: 'Your session has expired'});
}]
});
$httpProvider.interceptors.push('jwtInterceptor');
$urlRouterProvider.otherwise('/auth');
$stateProvider
.state('index', {
url: '/',
templateUrl: 'app/components/dashboard/dashboard.html',
controller: 'DashboardController'
.state('auth', {
url: '/auth',
params: {
logout: false,
error: ''
},
views: {
"content": {
templateUrl: 'app/components/auth/auth.html',
controller: 'AuthenticationController'
}
}
})
.state('containers', {
url: '/containers/',
templateUrl: 'app/components/containers/containers.html',
controller: 'ContainersController'
views: {
"content": {
templateUrl: 'app/components/containers/containers.html',
controller: 'ContainersController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('container', {
url: "^/containers/:id",
templateUrl: 'app/components/container/container.html',
controller: 'ContainerController'
views: {
"content": {
templateUrl: 'app/components/container/container.html',
controller: 'ContainerController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('stats', {
url: "^/containers/:id/stats",
templateUrl: 'app/components/stats/stats.html',
controller: 'StatsController'
views: {
"content": {
templateUrl: 'app/components/stats/stats.html',
controller: 'StatsController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('logs', {
url: "^/containers/:id/logs",
templateUrl: 'app/components/containerLogs/containerlogs.html',
controller: 'ContainerLogsController'
views: {
"content": {
templateUrl: 'app/components/containerLogs/containerlogs.html',
controller: 'ContainerLogsController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('console', {
url: "^/containers/:id/console",
templateUrl: 'app/components/containerConsole/containerConsole.html',
controller: 'ContainerConsoleController'
views: {
"content": {
templateUrl: 'app/components/containerConsole/containerConsole.html',
controller: 'ContainerConsoleController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('dashboard', {
url: '/dashboard',
views: {
"content": {
templateUrl: 'app/components/dashboard/dashboard.html',
controller: 'DashboardController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('actions', {
abstract: true,
url: "/actions",
template: '<ui-view/>'
views: {
"content": {
template: '<div ui-view="content"></div>'
},
"sidebar": {
template: '<div ui-view="sidebar"></div>'
}
}
})
.state('actions.create', {
abstract: true,
url: "/create",
template: '<ui-view/>'
views: {
"content": {
template: '<div ui-view="content"></div>'
},
"sidebar": {
template: '<div ui-view="sidebar"></div>'
}
}
})
.state('actions.create.container', {
url: "/container",
templateUrl: 'app/components/createContainer/createcontainer.html',
controller: 'CreateContainerController'
views: {
"content": {
templateUrl: 'app/components/createContainer/createcontainer.html',
controller: 'CreateContainerController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('actions.create.network', {
url: "/network",
templateUrl: 'app/components/createNetwork/createnetwork.html',
controller: 'CreateNetworkController'
views: {
"content": {
templateUrl: 'app/components/createNetwork/createnetwork.html',
controller: 'CreateNetworkController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('actions.create.service', {
url: "/service",
templateUrl: 'app/components/createService/createservice.html',
controller: 'CreateServiceController'
views: {
"content": {
templateUrl: 'app/components/createService/createservice.html',
controller: 'CreateServiceController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('actions.create.volume', {
url: "/volume",
templateUrl: 'app/components/createVolume/createvolume.html',
controller: 'CreateVolumeController'
views: {
"content": {
templateUrl: 'app/components/createVolume/createvolume.html',
controller: 'CreateVolumeController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('docker', {
url: '/docker/',
templateUrl: 'app/components/docker/docker.html',
controller: 'DockerController'
views: {
"content": {
templateUrl: 'app/components/docker/docker.html',
controller: 'DockerController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('events', {
url: '/events/',
templateUrl: 'app/components/events/events.html',
controller: 'EventsController'
views: {
"content": {
templateUrl: 'app/components/events/events.html',
controller: 'EventsController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('images', {
url: '/images/',
templateUrl: 'app/components/images/images.html',
controller: 'ImagesController'
views: {
"content": {
templateUrl: 'app/components/images/images.html',
controller: 'ImagesController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('image', {
url: '^/images/:id/',
templateUrl: 'app/components/image/image.html',
controller: 'ImageController'
views: {
"content": {
templateUrl: 'app/components/image/image.html',
controller: 'ImageController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('networks', {
url: '/networks/',
templateUrl: 'app/components/networks/networks.html',
controller: 'NetworksController'
views: {
"content": {
templateUrl: 'app/components/networks/networks.html',
controller: 'NetworksController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('network', {
url: '^/networks/:id/',
templateUrl: 'app/components/network/network.html',
controller: 'NetworkController'
views: {
"content": {
templateUrl: 'app/components/network/network.html',
controller: 'NetworkController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('services', {
url: '/services/',
templateUrl: 'app/components/services/services.html',
controller: 'ServicesController'
views: {
"content": {
templateUrl: 'app/components/services/services.html',
controller: 'ServicesController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('service', {
url: '^/service/:id/',
templateUrl: 'app/components/service/service.html',
controller: 'ServiceController'
views: {
"content": {
templateUrl: 'app/components/service/service.html',
controller: 'ServiceController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('settings', {
url: '/settings/',
views: {
"content": {
templateUrl: 'app/components/settings/settings.html',
controller: 'SettingsController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('task', {
url: '^/task/:id',
templateUrl: 'app/components/task/task.html',
controller: 'TaskController'
views: {
"content": {
templateUrl: 'app/components/task/task.html',
controller: 'TaskController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('templates', {
url: '/templates/',
templateUrl: 'app/components/templates/templates.html',
controller: 'TemplatesController'
views: {
"content": {
templateUrl: 'app/components/templates/templates.html',
controller: 'TemplatesController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('volumes', {
url: '/volumes/',
templateUrl: 'app/components/volumes/volumes.html',
controller: 'VolumesController'
views: {
"content": {
templateUrl: 'app/components/volumes/volumes.html',
controller: 'VolumesController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('swarm', {
url: '/swarm/',
templateUrl: 'app/components/swarm/swarm.html',
controller: 'SwarmController'
views: {
"content": {
templateUrl: 'app/components/swarm/swarm.html',
controller: 'SwarmController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
});
// The Docker API likes to return plaintext errors, this catches them and disp
@ -165,7 +470,7 @@ angular.module('portainer', [
return {
'response': function(response) {
if (typeof(response.data) === 'string' &&
(response.data.startsWith('Conflict.') || response.data.startsWith('conflict:'))) {
(response.data.startsWith('Conflict.') || response.data.startsWith('conflict:'))) {
$.gritter.add({
title: 'Error',
text: $('<div>').text(response.data).html(),
@ -182,12 +487,28 @@ angular.module('portainer', [
};
});
}])
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'EndpointMode', function ($rootScope, $state, Authentication, authManager, EndpointMode) {
authManager.checkAuthOnRefresh();
authManager.redirectWhenUnauthenticated();
Authentication.init();
$rootScope.$state = $state;
$rootScope.$on('tokenHasExpired', function($state) {
$state.go('auth', {error: 'Your session has expired'});
});
$rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) {
if ((fromState.name === 'auth' || fromState.name === '') && Authentication.isAuthenticated()) {
EndpointMode.determineEndpointMode();
}
});
}])
// This is your docker url that the api will use to make requests
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
.constant('DOCKER_ENDPOINT', 'dockerapi')
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is requred. If you have a port, prefix it with a ':' i.e. :4243
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243
.constant('CONFIG_ENDPOINT', 'settings')
.constant('AUTH_ENDPOINT', 'auth')
.constant('TEMPLATES_ENDPOINT', 'templates')
.constant('PAGINATION_MAX_ITEMS', 10)
.constant('UI_VERSION', 'v1.10.2');

View File

@ -0,0 +1,101 @@
<div class="login-wrapper">
<!-- login box -->
<div class="container login-box">
<div class="col-md-6 col-md-offset-3 col-sm-6 col-sm-offset-3">
<!-- login box logo -->
<div class="row">
<img ng-if="logo" ng-src="{{ logo }}" class="login-logo">
<img ng-if="!logo" src="images/logo_alt.png" class="login-logo" alt="Portainer">
</div>
<!-- !login box logo -->
<!-- init password panel -->
<div class="panel panel-default" ng-if="initPassword">
<div class="panel-body">
<!-- init password form -->
<form class="login-form form-horizontal" enctype="multipart/form-data" method="POST">
<!-- comment -->
<div class="input-group">
<p style="margin: 5px;">
Please specify a password for the <b>admin</b> user account.
</p>
</div>
<!-- !comment input -->
<!-- comment -->
<div class="input-group">
<p style="margin: 5px;">
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[initPasswordData.password.length >= 8]" aria-hidden="true"></i>
Your password must be at least 8 characters long
</p>
</div>
<!-- !comment input -->
<!-- password input -->
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input id="admin_password" type="password" class="form-control" name="password" ng-model="initPasswordData.password" autofocus>
</div>
<!-- !password input -->
<!-- comment -->
<div class="input-group">
<p style="margin: 5px;">
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[initPasswordData.password !== '' && initPasswordData.password === initPasswordData.password_confirmation]" aria-hidden="true"></i>
Confirm your password
</p>
</div>
<!-- !comment input -->
<!-- password confirmation input -->
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input id="password_confirmation" type="password" class="form-control" name="password" ng-model="initPasswordData.password_confirmation">
</div>
<!-- !password confirmation input -->
<!-- validate button -->
<div class="form-group">
<div class="col-sm-12 controls">
<p class="pull-left text-danger" ng-if="initPasswordData.error" style="margin: 5px;">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> Unable to create default user
</p>
<button type="submit" class="btn btn-primary pull-right" ng-disabled="initPasswordData.password.length < 8 || initPasswordData.password !== initPasswordData.password_confirmation" ng-click="createAdminUser()"><i class="fa fa-key" aria-hidden="true"></i> Validate</button>
</div>
</div>
<!-- !validate button -->
</form>
<!-- !init password form -->
</div>
</div>
<!-- !init password panel -->
<!-- login panel -->
<div class="panel panel-default" ng-if="!initPassword">
<div class="panel-body">
<!-- login form -->
<form class="login-form form-horizontal" enctype="multipart/form-data" method="POST">
<!-- username input -->
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-user" aria-hidden="true"></i></span>
<input id="username" type="text" class="form-control" name="username" ng-model="authData.username" placeholder="Username">
</div>
<!-- !username input -->
<!-- password input -->
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input id="password" type="password" class="form-control" name="password" ng-model="authData.password" autofocus>
</div>
<!-- !password input -->
<!-- login button -->
<div class="form-group">
<div class="col-sm-12 controls">
<p class="pull-left text-danger" ng-if="authData.error" style="margin: 5px;">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ authData.error }}
</p>
<button type="submit" class="btn btn-primary pull-right" ng-click="authenticateUser()"><i class="fa fa-sign-in" aria-hidden="true"></i> Login</button>
</div>
</div>
<!-- !login button -->
</form>
<!-- !login form -->
</div>
</div>
<!-- !login panel -->
</div>
</div>
<!-- !login box -->
</div>

View File

@ -0,0 +1,68 @@
angular.module('auth', [])
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Config', 'Authentication', 'Users', 'Messages',
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Authentication, Users, Messages) {
$scope.authData = {
username: 'admin',
password: '',
error: ''
};
$scope.initPasswordData = {
password: '',
password_confirmation: '',
error: false
};
if ($stateParams.logout) {
Authentication.logout();
}
if ($stateParams.error) {
$scope.authData.error = $stateParams.error;
Authentication.logout();
}
if (Authentication.isAuthenticated()) {
$state.go('dashboard');
}
Config.$promise.then(function (c) {
$scope.logo = c.logo;
});
Users.checkAdminUser({}, function (d) {},
function (e) {
if (e.status === 404) {
$scope.initPassword = true;
} else {
Messages.error("Failure", e, 'Unable to verify administrator account existence');
}
});
$scope.createAdminUser = function() {
var password = $sanitize($scope.initPasswordData.password);
Users.initAdminUser({password: password}, function (d) {
$scope.initPassword = false;
$timeout(function() {
var element = $window.document.getElementById('password');
if(element) {
element.focus();
}
});
}, function (e) {
$scope.initPassword.error = true;
});
};
$scope.authenticateUser = function() {
$scope.authenticationError = false;
var username = $sanitize($scope.authData.username);
var password = $sanitize($scope.authData.password);
Authentication.login(username, password)
.then(function() {
$state.go('dashboard');
}, function() {
$scope.authData.error = 'Invalid credentials';
});
};
}]);

View File

@ -66,7 +66,7 @@
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="swarm && !swarm_mode">
<th ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<a ui-sref="containers" ng-click="order('Host')">
Host IP
<span ng-show="sortType == 'Host' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
@ -86,11 +86,11 @@
<tr dir-paginate="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<td><input type="checkbox" ng-model="container.Checked" ng-change="selectItem(container)"/></td>
<td><span class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status|containerstatus }}</span></td>
<td ng-if="swarm && !swarm_mode"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername}}</a></td>
<td ng-if="!swarm || swarm_mode"><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td>
<td ng-if="endpointMode.provider === 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername}}</a></td>
<td ng-if="endpointMode.provider !== 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
<td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td>
<td ng-if="swarm && !swarm_mode">{{ container.hostIP }}</td>
<td ng-if="endpointMode.provider === 'DOCKER_SWARM'">{{ container.hostIP }}</td>
<td>
<a ng-if="container.Ports.length > 0" ng-repeat="p in container.Ports" class="image-tag" ng-href="http://{{p.host}}:{{p.public}}" target="_blank">
<i class="fa fa-external-link" aria-hidden="true"></i> {{p.public}}:{{ p.private }}

View File

@ -7,9 +7,7 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
$scope.sortType = 'State';
$scope.sortReverse = false;
$scope.state.selectedItemCount = 0;
$scope.swarm_mode = false;
$scope.pagination_count = Settings.pagination_count;
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
@ -28,7 +26,7 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
if (model.IP) {
$scope.state.displayIP = true;
}
if ($scope.swarm && !$scope.swarm_mode) {
if ($scope.endpointMode.provider === 'DOCKER_SWARM') {
model.hostIP = $scope.swarm_hosts[_.split(container.Names[0], '/')[1]];
}
return model;
@ -150,17 +148,11 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
return swarm_hosts;
}
$scope.swarm = false;
Config.$promise.then(function (c) {
$scope.containersToHideLabels = c.hiddenLabels;
$scope.swarm = c.swarm;
if (c.swarm) {
if (c.swarm && $scope.endpointMode.provider === 'DOCKER_SWARM') {
Info.get({}, function (d) {
if (!_.startsWith(d.ServerVersion, 'swarm')) {
$scope.swarm_mode = true;
} else {
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
}
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
update({all: Settings.displayAll ? 1 : 0});
});
} else {

View File

@ -53,11 +53,6 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
Config.$promise.then(function (c) {
$scope.swarm = c.swarm;
Info.get({}, function(info) {
if ($scope.swarm && !_.startsWith(info.ServerVersion, 'swarm')) {
$scope.swarm_mode = true;
}
});
var containersToHideLabels = c.hiddenLabels;
Volume.query({}, function (d) {
@ -216,7 +211,7 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
var containerName = container;
if (container && typeof container === 'object') {
containerName = $filter('trimcontainername')(container.Names[0]);
if ($scope.swarm && !$scope.swarm_mode) {
if ($scope.swarm && $scope.endpointMode.provider === 'DOCKER_SWARM') {
containerName = $filter('swarmcontainername')(container);
}
}

View File

@ -258,7 +258,7 @@
<!-- tab-network -->
<div class="tab-pane" id="network">
<form class="form-horizontal" style="margin-top: 15px;">
<div class="form-group" ng-if="globalNetworkCount === 0 && !swarm_mode">
<div class="form-group" ng-if="globalNetworkCount === 0 && endpointMode.provider !== 'DOCKER_SWARM_MODE'">
<div class="col-sm-12">
<span class="small text-muted">You don't have any shared network. Head over the <a ui-sref="networks">networks view</a> to create one.</span>
</div>
@ -278,10 +278,10 @@
<div class="form-group" ng-if="config.HostConfig.NetworkMode == 'container'">
<label for="container_network_container" class="col-sm-1 control-label text-left">Container</label>
<div class="col-sm-9">
<select ng-if="(!swarm || swarm && swarm_mode)" ng-options="container|containername for container in runningContainers" class="selectpicker form-control" ng-model="formValues.NetworkContainer">
<select ng-if="endpointMode.provider !== 'DOCKER_SWARM'" ng-options="container|containername for container in runningContainers" class="selectpicker form-control" ng-model="formValues.NetworkContainer">
<option selected disabled hidden value="">Select a container</option>
</select>
<select ng-if="swarm && !swarm_mode" ng-options="container|swarmcontainername for container in runningContainers" class="selectpicker form-control" ng-model="formValues.NetworkContainer">
<select ng-if="endpointMode.provider === 'DOCKER_SWARM'" ng-options="container|swarmcontainername for container in runningContainers" class="selectpicker form-control" ng-model="formValues.NetworkContainer">
<option selected disabled hidden value="">Select a container</option>
</select>
</div>

View File

@ -6,7 +6,7 @@
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="swarm_mode || !swarm">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="endpointMode.provider !== 'DOCKER_SWARM'">
<rd-widget>
<rd-widget-header icon="fa-tachometer" title="Node info"></rd-widget-header>
<rd-widget-body classes="no-padding">
@ -33,7 +33,7 @@
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="swarm && !swarm_mode">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<rd-widget>
<rd-widget-header icon="fa-tachometer" title="Cluster info"></rd-widget-header>
<rd-widget-body classes="no-padding">
@ -60,7 +60,7 @@
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="swarm && swarm_mode">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">
<rd-widget>
<rd-widget-header icon="fa-tachometer" title="Swarm info"></rd-widget-header>
<rd-widget-body classes="no-padding">

View File

@ -14,7 +14,6 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
$scope.volumeData = {
total: 0
};
$scope.swarm_mode = false;
function prepareContainerData(d, containersToHideLabels) {
var running = 0;
@ -64,9 +63,6 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
function prepareInfoData(d) {
var info = d;
$scope.infoData = info;
if ($scope.swarm && !_.startsWith(info.ServerVersion, 'swarm')) {
$scope.swarm_mode = true;
}
}
function fetchDashboardData(containersToHideLabels) {
@ -84,6 +80,9 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
prepareNetworkData(d[3]);
prepareInfoData(d[4]);
$('#loadingViewSpinner').hide();
}, function(e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to load dashboard data");
});
}

View File

@ -1,31 +1,15 @@
angular.module('dashboard')
.controller('MasterCtrl', ['$scope', '$cookieStore', 'Settings', 'Config', 'Info',
function ($scope, $cookieStore, Settings, Config, Info) {
angular.module('main', [])
.controller('MainController', ['$scope', '$cookieStore',
function ($scope, $cookieStore) {
/**
* Sidebar Toggle & Cookie Control
*/
var mobileView = 992;
$scope.getWidth = function() {
return window.innerWidth;
};
$scope.swarm_mode = false;
Config.$promise.then(function (c) {
$scope.logo = c.logo;
$scope.swarm = c.swarm;
Info.get({}, function(d) {
if ($scope.swarm && !_.startsWith(d.ServerVersion, 'swarm')) {
$scope.swarm_mode = true;
$scope.swarm_manager = false;
if (d.Swarm.ControlAvailable) {
$scope.swarm_manager = true;
}
}
});
});
$scope.$watch($scope.getWidth, function(newValue, oldValue) {
if (newValue >= mobileView) {
if (angular.isDefined($cookieStore.get('toggle'))) {
@ -47,6 +31,4 @@ function ($scope, $cookieStore, Settings, Config, Info) {
window.onresize = function() {
$scope.$apply();
};
$scope.uiVersion = Settings.uiVersion;
}]);

View File

@ -23,12 +23,12 @@
</div>
<!-- !name-input -->
<!-- tag-note -->
<div class="form-group" ng-if="swarm">
<div class="form-group" ng-if="endpointMode.provider === 'DOCKER_SWARM' || endpointMode.provider === 'DOCKER_SWARM_MODE'">
<div class="col-sm-12">
<span class="small text-muted">Note: The network will be created using the overlay driver and will allow containers to communicate across the hosts of your cluster.</span>
</div>
</div>
<div class="form-group" ng-if="!swarm">
<div class="form-group" ng-if="endpointMode.provider === 'DOCKER_STANDALONE'">
<div class="col-sm-12">
<span class="small text-muted">Note: The network will be created using the bridge driver.</span>
</div>

View File

@ -0,0 +1,67 @@
<rd-header>
<rd-header-title title="Settings">
</rd-header-title>
<rd-header-content>Settings</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-lock" title="Change user password"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal" style="margin-top: 15px;">
<!-- current-password-input -->
<div class="form-group">
<label for="current_password" class="col-sm-2 control-label text-left">Current password</label>
<div class="col-sm-8">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input type="password" class="form-control" ng-model="formValues.currentPassword" id="current_password">
</div>
</div>
</div>
<!-- !current-password-input -->
<div class="form-group" style="margin-left: 5px;">
<p>
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[formValues.newPassword.length >= 8]" aria-hidden="true"></i>
Your new password must be at least 8 characters long
</p>
</div>
<!-- new-password-input -->
<div class="form-group">
<label for="new_password" class="col-sm-2 control-label text-left">New password</label>
<div class="col-sm-8">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input type="password" class="form-control" ng-model="formValues.newPassword" id="new_password">
</div>
</div>
</div>
<!-- !new-password-input -->
<!-- confirm-password-input -->
<div class="form-group">
<label for="confirm_password" class="col-sm-2 control-label text-left">Confirm password</label>
<div class="col-sm-8">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input type="password" class="form-control" ng-model="formValues.confirmPassword" id="confirm_password">
<span class="input-group-addon"><i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[formValues.newPassword !== '' && formValues.newPassword === formValues.confirmPassword]" aria-hidden="true"></i></span>
</div>
</div>
</div>
<!-- !confirm-password-input -->
<div class="form-group">
<div class="col-sm-2">
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="!formValues.currentPassword || formValues.newPassword.length < 8 || formValues.newPassword !== formValues.confirmPassword" ng-click="updatePassword()">Update password</button>
</div>
<div class="col-sm-10">
<p class="pull-left text-danger" ng-if="invalidPassword" style="margin: 5px;">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> Current password is not valid
</p>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@ -0,0 +1,30 @@
angular.module('settings', [])
.controller('SettingsController', ['$scope', '$state', '$sanitize', 'Users', 'Messages',
function ($scope, $state, $sanitize, Users, Messages) {
$scope.formValues = {
currentPassword: '',
newPassword: '',
confirmPassword: ''
};
$scope.updatePassword = function() {
$scope.invalidPassword = false;
$scope.error = false;
var currentPassword = $sanitize($scope.formValues.currentPassword);
Users.checkPassword({ username: $scope.username, password: currentPassword }, function (d) {
if (d.valid) {
var newPassword = $sanitize($scope.formValues.newPassword);
Users.update({ username: $scope.username, password: newPassword }, function (d) {
Messages.send("Success", "Password successfully updated");
$state.go('settings', {}, {reload: true});
}, function (e) {
Messages.error("Failure", e, "Unable to update password");
});
} else {
$scope.invalidPassword = true;
}
}, function (e) {
Messages.error("Failure", e, "Unable to check password validity");
});
};
}]);

View File

@ -0,0 +1,55 @@
<!-- Sidebar -->
<div id="sidebar-wrapper">
<ul class="sidebar">
<li class="sidebar-main">
<a ng-click="toggleSidebar()" class="interactive">
<img ng-if="logo" ng-src="{{ logo }}" class="img-responsive logo">
<img ng-if="!logo" src="images/logo.png" class="img-responsive logo" alt="Portainer">
<span class="menu-icon glyphicon glyphicon-transfer"></span>
</a>
</li>
<li class="sidebar-title"><span>NAVIGATION</span></li>
<li class="sidebar-list">
<a ui-sref="dashboard">Dashboard <span class="menu-icon fa fa-tachometer"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="templates">App Templates <span class="menu-icon fa fa-rocket"></span></a>
</li>
<li class="sidebar-list" ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">
<a ui-sref="services">Services <span class="menu-icon fa fa-list-alt"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="containers">Containers <span class="menu-icon fa fa-server"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="images">Images <span class="menu-icon fa fa-clone"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="networks">Networks <span class="menu-icon fa fa-sitemap"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="volumes">Volumes <span class="menu-icon fa fa-cubes"></span></a>
</li>
<li class="sidebar-list" ng-if="endpointMode.provider === 'DOCKER_STANDALONE'">
<a ui-sref="events">Events <span class="menu-icon fa fa-history"></span></a>
</li>
<li class="sidebar-list" ng-if="endpointMode.provider === 'DOCKER_SWARM' || (endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER')">
<a ui-sref="swarm">Swarm <span class="menu-icon fa fa-object-group"></span></a>
</li>
<li class="sidebar-list" ng-if="endpointMode.provider === 'DOCKER_STANDALONE'">
<a ui-sref="docker">Docker <span class="menu-icon fa fa-th"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="settings">Settings <span class="menu-icon fa fa-wrench"></span></a>
</li>
</ul>
<div class="sidebar-footer">
<div class="col-xs-12">
<a href="https://github.com/portainer/portainer" target="_blank">
<i class="fa fa-github" aria-hidden="true"></i>
Portainer {{ uiVersion }}
</a>
</div>
</div>
</div>
<!-- End Sidebar -->

View File

@ -0,0 +1,10 @@
angular.module('sidebar', [])
.controller('SidebarController', ['$scope', 'Settings', 'Config', 'Info',
function ($scope, Settings, Config, Info) {
Config.$promise.then(function (c) {
$scope.logo = c.logo;
});
$scope.uiVersion = Settings.uiVersion;
}]);

View File

@ -16,14 +16,14 @@
<tbody>
<tr>
<td>Nodes</td>
<td ng-if="!swarm_mode">{{ swarm.Nodes }}</td>
<td ng-if="swarm_mode">{{ info.Swarm.Nodes }}</td>
<td ng-if="endpointMode.provider === 'DOCKER_SWARM'">{{ swarm.Nodes }}</td>
<td ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">{{ info.Swarm.Nodes }}</td>
</tr>
<tr ng-if="!swarm_mode">
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<td>Images</td>
<td>{{ info.Images }}</td>
</tr>
<tr ng-if="!swarm_mode">
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<td>Swarm version</td>
<td>{{ docker.Version|swarmversion }}</td>
</tr>
@ -31,29 +31,29 @@
<td>Docker API version</td>
<td>{{ docker.ApiVersion }}</td>
</tr>
<tr ng-if="!swarm_mode">
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<td>Strategy</td>
<td>{{ swarm.Strategy }}</td>
</tr>
<tr>
<td>Total CPU</td>
<td ng-if="!swarm_mode">{{ info.NCPU }}</td>
<td ng-if="swarm_mode">{{ totalCPU }}</td>
<td ng-if="endpointMode.provider === 'DOCKER_SWARM'">{{ info.NCPU }}</td>
<td ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">{{ totalCPU }}</td>
</tr>
<tr>
<td>Total memory</td>
<td ng-if="!swarm_mode">{{ info.MemTotal|humansize: 2 }}</td>
<td ng-if="swarm_mode">{{ totalMemory|humansize: 2 }}</td>
<td ng-if="endpointMode.provider === 'DOCKER_SWARM'">{{ info.MemTotal|humansize: 2 }}</td>
<td ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">{{ totalMemory|humansize: 2 }}</td>
</tr>
<tr ng-if="!swarm_mode">
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<td>Operating system</td>
<td>{{ info.OperatingSystem }}</td>
</tr>
<tr ng-if="!swarm_mode">
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<td>Kernel version</td>
<td>{{ info.KernelVersion }}</td>
</tr>
<tr ng-if="!swarm_mode">
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<td>Go version</td>
<td>{{ docker.GoVersion }}</td>
</tr>
@ -65,7 +65,7 @@
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="!swarm_mode">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<rd-widget>
<rd-widget-header icon="fa-hdd-o" title="Node status"></rd-widget-header>
<rd-widget-body classes="no-padding">
@ -133,7 +133,7 @@
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="swarm_mode">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">
<rd-widget>
<rd-widget-header icon="fa-hdd-o" title="Node status"></rd-widget-header>
<rd-widget-body classes="no-padding">

View File

@ -7,7 +7,6 @@ function ($scope, Info, Version, Node, Settings) {
$scope.info = {};
$scope.docker = {};
$scope.swarm = {};
$scope.swarm_mode = false;
$scope.totalCPU = 0;
$scope.totalMemory = 0;
$scope.pagination_count = Settings.pagination_count;
@ -23,8 +22,7 @@ function ($scope, Info, Version, Node, Settings) {
Info.get({}, function (d) {
$scope.info = d;
if (!_.startsWith(d.ServerVersion, 'swarm')) {
$scope.swarm_mode = true;
if ($scope.endpointMode.provider === 'DOCKER_SWARM_MODE') {
Node.query({}, function(d) {
$scope.nodes = d;
var CPU = 0, memory = 0;

View File

@ -13,12 +13,12 @@
</rd-widget-custom-header>
<rd-widget-body classes="padding">
<form class="form-horizontal">
<div class="form-group" ng-if="globalNetworkCount === 0 && !swarm_mode">
<div class="form-group" ng-if="globalNetworkCount === 0 && endpointMode.provider === 'DOCKER_SWARM'">
<div class="col-sm-12">
<span class="small text-muted">When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the <a ui-sref="networks">networks view</a> to create one.</span>
</div>
</div>
<div class="form-group" ng-if="swarm_mode">
<div class="form-group" ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">
<div class="col-sm-12">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
<span class="small text-muted">App templates cannot be used with swarm-mode at the moment. You can still use them to quickly deploy containers to the Docker host.</span>
@ -41,10 +41,10 @@
<div ng-repeat="var in state.selectedTemplate.env" ng-if="!var.set" class="form-group">
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}</label>
<div class="col-sm-10">
<select ng-if="(!swarm || swarm && swarm_mode) && var.type === 'container'" ng-options="container|containername for container in runningContainers" class="selectpicker form-control" ng-model="var.value">
<select ng-if="endpointMode.provider !== 'DOCKER_SWARM' && var.type === 'container'" ng-options="container|containername for container in runningContainers" class="selectpicker form-control" ng-model="var.value">
<option selected disabled hidden value="">Select a container</option>
</select>
<select ng-if="swarm && !swarm_mode && var.type === 'container'" ng-options="container|swarmcontainername for container in runningContainers" class="selectpicker form-control" ng-model="var.value">
<select ng-if="endpointMode.provider === 'DOCKER_SWARM' && var.type === 'container'" ng-options="container|swarmcontainername for container in runningContainers" class="selectpicker form-control" ng-model="var.value">
<option selected disabled hidden value="">Select a container</option>
</select>
<input ng-if="!var.type || !var.type === 'container'" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}">

View File

@ -204,11 +204,6 @@ function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, C
Config.$promise.then(function (c) {
$scope.swarm = c.swarm;
Info.get({}, function(info) {
if ($scope.swarm && !_.startsWith(info.ServerVersion, 'swarm')) {
$scope.swarm_mode = true;
}
});
var containersToHideLabels = c.hiddenLabels;
Network.query({}, function (d) {
var networks = d;

View File

@ -4,7 +4,7 @@ angular
var directive = {
requires: '^rdHeader',
transclude: true,
template: '<div class="breadcrumb-links" ng-transclude></div>',
template: '<div class="breadcrumb-links"><div class="pull-left" ng-transclude></div><div class="pull-right"><a ui-sref="auth({logout: true})" class="text-danger" style="margin-right: 25px;"><u>log out <i class="fa fa-sign-out" aria-hidden="true"></i></u></a></div></div>',
restrict: 'E'
};
return directive;

View File

@ -1,14 +1,17 @@
angular
.module('portainer')
.directive('rdHeaderTitle', function rdHeaderTitle() {
.directive('rdHeaderTitle', ['$rootScope', function rdHeaderTitle($rootScope) {
var directive = {
requires: '^rdHeader',
scope: {
title: '@'
},
link: function (scope, iElement, iAttrs) {
scope.username = $rootScope.username;
},
transclude: true,
template: '<div class="page">{{title}}<span class="header_title_content" ng-transclude><span></div>',
template: '<div class="page white-space-normal">{{title}}<span class="header_title_content" ng-transclude></span><span class="pull-right user-box"><i class="fa fa-user-circle-o" aria-hidden="true"></i> {{username}}</span></div>',
restrict: 'E'
};
return directive;
});
}]);

View File

@ -166,14 +166,6 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
get: {method: 'GET'}
});
}])
.factory('Auth', ['$resource', 'Settings', function AuthFactory($resource, Settings) {
'use strict';
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#check-auth-configuration
return $resource(Settings.url + '/auth', {}, {
get: {method: 'GET'},
update: {method: 'POST'}
});
}])
.factory('Info', ['$resource', 'Settings', function InfoFactory($resource, Settings) {
'use strict';
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#display-system-wide-information
@ -229,6 +221,89 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
pagination_count: PAGINATION_MAX_ITEMS
};
}])
.factory('Auth', ['$resource', 'AUTH_ENDPOINT', function AuthFactory($resource, AUTH_ENDPOINT) {
'use strict';
return $resource(AUTH_ENDPOINT, {}, {
login: {
method: 'POST'
}
});
}])
.factory('Users', ['$resource', function UsersFactory($resource) {
'use strict';
return $resource('/users/:username/:action', {}, {
create: { method: 'POST' },
get: {method: 'GET', params: { username: '@username' } },
update: { method: 'PUT', params: { username: '@username' } },
checkPassword: { method: 'POST', params: { username: '@username', action: 'passwd' } },
checkAdminUser: {method: 'GET', params: { username: 'admin', action: 'check' }},
initAdminUser: {method: 'POST', params: { username: 'admin', action: 'init' }}
});
}])
.factory('EndpointMode', ['$rootScope', 'Info', function EndpointMode($rootScope, Info) {
'use strict';
return {
determineEndpointMode: function() {
Info.get({}, function(d) {
var mode = {
provider: '',
role: ''
};
if (_.startsWith(d.ServerVersion, 'swarm')) {
mode.provider = "DOCKER_SWARM";
if (d.SystemStatus[0][1] === 'primary') {
mode.role = "PRIMARY";
} else {
mode.role = "REPLICA";
}
} else {
if (!d.Swarm || _.isEmpty(d.Swarm.NodeID)) {
mode.provider = "DOCKER_STANDALONE";
} else {
mode.provider = "DOCKER_SWARM_MODE";
if (d.Swarm.ControlAvailable) {
mode.role = "MANAGER";
} else {
mode.role = "WORKER";
}
}
}
$rootScope.endpointMode = mode;
});
}
};
}])
.factory('Authentication', ['$q', '$rootScope', 'Auth', 'jwtHelper', 'localStorageService', function AuthenticationFactory($q, $rootScope, Auth, jwtHelper, localStorageService) {
'use strict';
return {
init: function() {
var jwt = localStorageService.get('JWT');
if (jwt) {
var tokenPayload = jwtHelper.decodeToken(jwt);
$rootScope.username = tokenPayload.username;
}
},
login: function(username, password) {
return $q(function (resolve, reject) {
Auth.login({username: username, password: password}).$promise
.then(function(data) {
localStorageService.set('JWT', data.jwt);
$rootScope.username = username;
resolve();
}, function() {
reject();
});
});
},
logout: function() {
localStorageService.remove('JWT');
},
isAuthenticated: function() {
var jwt = localStorageService.get('JWT');
return jwt && !jwtHelper.isTokenExpired(jwt);
}
};
}])
.factory('Messages', ['$rootScope', '$sanitize', function MessagesFactory($rootScope, $sanitize) {
'use strict';
return {

View File

@ -1,18 +1,27 @@
html, body, #content-wrapper, .page-content, #view {
height: 100%;
width: 100%;
}
.white-space-normal {
white-space: normal !important;
}
.btn-group button {
margin: 3px;
margin: 3px;
}
.messages {
max-height: 50px;
overflow-x: hidden;
overflow-y: scroll;
max-height: 50px;
overflow-x: hidden;
overflow-y: scroll;
}
.legend .title {
padding: 0 0.3em;
margin: 0.5em;
border-style: solid;
border-width: 0 0 0 1em;
padding: 0 0.3em;
margin: 0.5em;
border-style: solid;
border-width: 0 0 0 1em;
}
.logo {
@ -203,6 +212,48 @@ input[type="radio"] {
margin-bottom: 5px;
}
.login-wrapper {
margin-top: 25px;
height: 100%;
width: 100%;
display: flex;
align-items: center;
}
.login-box {
margin-bottom: 80px;
}
.login-box > div:first-child {
padding-bottom: 10px;
}
.login-logo {
display: block;
margin: auto;
position: relative;
width: 240px;
margin-bottom: 10px;
}
.login-form > div {
margin-bottom: 25px;
}
.login-form > div:last-child {
margin-top: 10px;
margin-bottom: 10px;
}
.panel-body {
padding-top: 30px;
background-color: #ffffff;
}
.pagination-controls {
margin-left: 10px;
}
.user-box {
margin-right: 25px;
}

BIN
assets/images/logo_alt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -33,15 +33,17 @@
"angular-resource": "~1.5.0",
"angular-ui-select": "~0.17.1",
"angular-utils-pagination": "~0.11.1",
"angular-local-storage": "~0.5.2",
"angular-jwt": "~0.1.8",
"bootstrap": "~3.3.6",
"font-awesome": "~4.6.3",
"filesize": "~3.3.0",
"jquery": "1.11.1",
"jquery.gritter": "1.7.4",
"lodash": "4.12.0",
"rdash-ui": "1.0.*",
"moment": "~2.14.1",
"xterm.js": "~2.0.1"
"xterm.js": "~2.0.1",
"font-awesome": "~4.7.0"
},
"resolutions": {
"angular": "1.5.5"

View File

@ -193,6 +193,8 @@ module.exports = function (grunt) {
src: ['bower_components/angular/angular.min.js',
'bower_components/angular-sanitize/angular-sanitize.min.js',
'bower_components/angular-cookies/angular-cookies.min.js',
'bower_components/angular-local-storage/dist/angular-local-storage.min.js',
'bower_components/angular-jwt/dist/angular-jwt.min.js',
'bower_components/angular-ui-router/release/angular-ui-router.min.js',
'bower_components/angular-resource/angular-resource.min.js',
'bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js',
@ -295,7 +297,7 @@ module.exports = function (grunt) {
},
buildBinary: {
command: [
'docker run --rm -v $(pwd)/api:/src centurylink/golang-builder',
'docker run --rm -v $(pwd)/api:/src portainer/golang-builder',
'shasum api/portainer > portainer-checksum.txt',
'mkdir -p dist',
'mv api/portainer dist/'
@ -303,7 +305,7 @@ module.exports = function (grunt) {
},
buildUnixArmBinary: {
command: [
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="linux" -e BUILD_GOARCH="arm" centurylink/golang-builder-cross',
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="linux" -e BUILD_GOARCH="arm" portainer/golang-builder:cross-platform',
'shasum api/portainer-linux-arm > portainer-checksum.txt',
'mkdir -p dist',
'mv api/portainer-linux-arm dist/portainer'
@ -311,7 +313,7 @@ module.exports = function (grunt) {
},
buildDarwinBinary: {
command: [
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="darwin" -e BUILD_GOARCH="amd64" centurylink/golang-builder-cross',
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="darwin" -e BUILD_GOARCH="amd64" portainer/golang-builder:cross-platform',
'shasum api/portainer-darwin-amd64 > portainer-checksum.txt',
'mkdir -p dist',
'mv api/portainer-darwin-amd64 dist/portainer'
@ -319,7 +321,7 @@ module.exports = function (grunt) {
},
buildWindowsBinary: {
command: [
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="windows" -e BUILD_GOARCH="amd64" centurylink/golang-builder-cross',
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="windows" -e BUILD_GOARCH="amd64" portainer/golang-builder:cross-platform',
'shasum api/portainer-windows-amd64 > portainer-checksum.txt',
'mkdir -p dist',
'mv api/portainer-windows-amd64 dist/portainer.exe'

View File

@ -24,67 +24,16 @@
<link rel="apple-touch-icon-precomposed" href="ico/apple-touch-icon-precomposed.png">
</head>
<body ng-controller="MasterCtrl">
<div id="page-wrapper" ng-class="{'open': toggle}" ng-cloak>
<body ng-controller="MainController">
<div id="page-wrapper" ng-class="{open: toggle && $state.current.name !== 'auth', nopadding: $state.current.name === 'auth'}" ng-cloak>
<!-- Sidebar -->
<div id="sidebar-wrapper">
<ul class="sidebar">
<li class="sidebar-main">
<a ng-click="toggleSidebar()">
<img ng-if="logo" ng-src="{{ logo }}" class="img-responsive logo">
<img ng-if="!logo" src="images/logo.png" class="img-responsive logo" alt="Portainer">
<span class="menu-icon glyphicon glyphicon-transfer"></span>
</a>
</li>
<li class="sidebar-title"><span>NAVIGATION</span></li>
<li class="sidebar-list">
<a ui-sref="index">Dashboard <span class="menu-icon fa fa-tachometer"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="templates">App Templates <span class="menu-icon fa fa-rocket"></span></a>
</li>
<li class="sidebar-list" ng-if="swarm_mode">
<a ui-sref="services">Services <span class="menu-icon fa fa-list-alt"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="containers">Containers <span class="menu-icon fa fa-server"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="images">Images <span class="menu-icon fa fa-clone"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="networks">Networks <span class="menu-icon fa fa-sitemap"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="volumes">Volumes <span class="menu-icon fa fa-cubes"></span></a>
</li>
<li class="sidebar-list" ng-if="!swarm">
<a ui-sref="events">Events <span class="menu-icon fa fa-history"></span></a>
</li>
<li class="sidebar-list" ng-if="(swarm && !swarm_mode) || (swarm_mode && swarm_manager)">
<a ui-sref="swarm">Swarm <span class="menu-icon fa fa-object-group"></span></a>
</li>
<li class="sidebar-list" ng-if="!swarm">
<a ui-sref="docker">Docker <span class="menu-icon fa fa-th"></span></a>
</li>
</ul>
<div class="sidebar-footer">
<div class="col-xs-12">
<a href="https://github.com/portainer/portainer" target="_blank">
<i class="fa fa-github" aria-hidden="true"></i>
Portainer {{ uiVersion }}
</a>
</div>
</div>
</div>
<!-- End Sidebar -->
<div id="sideview" ui-view="sidebar"></div>
<div id="content-wrapper">
<div class="page-content">
<!-- Main Content -->
<div id="view" ui-view></div>
<div id="view" ui-view="content"></div>
</div><!-- End Page Content -->
</div><!-- End Content Wrapper -->