Browse Source

Stable version

pull/2/head
Per-Arne 5 years ago
parent
commit
3dd857a84c
  1. 47
      README.md
  2. BIN
      docs/images/0.png
  3. BIN
      docs/images/1.png
  4. BIN
      docs/images/2.png
  5. BIN
      docs/images/3.png
  6. BIN
      docs/images/4.png
  7. BIN
      docs/images/5.png
  8. BIN
      docs/images/6.png
  9. BIN
      docs/images/7.png
  10. BIN
      docs/images/8.png
  11. 25
      package-lock.json
  12. 13
      wg_dashboard_backend/database.py
  13. 8
      wg_dashboard_backend/db/user.py
  14. 92
      wg_dashboard_backend/db/wireguard.py
  15. 438
      wg_dashboard_backend/main.py
  16. 81
      wg_dashboard_backend/middleware.py
  17. 42
      wg_dashboard_backend/models.py
  18. 1
      wg_dashboard_backend/requirements.txt
  19. 0
      wg_dashboard_backend/routers/__init__.py
  20. 0
      wg_dashboard_backend/routers/v1/__init__.py
  21. 95
      wg_dashboard_backend/routers/v1/peer.py
  22. 138
      wg_dashboard_backend/routers/v1/server.py
  23. 86
      wg_dashboard_backend/routers/v1/user.py
  24. 25
      wg_dashboard_backend/routers/v1/wg.py
  25. 146
      wg_dashboard_backend/schemas.py
  26. 94
      wg_dashboard_backend/script/wireguard.py
  27. 16
      wg_dashboard_backend/templates/peer.j2
  28. 0
      wg_dashboard_backend/tests/__init__.py
  29. BIN
      wg_dashboard_backend/tests/database.db
  30. 80
      wg_dashboard_backend/tests/test_pytest.py
  31. 6
      wg_dashboard_frontend/angular.json
  32. 12
      wg_dashboard_frontend/browserslist
  33. 3792
      wg_dashboard_frontend/package-lock.json
  34. 12
      wg_dashboard_frontend/package.json
  35. 10
      wg_dashboard_frontend/src/app/app-routing.module.ts
  36. 61
      wg_dashboard_frontend/src/app/app.component.ts
  37. 13
      wg_dashboard_frontend/src/app/app.module.ts
  38. 80
      wg_dashboard_frontend/src/app/components/blank-layout-card/blank-layout-card.component.scss
  39. 10
      wg_dashboard_frontend/src/app/components/blank-layout-card/blank-layout-card.component.ts
  40. 1
      wg_dashboard_frontend/src/app/components/blank-layout-card/index.ts
  41. 2
      wg_dashboard_frontend/src/app/components/message-menu/index.ts
  42. 27
      wg_dashboard_frontend/src/app/components/message-menu/message-menu.component.html
  43. 54
      wg_dashboard_frontend/src/app/components/message-menu/message-menu.component.scss
  44. 19
      wg_dashboard_frontend/src/app/components/message-menu/message-menu.component.ts
  45. 44
      wg_dashboard_frontend/src/app/components/message-menu/message-menu.service.ts
  46. 2
      wg_dashboard_frontend/src/app/components/notification-menu/index.ts
  47. 27
      wg_dashboard_frontend/src/app/components/notification-menu/notification-menu.component.html
  48. 31
      wg_dashboard_frontend/src/app/components/notification-menu/notification-menu.component.scss
  49. 19
      wg_dashboard_frontend/src/app/components/notification-menu/notification-menu.component.ts
  50. 30
      wg_dashboard_frontend/src/app/components/notification-menu/notification-menu.service.ts
  51. 1
      wg_dashboard_frontend/src/app/components/sidebar/index.ts
  52. 3
      wg_dashboard_frontend/src/app/components/sidebar/sidebar.component.scss
  53. 15
      wg_dashboard_frontend/src/app/components/sidebar/sidebar.component.ts
  54. 4
      wg_dashboard_frontend/src/app/directives/var.directive.ts
  55. 7
      wg_dashboard_frontend/src/app/interfaces/peer.ts
  56. 5
      wg_dashboard_frontend/src/app/interfaces/server.ts
  57. 6
      wg_dashboard_frontend/src/app/interfaces/user.ts
  58. 42
      wg_dashboard_frontend/src/app/layout/layout.module.ts
  59. 37
      wg_dashboard_frontend/src/app/layout/layout/layout.component.html
  60. 4
      wg_dashboard_frontend/src/app/layout/layout/layout.component.scss
  61. 69
      wg_dashboard_frontend/src/app/layout/layout/layout.component.ts
  62. 26
      wg_dashboard_frontend/src/app/page/components/components.component.ts
  63. 32
      wg_dashboard_frontend/src/app/page/components/components.module.ts
  64. 65
      wg_dashboard_frontend/src/app/page/components/modal-confirm/modal-confirm.component.html
  65. 1
      wg_dashboard_frontend/src/app/page/components/modal-confirm/modal-confirm.component.scss
  66. 65
      wg_dashboard_frontend/src/app/page/components/modal-confirm/modal-confirm.component.ts
  67. 24
      wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.html
  68. 7
      wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.scss
  69. 48
      wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.ts
  70. 1
      wg_dashboard_frontend/src/app/page/dashboard/dashboard.component.css
  71. 10
      wg_dashboard_frontend/src/app/page/dashboard/dashboard.component.html
  72. 38
      wg_dashboard_frontend/src/app/page/dashboard/dashboard.component.ts
  73. 49
      wg_dashboard_frontend/src/app/page/dashboard/dashboard.module.ts
  74. 25
      wg_dashboard_frontend/src/app/page/dashboard/peer/peer.component.html
  75. 5
      wg_dashboard_frontend/src/app/page/dashboard/peer/peer.component.scss
  76. 70
      wg_dashboard_frontend/src/app/page/dashboard/peer/peer.component.ts
  77. 148
      wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html
  78. 13
      wg_dashboard_frontend/src/app/page/dashboard/server/server.component.scss
  79. 75
      wg_dashboard_frontend/src/app/page/dashboard/server/server.component.ts
  80. 18
      wg_dashboard_frontend/src/app/page/dashboard2/dashboard2.component.css
  81. 40
      wg_dashboard_frontend/src/app/page/dashboard2/dashboard2.component.spec.ts
  82. 44
      wg_dashboard_frontend/src/app/page/dashboard2/dashboard2.component.ts
  83. 49
      wg_dashboard_frontend/src/app/page/dashboard2/dashboard2.module.ts
  84. 84
      wg_dashboard_frontend/src/app/page/dashboard2/peer/peer.component.ts
  85. 248
      wg_dashboard_frontend/src/app/page/dashboard2/server/server.component.html
  86. 28
      wg_dashboard_frontend/src/app/page/dashboard2/server/server.component.scss
  87. 77
      wg_dashboard_frontend/src/app/page/dashboard2/server/server.component.ts
  88. 31
      wg_dashboard_frontend/src/app/page/page-routing.module.ts
  89. 22
      wg_dashboard_frontend/src/app/page/page.module.ts
  90. 73
      wg_dashboard_frontend/src/app/page/user/edit/edit.component.html
  91. 17
      wg_dashboard_frontend/src/app/page/user/edit/edit.component.ts
  92. 54
      wg_dashboard_frontend/src/app/page/user/login/login.component.html
  93. 16
      wg_dashboard_frontend/src/app/page/user/login/login.component.ts
  94. 14
      wg_dashboard_frontend/src/app/services/auth/auth.interceptor.ts
  95. 23
      wg_dashboard_frontend/src/app/services/auth/auth.service.ts
  96. 10
      wg_dashboard_frontend/src/app/services/config.service.ts
  97. 20
      wg_dashboard_frontend/src/app/services/data.service.ts
  98. 76
      wg_dashboard_frontend/src/app/services/server.service.ts
  99. 8
      wg_dashboard_frontend/src/app/validators/ip-address.validator.ts
  100. 8
      wg_dashboard_frontend/src/app/validators/number.validator.ts

47
README.md

@ -15,11 +15,14 @@ The interface runs in docker and requires the host to have installed wireguard,
* docker * docker
# Installation # Installation
## Docker
```bash ```bash
docker run -d \ docker run -d \
--cap-add NET_ADMIN \ --cap-add NET_ADMIN \
--name wireguard-manager \ --name wireguard-manager \
--net host \ --net host \
-p "51800-51900:51800-51900/udp" \
-v wireguard-manager:/config \ -v wireguard-manager:/config \
-e PORT="8888" \ -e PORT="8888" \
-e ADMIN_USERNAME="admin" \ -e ADMIN_USERNAME="admin" \
@ -27,11 +30,47 @@ docker run -d \
perara/wireguard-manager perara/wireguard-manager
``` ```
## Docker-compose
```yaml
wireguard:
container_name: wireguard-manager
image: perara/wireguard-manager
cap_add:
- NET_ADMIN
ports:
- 51800:51900/udp
- 8888:8888
volumes:
- ./ops/wireguard/_data:/config
environment:
HOST: 0.0.0.0
PORT: 8888
ADMIN_PASSWORD: admin
ADMIN_USERNAME: admin
WEB_CONCURRENCY: 1
```
# Environment variables
| Environment | Description | Recommended |
|------------------|--------------------------------------------------------------------------|-------------|
| GUNICORN_CONF | Location of custom gunicorn configuration | default |
| WORKERS_PER_CORE | How many concurrent workers should there be per available core (Gunicorn | default |
| WEB_CONCURRENCY | The number of worker processes for handling requests. (Gunicorn) | 1 |
| HOST | 0.0.0.0 or unix:/tmp/gunicorn.sock if reverse proxy. Remember to mount | 0.0.0.0 |
| PORT | The port to use if running with IP host bind | 80 |
| LOG_LEVEL | Logging level of gunicorn/python | info |
| ADMIN_USERNAME | Default admin username on database creation | admin |
| ADMIN_PASSWORD | Default admin password on database creation | admin |
# Usage # Usage
When docker container is started, go to http://localhost:80 When docker container is started, go to http://localhost:80
# Reverse Proxy
Use jwilder/nginx-proxy or similar.
# Showcase # Showcase
![Illustration](docs/images/0.png)
![Illustration](docs/images/1.png) ![Illustration](docs/images/1.png)
![Illustration](docs/images/2.png) ![Illustration](docs/images/2.png)
@ -40,6 +79,14 @@ When docker container is started, go to http://localhost:80
![Illustration](docs/images/4.png) ![Illustration](docs/images/4.png)
![Illustration](docs/images/5.png)
![Illustration](docs/images/6.png)
![Illustration](docs/images/7.png)
![Illustration](docs/images/8.png)
# Roadmap # Roadmap
* Eventual bugfixes * Eventual bugfixes
* Improve Auth * Improve Auth

BIN
docs/images/0.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

BIN
docs/images/1.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 94 KiB

BIN
docs/images/2.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 104 KiB

BIN
docs/images/3.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

After

Width:  |  Height:  |  Size: 74 KiB

BIN
docs/images/4.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 187 KiB

BIN
docs/images/5.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

BIN
docs/images/6.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

BIN
docs/images/7.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

BIN
docs/images/8.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

25
package-lock.json

@ -0,0 +1,25 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"@angular/cdk": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-9.2.0.tgz",
"integrity": "sha512-jeeznvNDpR9POuxzz8Y0zFvMynG9HCJo3ZPTqOjlOq8Lj8876+rLsHDvKEMeLdwlkdi1EweYJW1CLQzI+TwqDA==",
"requires": {
"parse5": "^5.0.0"
}
},
"@angular/flex-layout": {
"version": "9.0.0-beta.29",
"resolved": "https://registry.npmjs.org/@angular/flex-layout/-/flex-layout-9.0.0-beta.29.tgz",
"integrity": "sha512-93sxR+kYfYMOdnlWL0Q77FZ428gg8XnBu0YZm6GsCdkw/vLggIT/G1ZAqHlCPIODt6pxmCJ5KXh4ShvniIYDsA=="
},
"parse5": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
"integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
"optional": true
}
}
}

13
wg_dashboard_backend/database.py

