mirror of
https://github.com/OpenFactorioServerManager/factorio-server-manager.git
synced 2024-12-27 02:43:45 +02:00
Merge pull request #222 from OpenFactorioServerManager/new-authentication
New authentication
This commit is contained in:
commit
2d3772642f
5
.github/workflows/test-workflow.yml
vendored
5
.github/workflows/test-workflow.yml
vendored
@ -60,9 +60,10 @@ jobs:
|
||||
docker-push:
|
||||
needs: [test-npm, test-go]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
|
||||
if: github.event_name == 'push' && github.ref != 'refs/heads/master'
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
@ -78,4 +79,4 @@ jobs:
|
||||
context: ./docker/
|
||||
file: ./docker/Dockerfile-local
|
||||
push: true
|
||||
tags: ofsm/ofsm:develop
|
||||
tags: ofsm/ofsm:${{ env.GITHUB_REF_SLUG }}
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -18,3 +18,4 @@ mix-manifest.json
|
||||
/app/*.css*
|
||||
.vscode
|
||||
.env
|
||||
*.db
|
||||
|
@ -1,9 +1,6 @@
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "factorio",
|
||||
"database_file": "auth.leveldb",
|
||||
"cookie_encryption_key": "topsecretkey",
|
||||
"settings_file": "server-settings.json",
|
||||
"log_file": "factorio-server-manager.log",
|
||||
"rcon_pass": "factorio_rcon"
|
||||
"sq_lite_database_file": "sqlite.db",
|
||||
"log_file": "factorio-server-manager.log",
|
||||
"rcon_pass": "",
|
||||
"settings_file": "server-settings.json"
|
||||
}
|
||||
|
@ -1,6 +1,3 @@
|
||||
ADMIN_USER=admin
|
||||
ADMIN_PASS=factorio
|
||||
RCON_PASS=
|
||||
COOKIE_ENCRYPTION_KEY=
|
||||
DOMAIN_NAME=<YOUR DOMAIN NAME>
|
||||
EMAIL_ADDRESS=<YOUR EMAIL ADDRESS>
|
||||
|
@ -3,10 +3,7 @@ FROM frolvlad/alpine-glibc
|
||||
|
||||
ENV FACTORIO_VERSION=stable \
|
||||
MANAGER_VERSION=0.9.0 \
|
||||
ADMIN_USER=admin \
|
||||
ADMIN_PASS=factorio \
|
||||
RCON_PASS="" \
|
||||
COOKIE_ENCRYPTION_KEY=""
|
||||
RCON_PASS=""
|
||||
|
||||
VOLUME /opt/fsm-data /opt/factorio/saves /opt/factorio/mods /opt/factorio/config
|
||||
|
||||
|
@ -2,10 +2,7 @@
|
||||
FROM frolvlad/alpine-glibc
|
||||
|
||||
ENV FACTORIO_VERSION=latest \
|
||||
ADMIN_USER=admin \
|
||||
ADMIN_PASS=factorio \
|
||||
RCON_PASS="" \
|
||||
COOKIE_ENCRYPTION_KEY=""
|
||||
RCON_PASS=""
|
||||
|
||||
VOLUME /opt/fsm-data /opt/factorio/saves /opt/factorio/mods /opt/factorio/config
|
||||
|
||||
|
@ -9,13 +9,8 @@ and [Docker Compose](https://docs.docker.com/compose/install/) installed.
|
||||
Copy `docker-compose.yaml` and `.env` files from this repository to somewhere on your server.
|
||||
|
||||
Edit values in the `.env` file:
|
||||
* `ADMIN_USER` (default `admin`): Name of the default user created for FSM UI.
|
||||
* `ADMIN_PASS` (default `factorio`): Default user password. \
|
||||
__Important:__ _For security reasons, please change the default user name and password. Never use the defaults._
|
||||
* `RCON_PASS` (default empty string): Password for Factorio RCON (FSM uses it to communicate with the Factorio server). \
|
||||
If left empty, a random password will be generated and saved on the first start of the server. You can see the password in `fsm-data/conf.json` file.
|
||||
* `COOKIE_ENCRYPTION_KEY` (default empty string): The key used to encrypt auth cookie for FSM UI. \
|
||||
If left empty, a random key will be generated and saved on the first start of the server. You can see the key in `fsm-data/conf.json` file.
|
||||
* `DOMAIN_NAME` (must be set manually): The domain name where your FSM UI will be available. Must be set,
|
||||
so [Let's Encrypt](https://letsencrypt.org/) service can issue a valid HTTPS certificate for this domain.
|
||||
* `EMAIL_ADDRESS` (must be set manually): Your email address. Used only by Let's Encrypt service.
|
||||
@ -31,7 +26,7 @@ docker-compose up -d
|
||||
|
||||
### Simple configuration without HTTPS
|
||||
|
||||
If you don't care about HTTPS and want to run just the Factorio Server Manager, or want to run it on local machine you can use `docker-compose.simple.yaml`.
|
||||
If you don't care about HTTPS and want to run just the Factorio Server Manager, or want to run it on a local machine you can use `docker-compose.simple.yaml`.
|
||||
|
||||
Ignore `DOMAIN_NAME` and `EMAIL_ADDREESS` variables in `.env` file and run
|
||||
```
|
||||
@ -42,15 +37,16 @@ docker-compose -f docker-compose.simple.yaml up -d
|
||||
|
||||
By default container will download the latest version of factorio. If you want to use specific version, you can change
|
||||
the value of `FACTORIO_VERSION=latest` variable in the `docker-compose.yaml` file.
|
||||
Any version can be used. Using `latest` will download the newest beta version. Using `stable` will download the newest stable version.
|
||||
|
||||
## Accessing the application
|
||||
|
||||
Go to the domain specified in your `.env` file in your web browser. If running on localhost host access the application at http://localhost
|
||||
Go to the domain specified in your `.env` file in your web browser. If running on localhost access the application at http://localhost
|
||||
|
||||
### First start
|
||||
|
||||
When container starts it begins to dowload Factorio headless server archive, and only after that Factorio Server Manager server starts.
|
||||
So when Docker Compose writes
|
||||
When container starts it begins to download Factorio headless server archive, and only after that Factorio Server Manager server starts.
|
||||
So when docker-compose writes
|
||||
```
|
||||
Creating factorio-server-manager ... done
|
||||
```
|
||||
@ -68,7 +64,7 @@ Users can be added and deleted on the settings page.
|
||||
|
||||
## Updating Factorio
|
||||
|
||||
For now you can't update/downgrade the Factorio version from the UI.
|
||||
For now, you can't update/downgrade the Factorio version from the UI.
|
||||
|
||||
You can however do this using docker images while sustaining your security settings and map/modfiles.
|
||||
|
||||
@ -80,7 +76,7 @@ After container starts, latest Factorio version will be downloaded and installed
|
||||
|
||||
## Security
|
||||
|
||||
Authentication is supported in the application but it is recommended to ensure access to the Factorio manager UI is accessible via VPN or internal network.
|
||||
Authentication is supported in the application, but it is recommended to ensure access to the Factorio manager UI is accessible via VPN or internal network.
|
||||
|
||||
## Development
|
||||
For development purposes it also has the ability to create the docker image from local sourcecode. This is done by running `build.sh` in the `docker` directory. This will delete all old executables and the node_modules directory (runs `make build`). The created docker image will have the tag `factorio-server-manager:dev`.
|
||||
|
@ -6,10 +6,7 @@ services:
|
||||
restart: "unless-stopped"
|
||||
environment:
|
||||
- "FACTORIO_VERSION=latest"
|
||||
- "ADMIN_USER"
|
||||
- "ADMIN_PASS"
|
||||
- "RCON_PASS"
|
||||
- "COOKIE_ENCRYPTION_KEY"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "34197:34197/udp"
|
||||
|
@ -6,10 +6,7 @@ services:
|
||||
restart: "unless-stopped"
|
||||
environment:
|
||||
- "FACTORIO_VERSION=latest"
|
||||
- "ADMIN_USER"
|
||||
- "ADMIN_PASS"
|
||||
- "RCON_PASS"
|
||||
- "COOKIE_ENCRYPTION_KEY"
|
||||
volumes:
|
||||
- "./fsm-data:/opt/fsm-data"
|
||||
- "./factorio-data/saves:/opt/factorio/saves"
|
||||
|
@ -3,28 +3,12 @@
|
||||
init_config() {
|
||||
jq_cmd='.'
|
||||
|
||||
if [ -n $ADMIN_USER ]; then
|
||||
jq_cmd="${jq_cmd} | .username = \"$ADMIN_USER\""
|
||||
echo "Admin username is '$ADMIN_USER'"
|
||||
if [ -n "$RCON_PASS" ]; then
|
||||
jq_cmd="${jq_cmd} | .rcon_pass = \"$RCON_PASS\""
|
||||
echo "Factorio rcon password is '$RCON_PASS'"
|
||||
fi
|
||||
if [ -n $ADMIN_PASS ]; then
|
||||
jq_cmd="${jq_cmd} | .password = \"$ADMIN_PASS\""
|
||||
echo "Admin password is '$ADMIN_PASS'"
|
||||
fi
|
||||
echo "IMPORTANT! Please create new user and delete default admin user ASAP."
|
||||
|
||||
if [ -z $RCON_PASS ]; then
|
||||
RCON_PASS="$(random_pass)"
|
||||
fi
|
||||
jq_cmd="${jq_cmd} | .rcon_pass = \"$RCON_PASS\""
|
||||
echo "Factorio rcon password is '$RCON_PASS'"
|
||||
|
||||
if [ -z $COOKIE_ENCRYPTION_KEY ]; then
|
||||
COOKIE_ENCRYPTION_KEY="$(random_pass)"
|
||||
fi
|
||||
jq_cmd="${jq_cmd} | .cookie_encryption_key = \"$COOKIE_ENCRYPTION_KEY\""
|
||||
|
||||
jq_cmd="${jq_cmd} | .database_file = \"/opt/fsm-data/auth.leveldb\""
|
||||
jq_cmd="${jq_cmd} | .sq_lite_database_file = \"/opt/fsm-data/sqlite.db\""
|
||||
jq_cmd="${jq_cmd} | .log_file = \"/opt/fsm-data/factorio-server-manager.log\""
|
||||
|
||||
jq "${jq_cmd}" /opt/fsm/conf.json >/opt/fsm-data/conf.json
|
||||
@ -47,5 +31,5 @@ fi
|
||||
|
||||
install_game
|
||||
|
||||
cd /opt/fsm && ./factorio-server-manager --conf /opt/fsm-data/conf.json --dir /opt/factorio -port 80
|
||||
cd /opt/fsm && ./factorio-server-manager --conf /opt/fsm-data/conf.json --dir /opt/factorio --port 80
|
||||
|
||||
|
302
src/api/auth.go
302
src/api/auth.go
@ -1,121 +1,235 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/mroote/factorio-server-manager/bootstrap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/apexskier/httpauth"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type AuthHTTP struct {
|
||||
backend httpauth.LeveldbAuthBackend
|
||||
aaa httpauth.Authorizer
|
||||
type User bootstrap.User
|
||||
|
||||
type Auth struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Role string `json:"role"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
var (
|
||||
sessionStore *sessions.CookieStore
|
||||
auth Auth
|
||||
)
|
||||
|
||||
var once sync.Once
|
||||
var instantiated *AuthHTTP
|
||||
|
||||
func GetAuth() *AuthHTTP {
|
||||
once.Do(func() {
|
||||
Auth := &AuthHTTP{}
|
||||
config := bootstrap.GetConfig()
|
||||
_ = Auth.CreateAuth(config.DatabaseFile, config.CookieEncryptionKey)
|
||||
_ = Auth.CreateOrUpdateUser(config.Username, config.Password, "admin", "")
|
||||
instantiated = Auth
|
||||
})
|
||||
return instantiated
|
||||
}
|
||||
|
||||
func (auth *AuthHTTP) CreateAuth(backendFile string, cookieKey string) error {
|
||||
func SetupAuth() {
|
||||
var err error
|
||||
os.Mkdir(backendFile, 0755)
|
||||
|
||||
auth.backend, err = httpauth.NewLeveldbAuthBackend(backendFile)
|
||||
config := bootstrap.GetConfig()
|
||||
|
||||
cookieEncryptionKey, err := base64.StdEncoding.DecodeString(config.CookieEncryptionKey)
|
||||
if err != nil {
|
||||
log.Printf("Error creating Auth backend: %s", err)
|
||||
log.Printf("Error decoding base64 cookie encryption key: %s", err)
|
||||
panic(err)
|
||||
}
|
||||
sessionStore = sessions.NewCookieStore(cookieEncryptionKey)
|
||||
sessionStore.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
Secure: true,
|
||||
}
|
||||
|
||||
auth.db, err = gorm.Open(sqlite.Open(config.SQLiteDatabaseFile), nil)
|
||||
if err != nil {
|
||||
log.Printf("Error opening sqlite or goem database: %s", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = auth.db.AutoMigrate(&User{})
|
||||
if err != nil {
|
||||
log.Printf("Error AutoMigrating gorm database: %s", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var userCount int64
|
||||
auth.db.Model(&User{}).Count(&userCount)
|
||||
|
||||
if userCount == 0 {
|
||||
// no user created yet, create a default one
|
||||
var password = bootstrap.GenerateRandomPassword()
|
||||
|
||||
var user User
|
||||
user.Username = "admin"
|
||||
user.Password = password
|
||||
user.Role = "admin"
|
||||
|
||||
err := auth.addUser(user)
|
||||
if err != nil {
|
||||
log.Printf("Error adding admin user to db: %s", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.Println("Created default admin user. Please change it's password as soon as possible.")
|
||||
log.Printf("Username: %s", user.Username)
|
||||
log.Printf("Password: %s", password)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Auth) checkPassword(username, password string) error {
|
||||
var user User
|
||||
result := a.db.Where(&User{Username: username}).Take(&user)
|
||||
if result.Error != nil {
|
||||
log.Printf("Error reading user from database: %s", result.Error)
|
||||
return result.Error
|
||||
}
|
||||
|
||||
decodedHashPw, err := base64.StdEncoding.DecodeString(user.Password)
|
||||
if err != nil {
|
||||
log.Printf("Error decoding base64 password: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
roles := make(map[string]httpauth.Role)
|
||||
roles["user"] = 30
|
||||
roles["admin"] = 80
|
||||
|
||||
auth.aaa, err = httpauth.NewAuthorizer(auth.backend, []byte(cookieKey), "user", roles)
|
||||
err = bcrypt.CompareHashAndPassword(decodedHashPw, []byte(password))
|
||||
if err != nil {
|
||||
log.Printf("Error creating authorizer: %s", err)
|
||||
if err != bcrypt.ErrMismatchedHashAndPassword {
|
||||
log.Printf("Unexpected error comparing hash and pw: %s", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Password correct
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Auth) deleteUser(username string) error {
|
||||
result := a.db.Model(&User{}).Where(&User{Username: username}).Delete(&User{})
|
||||
if result.Error != nil {
|
||||
log.Printf("Error deleting user from database: %s", result.Error)
|
||||
return result.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Auth) hasUser(username string) (bool, error) {
|
||||
var count int64
|
||||
result := a.db.Model(&User{}).Where(&User{Username: username}).Count(&count)
|
||||
if result.Error != nil {
|
||||
log.Printf("Error checking if user exists in database: %s", result.Error)
|
||||
return false, result.Error
|
||||
}
|
||||
return count == 1, nil
|
||||
}
|
||||
|
||||
func (a *Auth) getUser(username string) (User, error) {
|
||||
var user User
|
||||
result := a.db.Model(&User{}).Where(&User{Username: username}).Take(&user)
|
||||
if result.Error != nil {
|
||||
log.Printf("Error reading user from database: %s", result.Error)
|
||||
return User{}, result.Error
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (a *Auth) listUsers() ([]User, error) {
|
||||
var users []User
|
||||
result := a.db.Find(&users)
|
||||
if result.Error != nil {
|
||||
log.Printf("Error listing all users in database: %s", result.Error)
|
||||
return nil, result.Error
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (a *Auth) addUser(user User) error {
|
||||
// encrypt password
|
||||
pwHash, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
log.Printf("Error generating bcrypt hash from password: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
user.Password = base64.StdEncoding.EncodeToString(pwHash)
|
||||
|
||||
// add user to db
|
||||
result := a.db.Create(&user)
|
||||
if result.Error != nil {
|
||||
log.Printf("Error creating user in database: %s", result.Error)
|
||||
return result.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *AuthHTTP) CreateOrUpdateUser(username, password, role, email string) error {
|
||||
user := httpauth.UserData{Username: username, Role: role, Email: email}
|
||||
err := auth.backend.SaveUser(user)
|
||||
if err != nil {
|
||||
log.Printf("Error saving user: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = auth.aaa.Update(nil, nil, username, password, email)
|
||||
if err != nil {
|
||||
log.Printf("Error updating user: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Created/Updated user: %s", user.Username)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *AuthHTTP) listUsers() ([]User, error) {
|
||||
var userResponse []User
|
||||
users, err := auth.backend.Users()
|
||||
if err != nil {
|
||||
log.Printf("Error list users: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
u := User{Username: user.Username, Role: user.Role, Email: user.Email}
|
||||
userResponse = append(userResponse, u)
|
||||
}
|
||||
|
||||
log.Printf("listing users: %v found", len(users))
|
||||
return userResponse, nil
|
||||
}
|
||||
|
||||
func (auth *AuthHTTP) addUser(username, password, email, role string) error {
|
||||
user := httpauth.UserData{Username: username, Hash: []byte(password), Email: email, Role: role}
|
||||
err := auth.backend.SaveUser(user)
|
||||
if err != nil {
|
||||
log.Printf("Error creating user %v: %s", user, err)
|
||||
}
|
||||
err = auth.aaa.Update(nil, nil, username, password, email)
|
||||
if err != nil {
|
||||
log.Printf("Error saving user: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Added user: %v", user)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *AuthHTTP) removeUser(username string) error {
|
||||
err := auth.backend.DeleteUser(username)
|
||||
if err != nil {
|
||||
log.Printf("Could not delete user %s, error: %s", username, err)
|
||||
return err
|
||||
func (a *Auth) addUserWithHash(user User) error {
|
||||
// add user to db
|
||||
result := a.db.Create(&user)
|
||||
if result.Error != nil {
|
||||
log.Printf("Error creating user in database: %s", result.Error)
|
||||
return result.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Auth) changePassword(username, password string) error {
|
||||
var user User
|
||||
result := a.db.Model(&User{}).Where(&User{Username: username}).Take(&user)
|
||||
if result.Error != nil {
|
||||
log.Printf("Error reading user from database: %s", result.Error)
|
||||
return result.Error
|
||||
}
|
||||
|
||||
hashPW, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
log.Printf("Error generatig bcrypt hash from new password: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
user.Password = base64.StdEncoding.EncodeToString(hashPW)
|
||||
|
||||
result = a.db.Save(&user)
|
||||
if result.Error != nil {
|
||||
log.Printf("Error resaving user in database: %s", result.Error)
|
||||
return result.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// middleware function, that will be called for every request, that has to be authorized
|
||||
func AuthMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := sessionStore.Get(r, "authentication")
|
||||
if err != nil {
|
||||
if session != nil {
|
||||
session.Options.MaxAge = -1
|
||||
err2 := session.Save(r, w)
|
||||
if err2 != nil {
|
||||
log.Printf("Error deleting cookie: %s", err2)
|
||||
}
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
username, ok := session.Values["username"]
|
||||
if !ok {
|
||||
http.Error(w, "Could not read username from sessioncookie", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
hasUser, err := auth.hasUser(username.(string))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if hasUser {
|
||||
next.ServeHTTP(w, r)
|
||||
} else {
|
||||
log.Printf("Unauthenticated request %s %s %s", r.Method, r.Host, r.RequestURI)
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -4,6 +4,9 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/mroote/factorio-server-manager/bootstrap"
|
||||
"github.com/mroote/factorio-server-manager/factorio"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@ -14,9 +17,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mroote/factorio-server-manager/bootstrap"
|
||||
"github.com/mroote/factorio-server-manager/factorio"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
@ -53,6 +53,33 @@ func ReadRequestBody(w http.ResponseWriter, r *http.Request, resp *interface{})
|
||||
return
|
||||
}
|
||||
|
||||
func ReadSessionStore(w http.ResponseWriter, r *http.Request, resp *interface{}, name string) (session *sessions.Session, err error) {
|
||||
session, err = sessionStore.Get(r, name)
|
||||
if err != nil {
|
||||
*resp = fmt.Sprintf("Error reading session cookie [%s]: %s", name, err)
|
||||
log.Println(*resp)
|
||||
if session != nil {
|
||||
session.Options.MaxAge = -1
|
||||
err2 := session.Save(r, w)
|
||||
if err2 != nil {
|
||||
log.Printf("Error deleting session cookie: %s", err2)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func SaveSession(w http.ResponseWriter, r *http.Request, resp *interface{}, session *sessions.Session) (err error) {
|
||||
err = session.Save(r, w)
|
||||
if err != nil {
|
||||
*resp = fmt.Sprintf("Error saving session cookie: %s", err)
|
||||
log.Println(*resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Lists all save files in the factorio/saves directory
|
||||
func ListSaves(w http.ResponseWriter, r *http.Request) {
|
||||
var resp interface{}
|
||||
@ -417,6 +444,7 @@ func UnmarshallUserJson(body []byte, resp *interface{}, w http.ResponseWriter) (
|
||||
return
|
||||
}
|
||||
|
||||
// Handler for the Login
|
||||
func LoginUser(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
@ -439,15 +467,31 @@ func LoginUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
log.Printf("Logging in user: %s", user.Username)
|
||||
Auth := GetAuth()
|
||||
err = Auth.aaa.Login(w, r, user.Username, user.Password, "/")
|
||||
|
||||
err = auth.checkPassword(user.Username, user.Password)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error loggin in user: %s, error: %s", user.Username, err)
|
||||
resp = fmt.Sprintf("Password for user %s wrong", user.Username)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
session, err := ReadSessionStore(w, r, &resp, "authentication")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
session.Values["username"] = user.Username
|
||||
|
||||
err = SaveSession(w, r, &resp, session)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("User: %s, logged in successfully", user.Username)
|
||||
|
||||
user.Password = ""
|
||||
resp = user
|
||||
}
|
||||
|
||||
func LogoutUser(w http.ResponseWriter, r *http.Request) {
|
||||
@ -459,10 +503,16 @@ func LogoutUser(w http.ResponseWriter, r *http.Request) {
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
Auth := GetAuth()
|
||||
if err = Auth.aaa.Logout(w, r); err != nil {
|
||||
log.Printf("Error logging out current user")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
session, err := ReadSessionStore(w, r, &resp, "authentication")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
delete(session.Values, "username")
|
||||
|
||||
err = SaveSession(w, r, &resp, session)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@ -479,15 +529,24 @@ func GetCurrentLogin(w http.ResponseWriter, r *http.Request) {
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
Auth := GetAuth()
|
||||
user, err := Auth.aaa.CurrentUser(w, r)
|
||||
|
||||
session, err := ReadSessionStore(w, r, &resp, "authentication")
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error getting user status: %s, error: %s", user.Username, err)
|
||||
return
|
||||
}
|
||||
|
||||
username := session.Values["username"].(string)
|
||||
|
||||
user, err := auth.getUser(username)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error getting user: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = ""
|
||||
|
||||
resp = user
|
||||
}
|
||||
|
||||
@ -499,8 +558,8 @@ func ListUsers(w http.ResponseWriter, r *http.Request) {
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
Auth := GetAuth()
|
||||
users, err := Auth.listUsers()
|
||||
|
||||
users, err := auth.listUsers()
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error listing users: %s", err)
|
||||
log.Println(resp)
|
||||
@ -524,14 +583,12 @@ func AddUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Adding user: %v", string(body))
|
||||
|
||||
user, err := UnmarshallUserJson(body, &resp, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
Auth := GetAuth()
|
||||
err = Auth.addUser(user.Username, user.Password, user.Email, user.Role)
|
||||
|
||||
err = auth.addUser(user)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error in adding user {%s}: %s", user.Username, err)
|
||||
log.Println(resp)
|
||||
@ -560,8 +617,8 @@ func RemoveUser(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
Auth := GetAuth()
|
||||
err = Auth.removeUser(user.Username)
|
||||
|
||||
err = auth.deleteUser(user.Username)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error in removing user {%s}, error: %s", user.Username, err)
|
||||
log.Println(resp)
|
||||
@ -571,6 +628,70 @@ func RemoveUser(w http.ResponseWriter, r *http.Request) {
|
||||
resp = fmt.Sprintf("User: %s successfully removed.", user.Username)
|
||||
}
|
||||
|
||||
func ChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
body, err := ReadRequestBody(w, r, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var user struct {
|
||||
OldPassword string `json:"old_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
NewPasswordConfirm string `json:"new_password_confirmation"`
|
||||
}
|
||||
err = json.Unmarshal(body, &user)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Unable to parse the request body: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// only allow to change its own password
|
||||
// get username from session cookie
|
||||
session, err := ReadSessionStore(w, r, &resp, "authentication")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
username := session.Values["username"].(string)
|
||||
|
||||
// check if password for user is correct
|
||||
err = auth.checkPassword(username, user.OldPassword)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Password for user %s wrong", username)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// only run, when confirmation correct
|
||||
if user.NewPassword != user.NewPasswordConfirm {
|
||||
resp = fmt.Sprintf("Password confirmation incorrect")
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = auth.changePassword(username, user.NewPassword)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error changing password: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp = true
|
||||
}
|
||||
|
||||
// GetServerSettings returns JSON response of server-settings.json file
|
||||
func GetServerSettings(w http.ResponseWriter, r *http.Request) {
|
||||
var resp interface{}
|
||||
|
@ -2,7 +2,6 @@ package api
|
||||
|
||||
import (
|
||||
"github.com/mroote/factorio-server-manager/api/websocket"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@ -20,36 +19,40 @@ type Routes []Route
|
||||
func NewRouter() *mux.Router {
|
||||
r := mux.NewRouter().StrictSlash(true)
|
||||
|
||||
// create subrouter for authenticated calls
|
||||
sr := r.NewRoute().Subrouter()
|
||||
sr.Use(AuthMiddleware)
|
||||
|
||||
// API subrouter
|
||||
// Serves all JSON REST handlers prefixed with /api
|
||||
s := r.PathPrefix("/api").Subrouter()
|
||||
s.Use(AuthMiddleware)
|
||||
for _, route := range apiRoutes {
|
||||
s.Methods(route.Method).
|
||||
Path(route.Pattern).
|
||||
Name(route.Name).
|
||||
Handler(AuthorizeHandler(route.HandlerFunc))
|
||||
Handler(route.HandlerFunc)
|
||||
}
|
||||
|
||||
// The login handler does not check for authentication.
|
||||
s.Path("/login").
|
||||
r.Path("/api/login").
|
||||
Methods("POST").
|
||||
Name("LoginUser").
|
||||
//HandlerFunc(LoginUser)
|
||||
HandlerFunc(LoginUser)
|
||||
|
||||
// Route for initializing websocket connection
|
||||
// Clients connecting to /ws establish websocket connection by upgrading
|
||||
// HTTP session.
|
||||
// Ensure user is logged in with the AuthorizeHandler middleware
|
||||
r.Path("/ws").
|
||||
sr.Path("/ws").
|
||||
Methods("GET").
|
||||
Name("Websocket").
|
||||
Handler(
|
||||
AuthorizeHandler(
|
||||
http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
websocket.ServeWs(w, r)
|
||||
},
|
||||
),
|
||||
http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
websocket.ServeWs(w, r)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@ -60,38 +63,39 @@ func NewRouter() *mux.Router {
|
||||
Methods("GET").
|
||||
Name("Login").
|
||||
Handler(http.StripPrefix("/login", http.FileServer(http.Dir("./app/"))))
|
||||
r.Path("/saves").
|
||||
|
||||
sr.Path("/saves").
|
||||
Methods("GET").
|
||||
Name("Saves").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/saves", http.FileServer(http.Dir("./app/")))))
|
||||
r.Path("/mods").
|
||||
Handler(http.StripPrefix("/saves", http.FileServer(http.Dir("./app/"))))
|
||||
sr.Path("/mods").
|
||||
Methods("GET").
|
||||
Name("Mods").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/mods", http.FileServer(http.Dir("./app/")))))
|
||||
r.Path("/server-settings").
|
||||
Handler(http.StripPrefix("/mods", http.FileServer(http.Dir("./app/"))))
|
||||
sr.Path("/server-settings").
|
||||
Methods("GET").
|
||||
Name("Server settings").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/server-settings", http.FileServer(http.Dir("./app/")))))
|
||||
r.Path("/game-settings").
|
||||
Handler(http.StripPrefix("/server-settings", http.FileServer(http.Dir("./app/"))))
|
||||
sr.Path("/game-settings").
|
||||
Methods("GET").
|
||||
Name("Game settings").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/game-settings", http.FileServer(http.Dir("./app/")))))
|
||||
r.Path("/console").
|
||||
Handler(http.StripPrefix("/game-settings", http.FileServer(http.Dir("./app/"))))
|
||||
sr.Path("/console").
|
||||
Methods("GET").
|
||||
Name("Console").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/console", http.FileServer(http.Dir("./app/")))))
|
||||
r.Path("/logs").
|
||||
Handler(http.StripPrefix("/console", http.FileServer(http.Dir("./app/"))))
|
||||
sr.Path("/logs").
|
||||
Methods("GET").
|
||||
Name("Logs").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/logs", http.FileServer(http.Dir("./app/")))))
|
||||
r.Path("/user-management").
|
||||
Handler(http.StripPrefix("/logs", http.FileServer(http.Dir("./app/"))))
|
||||
sr.Path("/user-management").
|
||||
Methods("GET").
|
||||
Name("User management").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/user-management", http.FileServer(http.Dir("./app/")))))
|
||||
r.Path("/help").
|
||||
Handler(http.StripPrefix("/user-management", http.FileServer(http.Dir("./app/"))))
|
||||
sr.Path("/help").
|
||||
Methods("GET").
|
||||
Name("Help").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/help", http.FileServer(http.Dir("./app/")))))
|
||||
Handler(http.StripPrefix("/help", http.FileServer(http.Dir("./app/"))))
|
||||
|
||||
// catch all route
|
||||
r.PathPrefix("/").
|
||||
@ -102,20 +106,6 @@ func NewRouter() *mux.Router {
|
||||
return r
|
||||
}
|
||||
|
||||
// Middleware returns a http.HandlerFunc which authenticates the users request
|
||||
// Redirects user to login page if no session is found
|
||||
func AuthorizeHandler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
Auth := GetAuth()
|
||||
if err := Auth.aaa.Authorize(w, r, true); err != nil {
|
||||
log.Printf("Unauthenticated request %s %s %s", r.Method, r.Host, r.RequestURI)
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// Defines all API REST endpoints
|
||||
// All routes are prefixed with /api
|
||||
var apiRoutes = Routes{
|
||||
@ -209,6 +199,11 @@ var apiRoutes = Routes{
|
||||
"POST",
|
||||
"/user/remove",
|
||||
RemoveUser,
|
||||
}, {
|
||||
"ChangePassword",
|
||||
"POST",
|
||||
"/user/password",
|
||||
ChangePassword,
|
||||
}, {
|
||||
"GetServerSettings",
|
||||
"GET",
|
||||
|
@ -1,16 +1,17 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/jessevdk/go-flags"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
)
|
||||
|
||||
type Flags struct {
|
||||
@ -30,35 +31,34 @@ type Flags struct {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
FactorioDir string `json:"factorio_dir"`
|
||||
FactorioSavesDir string `json:"saves_dir"`
|
||||
FactorioBaseModDir string `json:"basemod_dir"`
|
||||
FactorioModsDir string `json:"mods_dir"`
|
||||
FactorioModPackDir string `json:"mod_pack_dir"`
|
||||
FactorioConfigFile string `json:"config_file"`
|
||||
FactorioConfigDir string `json:"config_directory"`
|
||||
FactorioLog string `json:"logfile"`
|
||||
FactorioBinary string `json:"factorio_binary"`
|
||||
FactorioRconPort int `json:"rcon_port"`
|
||||
FactorioRconPass string `json:"rcon_pass"`
|
||||
FactorioCredentialsFile string `json:"factorio_credentials_file"`
|
||||
FactorioIP string `json:"factorio_ip"`
|
||||
FactorioDir string `json:"factorio_dir,omitempty"`
|
||||
FactorioSavesDir string `json:"saves_dir,omitempty"`
|
||||
FactorioBaseModDir string `json:"basemod_dir,omitempty"`
|
||||
FactorioModsDir string `json:"mods_dir,omitempty"`
|
||||
FactorioModPackDir string `json:"mod_pack_dir,omitempty"`
|
||||
FactorioConfigFile string `json:"config_file,omitempty"`
|
||||
FactorioConfigDir string `json:"config_directory,omitempty"`
|
||||
FactorioLog string `json:"logfile,omitempty"`
|
||||
FactorioBinary string `json:"factorio_binary,omitempty"`
|
||||
FactorioRconPort int `json:"rcon_port,omitempty"`
|
||||
FactorioRconPass string `json:"rcon_pass,omitempty"`
|
||||
FactorioCredentialsFile string `json:"factorio_credentials_file,omitempty"`
|
||||
FactorioIP string `json:"factorio_ip,omitempty"`
|
||||
FactorioAdminFile string `json:"-"`
|
||||
ServerIP string `json:"server_ip"`
|
||||
ServerPort string `json:"server_port"`
|
||||
MaxUploadSize int64 `json:"max_upload_size"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
DatabaseFile string `json:"database_file"`
|
||||
CookieEncryptionKey string `json:"cookie_encryption_key"`
|
||||
SettingsFile string `json:"settings_file"`
|
||||
LogFile string `json:"log_file"`
|
||||
ConfFile string
|
||||
GlibcCustom string
|
||||
GlibcLocation string
|
||||
GlibcLibLoc string
|
||||
Autostart string
|
||||
ConsoleCacheSize int `json:"console_cache_size"` // the amount of cached lines, inside the factorio output cache
|
||||
ServerIP string `json:"server_ip,omitempty"`
|
||||
ServerPort string `json:"server_port,omitempty"`
|
||||
MaxUploadSize int64 `json:"max_upload_size,omitempty"`
|
||||
DatabaseFile string `json:"database_file,omitempty"`
|
||||
SQLiteDatabaseFile string `json:"sq_lite_database_file,omitempty"`
|
||||
CookieEncryptionKey string `json:"cookie_encryption_key,omitempty"`
|
||||
SettingsFile string `json:"settings_file,omitempty"`
|
||||
LogFile string `json:"log_file,omitempty"`
|
||||
ConfFile string `json:"-"`
|
||||
GlibcCustom string `json:"-"`
|
||||
GlibcLocation string `json:"-"`
|
||||
GlibcLibLoc string `json:"-"`
|
||||
Autostart string `json:"-"`
|
||||
ConsoleCacheSize int `json:"console_cache_size,omitempty"` // the amount of cached lines, inside the factorio output cache
|
||||
}
|
||||
|
||||
var instantiated Config
|
||||
@ -82,12 +82,78 @@ func GetConfig() Config {
|
||||
return instantiated
|
||||
}
|
||||
|
||||
func (config *Config) updateConfigFile() {
|
||||
file, err := os.OpenFile(config.ConfFile, os.O_RDONLY, 0)
|
||||
failOnError(err, "Error opening file")
|
||||
defer file.Close()
|
||||
|
||||
var conf Config
|
||||
decoder := json.NewDecoder(file)
|
||||
decoder.Decode(&conf)
|
||||
|
||||
err = file.Close()
|
||||
failOnError(err, "Error closing json file")
|
||||
|
||||
var resave bool
|
||||
|
||||
// set cookie encryption key, if empty
|
||||
// also set it, if the base64 string is not valid
|
||||
_, base64Err := base64.StdEncoding.DecodeString(conf.CookieEncryptionKey)
|
||||
if conf.CookieEncryptionKey == "" || conf.CookieEncryptionKey == "topsecretkey" || base64Err != nil {
|
||||
log.Println("CookieEncryptionKey invalid or empty, create new random one")
|
||||
randomKey := securecookie.GenerateRandomKey(32)
|
||||
conf.CookieEncryptionKey = base64.StdEncoding.EncodeToString(randomKey)
|
||||
|
||||
resave = true
|
||||
}
|
||||
|
||||
if conf.FactorioRconPass == "" || conf.FactorioRconPass == "factorio_rcon" {
|
||||
// password is "factorio" .. change it
|
||||
conf.FactorioRconPass = GenerateRandomPassword()
|
||||
|
||||
log.Println("Rcon password default one or empty, generated new one:")
|
||||
log.Printf("Password: %s", conf.FactorioRconPass)
|
||||
|
||||
resave = true
|
||||
}
|
||||
|
||||
if conf.DatabaseFile != "" {
|
||||
// Migrate leveldb to sqlite
|
||||
// set new db name
|
||||
// just rename the file from the old path
|
||||
dbFileDir := filepath.Dir(conf.DatabaseFile)
|
||||
conf.SQLiteDatabaseFile = filepath.Join(dbFileDir, "sqlite.db")
|
||||
|
||||
MigrateLevelDBToSqlite(conf.DatabaseFile, conf.SQLiteDatabaseFile)
|
||||
|
||||
// remove old db name
|
||||
conf.DatabaseFile = ""
|
||||
resave = true
|
||||
}
|
||||
|
||||
if resave {
|
||||
// save json file again
|
||||
file, err = os.OpenFile(config.ConfFile, os.O_WRONLY, 0)
|
||||
failOnError(err, "Error opening file for writing")
|
||||
defer file.Close()
|
||||
|
||||
encoder := json.NewEncoder(file)
|
||||
encoder.SetIndent("", "\t")
|
||||
err = encoder.Encode(conf)
|
||||
failOnError(err, "Error encoding JSON config file.")
|
||||
}
|
||||
}
|
||||
|
||||
// Loads server configuration files
|
||||
// JSON config file contains default values,
|
||||
// config file will overwrite any provided flags
|
||||
func (config *Config) loadServerConfig() {
|
||||
file, err := os.Open(config.ConfFile)
|
||||
// load and potentially update conf.json
|
||||
config.updateConfigFile()
|
||||
|
||||
file, err := os.OpenFile(config.ConfFile, os.O_RDWR, 0)
|
||||
failOnError(err, "Error loading config file.")
|
||||
defer file.Close()
|
||||
|
||||
decoder := json.NewDecoder(file)
|
||||
err = decoder.Decode(&config)
|
||||
@ -101,6 +167,7 @@ func (config *Config) loadServerConfig() {
|
||||
config.FactorioBaseModDir = filepath.Join(config.FactorioDir, "data", "base")
|
||||
}
|
||||
|
||||
// Set random port as rconPort
|
||||
config.FactorioRconPort = randomPort()
|
||||
}
|
||||
|
||||
|
129
src/bootstrap/user.go
Normal file
129
src/bootstrap/user.go
Normal file
@ -0,0 +1,129 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Username string `json:"username",gorm:"uniqueIndex,not null"`
|
||||
Password string `json:"password",gorm:"not null"`
|
||||
Role string `json:"role",gorm:"not null"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func MigrateLevelDBToSqlite(oldDBFile, newDBFile string) {
|
||||
oldDB, err := leveldb.OpenFile(oldDBFile, nil)
|
||||
if err != nil {
|
||||
log.Printf("Error opening old leveldb: %s", err)
|
||||
panic(err)
|
||||
}
|
||||
defer oldDB.Close()
|
||||
|
||||
newDB, err := gorm.Open(sqlite.Open(newDBFile), nil)
|
||||
if err != nil {
|
||||
log.Printf("Error open sqlite and gorm: %s", err)
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
db, err2 := newDB.DB()
|
||||
if err2 != nil {
|
||||
log.Printf("Error getting real DB from gorm: %s", err2)
|
||||
}
|
||||
if db != nil {
|
||||
err2 = db.Close()
|
||||
if err2 != nil {
|
||||
log.Printf("Error closing real DB of gorm: %s", err2)
|
||||
panic(err2)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err = newDB.AutoMigrate(&User{})
|
||||
if err != nil {
|
||||
log.Printf("Error autoMigrating sqlite database with user: %s", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
oldUserData, err := oldDB.Get([]byte("httpauth::userdata"), nil)
|
||||
if err != nil {
|
||||
log.Printf("Error getting `httpauth::userdata` from leveldb: %s", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var migrationData map[string]struct {
|
||||
Username string
|
||||
Email string
|
||||
Hash string
|
||||
Role string
|
||||
}
|
||||
err = json.Unmarshal(oldUserData, &migrationData)
|
||||
if err != nil {
|
||||
log.Printf("Error unmarshalling old ")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, datum := range migrationData {
|
||||
// check if password is "factorio", which was the default password in the old system
|
||||
decodedHash, err := base64.StdEncoding.DecodeString(datum.Hash)
|
||||
if err != nil {
|
||||
log.Printf("Error decoding base64 hash: %s", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword(decodedHash, []byte("factorio"))
|
||||
if err == nil {
|
||||
// password is "factorio" .. change it
|
||||
newPassword := GenerateRandomPassword()
|
||||
|
||||
bcryptPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
log.Printf("Error generating has from password: %s", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
datum.Hash = base64.StdEncoding.EncodeToString(bcryptPassword)
|
||||
|
||||
log.Println(`Migrated user in database. It still had default password "factorio" set. New credentials:`)
|
||||
log.Printf("Username: %s", datum.Username)
|
||||
log.Printf("Password: %s", newPassword)
|
||||
}
|
||||
|
||||
user := &User{
|
||||
Username: datum.Username,
|
||||
Password: datum.Hash,
|
||||
Role: datum.Role,
|
||||
Email: datum.Email,
|
||||
}
|
||||
|
||||
newDB.Create(user)
|
||||
}
|
||||
|
||||
oldDB.Close()
|
||||
|
||||
// delete oldDB
|
||||
log.Println("Deleting old leveldb database.")
|
||||
err = os.RemoveAll(oldDBFile)
|
||||
if err != nil {
|
||||
log.Printf("Error removing leveldb: %s", err)
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
var randLetters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
func GenerateRandomPassword() string {
|
||||
pass := make([]rune, 24)
|
||||
for i := range pass {
|
||||
pass[i] = randLetters[rand.Intn(len(randLetters))]
|
||||
}
|
||||
return string(pass)
|
||||
}
|
18
src/go.mod
18
src/go.mod
@ -3,23 +3,23 @@ module github.com/mroote/factorio-server-manager
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/apexskier/httpauth v1.3.2
|
||||
github.com/go-ini/ini v1.49.0
|
||||
github.com/go-sql-driver/mysql v1.4.1 // indirect
|
||||
github.com/golang/protobuf v1.3.1 // indirect
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/sessions v1.2.0 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.0
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/hpcloud/tail v1.0.0
|
||||
github.com/jessevdk/go-flags v1.4.0
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/lib/pq v1.2.0 // indirect
|
||||
github.com/majormjr/rcon v0.0.0-20120923215419-8fbb8268b60a
|
||||
github.com/mattn/go-sqlite3 v1.11.0 // indirect
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 // indirect
|
||||
github.com/stretchr/testify v1.6.1
|
||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf // indirect
|
||||
google.golang.org/appengine v1.6.5 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65 // indirect
|
||||
golang.org/x/text v0.3.2 // indirect
|
||||
gopkg.in/ini.v1 v1.49.0 // indirect
|
||||
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect
|
||||
gorm.io/driver/sqlite v1.1.4
|
||||
gorm.io/gorm v1.20.11
|
||||
)
|
||||
|
23
src/go.sum
23
src/go.sum
@ -1,13 +1,9 @@
|
||||
github.com/apexskier/httpauth v1.3.2 h1:PHwrq/eBRBLIrUthchpbDVTVR/ofBrj2LUcukCRhfXw=
|
||||
github.com/apexskier/httpauth v1.3.2/go.mod h1:aEHd6x648VCocEK0vTsPKkjJ1sBPab3Z4V4MJs9YZAE=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-ini/ini v1.49.0 h1:ymWFBUkwN3JFPjvjcJJ5TSTwh84M66QrH+8vOytLgRY=
|
||||
github.com/go-ini/ini v1.49.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||
@ -28,16 +24,18 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
|
||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/majormjr/rcon v0.0.0-20120923215419-8fbb8268b60a h1:rXEd7/SA5sJvgl2zxM/nNblGa9kkpnx2phQATclw9Xk=
|
||||
github.com/majormjr/rcon v0.0.0-20120923215419-8fbb8268b60a/go.mod h1:RNVV4T548mxgb643odZHF+pWh/YmGLxiKvtkbI1vRYE=
|
||||
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
|
||||
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ=
|
||||
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@ -75,19 +73,20 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.49.0 h1:MW0aLMiezbm/Ray0gJJ+nQFE2uOC9EpK2p5zPN3NqpM=
|
||||
gopkg.in/ini.v1 v1.49.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw=
|
||||
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
|
||||
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
|
||||
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
gorm.io/gorm v1.20.11 h1:jYHQ0LLUViV85V8dM1TP9VBBkfzKTnuTXDjYObkI6yc=
|
||||
gorm.io/gorm v1.20.11/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
|
@ -25,7 +25,7 @@ func main() {
|
||||
}
|
||||
|
||||
// Initialize authentication system
|
||||
api.GetAuth()
|
||||
api.SetupAuth()
|
||||
|
||||
// Initialize HTTP router -- also initializes websocket
|
||||
router := api.NewRouter()
|
||||
|
@ -32,14 +32,12 @@ const App = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleAuthenticationStatus = useCallback(async () => {
|
||||
const status = await user.status();
|
||||
if (status?.Username) {
|
||||
const handleAuthenticationStatus = useCallback(async (status) => {
|
||||
if (status?.username) {
|
||||
setIsAuthenticated(true);
|
||||
await updateServerStatus();
|
||||
|
||||
await updateServerStatus()
|
||||
socket.emit('server status subscribe');
|
||||
socket.on('server_status', updateServerStatus)
|
||||
socket.on('server_status', updateServerStatus);
|
||||
}
|
||||
},[]);
|
||||
|
||||
@ -47,7 +45,6 @@ const App = () => {
|
||||
const loggedOut = await user.logout();
|
||||
if (loggedOut) {
|
||||
setIsAuthenticated(false);
|
||||
history.push('/login');
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
14
ui/App/components/Error.jsx
Normal file
14
ui/App/components/Error.jsx
Normal file
@ -0,0 +1,14 @@
|
||||
import React from "react"
|
||||
|
||||
const Error = ({error, message}) => {
|
||||
if (error) {
|
||||
return (
|
||||
<span className="block text-red">
|
||||
{message}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export default Error
|
@ -6,15 +6,21 @@ export const Flash = () => {
|
||||
let [message, setMessage] = useState('');
|
||||
let [color, setColor] = useState('');
|
||||
|
||||
let flashListener = ({message, color}) => {
|
||||
setVisibility(true);
|
||||
setMessage(message);
|
||||
setColor(color);
|
||||
setTimeout(() => {
|
||||
setVisibility(false);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
Bus.addListener('flash', ({message, color}) => {
|
||||
setVisibility(true);
|
||||
setMessage(message);
|
||||
setColor(color);
|
||||
setTimeout(() => {
|
||||
setVisibility(false);
|
||||
}, 4000);
|
||||
});
|
||||
Bus.addListener('flash', flashListener);
|
||||
|
||||
return function () {
|
||||
Bus.removeListener('flash', flashListener);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
@ -11,22 +11,26 @@ import {Flash} from "../components/Flash";
|
||||
const Login = ({handleLogin}) => {
|
||||
const {register, handleSubmit, errors} = useForm();
|
||||
const history = useHistory();
|
||||
const location = useLocation()
|
||||
const location = useLocation();
|
||||
|
||||
const onSubmit = async data => {
|
||||
const loginAttempt = await user.login(data)
|
||||
if (loginAttempt?.Username) {
|
||||
await handleLogin();
|
||||
history.push('/');
|
||||
try {
|
||||
const loginAttempt = await user.login(data)
|
||||
if (loginAttempt?.username) {
|
||||
await handleLogin(loginAttempt);
|
||||
history.push('/');
|
||||
}
|
||||
} catch (e) {
|
||||
window.flash("Login failed. Username or Password wrong.", "red");
|
||||
}
|
||||
};
|
||||
|
||||
// on mount check if user is authenticated
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const status = await user.status()
|
||||
if (status?.Username) {
|
||||
await handleLogin();
|
||||
const status = await user.status();
|
||||
if (status?.username) {
|
||||
await handleLogin(status);
|
||||
history.push(location?.state?.from || '/');
|
||||
}
|
||||
})();
|
||||
|
@ -2,6 +2,7 @@ import Panel from "../../components/Panel";
|
||||
import React, {useCallback, useEffect, useState} from "react";
|
||||
import user from "../../../api/resources/user";
|
||||
import CreateUserForm from "./components/CreateUserForm";
|
||||
import ChangePasswordForm from "./components/ChangePasswordForm"
|
||||
import {faTrashAlt} from "@fortawesome/free-solid-svg-icons";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
|
||||
@ -55,9 +56,15 @@ const UserManagement = () => {
|
||||
}
|
||||
className="mb-4"
|
||||
/>
|
||||
<Panel
|
||||
title="Change Password"
|
||||
content={<ChangePasswordForm/>}
|
||||
className="mb-4"
|
||||
/>
|
||||
<Panel
|
||||
title="Create User"
|
||||
content={<CreateUserForm updateUserList={updateList}/>}
|
||||
className="mb-4"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
@ -0,0 +1,55 @@
|
||||
import {useForm} from "react-hook-form";
|
||||
import React from "react";
|
||||
import user from "../../../../api/resources/user";
|
||||
import Button from "../../../components/Button";
|
||||
import Label from "../../../components/Label";
|
||||
import Input from "../../../components/Input";
|
||||
import Error from "../../../components/Error";
|
||||
|
||||
const ChangePasswordForm = () => {
|
||||
const {register, handleSubmit, errors, watch} = useForm();
|
||||
const password = watch('new_password');
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
const res = await user.changePassword(data);
|
||||
if (res) {
|
||||
// Update successful
|
||||
window.flash("Password changed", "green")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="mb-4">
|
||||
<Label htmlFor="old_password" text="Old Password"/>
|
||||
<Input inputRef={register({required: true})}
|
||||
name="old_password"
|
||||
type="password"
|
||||
placeholder="Old Password"
|
||||
/>
|
||||
<Error error={errors.old_password} message="Old Password is required"/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Label htmlFor="new_password" text="New Password"/>
|
||||
<Input inputRef={register({required: true})}
|
||||
name="new_password"
|
||||
type="password"
|
||||
placeholder="New Password"
|
||||
/>
|
||||
<Error error={errors.new_password} message="New Password is required"/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Label htmlFor="new_password_confirmation" text="New Password Confirmation"/>
|
||||
<Input inputRef={register({required: true})}
|
||||
name="new_password_confirmation"
|
||||
type="password"
|
||||
placeholder="New Password"
|
||||
/>
|
||||
<Error error={errors.new_password_confirmation} message="New Password Confirmation is required"/>
|
||||
</div>
|
||||
<Button isSubmit={true} type="success">Change</Button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChangePasswordForm
|
@ -2,6 +2,9 @@ import {useForm} from "react-hook-form";
|
||||
import React from "react";
|
||||
import user from "../../../../api/resources/user";
|
||||
import Button from "../../../components/Button";
|
||||
import Label from "../../../components/Label";
|
||||
import Input from "../../../components/Input";
|
||||
import Error from "../../../components/Error";
|
||||
|
||||
const CreateUserForm = ({updateUserList}) => {
|
||||
|
||||
@ -9,7 +12,7 @@ const CreateUserForm = ({updateUserList}) => {
|
||||
const password = watch('password');
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
const res = user.add(data);
|
||||
const res = await user.add(data);
|
||||
if (res) {
|
||||
updateUserList()
|
||||
}
|
||||
@ -18,61 +21,53 @@ const CreateUserForm = ({updateUserList}) => {
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-white text-sm font-bold mb-2" htmlFor="username">
|
||||
Username
|
||||
</label>
|
||||
<input className="shadow appearance-none border w-full py-2 px-3 text-black"
|
||||
ref={register({required: true})}
|
||||
id="username"
|
||||
<Label htmlFor="username" text="Username"/>
|
||||
<Input inputRef={register({required: true})}
|
||||
name="username"
|
||||
type="text" placeholder="Username"/>
|
||||
{errors.username && <span className="block text-red">Username is required</span>}
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
/>
|
||||
<Error error={errors.username} message="Username is required"/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-white text-sm font-bold mb-2" htmlFor="username">
|
||||
Role
|
||||
</label>
|
||||
<Label htmlFor="role" text="Role" />
|
||||
<input className="shadow appearance-none border w-full py-2 px-3 text-black"
|
||||
ref={register({required: true})}
|
||||
id="role"
|
||||
name="role"
|
||||
value="admin"
|
||||
disabled={true}
|
||||
type="text" placeholder="Role"/>
|
||||
{errors.role && <span className="block text-red">Role is required</span>}
|
||||
type="text"
|
||||
placeholder="Role"
|
||||
/>
|
||||
<Error error={errors.role} message="Role is required"/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-white text-sm font-bold mb-2" htmlFor="username">
|
||||
Email
|
||||
</label>
|
||||
<input className="shadow appearance-none border w-full py-2 px-3 text-black"
|
||||
ref={register({required: true})}
|
||||
id="email"
|
||||
<Label htmlFor="email" text="Email"/>
|
||||
<Input inputRef={register({required: true})}
|
||||
name="email"
|
||||
type="text" placeholder="Email"/>
|
||||
{errors.email && <span className="block text-red">Email is required</span>}
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
/>
|
||||
<Error error={errors.email} message="Email is required"/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-white text-sm font-bold mb-2" htmlFor="username">
|
||||
Password
|
||||
</label>
|
||||
<input className="shadow appearance-none border w-full py-2 px-3 text-black"
|
||||
ref={register({required: true})}
|
||||
id="password"
|
||||
<Label htmlFor="password" text="Password"/>
|
||||
<Input inputRef={register({required: true})}
|
||||
name="password"
|
||||
type="password" placeholder="Password"/>
|
||||
{errors.password && <span className="block text-red">Password is required</span>}
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
/>
|
||||
<Error error={errors.password} message="Password is required"/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-white text-sm font-bold mb-2" htmlFor="username">
|
||||
Password Confirmation
|
||||
</label>
|
||||
<input className="shadow appearance-none border w-full py-2 px-3 text-black"
|
||||
ref={register({required: true, validate: confirmation => confirmation === password})}
|
||||
id="password_confirmation"
|
||||
<Label htmlFor="password_confirmation" text="Password Confirmation"/>
|
||||
<Input inputRef={register({required: true, validate: conformation => conformation === password})}
|
||||
name="password_confirmation"
|
||||
type="password" placeholder="Password Confirmation"/>
|
||||
{errors.password_confirmation && <span className="block text-red">Password Confirmation is required and must match the Password</span>}
|
||||
type="password"
|
||||
placeholder="Password Confirmation"
|
||||
/>
|
||||
<Error error={errors.password_confirmation} message="Password Confirmation is required and must match the Password"/>
|
||||
</div>
|
||||
<Button isSubmit={true} type="success">Save</Button>
|
||||
</form>
|
||||
|
@ -24,5 +24,9 @@ export default {
|
||||
delete: async (username) => {
|
||||
const response = await client.post('/api/user/remove', JSON.stringify({username}));
|
||||
return response.data;
|
||||
},
|
||||
changePassword: async data => {
|
||||
const response = await client.post('/api/user/password', data);
|
||||
return response.data;
|
||||
}
|
||||
}
|
126
ui/api/socket.js
126
ui/api/socket.js
@ -1,69 +1,83 @@
|
||||
import EventEmitter from "events";
|
||||
|
||||
const ws_scheme = window.location.protocol === "https:" ? "wss" : "ws";
|
||||
const socket = new WebSocket(ws_scheme + "://" + window.location.host + "/ws");
|
||||
|
||||
const bus = new EventEmitter();
|
||||
|
||||
bus.on('log subscribe', () => {
|
||||
socket.send(
|
||||
JSON.stringify(
|
||||
{
|
||||
room_name: "",
|
||||
controls: {
|
||||
type: "subscribe",
|
||||
value: "gamelog"
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
const ws_scheme = window.location.protocol === "https:" ? "wss" : "ws";
|
||||
|
||||
bus.on('log unsubscribe', () => {
|
||||
socket.send(
|
||||
JSON.stringify(
|
||||
{
|
||||
room_name: "",
|
||||
controls: {
|
||||
type: "unsubscribe",
|
||||
value: "gamelog"
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
})
|
||||
function connect() {
|
||||
const socket = new WebSocket(ws_scheme + "://" + window.location.host + "/ws");
|
||||
|
||||
bus.on('server status subscribe', () => {
|
||||
socket.send(
|
||||
JSON.stringify(
|
||||
{
|
||||
room_name: "",
|
||||
controls: {
|
||||
type: "subscribe",
|
||||
value: "server_status"
|
||||
bus.on('log subscribe', () => {
|
||||
socket.send(
|
||||
JSON.stringify(
|
||||
{
|
||||
room_name: "",
|
||||
controls: {
|
||||
type: "subscribe",
|
||||
value: "gamelog"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
bus.on('command send', command => {
|
||||
socket.send(
|
||||
JSON.stringify(
|
||||
{
|
||||
room_name: "",
|
||||
controls: {
|
||||
type: "command",
|
||||
value: command
|
||||
bus.on('log unsubscribe', () => {
|
||||
socket.send(
|
||||
JSON.stringify(
|
||||
{
|
||||
room_name: "",
|
||||
controls: {
|
||||
type: "unsubscribe",
|
||||
value: "gamelog"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
)
|
||||
);
|
||||
})
|
||||
|
||||
socket.onmessage = e => {
|
||||
const {room_name, message} = JSON.parse(e.data);
|
||||
bus.emit(room_name, message);
|
||||
bus.on('server status subscribe', () => {
|
||||
socket.send(
|
||||
JSON.stringify(
|
||||
{
|
||||
room_name: "",
|
||||
controls: {
|
||||
type: "subscribe",
|
||||
value: "server_status"
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
bus.on('command send', command => {
|
||||
socket.send(
|
||||
JSON.stringify(
|
||||
{
|
||||
room_name: "",
|
||||
controls: {
|
||||
type: "command",
|
||||
value: command
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
socket.onmessage = e => {
|
||||
const {room_name, message} = JSON.parse(e.data);
|
||||
bus.emit(room_name, message);
|
||||
}
|
||||
|
||||
socket.onerror = e => {
|
||||
socket.close();
|
||||
}
|
||||
|
||||
socket.onclose = e => {
|
||||
// reconnect after 5 seconds
|
||||
setTimeout(connect, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
connect();
|
||||
|
||||
export default bus;
|
Loading…
Reference in New Issue
Block a user