feat(global): add authentication support with single admin account
parent
1e5207517d
commit
4e77c72fa2
41
api/api.go
41
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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
339
app/app.js
339
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/',
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/containerConsole/containerConsole.html',
|
||||
controller: 'ContainerConsoleController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('dashboard', {
|
||||
url: '/dashboard',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/dashboard/dashboard.html',
|
||||
controller: 'DashboardController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('actions', {
|
||||
abstract: true,
|
||||
url: "/actions",
|
||||
template: '<ui-view/>'
|
||||
views: {
|
||||
"content": {
|
||||
template: '<div ui-view="content"></div>'
|
||||
},
|
||||
"sidebar": {
|
||||
template: '<div ui-view="sidebar"></div>'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('actions.create', {
|
||||
abstract: true,
|
||||
url: "/create",
|
||||
template: '<ui-view/>'
|
||||
views: {
|
||||
"content": {
|
||||
template: '<div ui-view="content"></div>'
|
||||
},
|
||||
"sidebar": {
|
||||
template: '<div ui-view="sidebar"></div>'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('actions.create.container', {
|
||||
url: "/container",
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/createContainer/createcontainer.html',
|
||||
controller: 'CreateContainerController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('actions.create.network', {
|
||||
url: "/network",
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/createNetwork/createnetwork.html',
|
||||
controller: 'CreateNetworkController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('actions.create.service', {
|
||||
url: "/service",
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/createService/createservice.html',
|
||||
controller: 'CreateServiceController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('actions.create.volume', {
|
||||
url: "/volume",
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/createVolume/createvolume.html',
|
||||
controller: 'CreateVolumeController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('docker', {
|
||||
url: '/docker/',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/docker/docker.html',
|
||||
controller: 'DockerController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('events', {
|
||||
url: '/events/',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/events/events.html',
|
||||
controller: 'EventsController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('images', {
|
||||
url: '/images/',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/images/images.html',
|
||||
controller: 'ImagesController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('image', {
|
||||
url: '^/images/:id/',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/image/image.html',
|
||||
controller: 'ImageController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('networks', {
|
||||
url: '/networks/',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/networks/networks.html',
|
||||
controller: 'NetworksController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('network', {
|
||||
url: '^/networks/:id/',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/network/network.html',
|
||||
controller: 'NetworkController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('services', {
|
||||
url: '/services/',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/services/services.html',
|
||||
controller: 'ServicesController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('service', {
|
||||
url: '^/service/:id/',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/service/service.html',
|
||||
controller: 'ServiceController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('settings', {
|
||||
url: '/settings/',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/settings/settings.html',
|
||||
controller: 'SettingsController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('task', {
|
||||
url: '^/task/:id',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/task/task.html',
|
||||
controller: 'TaskController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('templates', {
|
||||
url: '/templates/',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/templates/templates.html',
|
||||
controller: 'TemplatesController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('volumes', {
|
||||
url: '/volumes/',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/volumes/volumes.html',
|
||||
controller: 'VolumesController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('swarm', {
|
||||
url: '/swarm/',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/swarm/swarm.html',
|
||||
controller: 'SwarmController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
});
|
||||
|
||||
// The Docker API likes to return plaintext errors, this catches them and disp
|
||||
|
@ -182,12 +487,28 @@ angular.module('portainer', [
|
|||
};
|
||||
});
|
||||
}])
|
||||
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'EndpointMode', function ($rootScope, $state, Authentication, authManager, EndpointMode) {
|
||||
authManager.checkAuthOnRefresh();
|
||||
authManager.redirectWhenUnauthenticated();
|
||||
Authentication.init();
|
||||
$rootScope.$state = $state;
|
||||
|
||||
$rootScope.$on('tokenHasExpired', function($state) {
|
||||
$state.go('auth', {error: 'Your session has expired'});
|
||||
});
|
||||
|
||||
$rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) {
|
||||
if ((fromState.name === 'auth' || fromState.name === '') && Authentication.isAuthenticated()) {
|
||||
EndpointMode.determineEndpointMode();
|
||||
}
|
||||
});
|
||||
}])
|
||||
// This is your docker url that the api will use to make requests
|
||||
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
|
||||
.constant('DOCKER_ENDPOINT', 'dockerapi')
|
||||
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is requred. If you have a port, prefix it with a ':' i.e. :4243
|
||||
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243
|
||||
.constant('CONFIG_ENDPOINT', 'settings')
|
||||
.constant('AUTH_ENDPOINT', 'auth')
|
||||
.constant('TEMPLATES_ENDPOINT', 'templates')
|
||||
.constant('PAGINATION_MAX_ITEMS', 10)
|
||||
.constant('UI_VERSION', 'v1.10.2');
|
||||
|
|
|
@ -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>
|
|
@ -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';
|
||||
});
|
||||
};
|
||||
}]);
|
|
@ -66,7 +66,7 @@
|
|||
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th ng-if="swarm && !swarm_mode">
|
||||
<th ng-if="endpointMode.provider === 'DOCKER_SWARM'">
|
||||
<a ui-sref="containers" ng-click="order('Host')">
|
||||
Host IP
|
||||
<span ng-show="sortType == 'Host' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
|
@ -86,11 +86,11 @@
|
|||
<tr dir-paginate="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
|
||||
<td><input type="checkbox" ng-model="container.Checked" ng-change="selectItem(container)"/></td>
|
||||
<td><span class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status|containerstatus }}</span></td>
|
||||
<td ng-if="swarm && !swarm_mode"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername}}</a></td>
|
||||
<td ng-if="!swarm || swarm_mode"><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td>
|
||||
<td ng-if="endpointMode.provider === 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername}}</a></td>
|
||||
<td ng-if="endpointMode.provider !== 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td>
|
||||
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
|
||||
<td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td>
|
||||
<td ng-if="swarm && !swarm_mode">{{ container.hostIP }}</td>
|
||||
<td ng-if="endpointMode.provider === 'DOCKER_SWARM'">{{ container.hostIP }}</td>
|
||||
<td>
|
||||
<a ng-if="container.Ports.length > 0" ng-repeat="p in container.Ports" class="image-tag" ng-href="http://{{p.host}}:{{p.public}}" target="_blank">
|
||||
<i class="fa fa-external-link" aria-hidden="true"></i> {{p.public}}:{{ p.private }}
|
||||
|
|
|
@ -7,9 +7,7 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
|
|||
$scope.sortType = 'State';
|
||||
$scope.sortReverse = false;
|
||||
$scope.state.selectedItemCount = 0;
|
||||
$scope.swarm_mode = false;
|
||||
$scope.pagination_count = Settings.pagination_count;
|
||||
|
||||
$scope.order = function (sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
|
@ -28,7 +26,7 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
|
|||
if (model.IP) {
|
||||
$scope.state.displayIP = true;
|
||||
}
|
||||
if ($scope.swarm && !$scope.swarm_mode) {
|
||||
if ($scope.endpointMode.provider === 'DOCKER_SWARM') {
|
||||
model.hostIP = $scope.swarm_hosts[_.split(container.Names[0], '/')[1]];
|
||||
}
|
||||
return model;
|
||||
|
@ -150,17 +148,11 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
|
|||
return swarm_hosts;
|
||||
}
|
||||
|
||||
$scope.swarm = false;
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.containersToHideLabels = c.hiddenLabels;
|
||||
$scope.swarm = c.swarm;
|
||||
if (c.swarm) {
|
||||
if (c.swarm && $scope.endpointMode.provider === 'DOCKER_SWARM') {
|
||||
Info.get({}, function (d) {
|
||||
if (!_.startsWith(d.ServerVersion, 'swarm')) {
|
||||
$scope.swarm_mode = true;
|
||||
} else {
|
||||
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
|
||||
}
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -53,11 +53,6 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
|
|||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.swarm = c.swarm;
|
||||
Info.get({}, function(info) {
|
||||
if ($scope.swarm && !_.startsWith(info.ServerVersion, 'swarm')) {
|
||||
$scope.swarm_mode = true;
|
||||
}
|
||||
});
|
||||
var containersToHideLabels = c.hiddenLabels;
|
||||
|
||||
Volume.query({}, function (d) {
|
||||
|
@ -216,7 +211,7 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
|
|||
var containerName = container;
|
||||
if (container && typeof container === 'object') {
|
||||
containerName = $filter('trimcontainername')(container.Names[0]);
|
||||
if ($scope.swarm && !$scope.swarm_mode) {
|
||||
if ($scope.swarm && $scope.endpointMode.provider === 'DOCKER_SWARM') {
|
||||
containerName = $filter('swarmcontainername')(container);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -258,7 +258,7 @@
|
|||
<!-- tab-network -->
|
||||
<div class="tab-pane" id="network">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<div class="form-group" ng-if="globalNetworkCount === 0 && !swarm_mode">
|
||||
<div class="form-group" ng-if="globalNetworkCount === 0 && endpointMode.provider !== 'DOCKER_SWARM_MODE'">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">You don't have any shared network. Head over the <a ui-sref="networks">networks view</a> to create one.</span>
|
||||
</div>
|
||||
|
@ -278,10 +278,10 @@
|
|||
<div class="form-group" ng-if="config.HostConfig.NetworkMode == 'container'">
|
||||
<label for="container_network_container" class="col-sm-1 control-label text-left">Container</label>
|
||||
<div class="col-sm-9">
|
||||
<select ng-if="(!swarm || swarm && swarm_mode)" ng-options="container|containername for container in runningContainers" class="selectpicker form-control" ng-model="formValues.NetworkContainer">
|
||||
<select ng-if="endpointMode.provider !== 'DOCKER_SWARM'" ng-options="container|containername for container in runningContainers" class="selectpicker form-control" ng-model="formValues.NetworkContainer">
|
||||
<option selected disabled hidden value="">Select a container</option>
|
||||
</select>
|
||||
<select ng-if="swarm && !swarm_mode" ng-options="container|swarmcontainername for container in runningContainers" class="selectpicker form-control" ng-model="formValues.NetworkContainer">
|
||||
<select ng-if="endpointMode.provider === 'DOCKER_SWARM'" ng-options="container|swarmcontainername for container in runningContainers" class="selectpicker form-control" ng-model="formValues.NetworkContainer">
|
||||
<option selected disabled hidden value="">Select a container</option>
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="swarm_mode || !swarm">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="endpointMode.provider !== 'DOCKER_SWARM'">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tachometer" title="Node info"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
|
@ -33,7 +33,7 @@
|
|||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="swarm && !swarm_mode">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="endpointMode.provider === 'DOCKER_SWARM'">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tachometer" title="Cluster info"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
|
@ -60,7 +60,7 @@
|
|||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="swarm && swarm_mode">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tachometer" title="Swarm info"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
|
|
|
@ -14,7 +14,6 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
|
|||
$scope.volumeData = {
|
||||
total: 0
|
||||
};
|
||||
$scope.swarm_mode = false;
|
||||
|
||||
function prepareContainerData(d, containersToHideLabels) {
|
||||
var running = 0;
|
||||
|
@ -64,9 +63,6 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
|
|||
function prepareInfoData(d) {
|
||||
var info = d;
|
||||
$scope.infoData = info;
|
||||
if ($scope.swarm && !_.startsWith(info.ServerVersion, 'swarm')) {
|
||||
$scope.swarm_mode = true;
|
||||
}
|
||||
}
|
||||
|
||||
function fetchDashboardData(containersToHideLabels) {
|
||||
|
@ -84,6 +80,9 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
|
|||
prepareNetworkData(d[3]);
|
||||
prepareInfoData(d[4]);
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function(e) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to load dashboard data");
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,31 +1,15 @@
|
|||
angular.module('dashboard')
|
||||
.controller('MasterCtrl', ['$scope', '$cookieStore', 'Settings', 'Config', 'Info',
|
||||
function ($scope, $cookieStore, Settings, Config, Info) {
|
||||
angular.module('main', [])
|
||||
.controller('MainController', ['$scope', '$cookieStore',
|
||||
function ($scope, $cookieStore) {
|
||||
|
||||
/**
|
||||
* Sidebar Toggle & Cookie Control
|
||||
*/
|
||||
var mobileView = 992;
|
||||
|
||||
$scope.getWidth = function() {
|
||||
return window.innerWidth;
|
||||
};
|
||||
|
||||
$scope.swarm_mode = false;
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.logo = c.logo;
|
||||
$scope.swarm = c.swarm;
|
||||
Info.get({}, function(d) {
|
||||
if ($scope.swarm && !_.startsWith(d.ServerVersion, 'swarm')) {
|
||||
$scope.swarm_mode = true;
|
||||
$scope.swarm_manager = false;
|
||||
if (d.Swarm.ControlAvailable) {
|
||||
$scope.swarm_manager = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$watch($scope.getWidth, function(newValue, oldValue) {
|
||||
if (newValue >= mobileView) {
|
||||
if (angular.isDefined($cookieStore.get('toggle'))) {
|
||||
|
@ -47,6 +31,4 @@ function ($scope, $cookieStore, Settings, Config, Info) {
|
|||
window.onresize = function() {
|
||||
$scope.$apply();
|
||||
};
|
||||
|
||||
$scope.uiVersion = Settings.uiVersion;
|
||||
}]);
|
|
@ -23,12 +23,12 @@
|
|||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- tag-note -->
|
||||
<div class="form-group" ng-if="swarm">
|
||||
<div class="form-group" ng-if="endpointMode.provider === 'DOCKER_SWARM' || endpointMode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">Note: The network will be created using the overlay driver and will allow containers to communicate across the hosts of your cluster.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="!swarm">
|
||||
<div class="form-group" ng-if="endpointMode.provider === 'DOCKER_STANDALONE'">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">Note: The network will be created using the bridge driver.</span>
|
||||
</div>
|
||||
|
|
|
@ -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>
|
|
@ -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");
|
||||
});
|
||||
};
|
||||
}]);
|
|
@ -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 -->
|
|
@ -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;
|
||||
}]);
|
|
@ -16,14 +16,14 @@
|
|||
<tbody>
|
||||
<tr>
|
||||
<td>Nodes</td>
|
||||
<td ng-if="!swarm_mode">{{ swarm.Nodes }}</td>
|
||||
<td ng-if="swarm_mode">{{ info.Swarm.Nodes }}</td>
|
||||
<td ng-if="endpointMode.provider === 'DOCKER_SWARM'">{{ swarm.Nodes }}</td>
|
||||
<td ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">{{ info.Swarm.Nodes }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!swarm_mode">
|
||||
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
|
||||
<td>Images</td>
|
||||
<td>{{ info.Images }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!swarm_mode">
|
||||
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
|
||||
<td>Swarm version</td>
|
||||
<td>{{ docker.Version|swarmversion }}</td>
|
||||
</tr>
|
||||
|
@ -31,29 +31,29 @@
|
|||
<td>Docker API version</td>
|
||||
<td>{{ docker.ApiVersion }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!swarm_mode">
|
||||
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
|
||||
<td>Strategy</td>
|
||||
<td>{{ swarm.Strategy }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total CPU</td>
|
||||
<td ng-if="!swarm_mode">{{ info.NCPU }}</td>
|
||||
<td ng-if="swarm_mode">{{ totalCPU }}</td>
|
||||
<td ng-if="endpointMode.provider === 'DOCKER_SWARM'">{{ info.NCPU }}</td>
|
||||
<td ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">{{ totalCPU }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total memory</td>
|
||||
<td ng-if="!swarm_mode">{{ info.MemTotal|humansize: 2 }}</td>
|
||||
<td ng-if="swarm_mode">{{ totalMemory|humansize: 2 }}</td>
|
||||
<td ng-if="endpointMode.provider === 'DOCKER_SWARM'">{{ info.MemTotal|humansize: 2 }}</td>
|
||||
<td ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">{{ totalMemory|humansize: 2 }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!swarm_mode">
|
||||
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
|
||||
<td>Operating system</td>
|
||||
<td>{{ info.OperatingSystem }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!swarm_mode">
|
||||
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
|
||||
<td>Kernel version</td>
|
||||
<td>{{ info.KernelVersion }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!swarm_mode">
|
||||
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
|
||||
<td>Go version</td>
|
||||
<td>{{ docker.GoVersion }}</td>
|
||||
</tr>
|
||||
|
@ -65,7 +65,7 @@
|
|||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="!swarm_mode">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="endpointMode.provider === 'DOCKER_SWARM'">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-hdd-o" title="Node status"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
|
@ -133,7 +133,7 @@
|
|||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="swarm_mode">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-hdd-o" title="Node status"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
|
|
|
@ -7,7 +7,6 @@ function ($scope, Info, Version, Node, Settings) {
|
|||
$scope.info = {};
|
||||
$scope.docker = {};
|
||||
$scope.swarm = {};
|
||||
$scope.swarm_mode = false;
|
||||
$scope.totalCPU = 0;
|
||||
$scope.totalMemory = 0;
|
||||
$scope.pagination_count = Settings.pagination_count;
|
||||
|
@ -23,8 +22,7 @@ function ($scope, Info, Version, Node, Settings) {
|
|||
|
||||
Info.get({}, function (d) {
|
||||
$scope.info = d;
|
||||
if (!_.startsWith(d.ServerVersion, 'swarm')) {
|
||||
$scope.swarm_mode = true;
|
||||
if ($scope.endpointMode.provider === 'DOCKER_SWARM_MODE') {
|
||||
Node.query({}, function(d) {
|
||||
$scope.nodes = d;
|
||||
var CPU = 0, memory = 0;
|
||||
|
|
|
@ -13,12 +13,12 @@
|
|||
</rd-widget-custom-header>
|
||||
<rd-widget-body classes="padding">
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group" ng-if="globalNetworkCount === 0 && !swarm_mode">
|
||||
<div class="form-group" ng-if="globalNetworkCount === 0 && endpointMode.provider === 'DOCKER_SWARM'">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the <a ui-sref="networks">networks view</a> to create one.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="swarm_mode">
|
||||
<div class="form-group" ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<div class="col-sm-12">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
<span class="small text-muted">App templates cannot be used with swarm-mode at the moment. You can still use them to quickly deploy containers to the Docker host.</span>
|
||||
|
@ -41,10 +41,10 @@
|
|||
<div ng-repeat="var in state.selectedTemplate.env" ng-if="!var.set" class="form-group">
|
||||
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}</label>
|
||||
<div class="col-sm-10">
|
||||
<select ng-if="(!swarm || swarm && swarm_mode) && var.type === 'container'" ng-options="container|containername for container in runningContainers" class="selectpicker form-control" ng-model="var.value">
|
||||
<select ng-if="endpointMode.provider !== 'DOCKER_SWARM' && var.type === 'container'" ng-options="container|containername for container in runningContainers" class="selectpicker form-control" ng-model="var.value">
|
||||
<option selected disabled hidden value="">Select a container</option>
|
||||
</select>
|
||||
<select ng-if="swarm && !swarm_mode && var.type === 'container'" ng-options="container|swarmcontainername for container in runningContainers" class="selectpicker form-control" ng-model="var.value">
|
||||
<select ng-if="endpointMode.provider === 'DOCKER_SWARM' && var.type === 'container'" ng-options="container|swarmcontainername for container in runningContainers" class="selectpicker form-control" ng-model="var.value">
|
||||
<option selected disabled hidden value="">Select a container</option>
|
||||
</select>
|
||||
<input ng-if="!var.type || !var.type === 'container'" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}">
|
||||
|
|
|
@ -204,11 +204,6 @@ function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, C
|
|||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.swarm = c.swarm;
|
||||
Info.get({}, function(info) {
|
||||
if ($scope.swarm && !_.startsWith(info.ServerVersion, 'swarm')) {
|
||||
$scope.swarm_mode = true;
|
||||
}
|
||||
});
|
||||
var containersToHideLabels = c.hiddenLabels;
|
||||
Network.query({}, function (d) {
|
||||
var networks = d;
|
||||
|
|
|
@ -4,7 +4,7 @@ angular
|
|||
var directive = {
|
||||
requires: '^rdHeader',
|
||||
transclude: true,
|
||||
template: '<div class="breadcrumb-links" ng-transclude></div>',
|
||||
template: '<div class="breadcrumb-links"><div class="pull-left" ng-transclude></div><div class="pull-right"><a ui-sref="auth({logout: true})" class="text-danger" style="margin-right: 25px;"><u>log out <i class="fa fa-sign-out" aria-hidden="true"></i></u></a></div></div>',
|
||||
restrict: 'E'
|
||||
};
|
||||
return directive;
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
angular
|
||||
.module('portainer')
|
||||
.directive('rdHeaderTitle', function rdHeaderTitle() {
|
||||
.directive('rdHeaderTitle', ['$rootScope', function rdHeaderTitle($rootScope) {
|
||||
var directive = {
|
||||
requires: '^rdHeader',
|
||||
scope: {
|
||||
title: '@'
|
||||
},
|
||||
link: function (scope, iElement, iAttrs) {
|
||||
scope.username = $rootScope.username;
|
||||
},
|
||||
transclude: true,
|
||||
template: '<div class="page">{{title}}<span class="header_title_content" ng-transclude><span></div>',
|
||||
template: '<div class="page white-space-normal">{{title}}<span class="header_title_content" ng-transclude></span><span class="pull-right user-box"><i class="fa fa-user-circle-o" aria-hidden="true"></i> {{username}}</span></div>',
|
||||
restrict: 'E'
|
||||
};
|
||||
return directive;
|
||||
});
|
||||
}]);
|
||||
|
|
|
@ -166,14 +166,6 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
|
|||
get: {method: 'GET'}
|
||||
});
|
||||
}])
|
||||
.factory('Auth', ['$resource', 'Settings', function AuthFactory($resource, Settings) {
|
||||
'use strict';
|
||||
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#check-auth-configuration
|
||||
return $resource(Settings.url + '/auth', {}, {
|
||||
get: {method: 'GET'},
|
||||
update: {method: 'POST'}
|
||||
});
|
||||
}])
|
||||
.factory('Info', ['$resource', 'Settings', function InfoFactory($resource, Settings) {
|
||||
'use strict';
|
||||
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#display-system-wide-information
|
||||
|
@ -229,6 +221,89 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
|
|||
pagination_count: PAGINATION_MAX_ITEMS
|
||||
};
|
||||
}])
|
||||
.factory('Auth', ['$resource', 'AUTH_ENDPOINT', function AuthFactory($resource, AUTH_ENDPOINT) {
|
||||
'use strict';
|
||||
return $resource(AUTH_ENDPOINT, {}, {
|
||||
login: {
|
||||
method: 'POST'
|
||||
}
|
||||
});
|
||||
}])
|
||||
.factory('Users', ['$resource', function UsersFactory($resource) {
|
||||
'use strict';
|
||||
return $resource('/users/:username/:action', {}, {
|
||||
create: { method: 'POST' },
|
||||
get: {method: 'GET', params: { username: '@username' } },
|
||||
update: { method: 'PUT', params: { username: '@username' } },
|
||||
checkPassword: { method: 'POST', params: { username: '@username', action: 'passwd' } },
|
||||
checkAdminUser: {method: 'GET', params: { username: 'admin', action: 'check' }},
|
||||
initAdminUser: {method: 'POST', params: { username: 'admin', action: 'init' }}
|
||||
});
|
||||
}])
|
||||
.factory('EndpointMode', ['$rootScope', 'Info', function EndpointMode($rootScope, Info) {
|
||||
'use strict';
|
||||
return {
|
||||
determineEndpointMode: function() {
|
||||
Info.get({}, function(d) {
|
||||
var mode = {
|
||||
provider: '',
|
||||
role: ''
|
||||
};
|
||||
if (_.startsWith(d.ServerVersion, 'swarm')) {
|
||||
mode.provider = "DOCKER_SWARM";
|
||||
if (d.SystemStatus[0][1] === 'primary') {
|
||||
mode.role = "PRIMARY";
|
||||
} else {
|
||||
mode.role = "REPLICA";
|
||||
}
|
||||
} else {
|
||||
if (!d.Swarm || _.isEmpty(d.Swarm.NodeID)) {
|
||||
mode.provider = "DOCKER_STANDALONE";
|
||||
} else {
|
||||
mode.provider = "DOCKER_SWARM_MODE";
|
||||
if (d.Swarm.ControlAvailable) {
|
||||
mode.role = "MANAGER";
|
||||
} else {
|
||||
mode.role = "WORKER";
|
||||
}
|
||||
}
|
||||
}
|
||||
$rootScope.endpointMode = mode;
|
||||
});
|
||||
}
|
||||
};
|
||||
}])
|
||||
.factory('Authentication', ['$q', '$rootScope', 'Auth', 'jwtHelper', 'localStorageService', function AuthenticationFactory($q, $rootScope, Auth, jwtHelper, localStorageService) {
|
||||
'use strict';
|
||||
return {
|
||||
init: function() {
|
||||
var jwt = localStorageService.get('JWT');
|
||||
if (jwt) {
|
||||
var tokenPayload = jwtHelper.decodeToken(jwt);
|
||||
$rootScope.username = tokenPayload.username;
|
||||
}
|
||||
},
|
||||
login: function(username, password) {
|
||||
return $q(function (resolve, reject) {
|
||||
Auth.login({username: username, password: password}).$promise
|
||||
.then(function(data) {
|
||||
localStorageService.set('JWT', data.jwt);
|
||||
$rootScope.username = username;
|
||||
resolve();
|
||||
}, function() {
|
||||
reject();
|
||||
});
|
||||
});
|
||||
},
|
||||
logout: function() {
|
||||
localStorageService.remove('JWT');
|
||||
},
|
||||
isAuthenticated: function() {
|
||||
var jwt = localStorageService.get('JWT');
|
||||
return jwt && !jwtHelper.isTokenExpired(jwt);
|
||||
}
|
||||
};
|
||||
}])
|
||||
.factory('Messages', ['$rootScope', '$sanitize', function MessagesFactory($rootScope, $sanitize) {
|
||||
'use strict';
|
||||
return {
|
||||
|
|
|
@ -1,3 +1,12 @@
|
|||
html, body, #content-wrapper, .page-content, #view {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.white-space-normal {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
.btn-group button {
|
||||
margin: 3px;
|
||||
}
|
||||
|
@ -203,6 +212,48 @@ input[type="radio"] {
|
|||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.login-wrapper {
|
||||
margin-top: 25px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
margin-bottom: 80px;
|
||||
}
|
||||
|
||||
.login-box > div:first-child {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
display: block;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
width: 240px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.login-form > div {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.login-form > div:last-child {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding-top: 30px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.user-box {
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
|
@ -33,15 +33,17 @@
|
|||
"angular-resource": "~1.5.0",
|
||||
"angular-ui-select": "~0.17.1",
|
||||
"angular-utils-pagination": "~0.11.1",
|
||||
"angular-local-storage": "~0.5.2",
|
||||
"angular-jwt": "~0.1.8",
|
||||
"bootstrap": "~3.3.6",
|
||||
"font-awesome": "~4.6.3",
|
||||
"filesize": "~3.3.0",
|
||||
"jquery": "1.11.1",
|
||||
"jquery.gritter": "1.7.4",
|
||||
"lodash": "4.12.0",
|
||||
"rdash-ui": "1.0.*",
|
||||
"moment": "~2.14.1",
|
||||
"xterm.js": "~2.0.1"
|
||||
"xterm.js": "~2.0.1",
|
||||
"font-awesome": "~4.7.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"angular": "1.5.5"
|
||||
|
|
10
gruntFile.js
10
gruntFile.js
|
@ -193,6 +193,8 @@ module.exports = function (grunt) {
|
|||
src: ['bower_components/angular/angular.min.js',
|
||||
'bower_components/angular-sanitize/angular-sanitize.min.js',
|
||||
'bower_components/angular-cookies/angular-cookies.min.js',
|
||||
'bower_components/angular-local-storage/dist/angular-local-storage.min.js',
|
||||
'bower_components/angular-jwt/dist/angular-jwt.min.js',
|
||||
'bower_components/angular-ui-router/release/angular-ui-router.min.js',
|
||||
'bower_components/angular-resource/angular-resource.min.js',
|
||||
'bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js',
|
||||
|
@ -295,7 +297,7 @@ module.exports = function (grunt) {
|
|||
},
|
||||
buildBinary: {
|
||||
command: [
|
||||
'docker run --rm -v $(pwd)/api:/src centurylink/golang-builder',
|
||||
'docker run --rm -v $(pwd)/api:/src portainer/golang-builder',
|
||||
'shasum api/portainer > portainer-checksum.txt',
|
||||
'mkdir -p dist',
|
||||
'mv api/portainer dist/'
|
||||
|
@ -303,7 +305,7 @@ module.exports = function (grunt) {
|
|||
},
|
||||
buildUnixArmBinary: {
|
||||
command: [
|
||||
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="linux" -e BUILD_GOARCH="arm" centurylink/golang-builder-cross',
|
||||
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="linux" -e BUILD_GOARCH="arm" portainer/golang-builder:cross-platform',
|
||||
'shasum api/portainer-linux-arm > portainer-checksum.txt',
|
||||
'mkdir -p dist',
|
||||
'mv api/portainer-linux-arm dist/portainer'
|
||||
|
@ -311,7 +313,7 @@ module.exports = function (grunt) {
|
|||
},
|
||||
buildDarwinBinary: {
|
||||
command: [
|
||||
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="darwin" -e BUILD_GOARCH="amd64" centurylink/golang-builder-cross',
|
||||
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="darwin" -e BUILD_GOARCH="amd64" portainer/golang-builder:cross-platform',
|
||||
'shasum api/portainer-darwin-amd64 > portainer-checksum.txt',
|
||||
'mkdir -p dist',
|
||||
'mv api/portainer-darwin-amd64 dist/portainer'
|
||||
|
@ -319,7 +321,7 @@ module.exports = function (grunt) {
|
|||
},
|
||||
buildWindowsBinary: {
|
||||
command: [
|
||||
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="windows" -e BUILD_GOARCH="amd64" centurylink/golang-builder-cross',
|
||||
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="windows" -e BUILD_GOARCH="amd64" portainer/golang-builder:cross-platform',
|
||||
'shasum api/portainer-windows-amd64 > portainer-checksum.txt',
|
||||
'mkdir -p dist',
|
||||
'mv api/portainer-windows-amd64 dist/portainer.exe'
|
||||
|
|
59
index.html
59
index.html
|
@ -24,67 +24,16 @@
|
|||
<link rel="apple-touch-icon-precomposed" href="ico/apple-touch-icon-precomposed.png">
|
||||
</head>
|
||||
|
||||
<body ng-controller="MasterCtrl">
|
||||
<div id="page-wrapper" ng-class="{'open': toggle}" ng-cloak>
|
||||
<body ng-controller="MainController">
|
||||
<div id="page-wrapper" ng-class="{open: toggle && $state.current.name !== 'auth', nopadding: $state.current.name === 'auth'}" ng-cloak>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div id="sidebar-wrapper">
|
||||
<ul class="sidebar">
|
||||
<li class="sidebar-main">
|
||||
<a ng-click="toggleSidebar()">
|
||||
<img ng-if="logo" ng-src="{{ logo }}" class="img-responsive logo">
|
||||
<img ng-if="!logo" src="images/logo.png" class="img-responsive logo" alt="Portainer">
|
||||
<span class="menu-icon glyphicon glyphicon-transfer"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-title"><span>NAVIGATION</span></li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="index">Dashboard <span class="menu-icon fa fa-tachometer"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="templates">App Templates <span class="menu-icon fa fa-rocket"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="swarm_mode">
|
||||
<a ui-sref="services">Services <span class="menu-icon fa fa-list-alt"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="containers">Containers <span class="menu-icon fa fa-server"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="images">Images <span class="menu-icon fa fa-clone"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="networks">Networks <span class="menu-icon fa fa-sitemap"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="volumes">Volumes <span class="menu-icon fa fa-cubes"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="!swarm">
|
||||
<a ui-sref="events">Events <span class="menu-icon fa fa-history"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="(swarm && !swarm_mode) || (swarm_mode && swarm_manager)">
|
||||
<a ui-sref="swarm">Swarm <span class="menu-icon fa fa-object-group"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="!swarm">
|
||||
<a ui-sref="docker">Docker <span class="menu-icon fa fa-th"></span></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
<div class="col-xs-12">
|
||||
<a href="https://github.com/portainer/portainer" target="_blank">
|
||||
<i class="fa fa-github" aria-hidden="true"></i>
|
||||
Portainer {{ uiVersion }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Sidebar -->
|
||||
<div id="sideview" ui-view="sidebar"></div>
|
||||
|
||||
<div id="content-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<!-- Main Content -->
|
||||
<div id="view" ui-view></div>
|
||||
<div id="view" ui-view="content"></div>
|
||||
|
||||
</div><!-- End Page Content -->
|
||||
</div><!-- End Content Wrapper -->
|
||||
|
|
Loading…
Reference in New Issue