@ -0,0 +1,13 @@
import sqlalchemy
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import const
engine = sqlalchemy.create_engine(
const.DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

8
wg_dashboard_backend/db/user.py

@ -6,16 +6,8 @@ from passlib.context import CryptContext
import schemas import schemas
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
def update_user(sess: Session, form_data: schemas.UserInDB): def update_user(sess: Session, form_data: schemas.UserInDB):
user = get_user_by_name(sess, form_data.username) user = get_user_by_name(sess, form_data.username)

92
wg_dashboard_backend/db/wireguard.py

@ -5,7 +5,7 @@ import typing
import const import const
import script.wireguard import script.wireguard
from sqlalchemy import exists from sqlalchemy import exists
from sqlalchemy.orm import Session from sqlalchemy.orm import Session, joinedload
import util import util
import models import models
import schemas import schemas
@ -17,43 +17,9 @@ _LOGGER.setLevel(logging.DEBUG)
def start_client(sess: Session, peer: schemas.WGPeer): def start_client(sess: Session, peer: schemas.WGPeer):
db_peer: models.WGPeer = peer_query_get_by_address(sess, peer.address, peer.server).one() db_peer: models.WGPeer = peer_query_get_by_address(sess, peer.address, peer.server).one()
client_file = os.path.join(const.CLIENT_DIR(db_peer.server_ref.interface), str(db_peer.id) + ".conf") client_file = os.path.join(const.CLIENT_DIR(db_peer.server.interface), str(db_peer.id) + ".conf")
import subprocess import subprocess
output = subprocess.check_output(const.CMD_WG_QUICK + ["up", client_file], stderr=subprocess.STDOUT) output = subprocess.check_output(const.CMD_WG_QUICK + ["up", client_file], stderr=subprocess.STDOUT)
print(output)
def server_generate_config(sess: Session, server: schemas.WGServer):
db_server: models.WGServer = server_query_get_by_interface(sess, server.interface).one()
result = util.jinja_env.get_template("server.j2").render(
data=db_server
)
interface = db_server.interface
server_file = const.SERVER_FILE(interface)
with open(server_file, "w+") as f:
f.write(result)
os.chmod(server_file, 0o600)
return result
def peer_generate_config(sess: Session, peer: schemas.WGPeer):
db_peer: models.WGPeer = peer_query_get_by_address(sess, peer.address, peer.server).one()
result = util.jinja_env.get_template("peer.j2").render(
data=db_peer
)
peer_file = const.PEER_FILE(db_peer)
with open(peer_file, "w+") as f:
f.write(result)
os.chmod(peer_file, 0o600)
return result
def peer_query_get_by_address(sess: Session, address: str, server: str): def peer_query_get_by_address(sess: Session, address: str, server: str):
@ -62,42 +28,6 @@ def peer_query_get_by_address(sess: Session, address: str, server: str):
.filter(models.WGPeer.server == server) .filter(models.WGPeer.server == server)
def peer_insert(sess: Session, peer: schemas.WGPeer) -> schemas.WGPeer:
db_server: models.WGServer = server_query_get_by_interface(sess, peer.server).one()
db_peer = models.WGPeer(**peer.dict())
address_space = set(ipaddress.ip_network(db_server.address, strict=False).hosts())
occupied_space = set()
for p in db_server.peers:
try:
occupied_space.add(ipaddress.ip_address(p.address.split("/")[0]))
except ValueError as e:
print(e)
pass # Ignore invalid addresses. These are out of address_space
address_space -= occupied_space
# Select first available address
db_peer.address = str(list(address_space).pop(0)) + "/32"
# Private public key generation
private_key, public_key = script.wireguard.generate_keys()
db_peer.private_key = private_key
db_peer.public_key = public_key
# Set 0.0.0.0/0, ::/0 as default allowed ips
db_peer.allowed_ips = ', '.join(const.PEER_DEFAULT_ALLOWED_IPS)
# Set unnamed
db_peer.name = "Unnamed"
db_peer.dns = db_server.endpoint
sess.add(db_peer)
sess.commit()
return peer.from_orm(db_peer)
def peer_dns_set(sess: Session, peer: schemas.WGPeer) -> schemas.WGPeer: def peer_dns_set(sess: Session, peer: schemas.WGPeer) -> schemas.WGPeer:
db_peer: models.WGPeer = peer_query_get_by_address(sess, peer.address, peer.server).one() db_peer: models.WGPeer = peer_query_get_by_address(sess, peer.address, peer.server).one()
db_peer.dns = peer.dns db_peer.dns = peer.dns
@ -109,14 +39,11 @@ def peer_dns_set(sess: Session, peer: schemas.WGPeer) -> schemas.WGPeer:
def peer_remove(sess: Session, peer: schemas.WGPeer) -> bool: def peer_remove(sess: Session, peer: schemas.WGPeer) -> bool:
db_peers: models.WGPeer = peer_query_get_by_address(sess, peer.address, peer.server).all() db_peers = peer.filter_query(sess).all()
for db_peer in db_peers: for db_peer in db_peers:
sess.delete(db_peer) sess.delete(db_peer)
sess.commit() sess.commit()
try:
os.remove(const.PEER_FILE(db_peer))
except:
pass
return True return True
@ -181,17 +108,6 @@ def server_get_all(sess: Session) -> typing.List[schemas.WGServer]:
return [schemas.WGServer.from_orm(db_interface) for db_interface in db_interfaces] return [schemas.WGServer.from_orm(db_interface) for db_interface in db_interfaces]
def server_add(sess: Session, server: schemas.WGServer) -> schemas.WGServer:
if sess.query(exists().where(models.WGServer.interface == server.interface)).scalar():
raise ValueError("The server interface %s already exists in the database" % server.interface)
db_server = server.convert()
sess.add(db_server)
sess.commit()
return server.from_orm(db_server)
def server_remove(sess: Session, server: schemas.WGServer) -> bool: def server_remove(sess: Session, server: schemas.WGServer) -> bool:
db_server = server_query_get_by_interface(sess, server.interface).one() db_server = server_query_get_by_interface(sess, server.interface).one()
if db_server is None: if db_server is None:

438
wg_dashboard_backend/main.py

@ -1,7 +1,11 @@
import logging import logging
import os import os
from sqlalchemy_utils import database_exists from sqlalchemy_utils import database_exists
from starlette.middleware.base import BaseHTTPMiddleware
import middleware
from database import engine, SessionLocal
from routers.v1 import user, server, peer, wg
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
@ -13,404 +17,91 @@ if not logger.hasHandlers():
import pkg_resources import pkg_resources
import uvicorn as uvicorn import uvicorn as uvicorn
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
import databases from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker, Session from starlette.responses import FileResponse
from starlette.responses import FileResponse, JSONResponse from fastapi import Depends, FastAPI
import sqlalchemy
import const
from datetime import datetime, timedelta
import db.wireguard
import db.user
import jwt
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jwt import PyJWTError
import script.wireguard
import typing
import models
import schemas
database = databases.Database(const.DATABASE_URL)
engine = sqlalchemy.create_engine( import models
const.DATABASE_URL, connect_args={"check_same_thread": False}
)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/token")
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
app = FastAPI() app = FastAPI()
app.add_middleware(BaseHTTPMiddleware, dispatch=middleware.db_session_middleware)
if not database_exists(engine.url):
models.Base.metadata.create_all(engine)
# Create default user
_db: Session = SessionLocal()
_db.add(models.User(
username=os.getenv("ADMIN_USERNAME", "admin"),
password=middleware.get_password_hash(os.getenv("ADMIN_PASSWORD", "admin")),
full_name="Admin",
role="admin",
email=""
))
_db.commit()
_db.close()
app.include_router(
user.router,
prefix="/api/v1",
tags=["user"],
dependencies=[],
responses={404: {"description": "Not found"}}
)
# Dependency
def get_db():
try:
db = SessionLocal()
yield db
finally:
db.close()
def create_access_token(*, data: dict, expires_delta: timedelta = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, const.SECRET_KEY, algorithm=const.ALGORITHM)
return encoded_jwt
def auth(token: str = Depends(oauth2_scheme), sess: Session = Depends(get_db)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, const.SECRET_KEY, algorithms=[const.ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except PyJWTError:
raise credentials_exception
user = db.user.get_user_by_name(sess, username)
if user is None:
raise credentials_exception
return user
@app.get("/api/logout")
def logout(user: schemas.User = Depends(auth)):
# TODO
return {}
@app.post("/api/user/edit", response_model=schemas.User)
def edit(form_data: schemas.UserInDB, user: schemas.User = Depends(auth), sess: Session = Depends(get_db)):
form_data.password = db.user.get_password_hash(form_data.password)
db_user = db.user.update_user(sess, form_data)
return schemas.User.from_orm(db_user)
@app.post("/api/login", response_model=schemas.Token)
def login(form_data: OAuth2PasswordRequestForm = Depends(), sess: Session = Depends(get_db)):
user = db.user.authenticate_user(sess, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
# Create token
access_token_expires = timedelta(minutes=const.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer", "user": schemas.User.from_orm(user)}
# @app.post("/wg/update/", response_model=List[schemas.WireGuard])
@app.get("/api/wg/server/all", response_model=typing.List[schemas.WGServer])
def get_interfaces(
sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
interfaces = db.wireguard.server_get_all(sess)
for iface in interfaces:
iface.is_running = script.wireguard.is_running(iface)
return interfaces app.include_router(
server.router,
prefix="/api/v1/server",
tags=["server"],
dependencies=[Depends(middleware.auth)],
responses={404: {"description": "Not found"}}
)
@app.post("/api/wg/server/add", response_model=schemas.WGServer) app.include_router(
def add_interface( peer.router,
form_data: schemas.WGServer, prefix="/api/v1/peer",
sess: Session = Depends(get_db), tags=["peer"],
user: schemas.User = Depends(auth) dependencies=[Depends(middleware.auth)],
): responses={404: {"description": "Not found"}}
if form_data.interface is None or form_data.listen_port is None or form_data.address is None: )
raise HTTPException(status_code=400,
detail="Interface, Listen-Port and Address must be included in the schema.")
try:
form_data.post_up = form_data.post_up if form_data.post_up != "" else const.DEFAULT_POST_UP
form_data.post_down = form_data.post_up if form_data.post_up != "" else const.DEFAULT_POST_UP
wg_server = db.wireguard.server_add(sess, form_data) app.include_router(
wg.router,
prefix="/api/v1/wg",
tags=["wg"],
dependencies=[Depends(middleware.auth)],
responses={404: {"description": "Not found"}}
)
# Public/Private key
private_key, public_key = script.wireguard.generate_keys()
wg_server.private_key = private_key
wg_server.public_key = public_key
db.wireguard.server_key_pair_set(sess, wg_server)
db.wireguard.server_generate_config(sess, wg_server)
return wg_server
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@app.post("/api/wg/server/edit", response_model=schemas.WGServer)
def edit_server(
data: dict, sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
interface = data["interface"]
server = schemas.WGServer(**data["server"])
# Stop if running
old = schemas.WGServer(interface=interface)
if script.wireguard.is_running(old):
script.wireguard.stop_interface(old)
fields = set(old.__fields__) - {"peers", "is_running"}
if not db.wireguard.server_update_field(sess, interface, server, fields):
raise HTTPException(400, detail="Were not able to edit %s" % old.interface)
script.wireguard.start_interface(server)
return server
@app.get("/api/wg/generate_keypair", response_model=schemas.KeyPair)
def generate_key_pair(
user: schemas.User = Depends(auth)
):
private_key, public_key = script.wireguard.generate_keys()
return schemas.KeyPair(
private_key=private_key,
public_key=public_key
)
@app.get("/api/wg/generate_psk", response_model=schemas.PSK)
def generate_psk(user: schemas.User = Depends(auth)):
return schemas.PSK(
psk=script.wireguard.generate_psk()
)
@app.post("/api/wg/server/stop", response_model=schemas.WGServer)
def start_server(
form_data: schemas.WGServer,
user: schemas.User = Depends(auth)
):
script.wireguard.stop_interface(form_data)
form_data.is_running = script.wireguard.is_running(form_data)
return form_data
@app.post("/api/wg/server/start", response_model=schemas.WGServer)
def start_server(
form_data: schemas.WGServer,
sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
db.wireguard.server_generate_config(sess, form_data)
script.wireguard.start_interface(form_data)
form_data.is_running = script.wireguard.is_running(form_data)
return form_data
@app.post("/api/wg/server/restart", response_model=schemas.WGServer)
def start_server(
form_data: schemas.WGServer,
sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
db.wireguard.server_generate_config(sess, form_data)
script.wireguard.restart_interface(form_data)
form_data.is_running = script.wireguard.is_running(form_data)
return form_data
@app.post("/api/wg/server/delete", response_model=schemas.WGServer)
def delete_server(
form_data: schemas.WGServer,
sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
# Stop if running
if script.wireguard.is_running(form_data):
script.wireguard.stop_interface(form_data)
if not db.wireguard.server_remove(sess, form_data):
raise HTTPException(400, detail="Were not able to delete %s" % form_data.interface)
return form_data
@app.post("/api/wg/server/peer/add", response_model=schemas.WGPeer)
def add_peer(
form_data: schemas.WGServer,
sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
wg_peer = schemas.WGPeer(server=form_data.interface)
# Insert initial peer
wg_peer = db.wireguard.peer_insert(sess, wg_peer)
# If server is running. Add peer
if script.wireguard.is_running(form_data):
script.wireguard.add_peer(form_data, wg_peer)
db.wireguard.peer_generate_config(sess, wg_peer)
return wg_peer
@app.post("/api/wg/server/peer/delete", response_model=schemas.WGPeer)
def delete_peer(
form_data: schemas.WGPeer,
sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
if not db.wireguard.peer_remove(sess, form_data):
raise HTTPException(400, detail="Were not able to delete peer %s (%s)" % (form_data.name, form_data.public_key))
server = schemas.WGServer(interface=form_data.server)
if script.wireguard.is_running(server):
script.wireguard.remove_peer(server, form_data)
return form_data
@app.post("/api/wg/server/peer/edit", response_model=schemas.WGPeer)
def edit_peer(
form_data: schemas.WGPeer,
sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
wg_peer = db.wireguard.peer_update(sess, form_data)
db.wireguard.peer_generate_config(sess, wg_peer)
return wg_peer
@app.post("/api/wg/server/stats")
def edit_peer(
form_data: schemas.WGServer,
user: schemas.User = Depends(auth)
):
stats = script.wireguard.get_stats(form_data)
return JSONResponse(content=stats)
@app.post("/api/wg/server/peer/config", response_model=schemas.WGPeerConfig)
def config_peer(
form_data: schemas.WGPeer,
sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
db_peer = db.wireguard.peer_query_get_by_address(sess, form_data.address, form_data.server).one()
with open(const.PEER_FILE(db_peer), "r") as f:
conf_file = f.read()
return schemas.WGPeerConfig(config=conf_file)
@app.post("/api/wg/server/config", response_model=schemas.WGPeerConfig)
def config_server(
form_data: schemas.WGServer,
user: schemas.User = Depends(auth)
):
with open(const.SERVER_FILE(form_data.interface), "r") as f:
conf_file = f.read()
return schemas.WGPeerConfig(config=conf_file)
@app.get("/", include_in_schema=True)
def root():
return FileResponse('build/index.html')
@app.post("/api/users/create/")
def create_user(
form_data: schemas.UserInDB,
sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
user = db.user.get_user_by_name(sess, form_data.username)
# User already exists app.mount("/", StaticFiles(directory=pkg_resources.resource_filename(__name__, 'build')), name="static")
if user:
if not db.user.authenticate_user(sess, form_data.username, form_data.password):
raise HTTPException(status_code=401, detail="Incorrect password")
else:
# Create the user
if not db.user.create_user(sess, models.User(
username=form_data.username,
password=form_data.password,
full_name=form_data.full_name,
email=form_data.email,
role=form_data.role,
)):
raise HTTPException(status_code=400, detail="Could not create user")
return login_for_access_token(OAuth2PasswordRequestForm(
username=form_data.username,
password=form_data.password,
scope=""
), sess)
@app.on_event("startup") @app.on_event("startup")
async def startup(): async def startup():
await database.connect() pass
# TODO - Fix
if not database_exists(engine.url):
models.Base.metadata.create_all(engine)
# Create default user
_db: Session = SessionLocal()
_db.add(models.User(
username=os.getenv("ADMIN_USERNAME", "admin"),
password=db.user.get_password_hash(os.getenv("ADMIN_PASSWORD", "admin")),
full_name="Admin",
role="admin",
email=""
))
_db.commit()
_db.close()
@app.on_event("shutdown") @app.on_event("shutdown")
async def shutdown(): async def shutdown():
await database.disconnect() pass
@app.get("/", include_in_schema=True)
def root():
return FileResponse('build/index.html')
app.mount("/", StaticFiles(directory=pkg_resources.resource_filename(__name__, 'build')), name="static")
# @app.get("/")
# async def read_root():
# return {"Hello": "World"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
return {"item_id": item_id, "q": q}
if __name__ == "__main__": if __name__ == "__main__":
async def main(): """async def main():
if not script.wireguard.is_installed(): if not script.wireguard.is_installed():
print("NOT INSTALLED!") print("NOT INSTALLED!")
exit(0) exit(0)
@ -507,11 +198,6 @@ if __name__ == "__main__":
script.wireguard.add_peer(wg_interface, wg_peer) script.wireguard.add_peer(wg_interface, wg_peer)
script.wireguard.remove_peer(wg_interface, wg_peer) script.wireguard.remove_peer(wg_interface, wg_peer)
db.wireguard.start_client(sess, wg_peer) db.wireguard.start_client(sess, wg_peer)"""
# loop = asyncio.get_event_loop()
# loop.create_task(main())
# asyncio.get_event_loop().run_forever()
uvicorn.run("__main__:app", reload=True) uvicorn.run("__main__:app", reload=True)

81
wg_dashboard_backend/middleware.py

@ -0,0 +1,81 @@
from datetime import timedelta, datetime
import jwt
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
from jwt import PyJWTError
from passlib.context import CryptContext
from sqlalchemy.orm import Session
from starlette import status
from starlette.requests import Request
from starlette.responses import Response
import const
import schemas
from database import SessionLocal
import db.user
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/login", auto_error=False)
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def get_password_hash(password):
return pwd_context.hash(password)
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
async def db_session_middleware(request: Request, call_next):
response = Response("Internal server error (Database error)", status_code=500)
try:
request.state.db = SessionLocal()
response = await call_next(request)
finally:
request.state.db.close()
return response
# NON MIDDLEWARE MIDDLEWARISH THING
# Dependency
def get_db(request: Request):
return request.state.db
def create_access_token(*, data: dict, expires_delta: timedelta = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, const.SECRET_KEY, algorithm=const.ALGORITHM)
return encoded_jwt
def auth(token: str = Depends(oauth2_scheme), sess: Session = Depends(get_db)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, const.SECRET_KEY, algorithms=[const.ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except PyJWTError:
raise credentials_exception
user = schemas.User.from_orm(
schemas.UserInDB(username=username, password="").from_db(sess)
)
if user is None:
raise credentials_exception
return user

42
wg_dashboard_backend/models.py

@ -1,9 +1,8 @@
import sqlalchemy import sqlalchemy
from sqlalchemy import Integer, Column from sqlalchemy import Integer, Column
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship, backref
from sqlalchemy.orm import relationship
Base = declarative_base() from database import Base
class User(Base): class User(Base):
@ -17,21 +16,6 @@ class User(Base):
role = Column(sqlalchemy.String) role = Column(sqlalchemy.String)
class WGPeer(Base):
__tablename__ = "peer"
id = Column(Integer, primary_key=True, index=True)
name = Column(sqlalchemy.String, default="Unnamed")
address = Column(sqlalchemy.String)
public_key = Column(sqlalchemy.String)
private_key = Column(sqlalchemy.String)
dns = Column(sqlalchemy.String)
allowed_ips = Column(sqlalchemy.String)
server = Column(Integer, sqlalchemy.ForeignKey('server.interface'))
server_ref = relationship("WGServer", backref="server")
class WGServer(Base): class WGServer(Base):
__tablename__ = "server" __tablename__ = "server"
@ -46,7 +30,25 @@ class WGServer(Base):
post_up = Column(sqlalchemy.String) post_up = Column(sqlalchemy.String)
post_down = Column(sqlalchemy.String) post_down = Column(sqlalchemy.String)
is_running = Column(sqlalchemy.Boolean) is_running = Column(sqlalchemy.Boolean)
configuration = Column(sqlalchemy.Text)
peers = relationship("WGPeer", cascade="all, delete", passive_deletes=True, lazy="joined")
class WGPeer(Base):
__tablename__ = "peer"
id = Column(Integer, primary_key=True, index=True)
name = Column(sqlalchemy.String, default="Unnamed")
address = Column(sqlalchemy.String)
public_key = Column(sqlalchemy.String)
private_key = Column(sqlalchemy.String)
dns = Column(sqlalchemy.String)
allowed_ips = Column(sqlalchemy.String)
server_id = Column(Integer, sqlalchemy.ForeignKey('server.id', ondelete="CASCADE", onupdate="CASCADE"))
server = relationship("WGServer", backref=backref("server"))
configuration = Column(sqlalchemy.Text)
peers = relationship("WGPeer", backref="peer")

1
wg_dashboard_backend/requirements.txt

@ -10,3 +10,4 @@ bcrypt
python-multipart python-multipart
jinja2 jinja2
sqlalchemy_utils sqlalchemy_utils
requests

0
wg_dashboard_frontend/src/app/pages/dashboard/add-server/add-server.component.scss → wg_dashboard_backend/routers/__init__.py

0
wg_dashboard_frontend/src/app/pages/dashboard/server/server.component.scss → wg_dashboard_backend/routers/v1/__init__.py

95
wg_dashboard_backend/routers/v1/peer.py

@ -0,0 +1,95 @@
import ipaddress
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
import const
import models
import schemas
import middleware
import db.wireguard
import script.wireguard
router = APIRouter()
@router.post("/add", response_model=schemas.WGPeer)
def add_peer(
peer_add: schemas.WGPeerAdd,
sess: Session = Depends(middleware.get_db)
):
server = schemas.WGServer(interface=peer_add.server_interface).from_db(sess)
peer = schemas.WGPeer(server_id=server.id)
address_space = set(ipaddress.ip_network(server.address, strict=False).hosts())
occupied_space = set()
for p in server.peers:
try:
occupied_space.add(ipaddress.ip_address(p.address.split("/")[0]))
except ValueError as e:
pass # Ignore invalid addresses. These are out of address_space
address_space -= occupied_space
# Select first available address
peer.address = str(list(sorted(address_space)).pop(0)) + "/32"
# Private public key generation
keys = script.wireguard.generate_keys()
peer.private_key = keys["private_key"]
peer.public_key = keys["public_key"]
# Set 0.0.0.0/0, ::/0 as default allowed ips
peer.allowed_ips = ', '.join(const.PEER_DEFAULT_ALLOWED_IPS)
# Set unnamed
peer.name = "Unnamed"
peer.dns = server.endpoint
peer.configuration = script.wireguard.generate_config(dict(
peer=peer,
server=server
))
peer.sync(sess)
# If server is running. Add peer
if script.wireguard.is_running(server):
script.wireguard.add_peer(server, peer)
return peer
@router.post("/delete", response_model=schemas.WGPeer)
def delete_peer(
peer: schemas.WGPeer,
sess: Session = Depends(middleware.get_db)
):
peer.from_db(sess) # Sync full object
if not db.wireguard.peer_remove(sess, peer):
raise HTTPException(400, detail="Were not able to delete peer %s (%s)" % (peer.name, peer.public_key))
server = schemas.WGServer(interface=peer.server_id)
if script.wireguard.is_running(server):
script.wireguard.remove_peer(server, peer)
return peer
@router.post("/edit", response_model=schemas.WGPeer)
def edit_peer(
peer: schemas.WGPeer,
sess: Session = Depends(middleware.get_db)
):
server = schemas.WGServer(interface="")\
.from_orm(sess.query(models.WGServer).filter_by(id=peer.server_id).one())
peer.configuration = script.wireguard.generate_config(dict(
peer=peer,
server=server
))
peer.sync(sess)
return peer

138
wg_dashboard_backend/routers/v1/server.py

@ -0,0 +1,138 @@
import tempfile
from os.path import exists
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from starlette.responses import JSONResponse
import const
import schemas
import middleware
import db.wireguard
import script.wireguard
import typing
router = APIRouter()
@router.get("/all", response_model=typing.List[schemas.WGServer])
def servers_all(
sess: Session = Depends(middleware.get_db)
):
interfaces = db.wireguard.server_get_all(sess)
for iface in interfaces:
iface.is_running = script.wireguard.is_running(iface)
return interfaces
@router.post("/add", response_model=schemas.WGServer)
def add_interface(
server: schemas.WGServerAdd,
sess: Session = Depends(middleware.get_db)
):
server.post_up = server.post_up if server.post_up != "" else const.DEFAULT_POST_UP
server.post_down = server.post_up if server.post_up != "" else const.DEFAULT_POST_DOWN
# Public/Private key
try:
if server.filter_query(sess).count() != 0:
raise HTTPException(status_code=400, detail="The server interface %s already exists in the database" % server.interface)
keys = script.wireguard.generate_keys()
server.private_key = keys["private_key"]
server.public_key = keys["public_key"]
server.configuration = script.wireguard.generate_config(server)
server.sync(sess)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
return server
@router.post("/stop", response_model=schemas.WGServer)
def start_server(
form_data: schemas.WGServer
):
script.wireguard.stop_interface(form_data)
form_data.is_running = script.wireguard.is_running(form_data)
return form_data
@router.post("/start", response_model=schemas.WGServer)
def start_server(
server: schemas.WGServer,
sess: Session = Depends(middleware.get_db)
):
script.wireguard.start_interface(server)
server.is_running = script.wireguard.is_running(server)
server.sync(sess)
return server
@router.post("/restart", response_model=schemas.WGServer)
def restart_server(
server: schemas.WGServer,
sess: Session = Depends(middleware.get_db)
):
script.wireguard.restart_interface(server)
server.is_running = script.wireguard.is_running(server)
server.sync(sess)
return server
@router.post("/delete", response_model=schemas.WGServer)
def delete_server(
form_data: schemas.WGServer,
sess: Session = Depends(middleware.get_db)
):
# Stop if running
if script.wireguard.is_running(form_data):
script.wireguard.stop_interface(form_data)
if not db.wireguard.server_remove(sess, form_data):
raise HTTPException(400, detail="Were not able to delete %s" % form_data.interface)
return form_data
@router.post("/stats", dependencies=[Depends(middleware.auth)])
def stats_server(server: schemas.WGServer):
stats = script.wireguard.get_stats(server)
return JSONResponse(content=stats)
@router.post("/edit", response_model=schemas.WGServer)
def edit_server(
data: dict, sess: Session = Depends(middleware.get_db)
):
interface = data["interface"]
old = schemas.WGServer(interface=interface).from_db(sess)
# Stop if running
if script.wireguard.is_running(old):
script.wireguard.stop_interface(old)
# Update server
server = schemas.WGServer(**data["server"])
server.configuration = script.wireguard.generate_config(server)
server = old.update(sess, new=server)
# Update peers
for peer_data in server.peers:
peer = schemas.WGPeer(**peer_data)
peer.configuration = script.wireguard.generate_config(dict(
peer=peer,
server=server
))
peer.sync(sess)
script.wireguard.start_interface(server)
server.is_running = script.wireguard.is_running(server)
server.sync(sess)
server.from_db(sess)
return server

86
wg_dashboard_backend/routers/v1/user.py

@ -0,0 +1,86 @@
from datetime import timedelta
from fastapi import APIRouter, HTTPException, Depends, Form
from sqlalchemy.orm import Session
from starlette import status
import const
import db.user
import middleware
import models
import schemas
router = APIRouter()
@router.get("/logout")
def logout(user: schemas.User = Depends(middleware.auth)):
return dict(message="ok")
@router.post("/user/edit", response_model=schemas.User)
def edit(form_data: schemas.UserInDB,
user: schemas.UserInDB = Depends(middleware.auth),
sess: Session = Depends(middleware.get_db)
):
form_data.password = middleware.get_password_hash(form_data.password)
form_data.sync(sess)
return form_data
@router.post("/login", response_model=schemas.Token)
def login(*, username: str = Form(...), password: str = Form(...), sess: Session = Depends(middleware.get_db)):
user: schemas.UserInDB = schemas.UserInDB(username=username, password="").from_db(sess)
# Verify password
if not user or not middleware.verify_password(password, user.password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
# Create token
access_token_expires = timedelta(minutes=const.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = middleware.create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return schemas.Token(
access_token=access_token,
token_type="bearer",
user=schemas.User(**user.dict())
)
@router.post("/users/create/")
def create_user(
form_data: schemas.UserInDB,
sess: Session = Depends(middleware.get_db),
user: schemas.User = Depends(middleware.auth)
):
user = db.user.get_user_by_name(sess, form_data.username)
# User already exists
if user:
if not db.user.authenticate_user(sess, form_data.username, form_data.password):
raise HTTPException(status_code=401, detail="Incorrect password")
else:
# Create the user
if not db.user.create_user(sess, models.User(
username=form_data.username,
password=form_data.password,
full_name=form_data.full_name,
email=form_data.email,
role=form_data.role,
)):
raise HTTPException(status_code=400, detail="Could not create user")
return login_for_access_token(OAuth2PasswordRequestForm(
username=form_data.username,
password=form_data.password,
scope=""
), sess)

25
wg_dashboard_backend/routers/v1/wg.py

@ -0,0 +1,25 @@
from fastapi import APIRouter
import middleware
import schemas
import script.wireguard
router = APIRouter()
@router.get("/generate_psk", response_model=schemas.PSK)
def generate_psk():
return schemas.PSK(
psk=script.wireguard.generate_psk()
)
@router.get("/generate_keypair", response_model=schemas.KeyPair)
def generate_key_pair():
keys = script.wireguard.generate_keys()
private_key = keys["private_key"]
public_key = keys["public_key"]
return schemas.KeyPair(
private_key=private_key,
public_key=public_key
)

146
wg_dashboard_backend/schemas.py

@ -1,62 +1,147 @@
import pydantic
from pydantic import BaseModel, typing from pydantic import BaseModel, typing
from sqlalchemy.orm import Session, Query
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
import logging
import models import models
_LOGGER = logging.getLogger(__name__)
class GenericModel(BaseModel):
class Meta:
model = None
key = None
excludes = {"id"}
class Config:
orm_mode = True
def _ensure_orm(self):
if not self.Config and not self.Config.orm_mode and not self.Meta.model and not self.Meta.key:
raise NotImplementedError("Incorrect configuration Config.orm_mode must be enabled and Meta.model must be "
"set to a sqlalchemy model. Additional Meta.key must be set to bind model and schema")
def filter_query(self, sess) -> Query:
query = sess.query(self.Meta.model).filter_by(**{
self.Meta.key: getattr(self, self.Meta.key)
})
return query
def update(self, sess: Session, new):
self._ensure_orm()
self.filter_query(sess).update(new.dict(include=self.columns()))
sess.commit()
for k, v in new.dict().items():
try:
setattr(self, k, v)
except ValueError:
pass
return self
def columns(self, no_exclude=False):
cols = set([x for x in dir(self.Meta.model) if not x.startswith("_")])
#cols = set([str(x).replace(f"{self.Meta.model.__table__.name}.", "") for x in self.Meta.model.__table__.columns])
return cols if no_exclude else cols - self.Meta.excludes
def sync(self, sess: Session):
self._ensure_orm()
# Count existing
n_results = self.filter_query(sess).count()
if n_results == 0:
# Insert, does not exists at all.
# Convert from schema to model
dbm = self.Meta.model(**self.dict())
sess.add(dbm)
else:
self.filter_query(sess).update(self.dict(include=self.columns()))
sess.commit()
def from_db(self, sess: Session):
self._ensure_orm()
try:
db_item = self.filter_query(sess).one()
class User(BaseModel): for c in self.columns(no_exclude=True):
username: str = None try:
setattr(self, c, getattr(db_item, c))
except ValueError as e:
pass
return self
except MultipleResultsFound as e:
_LOGGER.exception(e)
except NoResultFound as e:
_LOGGER.exception(e)
_LOGGER.warning("We did not find any records in the database that corresponds to the model. This means you "
"are trying to fetch a unsaved schema!")
return None
class User(GenericModel):
id: int = None
username: str
email: str = None email: str = None
full_name: str = None full_name: str = None
role: str = None role: str = None
class Config: class Meta:
orm_mode = True model = models.User
key = "username"
excludes = {"id"}
class UserInDB(User): class UserInDB(User):
password: str password: str
class Config:
orm_mode = True
class Token(BaseModel): class Token(GenericModel):
access_token: str access_token: str
token_type: str token_type: str
user: User user: User
class Config:
orm_mode = True
class WGPeer(BaseModel): class WGPeer(GenericModel):
id: int = None
name: str = None name: str = None
address: str = None address: str = None
private_key: str = None private_key: str = None
public_key: str = None public_key: str = None
server: str server_id: str
dns: str = None dns: str = None
allowed_ips: str = None allowed_ips: str = None
# TODO missing stuff configuration: str = None
class Config: class Meta:
orm_mode = True model = models.WGPeer
key = "address"
excludes = {"id"}
class WGPeerConfig(BaseModel): class WGPeerConfig(GenericModel):
config: str config: str
class KeyPair(BaseModel): class KeyPair(GenericModel):
public_key: str public_key: str
private_key: str private_key: str
class PSK(BaseModel): class PSK(GenericModel):
psk: str psk: str
class WGServer(BaseModel): class WGServer(GenericModel):
id: int = None
address: str = None address: str = None
interface: str interface: str
listen_port: int = None listen_port: int = None
@ -65,17 +150,28 @@ class WGServer(BaseModel):
public_key: str = None public_key: str = None
shared_key: str = None shared_key: str = None
is_running: bool = None is_running: bool = None
configuration: str = None
post_up: str = None post_up: str = None
post_down: str = None post_down: str = None
peers: typing.List[WGPeer] = None peers: pydantic.typing.List['WGPeer'] = []
class Config: class Meta:
orm_mode = True model = models.WGServer
key = "interface"
excludes = {"id", "peers"}
def convert(self): def convert(self):
self.peers = [] if not self.peers else self.peers self.peers = [] if not self.peers else self.peers
return models.WGServer(**self.dict(exclude={"is_running"})) return models.WGServer(**self.dict(exclude={"is_running"}))
class WGServerAdd(WGServer):
address: str
interface: str
listen_port: int
class WGPeerAdd(GenericModel):
server_interface: str

94
wg_dashboard_backend/script/wireguard.py

@ -1,10 +1,16 @@
import logging import logging
import subprocess import subprocess
import tempfile
import typing
import const import const
import schemas import schemas
import os import os
import re import re
import util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -20,6 +26,21 @@ class WGPermissionsError(Exception):
pass pass
class TempServerFile():
def __init__(self, server: schemas.WGServer):
self.server = server
self.td = tempfile.TemporaryDirectory(prefix="wg_man_")
self.server_file = os.path.join(self.td.name, f"{server.interface}.conf")
def __enter__(self):
with open(self.server_file, "w+") as f:
f.write(self.server.configuration)
return self.server_file
def __exit__(self, type, value, traceback):
self.td.cleanup()
def _run_wg(server: schemas.WGServer, command): def _run_wg(server: schemas.WGServer, command):
try: try:
output = subprocess.check_output(const.CMD_WG_COMMAND + command, stderr=subprocess.STDOUT) output = subprocess.check_output(const.CMD_WG_COMMAND + command, stderr=subprocess.STDOUT)
@ -28,19 +49,25 @@ def _run_wg(server: schemas.WGServer, command):
if b'Operation not permitted' in e.output: if b'Operation not permitted' in e.output:
raise WGPermissionsError("The user has insufficientt permissions for interface %s" % server.interface) raise WGPermissionsError("The user has insufficientt permissions for interface %s" % server.interface)
def is_installed(): def is_installed():
output = subprocess.check_output(const.CMD_WG_COMMAND) output = subprocess.check_output(const.CMD_WG_COMMAND)
return output == b'' or b'interface' in output return output == b'' or b'interface' in output
def generate_keys(): def generate_keys() -> typing.Dict[str, str]:
private_key = subprocess.check_output(const.CMD_WG_COMMAND + ["genkey"]) private_key = subprocess.check_output(const.CMD_WG_COMMAND + ["genkey"])
public_key = subprocess.check_output( public_key = subprocess.check_output(
const.CMD_WG_COMMAND + ["pubkey"], const.CMD_WG_COMMAND + ["pubkey"],
input=private_key input=private_key
) )
return private_key.decode("utf-8").strip(), public_key.decode("utf-8").strip()
private_key = private_key.decode("utf-8").strip()
public_key = public_key.decode("utf-8").strip()
return dict(
private_key=private_key,
public_key=public_key
)
def generate_psk(): def generate_psk():
@ -48,32 +75,28 @@ def generate_psk():
def start_interface(server: schemas.WGServer): def start_interface(server: schemas.WGServer):
server_file = os.path.join(const.SERVER_DIR(server.interface), server.interface + ".conf") with TempServerFile(server) as server_file:
try:
try: #print(*const.CMD_WG_QUICK, "up", server_file)
print(*const.CMD_WG_QUICK, "up", server_file) output = subprocess.check_output(const.CMD_WG_QUICK + ["up", server_file], stderr=subprocess.STDOUT)
output = subprocess.check_output(const.CMD_WG_QUICK + ["up", server_file], stderr=subprocess.STDOUT) return output
return output except Exception as e:
except Exception as e: if b'already exists' in e.output:
raise WGAlreadyStartedError("The wireguard device %s is already started." % server.interface)
if b'already exists' in e.output:
raise WGAlreadyStartedError("The wireguard device %s is already started." % server.interface)
def stop_interface(server: schemas.WGServer): def stop_interface(server: schemas.WGServer):
server_file = os.path.join(const.SERVER_DIR(server.interface), server.interface + ".conf") with TempServerFile(server) as server_file:
try:
output = subprocess.check_output(const.CMD_WG_QUICK + ["down", server_file], stderr=subprocess.STDOUT)
return output
except Exception as e:
try: if b'is not a WireGuard interface' in e.output:
output = subprocess.check_output(const.CMD_WG_QUICK + ["down", server_file], stderr=subprocess.STDOUT) raise WGAlreadyStoppedError("The wireguard device %s is already stopped." % server.interface)
return output
except Exception as e:
if b'is not a WireGuard interface' in e.output:
raise WGAlreadyStoppedError("The wireguard device %s is already stopped." % server.interface)
def restart_interface(server: schemas.WGServer): def restart_interface(server: schemas.WGServer):
try: try:
stop_interface(server) stop_interface(server)
except WGAlreadyStoppedError: except WGAlreadyStoppedError:
@ -82,7 +105,6 @@ def restart_interface(server: schemas.WGServer):
def is_running(server: schemas.WGServer): def is_running(server: schemas.WGServer):
try: try:
output = _run_wg(server, ["show", server.interface]) output = _run_wg(server, ["show", server.interface])
if output is None: if output is None:
@ -158,3 +180,29 @@ def get_stats(server: schemas.WGServer):
except Exception as e: except Exception as e:
_LOGGER.exception(e) _LOGGER.exception(e)
return [] return []
def move_server_dir(interface, interface1):
old_server_dir = const.SERVER_DIR(interface)
old_server_file = const.SERVER_FILE(interface)
new_server_dir = const.SERVER_DIR(interface1)
new_server_file = old_server_file.replace(f"{interface}.conf", f"{interface1}.conf")
os.rename(old_server_file, new_server_file)
os.rename(old_server_dir, new_server_dir)
def generate_config(obj: typing.Union[typing.Dict[schemas.WGPeer, schemas.WGServer], schemas.WGServer]):
if isinstance(obj, dict) and "server" in obj and "peer" in obj:
template = "peer.j2"
elif isinstance(obj, schemas.WGServer):
template = "server.j2"
else:
raise ValueError("Incorrect input type. Should be WGPeer or WGServer")
result = util.jinja_env.get_template(template).render(
data=obj
)
return result

16
wg_dashboard_backend/templates/peer.j2

@ -1,12 +1,12 @@
[Interface] [Interface]
Address = {{ data.address.replace("/32", "/24") }} Address = {{ data.peer.address.replace("/32", "/24") }}
PrivateKey = {{ data.private_key }} PrivateKey = {{ data.peer.private_key }}
DNS = {{ data.dns }} DNS = {{ data.peer.dns }}
[Peer] [Peer]
PublicKey = {{ data.server_ref.public_key }} PublicKey = {{ data.server.public_key }}
AllowedIPs = {{ data.allowed_ips }} AllowedIPs = {{ data.peer.allowed_ips }}
Endpoint = {{ data.server_ref.endpoint }}:{{ data.server_ref.listen_port }} Endpoint = {{ data.server.endpoint }}:{{ data.server.listen_port }}
{% if data.preshared_key %} {% if data.server.shared_key %}
PresharedKey = {{ data.server_ref.preshared_key }} PresharedKey = {{ data.server.shared_key }}
{% endif %} {% endif %}

0
wg_dashboard_backend/tests/__init__.py

BIN
wg_dashboard_backend/tests/database.db

Binary file not shown.

80
wg_dashboard_backend/tests/test_pytest.py

@ -0,0 +1,80 @@
import warnings
import schemas
from database import SessionLocal
with warnings.catch_warnings():
warnings.filterwarnings("ignore",category=DeprecationWarning)
from main import app
from fastapi.testclient import TestClient
client = TestClient(app)
sess = SessionLocal()
username = "admin"
password = "admin"
token_headers = {}
def test_logout_without_auth():
response = client.get("/api/logout")
assert response.status_code == 401
#assert response.json() == dict(message="ok")
def test_login_missing_username():
response = client.post("/api/login", json=dict(
password=password
))
assert response.status_code == 422
def test_login_missing_password():
response = client.post("/api/login", json=dict(
password=password
))
assert response.status_code == 422
def test_login():
response = client.post("/api/login", json=dict(
username=username,
password=password
)
)
assert response.status_code == 200 # Must have status code 200
assert "user" in response.json()
assert "token_type" in response.json()
assert "access_token" in response.json()
token_headers["Authorization"] = response.json()["token_type"] + " " + response.json()["access_token"]
return response
def test_logout_with_auth():
response = client.get("/api/logout", headers=token_headers)
assert response.status_code == 200
def test_user_edit():
user = schemas.UserInDB(
username="test",
password="test",
full_name="test",
email="test",
role="test"
)
user.sync(sess=sess)
db_user = user.from_db(sess)
#print(db_user.username)

6
wg_dashboard_frontend/angular.json

@ -22,11 +22,9 @@
], ],
"styles": [ "styles": [
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
"src/theme/styles.scss", "src/theme/styles.scss"
"node_modules/bootstrap/dist/css/bootstrap.min.css"
], ],
"scripts": [ "scripts": [
"node_modules/material-design-lite/material.js"
] ]
}, },
"configurations": { "configurations": {
@ -105,4 +103,4 @@
"prefix": "app" "prefix": "app"
} }
} }
} }

12
wg_dashboard_frontend/browserslist

@ -1,12 +0,0 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# You can see what browsers were selected by your queries by running:
# npx browserslist
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 9-11 # For IE 9-11 support, remove 'not'.

3792
wg_dashboard_frontend/package-lock.json

File diff suppressed because it is too large

12
wg_dashboard_frontend/package.json

@ -40,32 +40,26 @@
"@angular/platform-browser": "9.1.0", "@angular/platform-browser": "9.1.0",
"@angular/platform-browser-dynamic": "9.1.0", "@angular/platform-browser-dynamic": "9.1.0",
"@angular/router": "9.1.0", "@angular/router": "9.1.0",
"@ng-bootstrap/ng-bootstrap": "^6.0.2", "angular-material-dynamic-themes": "^1.0.4",
"angularx-qrcode": "^2.1.0", "angularx-qrcode": "^2.1.0",
"bootstrap": "^4.4.1",
"classlist.js": "1.1.20150312", "classlist.js": "1.1.20150312",
"core-js": "3.6.4", "core-js": "3.6.4",
"d3": "5.15.1",
"goog-webfont-dl": "^1.0.3",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"ip-cidr": "^2.0.10", "ip-cidr": "^2.0.10",
"material-design-lite": "1.3.0",
"material-icons": "^0.3.1", "material-icons": "^0.3.1",
"nvd3": "1.8.6", "ngx-cookie-service": "^3.0.4",
"rxjs": "6.5.5", "rxjs": "6.5.5",
"tslib": "^1.10.0", "tslib": "^1.10.0",
"web-animations-js": "^2.3.2", "web-animations-js": "^2.3.2",
"zone.js": "^0.10.3" "zone.js": "^0.10.3"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/schematics": "^9.1.1",
"@angular-devkit/build-angular": "~0.901.0", "@angular-devkit/build-angular": "~0.901.0",
"@angular/cli": "9.1.0", "@angular/cli": "9.1.0",
"@angular/compiler-cli": "9.1.0", "@angular/compiler-cli": "9.1.0",
"@angular/language-service": "9.1.0", "@angular/language-service": "9.1.0",
"@types/d3": "^5.7.2",
"@types/material-design-lite": "1.1.15",
"@types/node": "^13.11.0", "@types/node": "^13.11.0",
"@types/nvd3": "1.8.40",
"codelyzer": "^5.1.2", "codelyzer": "^5.1.2",
"node-sass": "4.13.1", "node-sass": "4.13.1",
"pre-commit": "1.2.2", "pre-commit": "1.2.2",

10
wg_dashboard_frontend/src/app/app-routing.module.ts

@ -1,10 +1,8 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import {LayoutModule} from "./layout/layout.module"; import { LayoutModule } from './layout/layout.module';
import {ErrorComponent} from "./page/error";
import { ErrorComponent } from './page/error';
@NgModule({ @NgModule({
imports: [ imports: [
@ -13,7 +11,6 @@ import {ErrorComponent} from "./page/error";
{ path: '', redirectTo: 'app/dashboard', pathMatch: 'full' }, { path: '', redirectTo: 'app/dashboard', pathMatch: 'full' },
{ path: 'page', loadChildren: () => import('./page/page.module').then(m => m.PageModule) }, { path: 'page', loadChildren: () => import('./page/page.module').then(m => m.PageModule) },
/*{ path: 'app', component: LayoutComponent, children: /*{ path: 'app', component: LayoutComponent, children:
[ [
//{ path: 'dashboard', component: DashboardComponent, pathMatch: 'full', canActivate: [AuthGuard]}, //{ path: 'dashboard', component: DashboardComponent, pathMatch: 'full', canActivate: [AuthGuard]},
@ -29,7 +26,7 @@ import {ErrorComponent} from "./page/error";
{ path: 'edit', component: EditComponent, pathMatch: 'full', canActivate: [AuthGuard]}, { path: 'edit', component: EditComponent, pathMatch: 'full', canActivate: [AuthGuard]},
] ]
},*/ },*/
{ path: '**', redirectTo: '/page/404'}, { path: '**', redirectTo: '/page/404' },
], ],
{ useHash: true }, { useHash: true },
@ -39,3 +36,4 @@ import {ErrorComponent} from "./page/error";
exports: [RouterModule], exports: [RouterModule],
}) })
export class AppRoutingModule {} export class AppRoutingModule {}

61
wg_dashboard_frontend/src/app/app.component.ts

@ -1,15 +1,68 @@
import { Component } from '@angular/core'; import {Component, HostBinding} from '@angular/core';
import {AuthService} from "@services/*"; import { AuthService } from '@services/*';
import {OverlayContainer} from "@angular/cdk/overlay";
import {DataService} from "./services/data.service";
import {CookieService} from "ngx-cookie-service";
const THEME_DARKNESS_SUFFIX = `-dark`;
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
template: `<router-outlet></router-outlet>`, template: `<router-outlet></router-outlet>`,
}) })
export class AppComponent { export class AppComponent {
@HostBinding('class') activeThemeCssClass: string;
isThemeDark = false;
activeTheme: string;
constructor(
private auth:
AuthService,
private overlayContainer: OverlayContainer,
private comm: DataService,
private cookieService: CookieService
) {
auth.init();
this.comm.on("changeTheme").subscribe( (data: {
theme: any,
darkMode: boolean
}) => {
this.setActiveTheme(data.theme.theme, /* darkness: */ data.darkMode)
});
if(this.cookieService.check("currentTheme")){
this.setActiveTheme(
JSON.parse(this.cookieService.get("currentTheme")).theme,
(this.cookieService.get("darkMode") === 'true')
);
}
constructor(private auth: AuthService) {
auth.init()
} }
setActiveTheme(theme: string, darkness: boolean = null) {
if (darkness === null)
darkness = this.isThemeDark;
else if (this.isThemeDark === darkness) {
if (this.activeTheme === theme) return
} else
this.isThemeDark = darkness;
this.activeTheme = theme;
const cssClass = darkness === true ? theme + THEME_DARKNESS_SUFFIX : theme;
const classList = this.overlayContainer.getContainerElement().classList;
if (classList.contains(this.activeThemeCssClass))
classList.replace(this.activeThemeCssClass, cssClass);
else
classList.add(cssClass);
this.activeThemeCssClass = cssClass
}
} }

13
wg_dashboard_frontend/src/app/app.module.ts

@ -6,7 +6,6 @@ import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { VarDirective } from './directives/var.directive'; import { VarDirective } from './directives/var.directive';
import { QRCodeModule } from 'angularx-qrcode'; import { QRCodeModule } from 'angularx-qrcode';
import {NgbModule} from "@ng-bootstrap/ng-bootstrap";
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatGridListModule } from '@angular/material/grid-list'; import { MatGridListModule } from '@angular/material/grid-list';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
@ -17,7 +16,8 @@ import { MatButtonModule } from '@angular/material/button';
import { MatToolbarModule } from '@angular/material/toolbar'; import { MatToolbarModule } from '@angular/material/toolbar';
import { MatSidenavModule } from '@angular/material/sidenav'; import { MatSidenavModule } from '@angular/material/sidenav';
import { MatListModule } from '@angular/material/list'; import { MatListModule } from '@angular/material/list';
import {FlexLayoutModule} from "@angular/flex-layout"; import { FlexLayoutModule } from '@angular/flex-layout';
import {CookieService} from "ngx-cookie-service";
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -28,7 +28,6 @@ import {FlexLayoutModule} from "@angular/flex-layout";
BrowserModule, BrowserModule,
AppRoutingModule, AppRoutingModule,
HttpClientModule, HttpClientModule,
NgbModule,
QRCodeModule, QRCodeModule,
BrowserAnimationsModule, BrowserAnimationsModule,
MatGridListModule, MatGridListModule,
@ -40,18 +39,20 @@ import {FlexLayoutModule} from "@angular/flex-layout";
MatSidenavModule, MatSidenavModule,
MatListModule, MatListModule,
FlexLayoutModule, FlexLayoutModule,
], ],
providers: [ providers: [
CookieService,
AuthService, AuthService,
{ {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor, useClass: AuthInterceptor,
multi: true, multi: true,
} },
], ],
bootstrap: [AppComponent], bootstrap: [AppComponent],
exports: [ exports: [
VarDirective VarDirective,
] ],
}) })
export class AppModule {} export class AppModule {}

