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 ( import (
"crypto/tls" "crypto/tls"
"errors"
"github.com/gorilla/securecookie"
"log" "log"
"net/http" "net/http"
"net/url" "net/url"
@ -15,6 +17,8 @@ type (
dataPath string dataPath string
tlsConfig *tls.Config tlsConfig *tls.Config
templatesURL string templatesURL string
dataStore *dataStore
secret []byte
} }
apiConfig struct { 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) { func (a *api) run(settings *Settings) {
err := a.initDatabase()
if err != nil {
log.Fatal(err)
}
defer a.cleanUp()
handler := a.newHandler(settings) handler := a.newHandler(settings)
log.Printf("Starting portainer on %s", a.bindAddress) log.Printf("Starting portainer on %s", a.bindAddress)
if err := http.ListenAndServe(a.bindAddress, handler); err != nil { 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 { func newAPI(apiConfig apiConfig) *api {
endpointURL, err := url.Parse(apiConfig.Endpoint) endpointURL, err := url.Parse(apiConfig.Endpoint)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
secret := securecookie.GenerateRandomKey(32)
if secret == nil {
log.Fatal(errSecretKeyGeneration)
}
var tlsConfig *tls.Config var tlsConfig *tls.Config
if apiConfig.TLSEnabled { if apiConfig.TLSEnabled {
tlsConfig = newTLSConfig(apiConfig.TLSCACertPath, apiConfig.TLSCertPath, apiConfig.TLSKeyPath) tlsConfig = newTLSConfig(apiConfig.TLSCACertPath, apiConfig.TLSCertPath, apiConfig.TLSKeyPath)
@ -57,5 +97,6 @@ func newAPI(apiConfig apiConfig) *api {
dataPath: apiConfig.DataPath, dataPath: apiConfig.DataPath,
tlsConfig: tlsConfig, tlsConfig: tlsConfig,
templatesURL: apiConfig.TemplatesURL, 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 package main
import ( import (
"github.com/gorilla/mux"
"golang.org/x/net/websocket" "golang.org/x/net/websocket"
"log" "log"
"net/http" "net/http"
@ -12,21 +13,35 @@ import (
// newHandler creates a new http.Handler with CSRF protection // newHandler creates a new http.Handler with CSRF protection
func (a *api) newHandler(settings *Settings) http.Handler { func (a *api) newHandler(settings *Settings) http.Handler {
var ( var (
mux = http.NewServeMux() mux = mux.NewRouter()
fileHandler = http.FileServer(http.Dir(a.assetPath)) fileHandler = http.FileServer(http.Dir(a.assetPath))
) )
handler := a.newAPIHandler() handler := a.newAPIHandler()
mux.Handle("/", fileHandler)
mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", handler))
mux.Handle("/ws/exec", websocket.Handler(a.execContainer)) 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) { mux.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
settingsHandler(w, r, settings) settingsHandler(w, r, settings)
}) })
mux.HandleFunc("/templates", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/templates", func(w http.ResponseWriter, r *http.Request) {
templatesHandler(w, r, a.templatesURL) 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 // CSRF protection is disabled for the moment
// CSRFHandler := newCSRFHandler(a.dataPath) // CSRFHandler := newCSRFHandler(a.dataPath)
// return CSRFHandler(newCSRFWrapper(mux)) // 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" "gopkg.in/alecthomas/kingpin.v2"
) )
const (
// Version number of portainer API
Version = "1.10.2"
)
// main is the entry point of the program // main is the entry point of the program
func main() { func main() {
kingpin.Version("1.10.2") kingpin.Version(Version)
var ( var (
endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String() 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() 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() 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() tlsverify = kingpin.Flag("tlsverify", "TLS support").Default("false").Bool()
tlscacert = kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String() 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() 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', 'ngCookies',
'ngSanitize', 'ngSanitize',
'angularUtils.directives.dirPagination', 'angularUtils.directives.dirPagination',
'LocalStorageModule',
'angular-jwt',
'portainer.services', 'portainer.services',
'portainer.helpers', 'portainer.helpers',
'portainer.filters', 'portainer.filters',
'auth',
'dashboard', 'dashboard',
'container', 'container',
'containerConsole', 'containerConsole',
@ -19,8 +22,11 @@ angular.module('portainer', [
'events', 'events',
'images', 'images',
'image', 'image',
'main',
'service', 'service',
'services', 'services',
'settings',
'sidebar',
'createService', 'createService',
'stats', 'stats',
'swarm', 'swarm',
@ -31,131 +37,430 @@ angular.module('portainer', [
'templates', 'templates',
'volumes', 'volumes',
'createVolume']) 'createVolume'])
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', function ($stateProvider, $urlRouterProvider, $httpProvider) { .config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider) {
'use strict'; '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 $stateProvider
.state('index', { .state('auth', {
url: '/', url: '/auth',
templateUrl: 'app/components/dashboard/dashboard.html', params: {
controller: 'DashboardController' logout: false,
error: ''
},
views: {
"content": {
templateUrl: 'app/components/auth/auth.html',
controller: 'AuthenticationController'
}
}
}) })
.state('containers', { .state('containers', {
url: '/containers/', url: '/containers/',
templateUrl: 'app/components/containers/containers.html', views: {
controller: 'ContainersController' "content": {
templateUrl: 'app/components/containers/containers.html',
controller: 'ContainersController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
}) })
.state('container', { .state('container', {
url: "^/containers/:id", url: "^/containers/:id",
templateUrl: 'app/components/container/container.html', views: {
controller: 'ContainerController' "content": {
templateUrl: 'app/components/container/container.html',
controller: 'ContainerController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
}) })
.state('stats', { .state('stats', {
url: "^/containers/:id/stats", url: "^/containers/:id/stats",
templateUrl: 'app/components/stats/stats.html', views: {
controller: 'StatsController' "content": {
templateUrl: 'app/components/stats/stats.html',
controller: 'StatsController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
}) })
.state('logs', { .state('logs', {
url: "^/containers/:id/logs", url: "^/containers/:id/logs",
templateUrl: 'app/components/containerLogs/containerlogs.html', views: {
controller: 'ContainerLogsController' "content": {
templateUrl: 'app/components/containerLogs/containerlogs.html',
controller: 'ContainerLogsController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
}) })
.state('console', { .state('console', {
url: "^/containers/:id/console", url: "^/containers/:id/console",
templateUrl: 'app/components/containerConsole/containerConsole.html', views: {
controller: 'ContainerConsoleController' "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', { .state('actions', {
abstract: true, abstract: true,
url: "/actions", url: "/actions",
template: '<ui-view/>' views: {
"content": {
template: '<div ui-view="content"></div>'
},
"sidebar": {
template: '<div ui-view="sidebar"></div>'
}
}
}) })
.state('actions.create', { .state('actions.create', {
abstract: true, abstract: true,
url: "/create", url: "/create",
template: '<ui-view/>' views: {
"content": {
template: '<div ui-view="content"></div>'
},
"sidebar": {
template: '<div ui-view="sidebar"></div>'
}
}
}) })
.state('actions.create.container', { .state('actions.create.container', {
url: "/container", url: "/container",
templateUrl: 'app/components/createContainer/createcontainer.html', views: {
controller: 'CreateContainerController' "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', { .state('actions.create.network', {
url: "/network", url: "/network",
templateUrl: 'app/components/createNetwork/createnetwork.html', views: {
controller: 'CreateNetworkController' "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', { .state('actions.create.service', {
url: "/service", url: "/service",
templateUrl: 'app/components/createService/createservice.html', views: {
controller: 'CreateServiceController' "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', { .state('actions.create.volume', {
url: "/volume", url: "/volume",
templateUrl: 'app/components/createVolume/createvolume.html', views: {
controller: 'CreateVolumeController' "content": {
templateUrl: 'app/components/createVolume/createvolume.html',
controller: 'CreateVolumeController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
}) })
.state('docker', { .state('docker', {
url: '/docker/', url: '/docker/',
templateUrl: 'app/components/docker/docker.html', views: {
controller: 'DockerController' "content": {
templateUrl: 'app/components/docker/docker.html',
controller: 'DockerController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
}) })
.state('events', { .state('events', {
url: '/events/', url: '/events/',
templateUrl: 'app/components/events/events.html', views: {
controller: 'EventsController' "content": {
templateUrl: 'app/components/events/events.html',
controller: 'EventsController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
}) })
.state('images', { .state('images', {
url: '/images/', url: '/images/',
templateUrl: 'app/components/images/images.html', views: {
controller: 'ImagesController' "content": {
templateUrl: 'app/components/images/images.html',
controller: 'ImagesController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
}) })
.state('image', { .state('image', {
url: '^/images/:id/', url: '^/images/:id/',
templateUrl: 'app/components/image/image.html', views: {
controller: 'ImageController' "content": {
templateUrl: 'app/components/image/image.html',
controller: 'ImageController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
}) })
.state('networks', { .state('networks', {
url: '/networks/', url: '/networks/',
templateUrl: 'app/components/networks/networks.html', views: {
controller: 'NetworksController' "content": {
templateUrl: 'app/components/networks/networks.html',
controller: 'NetworksController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
}) })
.state('network', { .state('network', {
url: '^/networks/:id/', url: '^/networks/:id/',
templateUrl: 'app/components/network/network.html', views: {
controller: 'NetworkController' "content": {
templateUrl: 'app/components/network/network.html',
controller: 'NetworkController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
}) })
.state('services', { .state('services', {
url: '/services/', url: '/services/',
templateUrl: 'app/components/services/services.html', views: {
controller: 'ServicesController' "content": {
templateUrl: 'app/components/services/services.html',
controller: 'ServicesController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
}) })
.state('service', { .state('service', {
url: '^/service/:id/', url: '^/service/:id/',
templateUrl: 'app/components/service/service.html', views: {
controller: 'ServiceController' "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', { .state('task', {
url: '^/task/:id', url: '^/task/:id',
templateUrl: 'app/components/task/task.html', views: {
controller: 'TaskController' "content": {
templateUrl: 'app/components/task/task.html',
controller: 'TaskController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
}) })
.state('templates', { .state('templates', {
url: '/templates/', url: '/templates/',
templateUrl: 'app/components/templates/templates.html', views: {
controller: 'TemplatesController' "content": {
templateUrl: 'app/components/templates/templates.html',
controller: 'TemplatesController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
}) })
.state('volumes', { .state('volumes', {
url: '/volumes/', url: '/volumes/',
templateUrl: 'app/components/volumes/volumes.html', views: {
controller: 'VolumesController' "content": {
templateUrl: 'app/components/volumes/volumes.html',
controller: 'VolumesController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
}) })
.state('swarm', { .state('swarm', {
url: '/swarm/', url: '/swarm/',
templateUrl: 'app/components/swarm/swarm.html', views: {
controller: 'SwarmController' "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 // The Docker API likes to return plaintext errors, this catches them and disp
@ -165,7 +470,7 @@ angular.module('portainer', [
return { return {
'response': function(response) { 'response': function(response) {
if (typeof(response.data) === 'string' && if (typeof(response.data) === 'string' &&
(response.data.startsWith('Conflict.') || response.data.startsWith('conflict:'))) { (response.data.startsWith('Conflict.') || response.data.startsWith('conflict:'))) {
$.gritter.add({ $.gritter.add({
title: 'Error', title: 'Error',
text: $('<div>').text(response.data).html(), 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 // 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 // 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_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('CONFIG_ENDPOINT', 'settings')
.constant('AUTH_ENDPOINT', 'auth')
.constant('TEMPLATES_ENDPOINT', 'templates') .constant('TEMPLATES_ENDPOINT', 'templates')
.constant('PAGINATION_MAX_ITEMS', 10) .constant('PAGINATION_MAX_ITEMS', 10)
.constant('UI_VERSION', 'v1.10.2'); .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> <span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a> </a>
</th> </th>
<th ng-if="swarm && !swarm_mode"> <th ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<a ui-sref="containers" ng-click="order('Host')"> <a ui-sref="containers" ng-click="order('Host')">
Host IP Host IP
<span ng-show="sortType == 'Host' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span> <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))"> <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><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><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="endpointMode.provider === 'DOCKER_SWARM'"><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|containername}}</a></td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</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="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> <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"> <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 }} <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.sortType = 'State';
$scope.sortReverse = false; $scope.sortReverse = false;
$scope.state.selectedItemCount = 0; $scope.state.selectedItemCount = 0;
$scope.swarm_mode = false;
$scope.pagination_count = Settings.pagination_count; $scope.pagination_count = Settings.pagination_count;
$scope.order = function (sortType) { $scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType; $scope.sortType = sortType;
@ -28,7 +26,7 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
if (model.IP) { if (model.IP) {
$scope.state.displayIP = true; $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]]; model.hostIP = $scope.swarm_hosts[_.split(container.Names[0], '/')[1]];
} }
return model; return model;
@ -150,17 +148,11 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
return swarm_hosts; return swarm_hosts;
} }
$scope.swarm = false;
Config.$promise.then(function (c) { Config.$promise.then(function (c) {
$scope.containersToHideLabels = c.hiddenLabels; $scope.containersToHideLabels = c.hiddenLabels;
$scope.swarm = c.swarm; if (c.swarm && $scope.endpointMode.provider === 'DOCKER_SWARM') {
if (c.swarm) {
Info.get({}, function (d) { Info.get({}, function (d) {
if (!_.startsWith(d.ServerVersion, 'swarm')) { $scope.swarm_hosts = retrieveSwarmHostsInfo(d);
$scope.swarm_mode = true;
} else {
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
}
update({all: Settings.displayAll ? 1 : 0}); update({all: Settings.displayAll ? 1 : 0});
}); });
} else { } else {

View File

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

View File

@ -258,7 +258,7 @@
<!-- tab-network --> <!-- tab-network -->
<div class="tab-pane" id="network"> <div class="tab-pane" id="network">
<form class="form-horizontal" style="margin-top: 15px;"> <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"> <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> <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> </div>
@ -278,10 +278,10 @@
<div class="form-group" ng-if="config.HostConfig.NetworkMode == 'container'"> <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> <label for="container_network_container" class="col-sm-1 control-label text-left">Container</label>
<div class="col-sm-9"> <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> <option selected disabled hidden value="">Select a container</option>
</select> </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> <option selected disabled hidden value="">Select a container</option>
</select> </select>
</div> </div>

View File

@ -6,7 +6,7 @@
</rd-header> </rd-header>
<div class="row"> <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>
<rd-widget-header icon="fa-tachometer" title="Node info"></rd-widget-header> <rd-widget-header icon="fa-tachometer" title="Node info"></rd-widget-header>
<rd-widget-body classes="no-padding"> <rd-widget-body classes="no-padding">
@ -33,7 +33,7 @@
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>
</div> </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>
<rd-widget-header icon="fa-tachometer" title="Cluster info"></rd-widget-header> <rd-widget-header icon="fa-tachometer" title="Cluster info"></rd-widget-header>
<rd-widget-body classes="no-padding"> <rd-widget-body classes="no-padding">
@ -60,7 +60,7 @@
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>
</div> </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>
<rd-widget-header icon="fa-tachometer" title="Swarm info"></rd-widget-header> <rd-widget-header icon="fa-tachometer" title="Swarm info"></rd-widget-header>
<rd-widget-body classes="no-padding"> <rd-widget-body classes="no-padding">

View File

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

View File

@ -1,31 +1,15 @@
angular.module('dashboard') angular.module('main', [])
.controller('MasterCtrl', ['$scope', '$cookieStore', 'Settings', 'Config', 'Info', .controller('MainController', ['$scope', '$cookieStore',
function ($scope, $cookieStore, Settings, Config, Info) { function ($scope, $cookieStore) {
/** /**
* Sidebar Toggle & Cookie Control * Sidebar Toggle & Cookie Control
*/ */
var mobileView = 992; var mobileView = 992;
$scope.getWidth = function() { $scope.getWidth = function() {
return window.innerWidth; 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) { $scope.$watch($scope.getWidth, function(newValue, oldValue) {
if (newValue >= mobileView) { if (newValue >= mobileView) {
if (angular.isDefined($cookieStore.get('toggle'))) { if (angular.isDefined($cookieStore.get('toggle'))) {
@ -47,6 +31,4 @@ function ($scope, $cookieStore, Settings, Config, Info) {
window.onresize = function() { window.onresize = function() {
$scope.$apply(); $scope.$apply();
}; };
$scope.uiVersion = Settings.uiVersion;
}]); }]);

View File

@ -23,12 +23,12 @@
</div> </div>
<!-- !name-input --> <!-- !name-input -->
<!-- tag-note --> <!-- 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"> <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> <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> </div>
<div class="form-group" ng-if="!swarm"> <div class="form-group" ng-if="endpointMode.provider === 'DOCKER_STANDALONE'">
<div class="col-sm-12"> <div class="col-sm-12">
<span class="small text-muted">Note: The network will be created using the bridge driver.</span> <span class="small text-muted">Note: The network will be created using the bridge driver.</span>
</div> </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> <tbody>
<tr> <tr>
<td>Nodes</td> <td>Nodes</td>
<td ng-if="!swarm_mode">{{ swarm.Nodes }}</td> <td ng-if="endpointMode.provider === 'DOCKER_SWARM'">{{ swarm.Nodes }}</td>
<td ng-if="swarm_mode">{{ info.Swarm.Nodes }}</td> <td ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">{{ info.Swarm.Nodes }}</td>
</tr> </tr>
<tr ng-if="!swarm_mode"> <tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<td>Images</td> <td>Images</td>
<td>{{ info.Images }}</td> <td>{{ info.Images }}</td>
</tr> </tr>
<tr ng-if="!swarm_mode"> <tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<td>Swarm version</td> <td>Swarm version</td>
<td>{{ docker.Version|swarmversion }}</td> <td>{{ docker.Version|swarmversion }}</td>
</tr> </tr>
@ -31,29 +31,29 @@
<td>Docker API version</td> <td>Docker API version</td>
<td>{{ docker.ApiVersion }}</td> <td>{{ docker.ApiVersion }}</td>
</tr> </tr>
<tr ng-if="!swarm_mode"> <tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<td>Strategy</td> <td>Strategy</td>
<td>{{ swarm.Strategy }}</td> <td>{{ swarm.Strategy }}</td>
</tr> </tr>
<tr> <tr>
<td>Total CPU</td> <td>Total CPU</td>
<td ng-if="!swarm_mode">{{ info.NCPU }}</td> <td ng-if="endpointMode.provider === 'DOCKER_SWARM'">{{ info.NCPU }}</td>
<td ng-if="swarm_mode">{{ totalCPU }}</td> <td ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">{{ totalCPU }}</td>
</tr> </tr>
<tr> <tr>
<td>Total memory</td> <td>Total memory</td>
<td ng-if="!swarm_mode">{{ info.MemTotal|humansize: 2 }}</td> <td ng-if="endpointMode.provider === 'DOCKER_SWARM'">{{ info.MemTotal|humansize: 2 }}</td>
<td ng-if="swarm_mode">{{ totalMemory|humansize: 2 }}</td> <td ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">{{ totalMemory|humansize: 2 }}</td>
</tr> </tr>
<tr ng-if="!swarm_mode"> <tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<td>Operating system</td> <td>Operating system</td>
<td>{{ info.OperatingSystem }}</td> <td>{{ info.OperatingSystem }}</td>
</tr> </tr>
<tr ng-if="!swarm_mode"> <tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<td>Kernel version</td> <td>Kernel version</td>
<td>{{ info.KernelVersion }}</td> <td>{{ info.KernelVersion }}</td>
</tr> </tr>
<tr ng-if="!swarm_mode"> <tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<td>Go version</td> <td>Go version</td>
<td>{{ docker.GoVersion }}</td> <td>{{ docker.GoVersion }}</td>
</tr> </tr>
@ -65,7 +65,7 @@
</div> </div>
<div class="row"> <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>
<rd-widget-header icon="fa-hdd-o" title="Node status"></rd-widget-header> <rd-widget-header icon="fa-hdd-o" title="Node status"></rd-widget-header>
<rd-widget-body classes="no-padding"> <rd-widget-body classes="no-padding">
@ -133,7 +133,7 @@
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>
</div> </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>
<rd-widget-header icon="fa-hdd-o" title="Node status"></rd-widget-header> <rd-widget-header icon="fa-hdd-o" title="Node status"></rd-widget-header>
<rd-widget-body classes="no-padding"> <rd-widget-body classes="no-padding">

View File

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

View File

@ -13,12 +13,12 @@
</rd-widget-custom-header> </rd-widget-custom-header>
<rd-widget-body classes="padding"> <rd-widget-body classes="padding">
<form class="form-horizontal"> <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"> <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> <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> </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"> <div class="col-sm-12">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> <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> <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"> <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> <label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}</label>
<div class="col-sm-10"> <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> <option selected disabled hidden value="">Select a container</option>
</select> </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> <option selected disabled hidden value="">Select a container</option>
</select> </select>
<input ng-if="!var.type || !var.type === 'container'" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}"> <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) { Config.$promise.then(function (c) {
$scope.swarm = c.swarm; $scope.swarm = c.swarm;
Info.get({}, function(info) {
if ($scope.swarm && !_.startsWith(info.ServerVersion, 'swarm')) {
$scope.swarm_mode = true;
}
});
var containersToHideLabels = c.hiddenLabels; var containersToHideLabels = c.hiddenLabels;
Network.query({}, function (d) { Network.query({}, function (d) {
var networks = d; var networks = d;

View File

@ -4,7 +4,7 @@ angular
var directive = { var directive = {
requires: '^rdHeader', requires: '^rdHeader',
transclude: true, 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' restrict: 'E'
}; };
return directive; return directive;

View File

@ -1,14 +1,17 @@
angular angular
.module('portainer') .module('portainer')
.directive('rdHeaderTitle', function rdHeaderTitle() { .directive('rdHeaderTitle', ['$rootScope', function rdHeaderTitle($rootScope) {
var directive = { var directive = {
requires: '^rdHeader', requires: '^rdHeader',
scope: { scope: {
title: '@' title: '@'
}, },
link: function (scope, iElement, iAttrs) {
scope.username = $rootScope.username;
},
transclude: true, 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' restrict: 'E'
}; };
return directive; return directive;
}); }]);

View File

@ -166,14 +166,6 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
get: {method: 'GET'} 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) { .factory('Info', ['$resource', 'Settings', function InfoFactory($resource, Settings) {
'use strict'; 'use strict';
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#display-system-wide-information // 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 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) { .factory('Messages', ['$rootScope', '$sanitize', function MessagesFactory($rootScope, $sanitize) {
'use strict'; 'use strict';
return { 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 { .btn-group button {
margin: 3px; margin: 3px;
} }
.messages { .messages {
max-height: 50px; max-height: 50px;
overflow-x: hidden; overflow-x: hidden;
overflow-y: scroll; overflow-y: scroll;
} }
.legend .title { .legend .title {
padding: 0 0.3em; padding: 0 0.3em;
margin: 0.5em; margin: 0.5em;
border-style: solid; border-style: solid;
border-width: 0 0 0 1em; border-width: 0 0 0 1em;
} }
.logo { .logo {
@ -203,6 +212,48 @@ input[type="radio"] {
margin-bottom: 5px; 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 { .pagination-controls {
margin-left: 10px; 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-resource": "~1.5.0",
"angular-ui-select": "~0.17.1", "angular-ui-select": "~0.17.1",
"angular-utils-pagination": "~0.11.1", "angular-utils-pagination": "~0.11.1",
"angular-local-storage": "~0.5.2",
"angular-jwt": "~0.1.8",
"bootstrap": "~3.3.6", "bootstrap": "~3.3.6",
"font-awesome": "~4.6.3",
"filesize": "~3.3.0", "filesize": "~3.3.0",
"jquery": "1.11.1", "jquery": "1.11.1",
"jquery.gritter": "1.7.4", "jquery.gritter": "1.7.4",
"lodash": "4.12.0", "lodash": "4.12.0",
"rdash-ui": "1.0.*", "rdash-ui": "1.0.*",
"moment": "~2.14.1", "moment": "~2.14.1",
"xterm.js": "~2.0.1" "xterm.js": "~2.0.1",
"font-awesome": "~4.7.0"
}, },
"resolutions": { "resolutions": {
"angular": "1.5.5" "angular": "1.5.5"

View File

@ -193,6 +193,8 @@ module.exports = function (grunt) {
src: ['bower_components/angular/angular.min.js', src: ['bower_components/angular/angular.min.js',
'bower_components/angular-sanitize/angular-sanitize.min.js', 'bower_components/angular-sanitize/angular-sanitize.min.js',
'bower_components/angular-cookies/angular-cookies.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-ui-router/release/angular-ui-router.min.js',
'bower_components/angular-resource/angular-resource.min.js', 'bower_components/angular-resource/angular-resource.min.js',
'bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js', 'bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js',
@ -295,7 +297,7 @@ module.exports = function (grunt) {
}, },
buildBinary: { buildBinary: {
command: [ 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', 'shasum api/portainer > portainer-checksum.txt',
'mkdir -p dist', 'mkdir -p dist',
'mv api/portainer dist/' 'mv api/portainer dist/'
@ -303,7 +305,7 @@ module.exports = function (grunt) {
}, },
buildUnixArmBinary: { buildUnixArmBinary: {
command: [ 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', 'shasum api/portainer-linux-arm > portainer-checksum.txt',
'mkdir -p dist', 'mkdir -p dist',
'mv api/portainer-linux-arm dist/portainer' 'mv api/portainer-linux-arm dist/portainer'
@ -311,7 +313,7 @@ module.exports = function (grunt) {
}, },
buildDarwinBinary: { buildDarwinBinary: {
command: [ 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', 'shasum api/portainer-darwin-amd64 > portainer-checksum.txt',
'mkdir -p dist', 'mkdir -p dist',
'mv api/portainer-darwin-amd64 dist/portainer' 'mv api/portainer-darwin-amd64 dist/portainer'
@ -319,7 +321,7 @@ module.exports = function (grunt) {
}, },
buildWindowsBinary: { buildWindowsBinary: {
command: [ 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', 'shasum api/portainer-windows-amd64 > portainer-checksum.txt',
'mkdir -p dist', 'mkdir -p dist',
'mv api/portainer-windows-amd64 dist/portainer.exe' '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"> <link rel="apple-touch-icon-precomposed" href="ico/apple-touch-icon-precomposed.png">
</head> </head>
<body ng-controller="MasterCtrl"> <body ng-controller="MainController">
<div id="page-wrapper" ng-class="{'open': toggle}" ng-cloak> <div id="page-wrapper" ng-class="{open: toggle && $state.current.name !== 'auth', nopadding: $state.current.name === 'auth'}" ng-cloak>
<!-- Sidebar --> <div id="sideview" ui-view="sidebar"></div>
<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="content-wrapper"> <div id="content-wrapper">
<div class="page-content"> <div class="page-content">
<!-- Main Content --> <!-- Main Content -->
<div id="view" ui-view></div> <div id="view" ui-view="content"></div>
</div><!-- End Page Content --> </div><!-- End Page Content -->
</div><!-- End Content Wrapper --> </div><!-- End Content Wrapper -->