diff --git a/api/api.go b/api/api.go
index af3eb23ed..5b71f7fdf 100644
--- a/api/api.go
+++ b/api/api.go
@@ -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,
}
}
diff --git a/api/auth.go b/api/auth.go
new file mode 100644
index 000000000..74355c00b
--- /dev/null
+++ b/api/auth.go
@@ -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)
+}
diff --git a/api/datastore.go b/api/datastore.go
new file mode 100644
index 000000000..58efd7f5e
--- /dev/null
+++ b/api/datastore.go
@@ -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
+}
diff --git a/api/handler.go b/api/handler.go
index ef60a8583..f0d902659 100644
--- a/api/handler.go
+++ b/api/handler.go
@@ -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))
diff --git a/api/jwt.go b/api/jwt.go
new file mode 100644
index 000000000..880a23a70
--- /dev/null
+++ b/api/jwt.go
@@ -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
+}
diff --git a/api/main.go b/api/main.go
index 82fbd5651..a63d5a534 100644
--- a/api/main.go
+++ b/api/main.go
@@ -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()
diff --git a/api/middleware.go b/api/middleware.go
new file mode 100644
index 000000000..da12ae730
--- /dev/null
+++ b/api/middleware.go
@@ -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)
+ })
+}
diff --git a/api/users.go b/api/users.go
new file mode 100644
index 000000000..d0a48aceb
--- /dev/null
+++ b/api/users.go
@@ -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)
+}
diff --git a/app/app.js b/app/app.js
index f06c050fc..144f407d8 100644
--- a/app/app.js
+++ b/app/app.js
@@ -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: '