80
wg_dashboard_frontend/src/app/components/blank-layout-card/blank-layout-card.component.scss

@ -1,80 +0,0 @@
@import '~theme/helpers';
.blank-layout-card {
margin: auto;
.mdl-button {
font-weight: 500;
}
font-family: Roboto, Helvetica, sans-serif;
font-size: 1rem;
.mdl-card__blank-layout-card.mdl-card {
max-width: 450px;
margin: auto;
.mdl-card__supporting-text {
min-height: inherit;
width: 100%;
padding: 32px 24px;
box-sizing: border-box;
.mdl-card__title-text {
font-size: 17px;
font-weight: bold;
}
.blank-layout-card-name {
font-size: 24px;
display: block;
padding: 0 0 8px 0;
}
.blank-layout-card-link {
padding: 12px 0;
}
.blank-layout-card-link,
.blank-layout-card-link * {
display: inline-block;
font-size: 1rem;
font-weight: inherit;
color: $color-alto;
}
.underlined {
display: inline-block;
border-bottom: 1px solid $color-light-blue;
}
.checkbox--inline {
display: inline;
padding-top: 4px;
padding-left: 35px;
}
.submit-cell {
display: flex;
}
.text--huge {
font-size: 120px;
font-weight: bold;
display: inline-block;
padding: 100px 0 40px 0;
}
.text--sorry {
font-size: 28px;
font-weight: 300;
}
.alignment--bottom-right {
position: absolute;
bottom: 39px;
right: 46px;
}
}
}
}

10
wg_dashboard_frontend/src/app/components/blank-layout-card/blank-layout-card.component.ts

@ -1,10 +0,0 @@
import { Component, HostBinding } from '@angular/core';
import { UpgradableComponent } from 'theme/components/upgradable';
@Component({
template: '',
})
export class BlankLayoutCardComponent extends UpgradableComponent {
@HostBinding('class.blank-layout-card') protected readonly blankLayoutCard = true;
}

1
wg_dashboard_frontend/src/app/components/blank-layout-card/index.ts

@ -1 +0,0 @@
export { BlankLayoutCardComponent } from './blank-layout-card.component';

2
wg_dashboard_frontend/src/app/components/message-menu/index.ts

@ -1,2 +0,0 @@
export { MessageMenuComponent } from './message-menu.component';
export { MessageMenuService } from './message-menu.service';

27
wg_dashboard_frontend/src/app/components/message-menu/message-menu.component.html

@ -1,27 +0,0 @@
<div class="material-icons mdl-badge mdl-badge--overlap mdl-button--icon message" id="inbox" [attr.data-badge]="messages.length">
mail_outline
</div>
<ul
class="mdl-menu mdl-list mdl-js-menu mdl-js-ripple-effect mdl-menu--bottom-right mdl-shadow--2dp messages-dropdown"
for="inbox">
<li class="mdl-list__item">
You have {{ messages.length }} new messages!
</li>
<li *ngFor="let item of messages" class="mdl-menu__item mdl-list__item mdl-list__item--two-line list__item--border-top">
<span class="mdl-list__item-primary-content">
<span class="mdl-list__item-avatar"
ngClass="background-color--{{ item.color }}">
<span class="text">{{ item.icon }}</span>
</span>
<span>{{ item.name }}</span>
<span
class="mdl-list__item-sub-title">{{ item.type }}</span>
</span>
<span class="mdl-list__item-secondary-content">
<span class="label label--transparent">{{ item.time }}</span>
</span>
</li>
<li class="mdl-list__item list__item--border-top">
<button href="#" class="mdl-button mdl-js-button mdl-js-ripple-effect">SHOW ALL MESSAGES</button>
</li>
</ul>

54
wg_dashboard_frontend/src/app/components/message-menu/message-menu.component.scss

@ -1,54 +0,0 @@
@import '~theme/helpers';
.message-menu {
position: relative;
}
.messages-dropdown {
&.mdl-menu {
width: 310px;
}
.label {
color: $messages-dropdown-label-text-color;
}
.mdl-list__item-primary-content {
font-weight: 400;
line-height: 18px;
.mdl-list__item-avatar {
padding: ($list-avatar-size - $list-icon-size);
text-align: center;
.material-icons {
vertical-align: top;
}
.text {
font-size: 19px;
vertical-align: middle;
}
}
.mdl-list__item-sub-title {
font-weight: 100;
font-size: 12px;
}
}
&.mdl-list {
.mdl-list__item {
@include typo-dropdown-menu-li;
&:first-child {
color: $dropdown-menu-header-font-color;
}
&:last-child {
padding-top: $list-min-padding/2;
padding-bottom: 0;
}
}
}
}

19
wg_dashboard_frontend/src/app/components/message-menu/message-menu.component.ts

@ -1,19 +0,0 @@
import { Component, HostBinding } from '@angular/core';
import { MessageMenuService } from './message-menu.service';
@Component({
selector: 'app-message-menu',
styleUrls: ['./message-menu.component.scss'],
templateUrl: './message-menu.component.html',
providers: [MessageMenuService],
})
export class MessageMenuComponent {
@HostBinding('class.message-menu') private readonly messageMenu = true;
public messages: object[];
constructor(messageMenuService: MessageMenuService) {
this.messages = messageMenuService.getMessages();
}
}

44
wg_dashboard_frontend/src/app/components/message-menu/message-menu.service.ts

@ -1,44 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable()
export class MessageMenuService {
public getMessages(): object[] {
return [
{
name: 'Alice',
type: 'Birthday Party',
time: 'just now',
icon: 'A',
color: 'primary',
},
{
name: 'Vladimir',
type: 'Deployment',
time: 'just now',
icon: 'V',
color: 'primary',
},
{
name: 'Mike',
type: 'No theme',
time: '5 min',
icon: 'M',
color: 'baby-blue',
},
{
name: 'Darth',
type: 'Suggestion',
time: '23 hours',
icon: 'D',
color: 'cerulean',
},
{
name: 'Don McDuket',
type: 'NEWS',
time: '30 Nov',
icon: 'D',
color: 'mint',
},
];
}
}

2
wg_dashboard_frontend/src/app/components/notification-menu/index.ts

@ -1,2 +0,0 @@
export { NotificationMenuComponent } from './notification-menu.component';
export { NotificationMenuService } from './notification-menu.service';

27
wg_dashboard_frontend/src/app/components/notification-menu/notification-menu.component.html

@ -1,27 +0,0 @@
<div class="material-icons mdl-badge mdl-badge--overlap mdl-button--icon notification" id="notification"
[attr.data-badge]="notifications.length">
notifications_none
</div>
<ul
class="mdl-menu mdl-list mdl-js-menu mdl-js-ripple-effect mdl-menu--bottom-right mdl-shadow--2dp notifications-dropdown"
for="notification">
<li class="mdl-list__item">
You have {{ notifications.length }} new notifications!
</li>
<li class="mdl-menu__item mdl-list__item list__item--border-top"
*ngFor="let item of notifications">
<span class="mdl-list__item-primary-content">
<span class="mdl-list__item-avatar" ngClass="background-color--{{ item.color }}">
<i class="material-icons">{{ item.icon }}</i>
</span>
<span>{{ item.text }}</span>
</span>
<span class="mdl-list__item-secondary-content">
<span class="label">{{ item.time }}</span>
</span>
</li>
<li class="mdl-list__item list__item--border-top">
<button href="#" class="mdl-button mdl-js-button mdl-js-ripple-effect">ALL NOTIFICATIONS</button>
</li>
</ul>

31
wg_dashboard_frontend/src/app/components/notification-menu/notification-menu.component.scss

@ -1,31 +0,0 @@
@import '~theme/helpers';
.notification-menu {
position: relative;
}
.notifications-dropdown.mdl-list {
width: 310px;
.mdl-list__item {
@include typo-dropdown-menu-li;
&:first-child {
color: $dropdown-menu-header-font-color;
}
.mdl-list__item-avatar {
padding: ($list-avatar-size - $list-icon-size);
text-align: center;
.material-icons {
vertical-align: top;
}
}
&:last-child {
padding-top: $list-min-padding/2;
padding-bottom: 0;
}
}
}

19
wg_dashboard_frontend/src/app/components/notification-menu/notification-menu.component.ts

@ -1,19 +0,0 @@
import { Component, HostBinding } from '@angular/core';
import { NotificationMenuService } from './notification-menu.service';
@Component({
selector: 'app-notification-menu',
styleUrls: ['./notification-menu.component.scss'],
templateUrl: './notification-menu.component.html',
providers: [NotificationMenuService],
})
export class NotificationMenuComponent {
@HostBinding('class.notification-menu') private readonly notificationMenu = true;
public notifications: object[];
constructor(notificationMenuService: NotificationMenuService) {
this.notifications = notificationMenuService.getNotifications();
}
}

30
wg_dashboard_frontend/src/app/components/notification-menu/notification-menu.service.ts

@ -1,30 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable()
export class NotificationMenuService {
public getNotifications(): object[] {
return [
{
text: 'You have 3 new orders.',
time: 'just now',
icon: 'plus_one',
color: 'primary',
}, {
text: 'Database error',
time: '1 min',
icon: 'error_outline',
color: 'secondary',
}, {
text: 'The Death Star is built!',
time: '2 hours',
icon: 'new_releases',
color: 'primary',
}, {
text: 'You have 4 new mails.',
time: '5 days',
icon: 'mail_outline',
color: 'primary',
},
];
}
}

1
wg_dashboard_frontend/src/app/components/sidebar/index.ts

@ -1 +0,0 @@
export { SidebarComponent } from './sidebar.component';

3
wg_dashboard_frontend/src/app/components/sidebar/sidebar.component.scss

@ -1,3 +0,0 @@
.mdl-navigation base-menu-item:nth-child(2) i.material-icons {
transform: rotate(180deg);
}

15
wg_dashboard_frontend/src/app/components/sidebar/sidebar.component.ts

@ -1,15 +0,0 @@
import { Component, Input } from '@angular/core';
import { SidebarComponent as BaseSidebarComponent } from 'theme/components/sidebar';
@Component({
selector: 'app-sidebar',
styleUrls: ['../../../theme/components/sidebar/sidebar.component.scss', './sidebar.component.scss'],
templateUrl: '../../../theme/components/sidebar/sidebar.component.html',
})
export class SidebarComponent extends BaseSidebarComponent {
public title = 'Wireguard';
public menu = [
{ name: 'Dashboard', link: '/app/dashboard', icon: 'dashboard' },
];
}

4
wg_dashboard_frontend/src/app/directives/var.directive.ts

@ -1,4 +1,4 @@
import {Directive, Input, TemplateRef, ViewContainerRef} from "@angular/core"; import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({ @Directive({
selector: '[ngVar]', selector: '[ngVar]',
@ -6,7 +6,7 @@ import {Directive, Input, TemplateRef, ViewContainerRef} from "@angular/core";
export class VarDirective { export class VarDirective {
@Input() @Input()
set ngVar(context: any) { set ngVar(context: any) {
console.log(context) console.log(context);
this.context.$implicit = this.context.ngVar = context; this.context.$implicit = this.context.ngVar = context;
this.updateView(); this.updateView();
} }

7
wg_dashboard_frontend/src/app/interfaces/peer.ts

@ -6,12 +6,13 @@ export interface Peer {
dns: string; dns: string;
allowed_ips: string; allowed_ips: string;
name: string; name: string;
configuration: string;
stats: { stats: {
sent: string, sent: string,
received: string, received: string,
handshake: string handshake: string,
} };
_expand?: boolean; _expand?: boolean;
_edit?: boolean _edit?: boolean;
} }

5
wg_dashboard_frontend/src/app/interfaces/server.ts

@ -1,4 +1,4 @@
import {Peer} from "./peer"; import { Peer } from './peer';
export interface Server { export interface Server {
address: string; address: string;
@ -11,5 +11,6 @@ export interface Server {
is_running: boolean; is_running: boolean;
post_up: string; post_up: string;
post_down: string; post_down: string;
peers: Array<Peer> configuration: string;
peers: Peer[];
} }

6
wg_dashboard_frontend/src/app/interfaces/user.ts

@ -1,11 +1,11 @@
import {Peer} from "./peer"; import { Peer } from './peer';
export interface User { export interface User {
full_name: string; full_name: string;
email: string; email: string;
role: string; role: string;
username: string; username: string;
access_token: string, access_token: string;
token_type: string, token_type: string;
} }

42
wg_dashboard_frontend/src/app/layout/layout.module.ts

@ -2,30 +2,32 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { LayoutComponent } from './layout/layout.component'; import { LayoutComponent } from './layout/layout.component';
import {MatSidenavModule} from "@angular/material/sidenav"; import { MatSidenavModule } from '@angular/material/sidenav';
import {MatToolbarModule} from "@angular/material/toolbar"; import { MatToolbarModule } from '@angular/material/toolbar';
import {MatListModule} from "@angular/material/list"; import { MatListModule } from '@angular/material/list';
import {MatIconModule} from "@angular/material/icon"; import { MatIconModule } from '@angular/material/icon';
import {MatButtonModule} from "@angular/material/button"; import { MatButtonModule } from '@angular/material/button';
import {FlexLayoutModule} from "@angular/flex-layout"; import { FlexLayoutModule } from '@angular/flex-layout';
import {RouterModule} from "@angular/router"; import { RouterModule } from '@angular/router';
import { MatMenuModule } from '@angular/material/menu';
import {MatSlideToggleModule} from "@angular/material/slide-toggle";
@NgModule({ @NgModule({
declarations: [LayoutComponent], declarations: [LayoutComponent],
imports: [ imports: [
CommonModule, CommonModule,
MatSidenavModule, MatSidenavModule,
MatToolbarModule, MatToolbarModule,
MatListModule, MatListModule,
MatIconModule, MatIconModule,
MatButtonModule, MatButtonModule,
FlexLayoutModule, FlexLayoutModule,
RouterModule RouterModule,
], MatMenuModule,
MatSlideToggleModule,
],
exports: [ exports: [
] ],
}) })
export class LayoutModule { } export class LayoutModule { }

37
wg_dashboard_frontend/src/app/layout/layout/layout.component.html

@ -1,4 +1,4 @@
<div style="height: 100vh;"> <div style="height: 100vh; ">
<mat-toolbar color="primary"> <mat-toolbar color="primary">
@ -7,17 +7,46 @@
<mat-icon>menu</mat-icon> <mat-icon>menu</mat-icon>
</button> </button>
<span>{{config.applicationName}}</span> <span>{{config.applicationName}}</span>
<span class="example-spacer"></span>
<div fxShow="true" fxHide.lt-md="true"> <div fxShow="true" fxHide.lt-md="true">
<!-- The following menu items will be hidden on both SM and XS screen sizes -->
<a *ngFor="let item of menu" [routerLink]="item.link" mat-button> <a *ngFor="let item of menu" [routerLink]="item.link" mat-button>
<mat-icon>{{item.icon}}</mat-icon> <mat-icon>{{item.icon}}</mat-icon>
{{item.text}} {{item.text}}
</a> </a>
</div> </div>
<span class="menu-spacer"></span>
<ng-container *ngIf="auth.isLoggedIn">
<button mat-button [matMenuTriggerFor]="menu2">Logged in as {{auth.user.username}}</button>
<mat-menu #menu2="matMenu">
<button mat-menu-item [routerLink]="['/page/user/edit']">
Edit User
</button>
<button mat-menu-item (click)="auth.logout().subscribe()">
Logout
</button>
<button mat-menu-item [matMenuTriggerFor]="themeMenu">
Themes
</button>
</mat-menu>
<mat-menu #themeMenu="matMenu">
<button mat-menu-item *ngFor="let theme of themes" (click)="setCurrentTheme(theme)">{{theme.name}}</button>
<mat-slide-toggle
style="margin-top: 10px;"
color="primary"
(click)="toggleDarkMode($event);"
[checked]="darkMode">
Dark
</mat-slide-toggle>
</mat-menu>
</ng-container>
</mat-toolbar-row> </mat-toolbar-row>
</mat-toolbar> </mat-toolbar>

4
wg_dashboard_frontend/src/app/layout/layout/layout.component.scss

@ -16,4 +16,6 @@
z-index: 1; z-index: 1;
} }
.menu-spacer {
flex: 1 1 auto;
}

69
wg_dashboard_frontend/src/app/layout/layout/layout.component.ts

@ -1,29 +1,78 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import {Observable} from "rxjs"; import { Observable } from 'rxjs';
import {BreakpointObserver, Breakpoints} from "@angular/cdk/layout"; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import {map, shareReplay} from "rxjs/operators"; import { map, shareReplay } from 'rxjs/operators';
import {ConfigService} from "../../services/config.service"; import { ConfigService } from '../../services/config.service';
import { AuthService } from '@services/*';
import {OverlayContainer} from "@angular/cdk/overlay";
import {DataService} from "../../services/data.service";
import {CookieService} from "ngx-cookie-service";
@Component({ @Component({
selector: 'app-layout', selector: 'app-layout',
templateUrl: './layout.component.html', templateUrl: './layout.component.html',
styleUrls: ['./layout.component.scss'] styleUrls: ['./layout.component.scss'],
}) })
export class LayoutComponent implements OnInit { export class LayoutComponent implements OnInit {
isHandset$: Observable<boolean> = this.breakpointObserver.observe(Breakpoints.Handset) isHandset$: Observable<boolean> = this.breakpointObserver.observe(Breakpoints.Handset)
.pipe( .pipe(
map(result => result.matches), map(result => result.matches),
shareReplay() shareReplay(),
); );
menu: Array<{link: Array<string>, icon: string, text: string}> = [ menu: {link: string[], icon: string, text: string}[] = [
{ link: ["/page/dashboard"], icon: "home", text: "Dashboard"} { link: ['/page/dashboard'], icon: 'home', text: 'Dashboard' },
]; ];
constructor(private breakpointObserver: BreakpointObserver, public config: ConfigService) {} themes = [
{theme: "indigo-pink", name: "Blue"},
{theme: "deeppurple-amber", name: "Purple"},
{theme: "pink-bluegrey", name: "Pink"},
{theme: "purple-green", name: "Purple-Green"},
];
currentTheme = null;
darkMode = false;
constructor(
private breakpointObserver: BreakpointObserver,
public config: ConfigService,
public auth: AuthService,
private comm: DataService,
private cookieService: CookieService
) {}
ngOnInit(): void { ngOnInit(): void {
console.log("Layout") console.log('Layout');
if(this.cookieService.check("currentTheme")){
this.currentTheme = JSON.parse(this.cookieService.get("currentTheme"));
this.darkMode = (this.cookieService.get("darkMode") === 'true');
}else {
this.currentTheme = { ... this.themes[0]}
}
}
toggleDarkMode($event){
$event.stopPropagation();
this.darkMode = !this.darkMode;
this.cookieService.set("darkMode", String(this.darkMode));
this.sendData();
}
setCurrentTheme(theme){
this.cookieService.set("currentTheme", JSON.stringify(theme));
this.currentTheme = theme;
this.sendData();
}
sendData(){
const send = {
theme: this.currentTheme,
darkMode: this.darkMode
};
this.comm.emit('changeTheme', send);
} }
} }

26
wg_dashboard_frontend/src/app/page/components/components.component.ts

@ -1,30 +1,10 @@
import { Component, HostBinding } from '@angular/core'; import { Component } from '@angular/core';
@Component({ @Component({
selector: 'app-components', selector: 'app-components',
templateUrl: './components.component.html', template: '',
styleUrls: ['./components.component.scss'], styles: [''],
}) })
export class ComponentsComponent { export class ComponentsComponent {
@HostBinding('class.mdl-grid') private readonly mdlGrid = true;
@HostBinding('class.ui-components') private readonly uiComponents = true;
public data = [
{
name: 'Nathan Fillion',
description: 'Malcolm “Mal” Reynolds',
image: 'nathan-fillion.png',
},
{
name: 'Gina Torres',
description: 'Zoe Alleyne Washburne',
image: 'gina-torres.png',
},
{
name: 'Alan Tudyk',
description: 'Hoban “Wash” Washburne',
image: 'tudyk.png',
},
];
} }

32
wg_dashboard_frontend/src/app/page/components/components.module.ts

@ -5,27 +5,35 @@ import { FormsModule } from '@angular/forms';
import { ThemeModule } from 'theme'; import { ThemeModule } from 'theme';
import { ComponentsComponent } from './components.component'; import { ComponentsComponent } from './components.component';
import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap"; import { ModalConfirmComponent } from './modal-confirm';
import {ModalConfirmComponent} from "./modal-confirm"; import { QRCodeModule } from 'angularx-qrcode';
import {QRCodeModule} from "angularx-qrcode"; import {MatButtonModule} from "@angular/material/button";
import {MatTooltipModule} from "@angular/material/tooltip";
import {MatCardModule} from "@angular/material/card";
import {MatIconModule} from "@angular/material/icon";
import {FlexModule} from "@angular/flex-layout";
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
ThemeModule, ThemeModule,
FormsModule, FormsModule,
QRCodeModule QRCodeModule,
], MatButtonModule,
MatTooltipModule,
MatCardModule,
MatIconModule,
FlexModule,
],
providers: [ providers: [
NgbActiveModal
], ],
exports: [ exports: [
ComponentsComponent, ComponentsComponent,
ModalConfirmComponent ModalConfirmComponent,
], ],
declarations: [ declarations: [
ComponentsComponent, ComponentsComponent,
ModalConfirmComponent ModalConfirmComponent,
], ],
}) })
export class ComponentsModule { } export class ComponentsModule { }

65
wg_dashboard_frontend/src/app/page/components/modal-confirm/modal-confirm.component.html

@ -1,36 +1,51 @@
<ng-template #content let-modal>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{title || "No 'title' defined" }}</h4>
<button type="button" class="close" aria-label="Close" (click)="modal.close('cancel')"> <div *ngIf="shown" fxLayout="row"
<span aria-hidden="true">&times;</span> fxLayout.xs="column"
</button> fxFlexFill
fxLayoutAlign="center center">
<mat-card style="position: absolute; z-index: 10; left: 50%; top: 0; width: 50%;">
<mat-card-header>
</div> <mat-card-title class="card-container-left">
<div class="modal-body"> {{title || "No 'title' defined" }}
<textarea *ngIf="area" readonly class=" </mat-card-title>
mdl-textfield--full-width <mat-card-title class="card-container-right">
mdl-layout__header" style="min-height: 250px; height: 100%;">{{text || "No Text Defined" }}</textarea>
<div class="form-group" *ngIf="!area">{{text || "No Text Defined" }}</div> <mat-icon matTooltip="Close" (click)="cancel($event)"
<div style="text-align: center;" *ngIf="qrCode"> class="app-material-icon-valign" style="cursor: pointer"
<qrcode [qrdata]="text" [width]="256" [errorCorrectionLevel]="'M'"></qrcode> >close</mat-icon>
</div>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<textarea *ngIf="area" readonly class="full-width" style="min-height: 250px; height: 100%;">{{text || "No Text Defined" }}</textarea>
<div class="form-group" *ngIf="!area">{{text || "No Text Defined" }}</div>
<div style="text-align: center;" *ngIf="qrCode">
<hr>
<qrcode [qrdata]="text" [width]="256" [errorCorrectionLevel]="'M'"></qrcode>
</div>
</div> </mat-card-content>
<div class="modal-footer">
<button type="button" class="btn btn-dark" (click)="modal.close('confirm')">Confirm</button>
<button type="button" class="btn btn-dark" (click)="modal.close('cancel')">Cancel</button>
</div>
</ng-template>
<mat-card-actions align="end">
<div class="button-row">
<button mat-flat-button color="primary" type="button" (click)="confirm($event)">Confirm</button>
<button mat-flat-button color="warn" type="button" (click)="cancel($event)">Cancel</button>
</div>
</mat-card-actions>
</mat-card>
</div>
<button <button
class="mdl-button mdl-button--icon mdl-js-button mdl-js-ripple-effect" mat-icon-button
data-placement="bottom" color="primary"
[title]="hover" [matTooltip]="hover"
(click)="open($event, content)"> (click)="open($event)">
<i class="material-icons">{{icon}}</i> <i class="material-icons">{{icon}}</i>
</button> </button>

1
wg_dashboard_frontend/src/app/page/components/modal-confirm/modal-confirm.component.scss

@ -6,6 +6,7 @@
.dark-modal .close { .dark-modal .close {
color: white; color: white;
} }
.light-blue-backdrop { .light-blue-backdrop {
background-color: #5cb3fd; background-color: #5cb3fd;
} }

65
wg_dashboard_frontend/src/app/page/components/modal-confirm/modal-confirm.component.ts

@ -1,15 +1,24 @@
import {Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation} from '@angular/core'; import {
import {NgbModal} from "@ng-bootstrap/ng-bootstrap"; Component,
ContentChild,
EventEmitter,
Input,
OnInit,
Output,
TemplateRef, ViewChild, ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import {NgForOfContext} from "@angular/common";
@Component({ @Component({
selector: 'app-modal-confirm', selector: 'app-modal-confirm',
templateUrl: './modal-confirm.component.html', templateUrl: './modal-confirm.component.html',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.Emulated,
styleUrls: ['./modal-confirm.component.scss'] styleUrls: ['./modal-confirm.component.scss'],
}) })
export class ModalConfirmComponent implements OnInit{ export class ModalConfirmComponent implements OnInit {
@Input() noConfirm: boolean = false; @Input() noConfirm = false;
@Input() qrCode: boolean = false; @Input() qrCode = false;
@Input() icon: string; @Input() icon: string;
@Input() hover: string; @Input() hover: string;
@Input() title: string; @Input() title: string;
@ -17,40 +26,42 @@ export class ModalConfirmComponent implements OnInit{
@Input() area: boolean; @Input() area: boolean;
@Output() onCancel: EventEmitter<any> = new EventEmitter(); @Output() onCancel: EventEmitter<any> = new EventEmitter();
@Output() onConfirm: EventEmitter<any> = new EventEmitter(); @Output() onConfirm: EventEmitter<any> = new EventEmitter();
constructor(public modal: NgbModal) {
@ViewChild('modal', { read: TemplateRef }) _template: TemplateRef<any>;
@ViewChild('vc', {read: ViewContainerRef}) vc: ViewContainerRef;
shown = false;
constructor() {
} }
open($event, content) { open($event){
$event.stopPropagation(); if (this.noConfirm) {
if(this.noConfirm) {
this.onConfirm.emit(); this.onConfirm.emit();
return true; return true;
} }
this.modal.open(content, { this.shown = true;
ariaLabelledBy: 'modal-basic-title', //this.vc.createEmbeddedView(this._template, {fromContext: 'John'});
backdropClass: "light-blue-backdrop",
windowClass: "dark-modal"
}).result.then((result) => {
if(result === "cancel"){
this.onCancel.emit()
}else if(result === "confirm"){
this.onConfirm.emit();
}
}, (reason) => { }
confirm($event){
$event.stopPropagation();
this.onConfirm.emit();
this.shown= false;
}); }
cancel($event){
this.onCancel.emit();
this.shown = false
} }
ngOnInit(): void { ngOnInit(): void {
this.area = this.area || false; this.area = this.area || false;
this.area = !!this.area this.area = !!this.area;
} }
} }

24
wg_dashboard_frontend/src/app/page/dashboard2/add-server/add-server.component.html → wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.html

@ -1,9 +1,9 @@
<mat-card class="dashboard-card"> <mat-card class="dashboard-card">
<mat-card-content class="dashboard-card-content"> <mat-card-content class="dashboard-card-content">
<form [formGroup]="serverForm" (ngSubmit)="serverForm.valid && add(serverForm.value)" class="add-server-form"> <form [formGroup]="serverForm" class="add-server-form">
<p>Essentials</p> <p><b>Essentials</b></p>
<table class="add-server-full-width" cellspacing="0"><tr> <table class="add-server-full-width" cellspacing="0"><tr>
<td> <td>
<mat-form-field class="add-server-full-width"> <mat-form-field class="add-server-full-width">
@ -34,7 +34,7 @@
</td> </td>
</tr></table> </tr></table>
<p>Keys</p> <p><b>Keys</b></p>
<p> <p>
<mat-form-field class="add-server-full-width"> <mat-form-field class="add-server-full-width">
<mat-label>Private-Key</mat-label> <mat-label>Private-Key</mat-label>
@ -56,18 +56,18 @@
</mat-form-field> </mat-form-field>
</p> </p>
<div class="add-server-button-group"> <div class="button-row">
<button type="button" [hidden]="!isEdit" (click)="getKeyPair()" mat-raised-button color="primary" disabled> <button type="button" [disabled]="!isEdit" (click)="getKeyPair()" mat-raised-button color="primary">
<i class="material-icons">vpn_key</i> <i class="material-icons">vpn_key</i>
Generate KeyPair Generate KeyPair
</button> </button>
<button type="button" [hidden]="!isEdit" (click)="getPSK()" mat-raised-button color="primary"> <button type="button" [disabled]="!isEdit" (click)="getPSK()" mat-raised-button color="primary">
<i class="material-icons">share</i> <i class="material-icons">share</i>
Generate PSK Generate PSK
</button> </button>
</div> </div>
<p>Scripts</p> <p><b>Scripts</b></p>
<p> <p>
<mat-form-field class="add-server-full-width"> <mat-form-field class="add-server-full-width">
<mat-label>Post-Up</mat-label> <mat-label>Post-Up</mat-label>
@ -82,9 +82,14 @@
</mat-form-field> </mat-form-field>
</p> </p>
<div class="button-row">
<div class="add-server-button-group"> <button mat-raised-button color="primary"
<button mat-raised-button color="primary" [disabled]="!serverForm.valid" type="submit"> type="submit"
[disabled]="!serverForm.valid"
(click)="serverForm.valid && add(serverForm.value)"
(keydown.enter)="serverForm.valid && add(serverForm.value)"
>
<ng-container *ngIf="!isEdit">Add Server</ng-container> <ng-container *ngIf="!isEdit">Add Server</ng-container>
<ng-container *ngIf="isEdit">Edit Server</ng-container> <ng-container *ngIf="isEdit">Edit Server</ng-container>
</button> </button>
@ -94,6 +99,7 @@
</button> </button>
</div> </div>
</form> </form>

7
wg_dashboard_frontend/src/app/page/dashboard2/add-server/add-server.component.scss → wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.scss

@ -12,7 +12,8 @@ td {
padding-right: 8px; padding-right: 8px;
} }
.add-server-button-group{
margin-right: 8px;
}
:host {
width: 100%;
}

48
wg_dashboard_frontend/src/app/page/dashboard2/add-server/add-server.component.ts → wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.ts

@ -1,54 +1,55 @@
import {Component, Input, OnInit, ViewEncapsulation} from '@angular/core'; import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
import {FormControl, FormGroup, Validators} from "@angular/forms"; import { FormControl, FormGroup, Validators } from '@angular/forms';
import {IPValidator} from "../../../validators/ip-address.validator"; import { IPValidator } from '../../../validators/ip-address.validator';
import {NumberValidator} from "../../../validators/number.validator"; import { NumberValidator } from '../../../validators/number.validator';
import {Server} from "../../../interfaces/server"; import { Server } from '../../../interfaces/server';
import {ServerService} from "../../../services/server.service"; import { ServerService } from '../../../services/server.service';
import {DataService} from "../../../services/data.service"; import { DataService } from '../../../services/data.service';
@Component({ @Component({
selector: 'app-add-server', selector: 'app-add-server',
templateUrl: './add-server.component.html', templateUrl: './add-server.component.html',
encapsulation: ViewEncapsulation.None, styleUrls: ['./add-server.component.scss', '../dashboard.component.css'],
styleUrls: ['./add-server.component.scss', '../dashboard2.component.css']
}) })
export class AddServerComponent implements OnInit { export class AddServerComponent implements OnInit {
@Input() servers: Array<Server>; @Input() servers: Server[];
serverForm = new FormGroup({ serverForm = new FormGroup({
address: new FormControl('', [IPValidator.isIPAddress]), address: new FormControl('', [IPValidator.isIPAddress]),
interface: new FormControl('', [Validators.required, Validators.minLength(3)]), interface: new FormControl('', [Validators.required, Validators.minLength(3)]),
listen_port: new FormControl('', [Validators.required, NumberValidator.stringIsNumber]), listen_port: new FormControl('', [Validators.required, NumberValidator.stringIsNumber]),
endpoint: new FormControl('', Validators.required), endpoint: new FormControl('', Validators.required),
private_key: new FormControl('', [Validators.minLength(44), Validators.maxLength(44)]), private_key: new FormControl('' ),
public_key: new FormControl('', [Validators.minLength(44), Validators.maxLength(44)]), public_key: new FormControl('' ),
shared_key: new FormControl('', [Validators.minLength(44), Validators.maxLength(44)]), shared_key: new FormControl('' ),
post_up: new FormControl(''), post_up: new FormControl(''),
post_down: new FormControl(''), post_down: new FormControl(''),
// Unused on backend // Unused on backend
configuration: new FormControl(''),
is_running: new FormControl(false), is_running: new FormControl(false),
peers: new FormControl([]), peers: new FormControl([]),
}); });
isEdit: boolean = false; isEdit = false;
editServer: Server = null; editServer: Server = null;
constructor(private serverAPI: ServerService, private comm: DataService) { } constructor(private serverAPI: ServerService, private comm: DataService) { }
ngOnInit(): void { ngOnInit(): void {
this.comm.on("server-edit").subscribe( (data: Server) => { this.comm.on('server-edit').subscribe((data: Server) => {
this.isEdit = true; this.isEdit = true;
this.serverForm.setValue(data); this.serverForm.patchValue(data);
this.editServer = data; this.editServer = data;
}) });
} }
add(form: Server) { add(form: Server) {
if(this.isEdit){ if (this.isEdit) {
const idx = this.servers.indexOf(this.editServer); const idx = this.servers.indexOf(this.editServer);
this.serverAPI.editServer(this.editServer, form).subscribe((server: Server) => { this.serverAPI.editServer(this.editServer, form).subscribe((server: Server) => {
this.servers[idx] = server; this.servers[idx] = server;
@ -62,24 +63,25 @@ export class AddServerComponent implements OnInit {
} }
this.isEdit = false; this.isEdit = false;
this.serverForm.reset();
this.editServer = null; this.editServer = null;
this.serverForm.reset();
this.serverForm.clearValidators();
} }
getKeyPair() { getKeyPair() {
this.serverAPI.getKeyPair().subscribe((kp: any) => { this.serverAPI.getKeyPair().subscribe((kp: any) => {
this.serverForm.patchValue({ this.serverForm.patchValue({
private_key: kp.private_key, private_key: kp.private_key,
public_key: kp.public_key public_key: kp.public_key,
}) });
}); });
} }
getPSK() { getPSK() {
this.serverAPI.getPSK().subscribe((psk: any) => { this.serverAPI.getPSK().subscribe((psk: any) => {
this.serverForm.patchValue({ this.serverForm.patchValue({
shared_key: psk.psk shared_key: psk.psk,
}) });
}); });
} }
} }

1
wg_dashboard_frontend/src/app/page/dashboard/dashboard.component.css

@ -0,0 +1 @@

10
wg_dashboard_frontend/src/app/page/dashboard2/dashboard2.component.html → wg_dashboard_frontend/src/app/page/dashboard/dashboard.component.html

@ -1,9 +1,15 @@
<div flex fxFill fxLayout="row" fxLayoutAlign="space-between" > <div
fxFlexFill
fxLayout="row"
fxLayout.lt-lg="column"
style="padding: 10px;" fxLayoutGap="20px">
<div fxFlex="65"> <div fxFlex="65">
<app-server [(server)]="servers[idx]" [(servers)]="servers" *ngFor="let server of servers; let idx = index"></app-server> <app-server [(server)]="servers[idx]" [(servers)]="servers" *ngFor="let server of servers; let idx = index"></app-server>
</div> </div>
<div fxFlex="34"> <div fxFlex="35">
<app-add-server [(servers)]="servers"></app-add-server> <app-add-server [(servers)]="servers"></app-add-server>
</div> </div>

38
wg_dashboard_frontend/src/app/page/dashboard/dashboard.component.ts

@ -0,0 +1,38 @@
import { Component, OnInit } from '@angular/core';
import { BreakpointObserver } from '@angular/cdk/layout';
import { Server } from '../../interfaces/server';
import { ServerService } from '../../services/server.service';
import { Peer } from '../../interfaces/peer';
@Component({
selector: 'dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.css'],
})
export class DashboardComponent implements OnInit {
servers: Server[] = [];
constructor(private breakpointObserver: BreakpointObserver, private serverAPI: ServerService) {
}
ngOnInit(): void {
this.serverAPI.getServers()
.subscribe((servers: Server[]) => {
this.servers.push(...servers);
servers.forEach((server) => {
this.serverAPI.serverStats(server).subscribe((stats: Peer[]) => {
stats.forEach(item => {
const peer = server.peers.find(x => x.public_key == item.public_key);
peer._stats = item;
});
});
});
});
}
}

49
wg_dashboard_frontend/src/app/page/dashboard/dashboard.module.ts

@ -0,0 +1,49 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DashboardComponent } from './dashboard.component';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatCardModule } from '@angular/material/card';
import { MatMenuModule } from '@angular/material/menu';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { ServerComponent } from './server/server.component';
import { MatExpansionModule } from '@angular/material/expansion';
import { AddServerComponent } from './add-server/add-server.component';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ComponentsModule } from '../components';
import { FlexModule } from '@angular/flex-layout';
import { MatTableModule } from '@angular/material/table';
import { PeerComponent } from './peer/peer.component';
import { QRCodeModule } from 'angularx-qrcode';
import {MatTooltipModule} from "@angular/material/tooltip";
@NgModule({
declarations: [
DashboardComponent,
ServerComponent,
AddServerComponent,
PeerComponent,
],
imports: [
CommonModule,
MatGridListModule,
MatCardModule,
MatMenuModule,
MatIconModule,
MatButtonModule,
MatExpansionModule,
MatFormFieldModule,
MatInputModule,
ReactiveFormsModule,
ComponentsModule,
FlexModule,
MatTableModule,
FormsModule,
QRCodeModule,
MatTooltipModule,
],
})
export class DashboardModule { }

25
wg_dashboard_frontend/src/app/page/dashboard2/peer/peer.component.html → wg_dashboard_frontend/src/app/page/dashboard/peer/peer.component.html

@ -1,5 +1,10 @@
<div flex fxLayout="row" fxLayoutAlign="space-between"> <div
<div fxFlex="50"> fxLayout="row"
>
<div
fxFlex="50"
fxFlex.lt-lg="100">
<form #peerForm="ngForm" class="peer-edit-form" (ngSubmit)="peerForm.valid && edit()" > <form #peerForm="ngForm" class="peer-edit-form" (ngSubmit)="peerForm.valid && edit()" >
@ -61,16 +66,20 @@
</form> </form>
</div> </div>
<div fxFlex="33"> <div
<textarea readonly class="mdl-textfield--full-width" style="min-height: 250px; height: 100%; background-color: #202020; color: #00bcd4;">{{config}}</textarea> fxFlex="25"
fxFlex.lt-lg="100"
>
<textarea readonly class="full-width" style=" height: 100%; border: 0;">{{peer.configuration || "Error fetching configuration..."}}</textarea>
</div> </div>
<div fxFlex="33"> <div
<qrcode [qrdata]="config" [width]="256" [errorCorrectionLevel]="'M'"></qrcode> fxFlex="20"
fxFlex.lt-lg="100"
>
<qrcode *ngIf="peer.configuration" [qrdata]="peer.configuration" width="100%" [errorCorrectionLevel]="'M'"></qrcode>
</div> </div>
</div> </div>

5
wg_dashboard_frontend/src/app/page/dashboard2/peer/peer.component.scss → wg_dashboard_frontend/src/app/page/dashboard/peer/peer.component.scss

@ -5,11 +5,6 @@
text-align: left; text-align: left;
} }
.full-width {
width: 100%;
}
td { td {
padding-left: 0 !important; padding-left: 0 !important;
padding-right: 8px; padding-right: 8px;

70
wg_dashboard_frontend/src/app/page/dashboard/peer/peer.component.ts

@ -0,0 +1,70 @@
import { Component, EventEmitter, Input, OnInit, ViewEncapsulation } from '@angular/core';
import { ServerService } from '../../../services/server.service';
import { Peer } from '../../../interfaces/peer';
import { Server } from '../../../interfaces/server';
import { FormControl, FormGroup } from '@angular/forms';
@Component({
selector: 'app-peer',
templateUrl: './peer.component.html',
encapsulation: ViewEncapsulation.None,
styleUrls: ['./peer.component.scss'],
})
export class PeerComponent implements OnInit {
@Input('peer') peer: Peer;
@Input('server') server: Server;
@Input('selectedPeer') selectedPeer: Peer;
@Input('onEvent') editPeerEmitter: EventEmitter<any> = new EventEmitter<any>();
constructor(public serverAPI: ServerService) { }
ngOnInit(): void {
this.editPeerEmitter.subscribe((msg) => {
if (msg.peer !== this.peer) {
return;
}
if (msg.type === 'edit') {
this.edit();
} else if (msg.type == 'delete') {
this.delete();
}
});
}
edit() {
if (this.peer._edit) {
// Submit the edit (True -> False)
const idx = this.server.peers.indexOf(this.peer);
this.serverAPI.editPeer(this.peer).subscribe((newPeer) => {
Object.keys(newPeer).forEach(k => {
this.server.peers[idx][k] = newPeer[k];
});
});
} else if (!this.peer._edit) {
this.peer._expand = true;
// Open for edit. aka do nothing (False -> True
}
this.peer._edit = !this.peer._edit;
}
delete() {
const idx = this.server.peers.indexOf(this.peer);
this.serverAPI.deletePeer(this.peer).subscribe((apiServer) => {
this.server.peers.splice(idx, 1);
});
}
}

148
wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html

@ -0,0 +1,148 @@
<mat-card class="dashboard-card">
<mat-card-header>
<mat-card-title class="card-container-left">
<mat-icon
class="app-material-icon-valign"
[class]="{'green': server.is_running, 'red': !server.is_running}"
matTooltip="Indicates if the server is online or offline"
>check_circle</mat-icon>
{{server.interface}}
</mat-card-title>
<mat-card-title class="card-container-right">
<app-modal-confirm
[qrCode]="true"
[noConfirm]="false"
area="true"
icon="settings"
title="Configuration"
[text]="server.configuration"
hover="Show config for {{server.interface}}">
</app-modal-confirm>
<app-modal-confirm
[noConfirm]="true"
(onConfirm)="addPeer()"
icon="person_add"
hover="Add peer to {{server.interface}}">
</app-modal-confirm>
<app-modal-confirm
*ngIf="!server.is_running"
[noConfirm]="true"
(onConfirm)="start()"
icon="play_arrow"
hover="Start {{server.interface}}">
</app-modal-confirm>
<app-modal-confirm
*ngIf="server.is_running"
[noConfirm]="false"
(onConfirm)="stop()"
title="Stop server {{server.interface}}?"
text="Are you sure you want to stop this server? This may cause you or your clients to lose connection to the server."
icon="stop"
hover="Stop {{server.interface}}">
</app-modal-confirm>
<app-modal-confirm
[noConfirm]="false"
(onConfirm)="restart()"
title="Restart server {{server.interface}}?"
text="Are you sure you want to restart this server? This may cause you or your clients to lose connection to the server."
icon="autorenew"
hover="Restart {{server.interface}}">
</app-modal-confirm>
<app-modal-confirm
[noConfirm]="true"
(onConfirm)="edit()"
icon="edit"
hover="Edit {{server.interface}}">
</app-modal-confirm>
<app-modal-confirm
(onConfirm)="delete()"
title="Delete {{server.interface}}"
text="Are you sure you want to delete {{server.interface}}"
icon="delete"
hover="Delete {{server.interface}}">
</app-modal-confirm>
</mat-card-title>
<mat-card-subtitle style="margin-top: 2px;">Endpoint: <b>{{server.endpoint}}:{{server.listen_port}}</b> - Address Space: <b>{{server.address}}</b></mat-card-subtitle>
</mat-card-header>
<mat-card-content class="dashboard-card-content">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Address</th>
<th>Public-Key</th>
<th>Total tx/rx</th>
<th>Handshake</th>
<th>Manage</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let peer of server.peers; let idx = index;" (click)="selectedPeer = (selectedPeer != peer)? peer : null">
<tr (click)="openPeer(peer)">
<td>
<i *ngIf="{ a: (peer._stats && peer._stats.handshake && (peer._stats.handshake.split(' ')[1] === 'seconds' || pInt(peer._stats.handshake.split(' ')[0]) < 3))}; let isRunning"class="material-icons table-icon app-material-icon-valign" [ngClass]="{'green': isRunning.a, 'red': !isRunning.a}">check_circle</i>
{{peer.name}}</td>
<td>{{peer.address}}</td>
<td>{{peer.public_key}}</td>
<td>{{peer._stats?.tx || '0'}}/{{peer._stats?.rx || '0'}}</td>
<td> {{peer._stats?.handshake || 'N/A'}}</td>
<td>
<!-- Edit buttons -->
<app-modal-confirm
[noConfirm]="true"
(onConfirm)="this.editPeerEmitter.emit({type: 'edit', peer: peer}); selectedPeer=peer"
icon="edit"
hover="Edit {{peer.name}}">
</app-modal-confirm>
<app-modal-confirm
[noConfirm]="false"
(onConfirm)="this.editPeerEmitter.emit({type: 'delete', peer: peer});"
text="Are you sure you want to delete {{peer.name}} ({{peer.public_key}})?"
title="Delete {{peer.name}}"
icon="delete"
hover="Delete {{peer.name}} ({{peer.public_key}})">
</app-modal-confirm>
</td>
</tr>
<tr [hidden]="peer !== selectedPeer">
<td colspan="6">
<app-peer [onEvent]="this.editPeerEmitter" [(peer)]="server.peers[idx]" [(server)]="server"></app-peer>
</td>
</tr>
</ng-container>
</tbody>
</table>
</mat-card-content>
<mat-card-actions>
</mat-card-actions>
</mat-card>

13
wg_dashboard_frontend/src/app/page/dashboard/server/server.component.scss

@ -0,0 +1,13 @@
table {
width: 100%;
}
:host {
width: 100%;
}
.table-icon{
font-size: 20px;
}

75
wg_dashboard_frontend/src/app/page/dashboard/server/server.component.ts

@ -0,0 +1,75 @@
import { Component, EventEmitter, Input, OnInit, ViewEncapsulation } from '@angular/core';
import { Server } from '../../../interfaces/server';
import { ServerService } from '../../../services/server.service';
import { DataService } from '../../../services/data.service';
import { Peer } from '../../../interfaces/peer';
@Component({
selector: 'app-server',
templateUrl: './server.component.html',
styleUrls: ['./server.component.scss', '../dashboard.component.css'],
})
export class ServerComponent implements OnInit {
@Input() server: Server;
@Input() servers: Server[];
public editPeerEmitter: EventEmitter<any> = new EventEmitter<any>();
selectedPeer: Peer | null;
constructor(private serverAPI: ServerService, private comm: DataService) { }
ngOnInit(): void {
console.log('Server');
}
edit() {
this.comm.emit('server-edit', this.server);
}
stop() {
this.serverAPI.stopServer(this.server).subscribe((apiServer) => {
this.server.is_running = apiServer.is_running;
});
}
start() {
this.serverAPI.startServer(this.server).subscribe((apiServer) => {
this.server.is_running = apiServer.is_running;
});
}
addPeer() {
this.serverAPI.addPeer({
server_interface: this.server.interface
}).subscribe((peer) => {
this.server.peers.push(peer);
});
}
restart() {
this.serverAPI.restartServer(this.server).subscribe((apiServer) => {
this.server.is_running = apiServer.is_running;
});
}
delete() {
const index = this.servers.indexOf(this.server);
this.serverAPI.deleteServer(this.server).subscribe((apiServer) => {
this.servers.splice(index, 1);
});
}
openPeer(peer: Peer) {
if (this.selectedPeer == peer) {
this.selectedPeer = null;
return;
}
this.selectedPeer = peer;
this.editPeerEmitter.emit({ type: 'open', peer });
}
pInt(string: string) {
return parseInt(string);
}
}

18
wg_dashboard_frontend/src/app/page/dashboard2/dashboard2.component.css

@ -1,18 +0,0 @@
.grid-container {
margin: 20px;
}
.dashboard-card {
position: absolute;
top: 15px;
left: 15px;
right: 15px;
bottom: 15px;
}
.more-button {
position: absolute;
top: 5px;
right: 10px;
}

40
wg_dashboard_frontend/src/app/page/dashboard2/dashboard2.component.spec.ts

@ -1,40 +0,0 @@
import { LayoutModule } from '@angular/cdk/layout';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { Dashboard2Component } from './dashboard2.component';
describe('Dashboard2Component', () => {
let component: Dashboard2Component;
let fixture: ComponentFixture<Dashboard2Component>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [Dashboard2Component],
imports: [
NoopAnimationsModule,
LayoutModule,
MatButtonModule,
MatCardModule,
MatGridListModule,
MatIconModule,
MatMenuModule,
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(Dashboard2Component);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should compile', () => {
expect(component).toBeTruthy();
});
});

44
wg_dashboard_frontend/src/app/page/dashboard2/dashboard2.component.ts

@ -1,44 +0,0 @@
import {Component, OnInit} from '@angular/core';
import { map } from 'rxjs/operators';
import { Breakpoints, BreakpointObserver } from '@angular/cdk/layout';
import {Server} from "../../interfaces/server";
import {ServerService} from "../../services/server.service";
import {Peer} from "../../interfaces/peer";
@Component({
selector: 'dashboard2',
templateUrl: './dashboard2.component.html',
styleUrls: ['./dashboard2.component.css']
})
export class Dashboard2Component implements OnInit
{
servers: Array<Server> = [];
constructor(private breakpointObserver: BreakpointObserver, private serverAPI: ServerService) {
}
ngOnInit(): void {
this.serverAPI.getServers()
.subscribe( (servers: Array<Server>) => {
this.servers.push(...servers);
servers.forEach((server) => {
this.serverAPI.serverStats(server).subscribe((stats: Peer[]) => {
stats.forEach( item => {
const peer = server.peers.find(x => x.public_key == item.public_key);
peer._stats = item
});
});
});
})
}
}

49
wg_dashboard_frontend/src/app/page/dashboard2/dashboard2.module.ts

@ -1,49 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {Dashboard2Component} from "./dashboard2.component";
import {MatGridListModule} from "@angular/material/grid-list";
import {MatCardModule} from "@angular/material/card";
import {MatMenuModule} from "@angular/material/menu";
import {MatIconModule} from "@angular/material/icon";
import {MatButtonModule} from "@angular/material/button";
import {ServerComponent} from "./server/server.component";
import {MatExpansionModule} from "@angular/material/expansion";
import {AddServerComponent} from "./add-server/add-server.component";
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatInputModule} from "@angular/material/input";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {ComponentsModule} from "../components";
import {FlexModule} from "@angular/flex-layout";
import {MatTableModule} from "@angular/material/table";
import {PeerComponent} from "./peer/peer.component";
import {QRCodeModule} from "angularx-qrcode";
@NgModule({
declarations: [
Dashboard2Component,
ServerComponent,
AddServerComponent,
PeerComponent
],
imports: [
CommonModule,
MatGridListModule,
MatCardModule,
MatMenuModule,
MatIconModule,
MatButtonModule,
MatExpansionModule,
MatFormFieldModule,
MatInputModule,
ReactiveFormsModule,
ComponentsModule,
FlexModule,
MatTableModule,
FormsModule,
QRCodeModule,
]
})
export class Dashboard2Module { }

84
wg_dashboard_frontend/src/app/page/dashboard2/peer/peer.component.ts

@ -1,84 +0,0 @@
import {Component, EventEmitter, Input, OnInit, ViewEncapsulation} from '@angular/core';
import {ServerService} from "../../../services/server.service";
import {Peer} from "../../../interfaces/peer";
import {Server} from "../../../interfaces/server";
import {FormControl, FormGroup} from "@angular/forms";
@Component({
selector: 'app-peer',
templateUrl: './peer.component.html',
encapsulation: ViewEncapsulation.None,
styleUrls: ['./peer.component.scss'],
})
export class PeerComponent implements OnInit {
@Input("peer") peer: Peer;
@Input("server") server: Server;
@Input("selectedPeer") selectedPeer: Peer;
@Input("onEvent") editPeerEmitter: EventEmitter<any> = new EventEmitter<any>();
config: string = "Loading...";
constructor(public serverAPI: ServerService) { }
ngOnInit(): void {
this.editPeerEmitter.subscribe( (msg) => {
if(msg.peer !== this.peer){
return;
}
if(msg.type === "edit"){
this.edit();
}else if(msg.type == "delete"){
this.delete();
}else if(msg.type == "open"){
this.fetchConfig();
}
})
}
edit(){
if(this.peer._edit) {
// Submit the edit (True -> False)
const idx = this.server.peers.indexOf(this.peer);
this.serverAPI.editPeer(this.peer).subscribe((newPeer) => {
Object.keys(newPeer).forEach(k => {
this.server.peers[idx][k] = newPeer[k];
});
});
} else if(!this.peer._edit) {
this.peer._expand = true;
// Open for edit. aka do nothing (False -> True
}
this.peer._edit = !this.peer._edit;
}
delete(){
const idx = this.server.peers.indexOf(this.peer);
this.serverAPI.deletePeer(this.peer).subscribe((apiServer) => {
this.server.peers.splice(idx, 1);
})
}
fetchConfig() {
this.serverAPI.peerConfig(this.peer).subscribe((config: any) => {
this.config = config.config
})
}
pInt(string: string) {
return parseInt(string)
}
}

248
wg_dashboard_frontend/src/app/page/dashboard2/server/server.component.html

@ -1,248 +0,0 @@
<mat-card class="dashboard-card">
<mat-card-header class="server-card-header">
<mat-card-title>
<span>{{server.interface}}</span>
<!-- This fills the remaining space of the current row -->
<span class="fill-remaining-space"></span>
<i class="material-icons" [ngClass]="{'text-success': server.is_running, 'text-danger': !server.is_running}">check_circle</i>
<app-modal-confirm
[qrCode]="true"
[noConfirm]="false"
area="true"
icon="settings"
title="Configuration"
[text]="serverConfig"
hover="Show config for {{server.interface}}">
</app-modal-confirm>
<span>
<app-modal-confirm
[noConfirm]="true"
(onConfirm)="addPeer()"
icon="person_add"
hover="Add peer to {{server.interface}}">
</app-modal-confirm>
<app-modal-confirm
*ngIf="!server.is_running"
[noConfirm]="true"
(onConfirm)="start()"
icon="play_arrow"
hover="Start {{server.interface}}">
</app-modal-confirm>
<app-modal-confirm
*ngIf="server.is_running"
[noConfirm]="false"
(onConfirm)="stop()"
title="Stop server {{server.interface}}?"
text="Are you sure you want to stop this server? This may cause you or your clients to lose connection to the server."
icon="stop"
hover="Stop {{server.interface}}">
</app-modal-confirm>
<app-modal-confirm
[noConfirm]="false"
(onConfirm)="restart()"
title="Restart server {{server.interface}}?"
text="Are you sure you want to restart this server? This may cause you or your clients to lose connection to the server."
icon="autorenew"
hover="Restart {{server.interface}}">
</app-modal-confirm>
<app-modal-confirm
[noConfirm]="true"
(onConfirm)="edit()"
icon="edit"
hover="Edit {{server.interface}}">
</app-modal-confirm>
<app-modal-confirm
(onConfirm)="delete()"
title="Delete {{server.interface}}"
text="Are you sure you want to delete {{server.interface}}"
icon="delete"
hover="Delete {{server.interface}}">
</app-modal-confirm>
</span>
</mat-card-title>
<mat-card-subtitle>{{server.address}} @ {{server.endpoint}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content class="dashboard-card-content">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Address</th>
<th>Public-Key</th>
<th>Total tx/rx</th>
<th>Handshake</th>
<th>Manage</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let peer of server.peers; let idx = index;" (click)="selectedPeer = (selectedPeer != peer)? peer : null">
<tr (click)="openPeer(peer)">
<td>{{peer.name}}</td>
<td>{{peer.address}}</td>
<td>{{peer.public_key}}</td>
<td>{{peer._stats?.tx || '0'}}/{{peer._stats?.rx || '0'}}</td>
<td>{{peer._stats?.handshake || 'N/A'}}</td>
<td>
<!-- Edit buttons -->
<app-modal-confirm
[noConfirm]="true"
(onConfirm)="this.editPeerEmitter.emit({type: 'edit', peer: peer})"
icon="edit"
hover="Edit {{peer.name}}">
</app-modal-confirm>
<app-modal-confirm
[noConfirm]="false"
(onConfirm)="this.editPeerEmitter.emit({type: 'delete', peer: peer});"
text="Are you sure you want to delete {{peer.name}} ({{peer.public_key}})?"
title="Delete {{peer.name}}"
icon="delete"
hover="Delete {{peer.name}} ({{peer.public_key}})">
</app-modal-confirm>
</td>
</tr>
<tr [hidden]="peer !== selectedPeer">
<td colspan="6">
<app-peer [onEvent]="this.editPeerEmitter" [(peer)]="server.peers[idx]" [(server)]="server"></app-peer>
</td>
</tr>
</ng-container>
</tbody>
</table>
</mat-card-content>
<mat-card-actions>
</mat-card-actions>
</mat-card>
<!--
<mat-card class="dashboard-card">
<mat-card-content class="dashboard-card-content">
*Server*
<ng-container >
</ng-container>
</mat-card-content>
</mat-card>
-->
<!--
<div class=" mdl-card mdl-shadow--2dp">
<div class="mdl-card__title mdl-card--border">
<h2 class="mdl-card__title-text">{{server.interface}}</h2>
<span style="width:20px;"></span>
</div>
<div class="mdl-card__actions">
<div class="mdl-grid peer-item-header">
<div class="mdl-cell--2-col mdl-cell--12-col-phone">Name</div>
<div class="mdl-cell--2-col mdl-cell--12-col-phone">Address</div>
<div class="mdl-cell--3-col mdl-cell--12-col-phone">Public-Key</div>
<div class="mdl-cell--2-col mdl-cell--12-col-phone">Total tx/rx</div>
<div class="mdl-cell--2-col mdl-cell--12-col-phone">Handshake</div>
<div class="mdl-cell--2-col mdl-cell--12-col-phone">Manage</div>
</div>
<div style="cursor: pointer;" *ngFor="let peer of server.peers; let idx = index;" >
<app-peer [(peer)]="server.peers[idx]" [(server)]="server"></app-peer>
</div>
</div>
<div class="mdl-card__supporting-text">
</div>
<div class="mdl-card__menu">
<app-modal-confirm
[noConfirm]="true"
(onConfirm)="addPeer()"
icon="person_add"
hover="Add peer to {{server.interface}}">
</app-modal-confirm>
<app-modal-confirm
*ngIf="!server.is_running"
[noConfirm]="true"
(onConfirm)="start()"
icon="play_arrow"
hover="Start {{server.interface}}">
</app-modal-confirm>
<app-modal-confirm
*ngIf="server.is_running"
[noConfirm]="false"
(onConfirm)="stop()"
title="Stop server {{server.interface}}?"
text="Are you sure you want to stop this server? This may cause you or your clients to lose connection to the server."
icon="stop"
hover="Stop {{server.interface}}">
</app-modal-confirm>
<app-modal-confirm
[noConfirm]="false"
(onConfirm)="restart()"
title="Restart server {{server.interface}}?"
text="Are you sure you want to restart this server? This may cause you or your clients to lose connection to the server."
icon="autorenew"
hover="Restart {{server.interface}}">
</app-modal-confirm>
<app-modal-confirm
[noConfirm]="true"
(onConfirm)="edit()"
icon="edit"
hover="Edit {{server.interface}}">
</app-modal-confirm>
<app-modal-confirm
(onConfirm)="delete()"
title="Delete {{server.interface}}"
text="Are you sure you want to delete {{server.interface}}"
icon="delete"
hover="Delete {{server.interface}}">
</app-modal-confirm>
</div>
</div>-->

28
wg_dashboard_frontend/src/app/page/dashboard2/server/server.component.scss

@ -1,28 +0,0 @@
table {
width: 100%;
}
tr.example-detail-row {
height: 0 !important;
}
tr.example-element-row:not(.example-expanded-row):hover {
background: whitesmoke;
}
tr.example-element-row:not(.example-expanded-row):active {
background: #efefef;
}
.example-element-row td {
border-bottom-width: 0;
}
.example-element-detail {
overflow: hidden;
display: flex;
}

77
wg_dashboard_frontend/src/app/page/dashboard2/server/server.component.ts

@ -1,77 +0,0 @@
import {Component, EventEmitter, Input, OnInit, ViewEncapsulation} from '@angular/core';
import {Server} from "../../../interfaces/server";
import {ServerService} from "../../../services/server.service";
import {DataService} from "../../../services/data.service";
import {Peer} from "../../../interfaces/peer";
@Component({
selector: 'app-server',
templateUrl: './server.component.html',
encapsulation: ViewEncapsulation.None,
styleUrls: ['./server.component.scss', '../dashboard2.component.css'],
})
export class ServerComponent implements OnInit {
@Input() server: Server;
@Input() servers: Array<Server>;
public editPeerEmitter: EventEmitter<any> = new EventEmitter<any>();
serverConfig: string;
selectedPeer: Peer | null;
constructor(private serverAPI: ServerService, private comm: DataService) { }
ngOnInit(): void {
console.log("Server");
this.serverAPI.serverConfig(this.server).subscribe((x: any) => this.serverConfig = x.config)
}
edit(){
this.comm.emit('server-edit', this.server);
}
stop() {
this.serverAPI.stopServer(this.server).subscribe((apiServer) => {
this.server.is_running = apiServer.is_running
})
}
start() {
this.serverAPI.startServer(this.server).subscribe((apiServer) => {
this.server.is_running = apiServer.is_running
})
}
addPeer() {
this.serverAPI.addPeer(this.server).subscribe((peer) => {
this.server.peers.push(peer)
})
}
restart() {
this.serverAPI.restartServer(this.server).subscribe((apiServer) => {
this.server.is_running = apiServer.is_running
})
}
delete() {
const index = this.servers.indexOf(this.server);
this.serverAPI.deleteServer(this.server).subscribe((apiServer) => {
this.servers.splice(index, 1);
})
}
openPeer(peer: Peer) {
if(this.selectedPeer == peer){
this.selectedPeer = null;
return
}
this.selectedPeer = peer;
this.editPeerEmitter.emit({type: 'open', peer: peer});
}
}

31
wg_dashboard_frontend/src/app/page/page-routing.module.ts

@ -1,32 +1,29 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import {Dashboard2Component} from "./dashboard2/dashboard2.component"; import { DashboardComponent } from './dashboard/dashboard.component';
import {LayoutComponent} from "../layout/layout/layout.component"; import { LayoutComponent } from '../layout/layout/layout.component';
import {ErrorComponent} from "./error"; import { ErrorComponent } from './error';
import {LoginComponent} from "./user/login/login.component"; import { LoginComponent } from './user/login/login.component';
import {AuthGuard} from "@services/*"; import { AuthGuard } from '@services/*';
import { EditComponent } from './user/edit/edit.component';
const routes: Routes = [ const routes: Routes = [
{ path: '', component: LayoutComponent, children: { path: '', component: LayoutComponent, children:
[ [
//{ path: 'dashboard', component: DashboardComponent, pathMatch: 'full', canActivate: [AuthGuard]}, { path: 'dashboard', component: DashboardComponent, pathMatch: 'full', canActivate: [AuthGuard] },
{ path: 'dashboard', component: Dashboard2Component, pathMatch: 'full', canActivate: [AuthGuard]},
{ path: '404', component: ErrorComponent, pathMatch: 'full' }, { path: '404', component: ErrorComponent, pathMatch: 'full' },
] ],
}, },
{ path: 'user', component: LayoutComponent, children: { path: 'user', component: LayoutComponent, children:
[ [
//{ path: 'dashboard', component: DashboardComponent, pathMatch: 'full', canActivate: [AuthGuard]}, { path: 'edit', component: EditComponent, pathMatch: 'full' },
{ path: 'login', component: LoginComponent, pathMatch: 'full'}, { path: 'login', component: LoginComponent, pathMatch: 'full' },
] ],
}, },
]; ];
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule] exports: [RouterModule],
}) })
export class PageRoutingModule { } export class PageRoutingModule { }

22
wg_dashboard_frontend/src/app/page/page.module.ts

@ -1,23 +1,27 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import {PageRoutingModule} from "./page-routing.module"; import { PageRoutingModule } from './page-routing.module';
import {Dashboard2Module} from "./dashboard2/dashboard2.module"; import { DashboardModule } from './dashboard/dashboard.module';
import {LoginComponent} from "./user/login/login.component"; import { LoginComponent } from './user/login/login.component';
import {MatCardModule} from "@angular/material/card"; import { MatCardModule } from '@angular/material/card';
import {FormsModule, ReactiveFormsModule} from "@angular/forms"; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import {MatInputModule} from "@angular/material/input"; import { MatInputModule } from '@angular/material/input';
import { FlexModule } from '@angular/flex-layout';
import { EditComponent } from './user/edit/edit.component';
import { MatButtonModule } from '@angular/material/button';
@NgModule({ @NgModule({
declarations: [LoginComponent], declarations: [LoginComponent, EditComponent],
imports: [ imports: [
CommonModule, CommonModule,
PageRoutingModule, PageRoutingModule,
FormsModule, FormsModule,
Dashboard2Module, DashboardModule,
MatCardModule, MatCardModule,
ReactiveFormsModule, ReactiveFormsModule,
MatInputModule, MatInputModule,
FlexModule,
MatButtonModule,
], ],
}) })

73
wg_dashboard_frontend/src/app/page/user/edit/edit.component.html

@ -1,48 +1,55 @@
<div class="container"> <div flex fxFill fxLayout="row" fxLayoutAlign="center center" >
<div fxFlex="33">
<base-card> <mat-card>
<base-card-title> <mat-card-title>
<h2 class="mdl-card__title-text">Edit User</h2> Edit User
</base-card-title> </mat-card-title>
<base-card-body>
<form [formGroup]="editForm" (ngSubmit)="editForm.valid && edit()" class="form">
<div class="mdl-grid"> <mat-card-content>
<div class="mdl-cell mdl-cell--6-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label"> <form [formGroup]="editForm" (ngSubmit)="editForm.valid && edit()" class="form">
<input formControlName="full_name" class="mdl-textfield__input" type="text" id="full_name" value=""/>
<label class="mdl-textfield__label" for="full_name">Full Name</label>
</div>
<p>
<mat-form-field class="full-width">
<mat-label>Full Name</mat-label>
<input type="text" id="full_name" formControlName="full_name" matInput>
</mat-form-field>
</p>
<div class="mdl-cell mdl-cell--6-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label"> <p>
<input formControlName="username" class="mdl-textfield__input" type="text" id="username" /> <mat-form-field class="full-width">
<label class="mdl-textfield__label" for="username">Username</label> <mat-label>Username</mat-label>
</div> <input type="text" id="username" formControlName="username" matInput>
</mat-form-field>
</p>
<div class="mdl-cell mdl-cell--6-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label"> <p>
<input formControlName="password" class="mdl-textfield__input" type="text" id="password"/> <mat-form-field class="full-width">
<label class="mdl-textfield__label" for="password">Password</label> <mat-label>E-Mail</mat-label>
</div> <input type="text" id="email" formControlName="email" matInput>
</mat-form-field>
</p>
<div class="mdl-cell mdl-cell--6-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label"> <p>
<input formControlName="email" class="mdl-textfield__input" type="text" id="email"/> <mat-form-field class="full-width">
<label class="mdl-textfield__label" for="email">Email</label> <mat-label>Password</mat-label>
</div> <input type="password" id="password" formControlName="password" matInput>
</mat-form-field>
</p>
</div> <button mat-raised-button color="primary" [disabled]="!editForm.valid" type="submit">
Edit User
</button>
<button [disabled]="!editForm.valid" type="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect button--colored-light-blue"> </form>
Edit User </mat-card-content>
</button> </mat-card>
</div>
</form>
</div>
</base-card-body>
</base-card>
</div>

17
wg_dashboard_frontend/src/app/page/user/edit/edit.component.ts

@ -1,12 +1,12 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import {FormControl, FormGroup, Validators} from "@angular/forms"; import { FormControl, FormGroup, Validators } from '@angular/forms';
import {AuthService} from "@services/*"; import { AuthService } from '@services/*';
import {Router} from "@angular/router"; import { Router } from '@angular/router';
@Component({ @Component({
selector: 'app-edit', selector: 'app-edit',
templateUrl: './edit.component.html', templateUrl: './edit.component.html',
styleUrls: ['./edit.component.scss'] styleUrls: ['./edit.component.scss'],
}) })
export class EditComponent implements OnInit { export class EditComponent implements OnInit {
@ -31,20 +31,19 @@ export class EditComponent implements OnInit {
public ngOnInit() { public ngOnInit() {
this.user = this.authService.user; this.user = this.authService.user;
this.editForm.setValue({ this.editForm.setValue({
full_name: this.user.full_name, full_name: this.user.full_name,
password: "", password: '',
email: this.user.email, email: this.user.email,
username: this.user.username username: this.user.username,
}) });
} }
public edit() { public edit() {
if (this.editForm.valid) { if (this.editForm.valid) {
this.authService.edit(this.editForm.getRawValue()) this.authService.edit(this.editForm.getRawValue())
.subscribe(res => this.router.navigate(['/app/dashboard']), .subscribe(res => this.router.navigate(['/app/dashboard']),
error => this.error = error.message); error => this.error = error.message);
} }
} }

54
wg_dashboard_frontend/src/app/page/user/login/login.component.html

@ -1,40 +1,42 @@
<div flex fxFill fxLayout="row" fxLayoutAlign="center center" >
<div fxFlex="33">
<mat-card>
<mat-card-title>
Authenticate to Wireguard Management
</mat-card-title>
<mat-card> <mat-card-content>
<mat-card-title> <form [formGroup]="loginForm" (ngSubmit)="loginForm.valid && login()" class="form">
Authenticate to Wireguard Management
</mat-card-title>
<mat-card-content> <p>
<form [formGroup]="loginForm" (ngSubmit)="loginForm.valid && login()" class="form"> <mat-form-field class="full-width">
<mat-label>Username</mat-label>
<input type="text" id="username" formControlName="username" matInput>
</mat-form-field>
</p>
<p> <p>
<mat-form-field class="full-width"> <mat-form-field class="full-width">
<mat-label>Username</mat-label> <mat-label>Password</mat-label>
<input type="text" id="username" formControlName="username" matInput> <input type="password" id="password" autocomplete="on" formControlName="password" matInput>
</mat-form-field> </mat-form-field>
</p> </p>
<p>
<mat-form-field class="full-width">
<mat-label>Password</mat-label>
<input type="text" id="password" formControlName="password" matInput>
</mat-form-field>
</p>
<button mat-raised-button color="primary" [disabled]="!loginForm.valid" type="submit">
SIGN IN
</button>
<button [disabled]="!loginForm.valid" type="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect button--colored-light-blue"> </form>
SIGN IN </mat-card-content>
</button> </mat-card>
</div>
</div>
</form>
</mat-card-content>
</mat-card>

16
wg_dashboard_frontend/src/app/page/user/login/login.component.ts

@ -1,12 +1,13 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms"; import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import {AuthService} from "@services/*"; import { AuthService } from '@services/*';
import {Router} from "@angular/router"; import { Router } from '@angular/router';
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
templateUrl: './login.component.html', templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'] encapsulation: ViewEncapsulation.None,
styleUrls: ['./login.component.scss'],
}) })
export class LoginComponent implements OnInit { export class LoginComponent implements OnInit {
@ -19,7 +20,6 @@ export class LoginComponent implements OnInit {
private fb: FormBuilder, private fb: FormBuilder,
private router: Router) { private router: Router) {
this.loginForm = this.fb.group({ this.loginForm = this.fb.group({
password: new FormControl('', Validators.required), password: new FormControl('', Validators.required),
username: new FormControl('', [ username: new FormControl('', [
@ -31,7 +31,7 @@ export class LoginComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.authService.logout();
this.loginForm.valueChanges.subscribe(() => { this.loginForm.valueChanges.subscribe(() => {
this.error = null; this.error = null;
}); });
@ -42,7 +42,7 @@ export class LoginComponent implements OnInit {
if (this.loginForm.valid) { if (this.loginForm.valid) {
this.authService.login(this.loginForm.getRawValue()) this.authService.login(this.loginForm.getRawValue())
.subscribe(res => this.router.navigate(['/page/dashboard']), .subscribe(res => this.router.navigate(['/page/dashboard']),
error => this.error = error.message); error => this.error = error.message);
} }
} }

14
wg_dashboard_frontend/src/app/services/auth/auth.interceptor.ts

@ -10,8 +10,8 @@ import {
} from '@angular/common/http'; } from '@angular/common/http';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import {tap} from "rxjs/operators"; import { tap } from 'rxjs/operators';
import {Router} from "@angular/router"; import { Router } from '@angular/router';
@Injectable() @Injectable()
export class AuthInterceptor implements HttpInterceptor { export class AuthInterceptor implements HttpInterceptor {
@ -21,13 +21,13 @@ export class AuthInterceptor implements HttpInterceptor {
// add authorization token for full api requests // add authorization token for full api requests
if (request.url.includes('api') && this.auth.isLoggedIn) { if (request.url.includes('api') && this.auth.isLoggedIn) {
request = request.clone({ request = request.clone({
setHeaders: { Authorization: `Bearer ${this.auth.user.access_token}`}, setHeaders: { Authorization: `Bearer ${this.auth.user.access_token}` },
}); });
} }
return next.handle(request).pipe( tap(() => {}, return next.handle(request).pipe(tap(() => {},
(err: any) => { (err: any) => {
if (err instanceof HttpErrorResponse) { if (err instanceof HttpErrorResponse) {
if (err.status !== 401 && err.status !== 403) { if (err.status !== 401 && err.status !== 403) {
return; return;
} }
@ -35,6 +35,6 @@ export class AuthInterceptor implements HttpInterceptor {
this.auth.clearData(); this.auth.clearData();
this.router.navigate(['/page/user/login']); this.router.navigate(['/page/user/login']);
} }
})); }));
} }
} }

23
wg_dashboard_frontend/src/app/services/auth/auth.service.ts

@ -4,7 +4,8 @@ import { BehaviorSubject, Observable, of } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import {User} from "../../interfaces/user"; import { User } from '../../interfaces/user';
import { Router } from '@angular/router';
const tokenName = 'token'; const tokenName = 'token';
@ -14,21 +15,20 @@ const tokenName = 'token';
export class AuthService { export class AuthService {
public user: User = null; public user: User = null;
private url = `${environment.apiBaseUrl}/api`; private url = `${environment.apiBaseUrl}/api/v1`;
constructor(private http: HttpClient) {} constructor(private http: HttpClient, private router: Router) {}
public get isLoggedIn(): boolean { public get isLoggedIn(): boolean {
return !!this.user?.access_token return !!this.user?.access_token;
} }
public login(data): Observable<any> { public login(data): Observable<any> {
// Create form // Create form
let formData: FormData = new FormData(); const formData: FormData = new FormData();
formData.append('username', data.username); formData.append('username', data.username);
formData.append('password', data.password); formData.append('password', data.password);
return this.http.post(`${this.url}/login`, formData) return this.http.post(`${this.url}/login`, formData)
.pipe( .pipe(
map((res: any) => { map((res: any) => {
@ -36,18 +36,18 @@ export class AuthService {
})); }));
} }
public edit(formData: any){ public edit(formData: any) {
return this.http.post(`${this.url}/user/edit`, formData) return this.http.post(`${this.url}/user/edit`, formData)
.pipe(map((res: any) => { .pipe(map((res: any) => {
this._handleUser(res); this._handleUser(res);
})); }));
} }
_handleUser(res: any){ _handleUser(res: any) {
const user: any = res.user; const user: any = res.user;
user.access_token = res.access_token; user.access_token = res.access_token;
user.token_type = res.token_type; user.token_type = res.token_type;
localStorage.setItem("session", JSON.stringify(user)); localStorage.setItem('session', JSON.stringify(user));
this.init(); this.init();
} }
@ -55,12 +55,12 @@ export class AuthService {
return this.http.get(`${this.url}/logout`) return this.http.get(`${this.url}/logout`)
.pipe(map((data) => { .pipe(map((data) => {
this.clearData(); this.clearData();
this.router.navigate(['/page/user/login']);
return of(false); return of(false);
})); }));
} }
public clearData() {
public clearData(){
this.user = null; this.user = null;
localStorage.clear(); localStorage.clear();
@ -70,7 +70,6 @@ export class AuthService {
return localStorage.getItem(tokenName); return localStorage.getItem(tokenName);
} }
public init() { public init() {
this.user = JSON.parse(localStorage.getItem('session')); this.user = JSON.parse(localStorage.getItem('session'));
} }

10
wg_dashboard_frontend/src/app/services/config.service.ts

@ -1,13 +1,13 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import {HttpErrorResponse} from "@angular/common/http"; import { HttpErrorResponse } from '@angular/common/http';
import {throwError} from "rxjs"; import { throwError } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class ConfigService { export class ConfigService {
public applicationName = "Wireguard Manager"; public applicationName = 'Wireguard Manager';
constructor() { } constructor() { }
@ -25,5 +25,5 @@ export class ConfigService {
// return an observable with a user-facing error message // return an observable with a user-facing error message
return throwError( return throwError(
'Something bad happened; please try again later.'); 'Something bad happened; please try again later.');
}; }
} }

20
wg_dashboard_frontend/src/app/services/data.service.ts

@ -1,28 +1,26 @@
import {EventEmitter, Injectable} from '@angular/core'; import { EventEmitter, Injectable } from '@angular/core';
import {Observable} from "rxjs"; import { Observable } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class DataService { export class DataService {
_observables: any = {}; _observables: any = {};
constructor() {} constructor() {}
emit(event: string, value: any): void {
emit(event: string, value: any): void{ if (this._observables.hasOwnProperty(event)) {
if(this._observables.hasOwnProperty(event)) { this._observables[event].emit(value);
this._observables[event].emit(value)
} }
} }
on(event: string): Observable<any> { on(event: string): Observable<any> {
if(!this._observables.hasOwnProperty(event)) { if (!this._observables.hasOwnProperty(event)) {
this._observables[event] = new EventEmitter<any>() this._observables[event] = new EventEmitter<any>();
} }
return this._observables[event].asObservable() return this._observables[event].asObservable();
} }
} }

76
wg_dashboard_frontend/src/app/services/server.service.ts

@ -1,93 +1,93 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import {ConfigService} from "./config.service"; import { ConfigService } from './config.service';
import {HttpClient} from "@angular/common/http"; import { HttpClient } from '@angular/common/http';
import {catchError} from "rxjs/operators"; import { catchError } from 'rxjs/operators';
import {Server} from "../interfaces/server"; import { Server } from '../interfaces/server';
import {Peer} from "../interfaces/peer"; import { Peer } from '../interfaces/peer';
import {Observable, Subscribable} from "rxjs"; import { Observable, Subscribable } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class ServerService { export class ServerService {
public_url_wg: string = "/api/wg"; public base = '/api/v1/';
public url: string = this.public_url_wg + "/server"; public serverURL = this.base + "server";
constructor(private config: ConfigService, private http: HttpClient) { public peerURL = this.base + "peer";
public wgURL = this.base + "wg";
constructor(private config: ConfigService, private http: HttpClient) {
} }
public deletePeer(peer: Peer): Subscribable<Peer>{ public deletePeer(peer: Peer): Subscribable<Peer> {
return this.http.post(this.url + "/peer/delete", peer) return this.http.post(this.peerURL + '/delete', peer);
} }
public serverPerformAction(action: string, item: any): Subscribable<Server> { public serverPerformAction(action: string, item: any): Subscribable<Server> {
return this.http.post(this.url + "/" + action, item) return this.http.post(this.serverURL + '/' + action, item)
.pipe(catchError(this.config.handleError.bind(this))) .pipe(catchError(this.config.handleError.bind(this)));
} }
public addPeer(server: Server): Subscribable<Peer>{ public addPeer(server_interface: any): Subscribable<Peer> {
return this.http.post(this.url + "/peer/add", server) return this.http.post(this.peerURL + '/add', server_interface);
} }
public editPeer(peer: Peer): Subscribable<Peer>{ public editPeer(peer: Peer): Subscribable<Peer> {
return this.http.post(this.url + "/peer/edit", peer) return this.http.post(this.peerURL + '/edit', peer);
} }
public getServers(): Observable<Array<Server>>{ public getServers(): Observable<Server[]> {
return this.http.get<Array<Server>>(this.url + "/all") return this.http.get<Server[]>(this.serverURL + '/all')
.pipe(catchError(this.config.handleError.bind(this))) .pipe(catchError(this.config.handleError.bind(this)));
} }
public addServer(item: Server): Subscribable<Server> { public addServer(item: Server): Subscribable<Server> {
return this.http.post(this.url + "/add", item) return this.http.post(this.serverURL + '/add', item)
.pipe(catchError(this.config.handleError.bind(this))) .pipe(catchError(this.config.handleError.bind(this)));
} }
public startServer(item: Server): Subscribable<Server> { public startServer(item: Server): Subscribable<Server> {
return this.serverPerformAction("start", item) return this.serverPerformAction('start', item);
} }
public stopServer(item: Server): Subscribable<Server> { public stopServer(item: Server): Subscribable<Server> {
return this.serverPerformAction("stop", item) return this.serverPerformAction('stop', item);
} }
public restartServer(item: Server): Subscribable<Server> { public restartServer(item: Server): Subscribable<Server> {
return this.serverPerformAction("restart", item) return this.serverPerformAction('restart', item);
} }
public deleteServer(item: Server): Subscribable<Server> { public deleteServer(item: Server): Subscribable<Server> {
return this.serverPerformAction("delete", item) return this.serverPerformAction('delete', item);
} }
public editServer(oldServer: Server, newServer: Server): Subscribable<Server> { public editServer(oldServer: Server, newServer: Server): Subscribable<Server> {
return this.serverPerformAction("edit", { return this.serverPerformAction('edit', {
"interface": oldServer.interface, interface: oldServer.interface,
"server": newServer server: newServer,
}) });
} }
public getKeyPair() { public getKeyPair() {
return this.http.get(this.public_url_wg + "/generate_keypair") return this.http.get(this.wgURL + '/generate_keypair');
} }
public getPSK() { public getPSK() {
return this.http.get(this.public_url_wg + "/generate_psk") return this.http.get(this.wgURL + '/generate_psk');
} }
public peerConfig(peer: Peer) { public peerConfig(peer: Peer) {
return this.http.post(this.public_url_wg + "/server/peer/config", peer) return this.http.post(this.peerURL + '/config', peer);
} }
public serverConfig(server: Server) { public serverConfig(server: Server) {
return this.http.post(this.url + "/config", server) return this.http.post(this.serverURL + '/config', server);
} }
public serverStats(server: Server) { public serverStats(server: Server) {
return this.http.post(this.url + "/stats", server) return this.http.post(this.serverURL + '/stats', server);
} }
} }

8
wg_dashboard_frontend/src/app/validators/ip-address.validator.ts

@ -1,11 +1,11 @@
import { AbstractControl, ValidationErrors } from '@angular/forms'; import { AbstractControl, ValidationErrors } from '@angular/forms';
import * as IPCIDR from "ip-cidr"; import * as IPCIDR from 'ip-cidr';
export class IPValidator { export class IPValidator {
static isIPAddress(control: AbstractControl) : ValidationErrors | null { static isIPAddress(control: AbstractControl): ValidationErrors | null {
if(!control.value || !(new IPCIDR(control.value).isValid()) || !control.value.includes("/")){ if (!control.value || !(new IPCIDR(control.value).isValid()) || !control.value.includes('/')) {
return {validIP: true} return { validIP: true };
} }
return null; return null;
} }

8
wg_dashboard_frontend/src/app/validators/number.validator.ts

@ -1,11 +1,11 @@
import { AbstractControl, ValidationErrors } from '@angular/forms'; import { AbstractControl, ValidationErrors } from '@angular/forms';
import * as IPCIDR from "ip-cidr"; import * as IPCIDR from 'ip-cidr';
export class NumberValidator { export class NumberValidator {
static stringIsNumber(control: AbstractControl) : ValidationErrors | null { static stringIsNumber(control: AbstractControl): ValidationErrors | null {
if(isNaN(control.value)){ if (isNaN(control.value)) {
return {validNumber: true} return { validNumber: true };
} }
return null; return null;
} }

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save