Browse Source

Initial Version

pull/2/head
Per-Arne 5 years ago
commit
a33a1a8661
  1. 9
      .dockerignore
  2. 42
      .gitignore
  3. 21
      Dockerfile
  4. 28
      README.md
  5. 0
      wg_dashboard_backend/__init__.py
  6. 50
      wg_dashboard_backend/const.py
  7. 0
      wg_dashboard_backend/db/__init__.py
  8. 38
      wg_dashboard_backend/db/user.py
  9. 240
      wg_dashboard_backend/db/wireguard.py
  10. 430
      wg_dashboard_backend/main.py
  11. 52
      wg_dashboard_backend/models.py
  12. 11
      wg_dashboard_backend/requirements.txt
  13. 78
      wg_dashboard_backend/schemas.py
  14. 0
      wg_dashboard_backend/script/__init__.py
  15. 158
      wg_dashboard_backend/script/wireguard.py
  16. 12
      wg_dashboard_backend/templates/peer.j2
  17. 17
      wg_dashboard_backend/templates/server.j2
  18. 2
      wg_dashboard_backend/util.py
  19. 13
      wg_dashboard_frontend/.editorconfig
  20. 31
      wg_dashboard_frontend/.github/CONTRIBUTING.md
  21. 20
      wg_dashboard_frontend/.github/ISSUE_TEMPLATE.md
  22. 6
      wg_dashboard_frontend/.github/PULL_REQUEST_TEMPLATE.md
  23. 42
      wg_dashboard_frontend/.stylelintrc
  24. 9
      wg_dashboard_frontend/LICENSE
  25. 107
      wg_dashboard_frontend/angular.json
  26. 12
      wg_dashboard_frontend/browserslist
  27. 15783
      wg_dashboard_frontend/package-lock.json
  28. 78
      wg_dashboard_frontend/package.json
  29. 6
      wg_dashboard_frontend/proxy.conf.json
  30. 31
      wg_dashboard_frontend/src/app/app-routing.module.ts
  31. 7
      wg_dashboard_frontend/src/app/app.component.ts
  32. 44
      wg_dashboard_frontend/src/app/app.module.ts
  33. 80
      wg_dashboard_frontend/src/app/components/blank-layout-card/blank-layout-card.component.scss
  34. 10
      wg_dashboard_frontend/src/app/components/blank-layout-card/blank-layout-card.component.ts
  35. 1
      wg_dashboard_frontend/src/app/components/blank-layout-card/index.ts
  36. 2
      wg_dashboard_frontend/src/app/components/message-menu/index.ts
  37. 27
      wg_dashboard_frontend/src/app/components/message-menu/message-menu.component.html
  38. 54
      wg_dashboard_frontend/src/app/components/message-menu/message-menu.component.scss
  39. 19
      wg_dashboard_frontend/src/app/components/message-menu/message-menu.component.ts
  40. 44
      wg_dashboard_frontend/src/app/components/message-menu/message-menu.service.ts
  41. 2
      wg_dashboard_frontend/src/app/components/notification-menu/index.ts
  42. 27
      wg_dashboard_frontend/src/app/components/notification-menu/notification-menu.component.html
  43. 31
      wg_dashboard_frontend/src/app/components/notification-menu/notification-menu.component.scss
  44. 19
      wg_dashboard_frontend/src/app/components/notification-menu/notification-menu.component.ts
  45. 30
      wg_dashboard_frontend/src/app/components/notification-menu/notification-menu.service.ts
  46. 1
      wg_dashboard_frontend/src/app/components/sidebar/index.ts
  47. 3
      wg_dashboard_frontend/src/app/components/sidebar/sidebar.component.scss
  48. 15
      wg_dashboard_frontend/src/app/components/sidebar/sidebar.component.ts
  49. 22
      wg_dashboard_frontend/src/app/directives/var.directive.ts
  50. 2
      wg_dashboard_frontend/src/app/index.ts
  51. 17
      wg_dashboard_frontend/src/app/interfaces/peer.ts
  52. 15
      wg_dashboard_frontend/src/app/interfaces/server.ts
  53. 5
      wg_dashboard_frontend/src/app/layouts/blank-layout/blank-layout.component.html
  54. 11
      wg_dashboard_frontend/src/app/layouts/blank-layout/blank-layout.component.scss
  55. 18
      wg_dashboard_frontend/src/app/layouts/blank-layout/blank-layout.component.ts
  56. 1
      wg_dashboard_frontend/src/app/layouts/blank-layout/index.ts
  57. 89
      wg_dashboard_frontend/src/app/layouts/common-layout/common-layout.component.html
  58. 31
      wg_dashboard_frontend/src/app/layouts/common-layout/common-layout.component.ts
  59. 1
      wg_dashboard_frontend/src/app/layouts/common-layout/index.ts
  60. 1
      wg_dashboard_frontend/src/app/layouts/index.ts
  61. 32
      wg_dashboard_frontend/src/app/layouts/layouts.module.ts
  62. 0
      wg_dashboard_frontend/src/app/pages/components/components.component.html
  63. 0
      wg_dashboard_frontend/src/app/pages/components/components.component.scss
  64. 31
      wg_dashboard_frontend/src/app/pages/components/components.component.ts
  65. 31
      wg_dashboard_frontend/src/app/pages/components/components.module.ts
  66. 2
      wg_dashboard_frontend/src/app/pages/components/index.ts
  67. 1
      wg_dashboard_frontend/src/app/pages/components/modal-confirm/index.ts
  68. 36
      wg_dashboard_frontend/src/app/pages/components/modal-confirm/modal-confirm.component.html
  69. 11
      wg_dashboard_frontend/src/app/pages/components/modal-confirm/modal-confirm.component.scss
  70. 56
      wg_dashboard_frontend/src/app/pages/components/modal-confirm/modal-confirm.component.ts
  71. 34
      wg_dashboard_frontend/src/app/pages/dashboard/accordion.scss
  72. 96
      wg_dashboard_frontend/src/app/pages/dashboard/add-server/add-server.component.html
  73. 0
      wg_dashboard_frontend/src/app/pages/dashboard/add-server/add-server.component.scss
  74. 84
      wg_dashboard_frontend/src/app/pages/dashboard/add-server/add-server.component.ts
  75. 29
      wg_dashboard_frontend/src/app/pages/dashboard/dashboard.component.html
  76. 2
      wg_dashboard_frontend/src/app/pages/dashboard/dashboard.component.scss
  77. 55
      wg_dashboard_frontend/src/app/pages/dashboard/dashboard.component.ts
  78. 33
      wg_dashboard_frontend/src/app/pages/dashboard/dashboard.module.ts
  79. 2
      wg_dashboard_frontend/src/app/pages/dashboard/index.ts
  80. 106
      wg_dashboard_frontend/src/app/pages/dashboard/peer/peer.component.html
  81. 16
      wg_dashboard_frontend/src/app/pages/dashboard/peer/peer.component.scss
  82. 61
      wg_dashboard_frontend/src/app/pages/dashboard/peer/peer.component.ts
  83. 96
      wg_dashboard_frontend/src/app/pages/dashboard/server/server.component.html
  84. 0
      wg_dashboard_frontend/src/app/pages/dashboard/server/server.component.scss
  85. 60
      wg_dashboard_frontend/src/app/pages/dashboard/server/server.component.ts
  86. 20
      wg_dashboard_frontend/src/app/pages/pages/error/error.component.html
  87. 10
      wg_dashboard_frontend/src/app/pages/pages/error/error.component.ts
  88. 1
      wg_dashboard_frontend/src/app/pages/pages/error/index.ts
  89. 27
      wg_dashboard_frontend/src/app/pages/pages/forgot-password/forgot-password.component.html
  90. 10
      wg_dashboard_frontend/src/app/pages/pages/forgot-password/forgot-password.component.ts
  91. 1
      wg_dashboard_frontend/src/app/pages/pages/forgot-password/index.ts
  92. 1
      wg_dashboard_frontend/src/app/pages/pages/index.ts
  93. 1
      wg_dashboard_frontend/src/app/pages/pages/login/index.ts
  94. 85
      wg_dashboard_frontend/src/app/pages/pages/login/login.component.html
  95. 57
      wg_dashboard_frontend/src/app/pages/pages/login/login.component.ts
  96. 30
      wg_dashboard_frontend/src/app/pages/pages/pages-routing.module.ts
  97. 30
      wg_dashboard_frontend/src/app/pages/pages/pages.module.ts
  98. 1
      wg_dashboard_frontend/src/app/pages/pages/sign-up/index.ts
  99. 73
      wg_dashboard_frontend/src/app/pages/pages/sign-up/sign-up.component.html
  100. 61
      wg_dashboard_frontend/src/app/pages/pages/sign-up/sign-up.component.ts

9
.dockerignore

@ -0,0 +1,9 @@
**/config
**/build
**/database.db
**/node_modules
**/.github
**/dist

42
.gitignore

@ -0,0 +1,42 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
testem.log
/typings
# e2e
/e2e/*.js
/e2e/*.map
# System Files
.DS_Store
Thumbs.db

21
Dockerfile

@ -0,0 +1,21 @@
FROM node
COPY ./wg_dashboard_frontend /tmp/build
WORKDIR /tmp/build
RUN npm install && npm install -g @angular/cli
RUN ng build --configuration="production"
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7-alpine3.8
ENV IS_DOCKER True
RUN apk add --no-cache build-base libffi-dev
RUN echo "http://dl-cdn.alpinelinux.org/alpine/v3.10/main" >> /etc/apk/repositories
RUN echo "http://dl-cdn.alpinelinux.org/alpine/v3.10/community" >> /etc/apk/repositories
RUN apk update && apk add --no-cache wireguard-tools
COPY ./wg_dashboard_backend /app
RUN pip install -r /app/requirements.txt
COPY --from=0 /tmp/build/dist /app/build

28
README.md

@ -0,0 +1,28 @@
# wireguard-manager
The wireguard-manager provides a easy-to-use graphical interface to setup and manage wireguard server(s).
The following features is implemented:
* Create/Delete/Modify Server
* Create/Delete/Modify Users
* QRCode export
* Text export
* Start/Stop server
* User bandwidth usage statistics
The interface runs in docker and requires the host to have installed wireguard, either as a dkms module, or by using newer kernels (5.6+)
# Installation
```bash
docker build -t perara/wireguard-manager https://github.com/perara/wireguard-manager.git \
&& docker run
-v ./config:/config
--cap-add NET_ADMIN
--net host
perara/wireguard-manager
```
# Usage
When docker container is started, go to http://localhost:80
# Roadmap
* Add some insecure authentication
* Eventual bugfixes

0
wg_dashboard_backend/__init__.py

50
wg_dashboard_backend/const.py

@ -0,0 +1,50 @@
import os
import random
import string
IS_DOCKER = os.getenv("IS_DOCKER", "False") == "True"
DATABASE_FILE = "/config/database.db" if IS_DOCKER else "./database.db"
DATABASE_URL = f"sqlite:///{DATABASE_FILE}"
SECRET_KEY = ''.join(random.choices(string.ascii_uppercase + string.digits, k=64))
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
CMD_WG_COMMAND = ["wg"]
CMD_WG_QUICK = ["wg-quick"]
if not IS_DOCKER:
CMD_WG_COMMAND = ["sudo"] + CMD_WG_COMMAND
CMD_WG_QUICK = ["sudo"] + CMD_WG_QUICK
DEFAULT_CONFIG_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "config")
else:
DEFAULT_CONFIG_DIR = "/config"
os.makedirs(DEFAULT_CONFIG_DIR, exist_ok=True)
PEER_DEFAULT_ALLOWED_IPS = ["0.0.0.0/0", "::/0"]
ENV_CONFIG_DIR = os.getenv("ENV_CONFIG_DIR", DEFAULT_CONFIG_DIR)
os.makedirs(ENV_CONFIG_DIR, exist_ok=True)
def _server_dir(interface):
s_dir = os.path.join(ENV_CONFIG_DIR, "server", interface)
os.makedirs(s_dir, exist_ok=True)
return s_dir
SERVER_DIR = _server_dir
def _client_dir(interface):
c_dir = os.path.join(ENV_CONFIG_DIR, "server", interface, "clients")
os.makedirs(c_dir, exist_ok=True)
return c_dir
CLIENT_DIR = _client_dir
PEER_FILE = lambda db_peer: os.path.join(CLIENT_DIR(db_peer.server_ref.interface), str(db_peer.id) + ".conf")
SERVER_FILE = lambda interface: os.path.join(SERVER_DIR(interface), interface + ".conf")

0
wg_dashboard_backend/db/__init__.py

38
wg_dashboard_backend/db/user.py

@ -0,0 +1,38 @@
from sqlalchemy.orm import Session
import models
from passlib.context import CryptContext
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 authenticate_user(sess, username: str, password: str):
user = get_user_by_name(sess, username)
if not user:
return False
if not verify_password(password, user.password):
return False
return user
def get_user_by_name(db: Session, username: str) -> models.User:
return db.query(models.User).filter(models.User.username == username).first()
def get_user_by_username_and_password(db: Session, username: str, password: str) -> models.User:
return db.query(models.User).filter((models.User.username == username) & (models.User.password == password)).first()
def create_user(sess: Session, user: models.User):
user.password = get_password_hash(user.password)
sess.add(user)
sess.commit()
return user.id is not None

240
wg_dashboard_backend/db/wireguard.py

@ -0,0 +1,240 @@
import ipaddress
import os
import shutil
import typing
import const
import script.wireguard
from sqlalchemy import exists
from sqlalchemy.orm import Session
import util
import models
import schemas
import logging
_LOGGER = logging.getLogger(__name__)
_LOGGER.setLevel(logging.DEBUG)
def start_client(sess: Session, peer: schemas.WGPeer):
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")
import subprocess
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):
return sess.query(models.WGPeer) \
.filter(models.WGPeer.address == address) \
.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:
db_peer: models.WGPeer = peer_query_get_by_address(sess, peer.address, peer.server).one()
db_peer.dns = peer.dns
sess.add(db_peer)
sess.commit()
return peer.from_orm(db_peer)
def peer_remove(sess: Session, peer: schemas.WGPeer) -> bool:
db_peers: models.WGPeer = peer_query_get_by_address(sess, peer.address, peer.server).all()
for db_peer in db_peers:
sess.delete(db_peer)
sess.commit()
try:
os.remove(const.PEER_FILE(db_peer))
except:
pass
return True
def peer_key_pair_generate(sess: Session, peer: schemas.WGPeer) -> schemas.WGPeer:
db_peer: models.WGPeer = peer_query_get_by_address(sess, peer.address, peer.server).one()
private_key, public_key = script.wireguard.generate_keys()
db_peer.private_key = private_key
db_peer.public_key = public_key
sess.add(db_peer)
sess.commit()
return peer.from_orm(db_peer)
def peer_ip_address_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.address = peer.address
sess.add(db_peer)
sess.commit()
return peer.from_orm(db_peer)
def peer_update(sess: Session, peer: schemas.WGPeer) -> schemas.WGPeer:
db_peer: models.WGPeer = peer_query_get_by_address(sess, peer.address, peer.server).one()
db_peer.address = peer.address
db_peer.public_key = peer.public_key
db_peer.private_key = peer.private_key
db_peer.name = peer.name
db_peer.dns = peer.dns
db_peer.allowed_ips = peer.allowed_ips
sess.add(db_peer)
sess.commit()
return peer.from_orm(db_peer)
def peer_get(sess: Session, server: schemas.WGServer) -> typing.List[schemas.WGPeer]:
db_server = server_query_get_by_interface(sess, server.interface).one()
return db_server.peers
def server_query_get_by_interface(sess: Session, interface: str):
return sess.query(models.WGServer) \
.filter(models.WGServer.interface == interface)
def server_update_field(sess: Session, interface: str, server: schemas.WGServer, fields: typing.Set):
if server_query_get_by_interface(sess, interface) \
.update(
server.dict(include=fields), synchronize_session=False
) == 1:
sess.commit()
return True
return False
def server_get_all(sess: Session) -> typing.List[schemas.WGServer]:
db_interfaces = sess.query(models.WGServer).all()
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:
db_server = server_query_get_by_interface(sess, server.interface).one()
if db_server is None:
raise ValueError("The server with interface %s is already deleted." % server.interface)
sess.delete(db_server)
sess.commit()
shutil.rmtree(const.SERVER_DIR(db_server.interface))
return True
def server_preshared_key(sess: Session, server: schemas.WGServer) -> bool:
return server_update_field(sess, server.interface, server, {"shared_key"})
def server_key_pair_set(sess: Session, server: schemas.WGServer) -> bool:
return server_update_field(sess, server.interface, server, {"private_key", "public_key"})
def server_listen_port_set(sess: Session, server: schemas.WGServer) -> bool:
if server.listen_port < 1024 or server.listen_port > 65535:
raise ValueError("The listen_port is not in port range 1024 < x < 65535")
return server_update_field(sess, server.interface, server, {"listen_port"})
def server_ip_address_set(sess: Session, server: schemas.WGServer) -> bool:
network = ipaddress.ip_network(server.address, False)
if not network.is_private:
raise ValueError("The network is not in private range")
return server_update_field(sess, server.interface, server, {"address"})
def server_post_up_set(sess: Session, server: schemas.WGServer) -> bool:
return server_update_field(sess, server.interface, server, {"post_up"})
def server_post_down_set(sess: Session, server: schemas.WGServer) -> bool:
return server_update_field(sess, server.interface, server, {"post_down"})
def server_endpoint_set(sess: Session, server: schemas.WGServer) -> bool:
return server_update_field(sess, server.interface, server, {"endpoint"})

430
wg_dashboard_backend/main.py

@ -0,0 +1,430 @@
import logging
from sqlalchemy.exc import OperationalError
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
if not logger.hasHandlers():
sh = logging.StreamHandler()
fmt = logging.Formatter(fmt="%(asctime)s %(name)-12s %(levelname)-8s %(message)s")
sh.setFormatter(fmt)
logger.addHandler(sh)
import pkg_resources
import uvicorn as uvicorn
from fastapi.staticfiles import StaticFiles
import databases
from sqlalchemy.orm import sessionmaker, Session
from starlette.responses import FileResponse, JSONResponse
import sqlalchemy
import const
from datetime import datetime, timedelta
import db.wireguard
import db.user
import jwt
from fastapi import Depends, FastAPI, HTTPException, status, Form
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(
const.DATABASE_URL, connect_args={"check_same_thread": False}
)
try:
models.Base.metadata.create_all(engine)
except OperationalError as e:
pass
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
app = FastAPI()
# 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 get_current_user(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
token_data = schemas.TokenData(username=username)
except PyJWTError:
raise credentials_exception
user = db.user.get_user_by_name(sess, token_data.username)
if user is None:
raise credentials_exception
return user
@app.post("/api/token", response_model=schemas.Token)
def login_for_access_token(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"},
)
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"}
# @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)):
interfaces = db.wireguard.server_get_all(sess)
for iface in interfaces:
iface.is_running = script.wireguard.is_running(iface)
return interfaces
@app.post("/api/wg/server/add", response_model=schemas.WGServer)
def add_interface(form_data: schemas.WGServer, sess: Session = Depends(get_db)):
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:
wg_server = db.wireguard.server_add(sess, form_data)
# 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)):
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():
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():
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, ):
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)):
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)):
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)):
# 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)):
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)):
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)):
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):
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)):
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):
with open(const.SERVER_FILE(form_data.interface), "r") as f:
conf_file = f.read()
return schemas.WGPeerConfig(config=conf_file)
@app.post("/api/users/create/")
def create_user(form_data: schemas.UserInDB, sess: Session = Depends(get_db)):
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)
@app.on_event("startup")
async def startup():
await database.connect()
@app.on_event("shutdown")
async def shutdown():
await database.disconnect()
@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__":
async def main():
if not script.wireguard.is_installed():
print("NOT INSTALLED!")
exit(0)
await database.connect()
# TODO - GUI EVENT - Create new WG interface and post to backend
wg_interface_in = dict(
address="10.0.100.1/24",
listen_port=5192,
interface="wg0",
endpoint=None,
private_key=None,
public_key=None,
shared_key=None,
post_up=None,
post_down=None,
peers=[]
)
wg_interface = schemas.WGServer(**wg_interface_in)
sess = SessionLocal()
if not db.wireguard.server_add(sess, wg_interface):
print("Already exists. error")
# Public/Private key
private_key, public_key = script.wireguard.generate_keys()
wg_interface.private_key = private_key
wg_interface.public_key = public_key
db.wireguard.server_key_pair_set(sess, wg_interface)
# Shared key
shared_key = script.wireguard.generate_psk()
wg_interface.shared_key = shared_key
db.wireguard.server_preshared_key(sess, wg_interface)
# Post UP
wg_interface.post_up = "echo 'LOL2'"
db.wireguard.server_post_up_set(sess, wg_interface)
# Post DOWN
wg_interface.post_down = "echo 'LOL'"
db.wireguard.server_post_down_set(sess, wg_interface)
# Listen port
wg_interface.listen_port = 5192
if not db.wireguard.server_listen_port_set(sess, wg_interface):
print("FAILED!")
# Address
wg_interface.address = "10.0.100.1/24"
db.wireguard.server_ip_address_set(sess, wg_interface)
# Endpoint
wg_interface.endpoint = "10.0.0.135"
db.wireguard.server_endpoint_set(sess, wg_interface)
# TODO - Creates peer
wg_peer_in = dict(
server="wg0"
)
# CReate schema instance
wg_peer = schemas.WGPeer(**wg_peer_in)
# Insert initial peer
wg_peer = db.wireguard.peer_insert(sess, wg_peer)
print(wg_peer)
# Set DNS of peer
wg_peer.dns = ["sysx.no"]
wg_peer = db.wireguard.peer_dns_set(sess, wg_peer)
# Update priv_pub key
wg_peer = db.wireguard.peer_key_pair_generate(sess, wg_peer)
print(wg_peer)
all_peers = db.wireguard.peer_get(sess, wg_interface)
print(all_peers)
db.wireguard.peer_generate_config(sess, wg_peer)
print("-----")
db_interface = db.wireguard.server_query_get_by_interface(sess, wg_interface.interface).one()
db.wireguard.server_generate_config(sess, wg_interface.from_orm(db_interface))
if script.wireguard.is_running(db_interface):
script.wireguard.stop_interface(db_interface)
script.wireguard.start_interface(db_interface)
# script.wireguard.stop_interface(db_interface)
if script.wireguard.is_running(db_interface):
script.wireguard.add_peer(wg_interface, wg_peer)
script.wireguard.remove_peer(wg_interface, 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)

52
wg_dashboard_backend/models.py

@ -0,0 +1,52 @@
import sqlalchemy
from sqlalchemy import Integer, Column
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(sqlalchemy.String, unique=True, index=True)
password = Column(sqlalchemy.String)
username = Column(sqlalchemy.String, unique=True)
full_name = 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):
__tablename__ = "server"
id = Column(Integer, primary_key=True, index=True)
interface = Column(sqlalchemy.String, unique=True, index=True)
address = Column(sqlalchemy.String, unique=True)
listen_port = Column(sqlalchemy.String, unique=True)
private_key = Column(sqlalchemy.String)
public_key = Column(sqlalchemy.String)
shared_key = Column(sqlalchemy.String)
endpoint = Column(sqlalchemy.String)
post_up = Column(sqlalchemy.String)
post_down = Column(sqlalchemy.String)
is_running = Column(sqlalchemy.Boolean)
peers = relationship("WGPeer", backref="peer")

11
wg_dashboard_backend/requirements.txt

@ -0,0 +1,11 @@
pydantic
fastapi
aiofiles
aiosqlite
sqlalchemy
databases
PyJWT
passlib
bcrypt
python-multipart
jinja2

78
wg_dashboard_backend/schemas.py

@ -0,0 +1,78 @@
from pydantic import BaseModel, typing
import models
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: str = None
class User(BaseModel):
username: str = None
email: str = None
full_name: str = None
role: str = None
class UserInDB(User):
password: str
class Config:
orm_mode = True
class WGPeer(BaseModel):
name: str = None
address: str = None
private_key: str = None
public_key: str = None
server: str
dns: str = None
allowed_ips: str = None
# TODO missing stuff
class Config:
orm_mode = True
class WGPeerConfig(BaseModel):
config: str
class KeyPair(BaseModel):
public_key: str
private_key: str
class PSK(BaseModel):
psk: str
class WGServer(BaseModel):
address: str = None
interface: str
listen_port: int = None
endpoint: str = None
private_key: str = None
public_key: str = None
shared_key: str = None
is_running: bool = None
post_up: str = None
post_down: str = None
peers: typing.List[WGPeer] = None
class Config:
orm_mode = True
def convert(self):
self.peers = [] if not self.peers else self.peers
return models.WGServer(**self.dict(exclude={"is_running"}))

0
wg_dashboard_backend/script/__init__.py

158
wg_dashboard_backend/script/wireguard.py

@ -0,0 +1,158 @@
import logging
import subprocess
import const
import schemas
import os
import re
_LOGGER = logging.getLogger(__name__)
class WGAlreadyStartedError(Exception):
pass
class WGAlreadyStoppedError(Exception):
pass
class WGPermissionsError(Exception):
pass
def _run_wg(server: schemas.WGServer, command):
try:
output = subprocess.check_output(const.CMD_WG_COMMAND + command, stderr=subprocess.STDOUT)
return output
except Exception as e:
if b'Operation not permitted' in e.output:
raise WGPermissionsError("The user has insufficientt permissions for interface %s" % server.interface)
def is_installed():
output = subprocess.check_output(const.CMD_WG_COMMAND)
return output == b'' or b'interface' in output
def generate_keys():
private_key = subprocess.check_output(const.CMD_WG_COMMAND + ["genkey"])
public_key = subprocess.check_output(
const.CMD_WG_COMMAND + ["pubkey"],
input=private_key
)
return private_key.decode("utf-8").strip(), public_key.decode("utf-8").strip()
def generate_psk():
return subprocess.check_output(const.CMD_WG_COMMAND + ["genpsk"]).decode("utf-8").strip()
def start_interface(server: schemas.WGServer):
server_file = os.path.join(const.SERVER_DIR(server.interface), server.interface + ".conf")
try:
print(*const.CMD_WG_QUICK, "up", server_file)
output = subprocess.check_output(const.CMD_WG_QUICK + ["up", server_file], stderr=subprocess.STDOUT)
return output
except Exception as e:
if b'already exists' in e.output:
raise WGAlreadyStartedError("The wireguard device %s is already started." % server.interface)
def stop_interface(server: schemas.WGServer):
server_file = os.path.join(const.SERVER_DIR(server.interface), server.interface + ".conf")
try:
output = subprocess.check_output(const.CMD_WG_QUICK + ["down", server_file], stderr=subprocess.STDOUT)
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):
try:
stop_interface(server)
except WGAlreadyStoppedError:
pass
start_interface(server)
def is_running(server: schemas.WGServer):
try:
output = _run_wg(server, ["show", server.interface])
if output is None:
return False
except Exception as e:
if b'No such device' in e.output:
return False
return True
def add_peer(server: schemas.WGServer, peer: schemas.WGPeer):
try:
output = _run_wg(server, ["set", server.interface, "peer", peer.public_key, "allowed-ips", peer.address])
return output == b''
except Exception as e:
_LOGGER.exception(e)
return False
def remove_peer(server: schemas.WGServer, peer: schemas.WGPeer):
try:
output = _run_wg(server, ["set", server.interface, "peer", peer.public_key, "remove"])
return output == b''
except Exception as e:
_LOGGER.exception(e)
return False
def get_stats(server: schemas.WGServer):
try:
output = _run_wg(server, ["show", server.interface])
regex = r"peer:.*?^\n"
test_str = output.decode("utf-8") + "\n"
peers = []
peers_raw = re.findall(regex, test_str, re.MULTILINE | re.DOTALL)
for peer in peers_raw:
peer = peer.strip()
lines = [x.split(": ")[1] for x in peer.split("\n")]
if len(lines) == 2:
public_key, allowed_ips = lines
peers.append(dict(
public_key=public_key,
client_endpoint=None,
allowed_ips=allowed_ips,
handshake=None,
rx=None,
tx=None
))
elif len(lines) == 5:
public_key, client_endpoint, allowed_ips, handshake, rx_tx = lines
rx = re.match(r"^(.*) received", rx_tx).group(1)
tx = re.match(r"^.*, (.*)sent", rx_tx).group(1)
peers.append(dict(
public_key=public_key,
client_endpoint=client_endpoint,
allowed_ips=allowed_ips,
handshake=handshake,
rx=rx,
tx=tx
))
else:
ValueError("We have not handled peers with line number of %s" % str(len(lines)))
return peers
except Exception as e:
_LOGGER.exception(e)
return False

12
wg_dashboard_backend/templates/peer.j2

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

17
wg_dashboard_backend/templates/server.j2

@ -0,0 +1,17 @@
[Interface]
Address = {{ data.address }}
ListenPort = {{ data.listen_port }}
PrivateKey = {{ data.private_key }}
PostUp = {{ data.post_up }}
PostDown = {{ data.post_down }}
{% for peer in data.peers %}
[Peer]
# Client Name: {{ peer.name }}
PublicKey = {{ peer.public_key }}
{%- if peer.preshared_key -%}
PresharedKey = {{ peer.preshared_key }}
{% endif %}
AllowedIPs = {{ peer.address }}
{% endfor %}

2
wg_dashboard_backend/util.py

@ -0,0 +1,2 @@
from jinja2 import Environment, PackageLoader
jinja_env = Environment(loader=PackageLoader(__name__, 'templates'))

13
wg_dashboard_frontend/.editorconfig

@ -0,0 +1,13 @@
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false

31
wg_dashboard_frontend/.github/CONTRIBUTING.md

@ -0,0 +1,31 @@
## How to contribute to wireguard-manager
#### **Did you find a bug?**
* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/perara/wireguard-manager/issues).
* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/perara/wireguard-manager/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** demonstrating the expected behavior that is not occurring.
* If possible, use the [bug report template](.github/ISSUE_TEMPLATE.md) to create the issue.
#### **Did you write a patch that fixes a bug?**
* Open a new GitHub pull request with the patch.
* Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
#### **Do you intend to add a new feature or change an existing one?**
* Send a [GitHub Pull Request](https://github.com/perara/wireguard-manager/pull/new) with a clear list of what you've done (read more about [pull requests](http://help.github.com/pull-requests/)). Please follow our coding conventions (below) and make sure all of your commits are atomic (one feature per commit).
* We use [Karma commit message convention](http://karma-runner.github.io/0.13/dev/git-commit-msg.html). Please follow it.
* Use [Angular](https://angular.io/guide/styleguide), [TypeScript](https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines) and [JavaScript](https://github.com/airbnb/javascript) style guides. You can check your code using `npm run lint` or `ng lint` or using standalone linters (for example, linter plugins for your editor).
#### **Do you have questions about the source code?**
* Ask any question about how to use wireguard-manager in the [Issues](https://github.com/perara/wireguard-manager/issues).
Thanks! :heart: :heart: :heart:
Per-Arne Andersen

20
wg_dashboard_frontend/.github/ISSUE_TEMPLATE.md

@ -0,0 +1,20 @@
#### Expected behavior
#### Actual behavior
#### Steps to reproduce the behavior
#### Relevant code
```
```
#### Environment description

6
wg_dashboard_frontend/.github/PULL_REQUEST_TEMPLATE.md

@ -0,0 +1,6 @@
#### Related issues
#### Changes proposed in this pull request

42
wg_dashboard_frontend/.stylelintrc

@ -0,0 +1,42 @@
{
"extends": [
"stylelint-config-recommended-scss",
"stylelint-config-standard"
],
"plugins": [
"stylelint-scss"
],
"rules": {
"at-rule-no-unknown": [
true,
{
ignoreAtRules: [
'extend',
'at-root',
'debug',
'warn',
'error',
'if',
'else',
'for',
'each',
'while',
'mixin',
'include',
'content',
'return',
'function'
]
}
],
"selector-type-no-unknown": [
true,
{
"ignore": [
"custom-elements"
]
}
],
"no-descending-specificity": null
}
}

9
wg_dashboard_frontend/LICENSE

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2020 Per-Arne Andersen
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

107
wg_dashboard_frontend/angular.json

@ -0,0 +1,107 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"material-angular-dashboard": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"aot": true,
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"assets": [
"src/assets"
],
"styles": [
"src/theme/styles.scss",
"node_modules/bootstrap/dist/css/bootstrap.min.css"
],
"scripts": [
"node_modules/material-design-lite/material.js"
]
},
"configurations": {
"production": {
"budgets": [
{
"type": "anyComponentStyle",
"maximumWarning": "6kb"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "material-angular-dashboard:build",
"proxyConfig": "proxy.conf.json"
},
"configurations": {
"production": {
"browserTarget": "material-angular-dashboard:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "material-angular-dashboard:build"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"material-angular-dashboard-e2e": {
"root": "",
"sourceRoot": "",
"projectType": "application"
}
},
"defaultProject": "material-angular-dashboard",
"cli": {
"packageManager": "yarn"
},
"schematics": {
"@schematics/angular:component": {
"prefix": "app",
"style": "scss"
},
"@schematics/angular:directive": {
"prefix": "app"
}
}
}

12
wg_dashboard_frontend/browserslist

@ -0,0 +1,12 @@
# 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'.

15783
wg_dashboard_frontend/package-lock.json

File diff suppressed because it is too large

78
wg_dashboard_frontend/package.json

@ -0,0 +1,78 @@
{
"name": "wg_dashboard_frontend",
"version": "1.0.0",
"description": "WG Dashboard",
"keywords": [
"dashboard"
],
"license": "MIT",
"scripts": {
"ng": "ng",
"start": "ng serve --host 0.0.0.0 --disable-host-check",
"build": "ng build",
"buildwatch": "ng build --watch --aot --outputPath=../wg_dashboard_backend/build/ --host 0.0.0.0 --disable-host-check",
"tlint": "ng lint",
"tlint:fix": "ng lint --fix",
"slint": "stylelint --syntax scss ./**/*.scss",
"slint:fix": "stylelint --syntax scss --fix ./**/*.scss",
"lint": "ng lint && stylelint --syntax scss ./**/*.scss"
},
"pre-commit": "lint",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/CreativeIT/material-angular-dashboard"
},
"bugs": {
"url": "https://github.com/CreativeIT/material-angular-dashboard/issues"
},
"homepage": "https://github.com/CreativeIT/material-angular-dashboard",
"dependencies": {
"@angular/animations": "9.1.0",
"@angular/common": "9.1.0",
"@angular/compiler": "9.1.0",
"@angular/core": "9.1.0",
"@angular/forms": "9.1.0",
"@angular/localize": "9.1.0",
"@angular/platform-browser": "9.1.0",
"@angular/platform-browser-dynamic": "9.1.0",
"@angular/router": "9.1.0",
"@ng-bootstrap/ng-bootstrap": "^6.0.2",
"angularx-qrcode": "^2.1.0",
"bootstrap": "^4.4.1",
"classlist.js": "1.1.20150312",
"core-js": "3.6.4",
"d3": "5.15.1",
"goog-webfont-dl": "^1.0.3",
"ip-cidr": "^2.0.10",
"material-design-lite": "1.3.0",
"material-icons": "^0.3.1",
"nvd3": "1.8.6",
"rxjs": "6.5.5",
"tslib": "^1.10.0",
"web-animations-js": "^2.3.2",
"zone.js": "^0.10.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.901.0",
"@angular/cli": "9.1.0",
"@angular/compiler-cli": "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/nvd3": "1.8.40",
"codelyzer": "^5.1.2",
"node-sass": "4.13.1",
"pre-commit": "1.2.2",
"stylelint": "13.3.0",
"stylelint-config-recommended-scss": "4.2.0",
"stylelint-config-standard": "20.0.0",
"stylelint-scss": "3.16.0",
"ts-node": "8.8.2",
"tslint": "6.1.1",
"tslint-angular": "3.0.2",
"tslint-config-airbnb": "5.11.2",
"typescript": "3.8.3"
}
}

6
wg_dashboard_frontend/proxy.conf.json

@ -0,0 +1,6 @@
{
"/api": {
"target": "http://localhost:8000",
"secure": false
}
}

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

@ -0,0 +1,31 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { LayoutsModule } from './layouts';
import { CommonLayoutComponent } from './layouts/common-layout';
import { DashboardComponent } from './pages/dashboard';
import {LoginComponent} from "./pages/pages/login";
@NgModule({
imports: [
RouterModule.forRoot(
[
{ path: '', redirectTo: 'app/dashboard', pathMatch: 'full' },
{ path: 'app', component: CommonLayoutComponent, children:
[
{ path: 'dashboard', component: DashboardComponent, pathMatch: 'full'}, // canActivate: [AuthGuard]
{ path: 'login', component: LoginComponent, pathMatch: 'full'},
{ path: '**', redirectTo: '/pages/404' },
]
},
{ path: 'pages', loadChildren: () => import('./pages/pages/pages.module').then(m => m.PagesModule) },
{ path: '**', redirectTo: '/pages/404' },
],
{ useHash: true },
),
LayoutsModule,
],
exports: [RouterModule],
})
export class AppRoutingModule {}

7
wg_dashboard_frontend/src/app/app.component.ts

@ -0,0 +1,7 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `<router-outlet></router-outlet>`,
})
export class AppComponent { }

44
wg_dashboard_frontend/src/app/app.module.ts

@ -0,0 +1,44 @@
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AuthInterceptor, AuthService, FakeBackendInterceptor } from '@services/*';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ComponentsModule } from './pages/components';
import { DashboardModule } from './pages/dashboard';
import { VarDirective } from './directives/var.directive';
import { QRCodeModule } from 'angularx-qrcode';
import {NgbModule} from "@ng-bootstrap/ng-bootstrap";
@NgModule({
declarations: [AppComponent, VarDirective],
imports: [
BrowserModule,
AppRoutingModule,
ComponentsModule,
DashboardModule,
HttpClientModule,
NgbModule,
QRCodeModule
],
providers: [
AuthService,
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true,
},
{
provide: HTTP_INTERCEPTORS,
useClass: FakeBackendInterceptor,
multi: true,
},
],
bootstrap: [AppComponent],
exports: [
VarDirective
]
})
export class AppModule {}

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

@ -0,0 +1,80 @@
@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

@ -0,0 +1,10 @@
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

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

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

@ -0,0 +1,2 @@
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

@ -0,0 +1,27 @@
<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

@ -0,0 +1,54 @@
@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

@ -0,0 +1,19 @@
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

@ -0,0 +1,44 @@
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

@ -0,0 +1,2 @@
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

@ -0,0 +1,27 @@
<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

@ -0,0 +1,31 @@
@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

@ -0,0 +1,19 @@
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

@ -0,0 +1,30 @@
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

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

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

@ -0,0 +1,3 @@
.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

@ -0,0 +1,15 @@
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' },
];
}

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

@ -0,0 +1,22 @@
import {Directive, Input, TemplateRef, ViewContainerRef} from "@angular/core";
@Directive({
selector: '[ngVar]',
})
export class VarDirective {
@Input()
set ngVar(context: any) {
console.log(context)
this.context.$implicit = this.context.ngVar = context;
this.updateView();
}
context: any = {};
constructor(private vcRef: ViewContainerRef, private templateRef: TemplateRef<any>) {}
updateView() {
this.vcRef.clear();
this.vcRef.createEmbeddedView(this.templateRef, this.context);
}
}

2
wg_dashboard_frontend/src/app/index.ts

@ -0,0 +1,2 @@
export { AppComponent } from './app.component';
export { AppModule } from './app.module';

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

@ -0,0 +1,17 @@
export interface Peer {
_stats: any;
address: string;
public_key: string;
private_key: string;
dns: string;
allowed_ips: string;
name: string;
stats: {
sent: string,
received: string,
handshake: string
}
_expand?: boolean;
_edit?: boolean
}

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

@ -0,0 +1,15 @@
import {Peer} from "./peer";
export interface Server {
addresss: string;
interface: string;
listen_port: string;
endpoint: string;
private_key: string;
public_key: string;
shared_key: string;
is_running: boolean;
post_up: string;
post_down: string;
peers: Array<Peer>
}

5
wg_dashboard_frontend/src/app/layouts/blank-layout/blank-layout.component.html

@ -0,0 +1,5 @@
<div class="mdl-layout mdl-js-layout">
<main class="mdl-layout__content">
<router-outlet></router-outlet>
</main>
</div>

11
wg_dashboard_frontend/src/app/layouts/blank-layout/blank-layout.component.scss

@ -0,0 +1,11 @@
@import '~theme/helpers';
app-blank-layout .mdl-layout .mdl-layout__content {
padding: 16px;
display: flex;
}
// FIXME: responsibility leak
.not-found .mdl-layout__content {
background-image: url('#{$image-path}/404.svg');
}

18
wg_dashboard_frontend/src/app/layouts/blank-layout/blank-layout.component.ts

@ -0,0 +1,18 @@
import { Component, HostBinding } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-blank-layout',
styleUrls: ['./blank-layout.component.scss'],
templateUrl: './blank-layout.component.html',
})
export class BlankLayoutComponent {
// FIXME: responsibility leak
@HostBinding('class.not-found') private get notFound() {
return this.router.url === '/pages/404';
}
constructor(
private router: Router,
) { }
}

1
wg_dashboard_frontend/src/app/layouts/blank-layout/index.ts

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

89
wg_dashboard_frontend/src/app/layouts/common-layout/common-layout.component.html

@ -0,0 +1,89 @@
<div class="mdl-layout mdl-js-layout mdl-layout--fixed-drawer mdl-layout--fixed-header has-drawer">
<div class="mdl-layout__header">
<base-page-top>
<span class="mdl-layout__title">{{title}}</span>
<nav class="mdl-navigation">
<base-menu-item *ngFor="let item of menu" [data]="item"></base-menu-item>
<div class="mdl-layout-spacer"></div>
</nav>
<div class="mdl-layout-spacer"></div>
<!--
<div class="mdl-textfield mdl-js-textfield mdl-textfield--expandable search">
<label class="mdl-button mdl-js-button mdl-button--icon" for="search">
<i class="material-icons">search</i>
</label>
<div class="mdl-textfield__expandable-holder">
<input class="mdl-textfield__input" type="text" id="search"/>
<label class="mdl-textfield__label" for="search">Enter your query...</label>
</div>
</div>-->
<div class="avatar-dropdown" id="icon">
<span>Logged in as Admin</span>
<!--<img src="assets/images/Icon_header.png">-->
</div>
<!--<ul
class="mdl-menu mdl-list mdl-menu--bottom-right mdl-js-menu mdl-js-ripple-effect mdl-shadow--2dp account-dropdown"
for="icon">
<li class="mdl-list__item mdl-list__item--two-line">
<span class="mdl-list__item-primary-content">
<span class="material-icons mdl-list__item-avatar"></span>
<span>{{ user.username }}</span>
<span class="mdl-list__item-sub-title">{{ user.email }}</span>
</span>
</li>
<li class="list__item--border-top"></li>
<li class="mdl-menu__item mdl-list__item">
<span class="mdl-list__item-primary-content">
<i class="material-icons mdl-list__item-icon">account_circle</i>
My account
</span>
</li>
<li class="mdl-menu__item mdl-list__item">
<span class="mdl-list__item-primary-content">
<i class="material-icons mdl-list__item-icon">check_box</i>
My tasks
</span>
<span class="mdl-list__item-secondary-content">
<span class="label background-color--primary">3 new</span>
</span>
</li>
<li class="mdl-menu__item mdl-list__item">
<span class="mdl-list__item-primary-content">
<i class="material-icons mdl-list__item-icon">perm_contact_calendar</i>
My events
</span>
</li>
<li class="list__item--border-top"></li>
<li class="mdl-menu__item mdl-list__item">
<span class="mdl-list__item-primary-content">
<i class="material-icons mdl-list__item-icon">settings</i>
Settings
</span>
</li>
<li class="mdl-menu__item mdl-list__item">
<span class="mdl-list__item-primary-content" (click)="logout()">
<i class="material-icons mdl-list__item-icon text-color--secondary">exit_to_app</i>
Log out
</span>
</li>
</ul>-->
</base-page-top>
</div>
<!--<div class="mdl-layout__drawer">
<app-sidebar></app-sidebar>
</div>-->
<main class="mdl-layout__content">
<router-outlet></router-outlet>
</main>
</div>

31
wg_dashboard_frontend/src/app/layouts/common-layout/common-layout.component.ts

@ -0,0 +1,31 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '@services/*';
@Component({
selector: 'app-common-layout',
templateUrl: './common-layout.component.html',
})
export class CommonLayoutComponent implements OnInit {
public title = 'Wireguard Manager';
public menu = [
{ name: 'Dashboard', link: 'dashboard', icon: 'dashboard' },
];
public user;
constructor(private authService: AuthService,
private router: Router) {}
public ngOnInit() {
this.authService.userData.subscribe(user => this.user = user ? user : {
username: 'Luke',
email: 'Luke@skywalker.com',
});
}
public logout() {
this.authService.logout()
.subscribe(res => this.router.navigate(['/pages/login']));
}
}

1
wg_dashboard_frontend/src/app/layouts/common-layout/index.ts

@ -0,0 +1 @@
export { CommonLayoutComponent } from './common-layout.component';

1
wg_dashboard_frontend/src/app/layouts/index.ts

@ -0,0 +1 @@
export { LayoutsModule } from './layouts.module';

32
wg_dashboard_frontend/src/app/layouts/layouts.module.ts

@ -0,0 +1,32 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { BlankLayoutCardComponent } from 'app/components/blank-layout-card';
import { MessageMenuComponent } from 'app/components/message-menu';
import { NotificationMenuComponent } from 'app/components/notification-menu';
import { SidebarComponent } from 'app/components/sidebar';
import { ThemeModule } from 'theme';
import { BlankLayoutComponent } from './blank-layout';
import { CommonLayoutComponent } from './common-layout';
@NgModule({
imports: [
CommonModule,
ThemeModule,
RouterModule,
],
declarations: [
CommonLayoutComponent,
BlankLayoutComponent,
BlankLayoutCardComponent,
SidebarComponent,
MessageMenuComponent,
NotificationMenuComponent,
],
exports: [
CommonLayoutComponent,
BlankLayoutComponent,
],
})
export class LayoutsModule { }

0
wg_dashboard_frontend/src/app/pages/components/components.component.html

0
wg_dashboard_frontend/src/app/pages/components/components.component.scss

31
wg_dashboard_frontend/src/app/pages/components/components.component.ts

@ -0,0 +1,31 @@
import { Component, HostBinding } from '@angular/core';
import { UpgradableComponent } from 'theme/components/upgradable';
@Component({
selector: 'app-components',
templateUrl: './components.component.html',
styleUrls: ['./components.component.scss'],
})
export class ComponentsComponent extends UpgradableComponent {
@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',
},
];
}

31
wg_dashboard_frontend/src/app/pages/components/components.module.ts

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ThemeModule } from 'theme';
import { ComponentsComponent } from './components.component';
import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
import {ModalConfirmComponent} from "./modal-confirm";
import {QRCodeModule} from "angularx-qrcode";
@NgModule({
imports: [
CommonModule,
ThemeModule,
FormsModule,
QRCodeModule
],
providers: [
NgbActiveModal
],
exports: [
ComponentsComponent,
ModalConfirmComponent
],
declarations: [
ComponentsComponent,
ModalConfirmComponent
],
})
export class ComponentsModule { }

2
wg_dashboard_frontend/src/app/pages/components/index.ts

@ -0,0 +1,2 @@
export { ComponentsComponent } from './components.component';
export { ComponentsModule } from './components.module';

1
wg_dashboard_frontend/src/app/pages/components/modal-confirm/index.ts

@ -0,0 +1 @@
export { ModalConfirmComponent } from './modal-confirm.component';

36
wg_dashboard_frontend/src/app/pages/components/modal-confirm/modal-confirm.component.html

@ -0,0 +1,36 @@
<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')">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<textarea *ngIf="area" readonly class="
mdl-textfield--full-width
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>
<div style="text-align: center;" *ngIf="qrCode">
<qrcode [qrdata]="text" [width]="256" [errorCorrectionLevel]="'M'"></qrcode>
</div>
</div>
<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>
<button
class="mdl-button mdl-button--icon mdl-js-button mdl-js-ripple-effect"
data-placement="bottom"
[title]="hover"
(click)="open($event, content)">
<i class="material-icons">{{icon}}</i>
</button>

11
wg_dashboard_frontend/src/app/pages/components/modal-confirm/modal-confirm.component.scss

@ -0,0 +1,11 @@
.dark-modal .modal-content {
background-color: #292b2c;
color: white;
}
.dark-modal .close {
color: white;
}
.light-blue-backdrop {
background-color: #5cb3fd;
}

56
wg_dashboard_frontend/src/app/pages/components/modal-confirm/modal-confirm.component.ts

@ -0,0 +1,56 @@
import {Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation} from '@angular/core';
import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
@Component({
selector: 'app-modal-confirm',
templateUrl: './modal-confirm.component.html',
encapsulation: ViewEncapsulation.None,
styleUrls: ['./modal-confirm.component.scss']
})
export class ModalConfirmComponent implements OnInit{
@Input() noConfirm: boolean = false;
@Input() qrCode: boolean = false;
@Input() icon: string;
@Input() hover: string;
@Input() title: string;
@Input() text: string;
@Input() area: boolean;
@Output() onCancel: EventEmitter<any> = new EventEmitter();
@Output() onConfirm: EventEmitter<any> = new EventEmitter();
constructor(public modal: NgbModal) {
}
open($event, content) {
$event.stopPropagation();
if(this.noConfirm) {
this.onConfirm.emit();
return true;
}
this.modal.open(content, {
ariaLabelledBy: 'modal-basic-title',
backdropClass: "light-blue-backdrop",
windowClass: "dark-modal"
}).result.then((result) => {
if(result === "cancel"){
this.onCancel.emit()
}else if(result === "confirm"){
this.onConfirm.emit();
}
}, (reason) => {
});
}
ngOnInit(): void {
this.area = this.area || false;
this.area = !!this.area
}
}

34
wg_dashboard_frontend/src/app/pages/dashboard/accordion.scss

@ -0,0 +1,34 @@
.mdl-accordion.mdl-accordion--opened {
border-top: 1px solid #e0e0e0;
border-bottom: 1px solid #e0e0e0;
margin-top: -1px;
}
.mdl-accordion.mdl-accordion--opened + .mdl-accordion.mdl-accordion--opened {
border-top: none;
margin-top: 0
}
.mdl-accordion .mdl-accordion__content-wrapper {
overflow: hidden;
}
.mdl-accordion .mdl-accordion__content {
transition-property: margin-top;
transition-duration: 0.2s;
}
.mdl-accordion .mdl-accordion__icon {
transition-property: transform;
transition-duration: 0.2s;
color: rgba(0,0,0,0.3);
position: absolute;
right: 6px;
margin-top: -3px;
}
.mdl-accordion.mdl-accordion--opened .mdl-accordion__icon {
transform: rotate(-180deg);
}
.mdl-accordion.mdl-accordion--opened .mdl-accordion__button {
color: teal;
}
.mdl-accordion.mdl-accordion--opened .mdl-accordion__content {
margin-top: 0 !important;
}

96
wg_dashboard_frontend/src/app/pages/dashboard/add-server/add-server.component.html

@ -0,0 +1,96 @@
<base-card>
<base-card-title>
<h2 class="mdl-card__title-text">Add Server</h2>
</base-card-title>
<base-card-body>
<form [formGroup]="serverForm" (ngSubmit)="serverForm.valid && add(serverForm.value)" class="form">
<p style="margin-bottom: 10px;">Essentials</p>
<div class="mdl-grid">
<div class="mdl-cell mdl-cell--6-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input formControlName="interface" class="mdl-textfield__input" type="text" id="interface" placeholder="wg0" value=""/>
<label class="mdl-textfield__label" for="interface">Interface</label>
</div>
<div class="mdl-cell mdl-cell--6-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input formControlName="address" class="mdl-textfield__input" type="text" id="address" placeholder="10.0.200.1/24" value="10.0.200.1/24"/>
<label class="mdl-textfield__label" for="address">Address</label>
</div>
<div class="mdl-cell mdl-cell--6-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input formControlName="endpoint" class="mdl-textfield__input" type="text" id="endpoint" placeholder="my-address.com" value=""/>
<label class="mdl-textfield__label" for="endpoint">Endpoint</label>
</div>
<div class="mdl-cell mdl-cell--6-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input formControlName="listen_port" class="mdl-textfield__input" type="text" id="listen_port" placeholder="51820" value="51820"/>
<label class="mdl-textfield__label" for="listen_port">Listen-Port</label>
</div>
</div>
<p style="margin-bottom: 10px;">Keys</p>
<div class="mdl-grid">
<div class="mdl-cell mdl-cell--12-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input formControlName="private_key" class="mdl-textfield__input" type="text" id="private_key" placeholder="" value=""/>
<label class="mdl-textfield__label" for="private_key">Private-Key</label>
</div>
<div class="mdl-cell mdl-cell--12-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input formControlName="public_key" class="mdl-textfield__input" type="text" id="public_key" placeholder="" value=""/>
<label class="mdl-textfield__label" for="public_key">Public-Key</label>
</div>
<div class="mdl-cell mdl-cell--12-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input formControlName="shared_key" class="mdl-textfield__input" type="text" id="shared_key" placeholder="" value=""/>
<label class="mdl-textfield__label" for="shared_key">Shared-Key</label>
</div>
<button type="button" [hidden]="!isEdit" (click)="getKeyPair()" class="mdl-button mdl-js-button mdl-button--primary">
<i class="material-icons">vpn_key</i>
Generate KeyPair
</button>
<button type="button" [hidden]="!isEdit" (click)="getPSK()" class="mdl-button mdl-js-button mdl-button--primary">
<i class="material-icons">share</i>
Generate PSK
</button>
</div>
<p style="margin-bottom: 20px;">Scripts</p>
<div class="mdl-grid ">
<div class="mdl-cell mdl-cell--12-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input formControlName="post_up" class="mdl-textfield__input" type="text" id="post_up" placeholder="" value=""/>
<label class="mdl-textfield__label" for="post_up">Post-Up</label>
</div>
<div class="mdl-cell mdl-cell--12-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input formControlName="post_down" class="mdl-textfield__input" type="text" id="post_down" placeholder="" value=""/>
<label class="mdl-textfield__label" for="post_down">Post-Down</label>
</div>
</div>
<button [disabled]="!serverForm.valid" type="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect button--colored-light-blue">
<ng-container *ngIf="!isEdit">Add Server</ng-container>
<ng-container *ngIf="isEdit">Edit Server</ng-container>
</button>
<button (click)="isEdit = false; serverForm.reset()" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect button--colored-light-blue">
Reset
</button>
</form>
</base-card-body>
</base-card>

0
wg_dashboard_frontend/src/app/pages/dashboard/add-server/add-server.component.scss

84
wg_dashboard_frontend/src/app/pages/dashboard/add-server/add-server.component.ts

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

29
wg_dashboard_frontend/src/app/pages/dashboard/dashboard.component.html

@ -0,0 +1,29 @@
<div class="mdl-grid mdl-cell mdl-cell--8-col-desktop mdl-cell--8-col-tablet mdl-cell--12-col-phone mdl-cell--top">
<!-- No Servers -->
<div *ngIf="servers.length == 0" class="mdl-cell mdl-cell--12-col mdl-cell--12-col-tablet">
<base-card>
<base-card-title>
<h2 class="mdl-card__title-text">Servers</h2>
</base-card-title>
<base-card-body>
<p>No Servers</p>
</base-card-body>
</base-card>
</div>
<!-- Servers For Loop -->
<div class="mdl-cell mdl-cell--12-col mdl-cell--12-col-tablet" *ngFor="let server of servers; let idx = index">
<app-server [(server)]="servers[idx]" [(servers)]="servers"></app-server>
</div>
</div>
<div class="mdl-grid mdl-cell mdl-cell--4-col-desktop mdl-cell--4-col-tablet mdl-cell--12-col-phone mdl-cell--top" style="margin-top: 16px;">
<app-add-server [(servers)]="servers"></app-add-server>
</div>

2
wg_dashboard_frontend/src/app/pages/dashboard/dashboard.component.scss

@ -0,0 +1,2 @@

55
wg_dashboard_frontend/src/app/pages/dashboard/dashboard.component.ts

@ -0,0 +1,55 @@
import {Component, HostBinding, OnInit} from '@angular/core';
import { UpgradableComponent } from 'theme/components/upgradable';
import {AbstractControl, FormControl, FormGroup, NgForm, Validators} from "@angular/forms";
// goog-webfont-dl -o src/theme/fonts/font-roboto.css -p assets/fonts/Roboto -d src/assets/fonts/Roboto -a Roboto
import * as IPCIDR from "ip-cidr";
import {Server} from "../../interfaces/server";
import {ServerService} from "../../services/server.service";
import {Peer} from "../../interfaces/peer";
import {IPValidator} from "../../validators/ip-address.validator";
@Component({
selector: 'app-dashboard',
styleUrls: ['./dashboard.component.scss', './accordion.scss'],
templateUrl: './dashboard.component.html',
})
export class DashboardComponent extends UpgradableComponent implements OnInit
{
@HostBinding('class.mdl-grid') private readonly mdlGrid = true;
@HostBinding('class.mdl-grid--no-spacing') private readonly mdlGridNoSpacing = true;
servers: Array<Server> = [];
constructor(private serverAPI: ServerService) {
super();
}
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
});
});
});
})
}
}

33
wg_dashboard_frontend/src/app/pages/dashboard/dashboard.module.ts

@ -0,0 +1,33 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import { ThemeModule } from 'theme';
import { DashboardComponent } from './dashboard.component';
import { PeerComponent } from './peer/peer.component';
import { ComponentsModule } from "../components";
import { AddServerComponent } from './add-server/add-server.component';
import { ServerComponent } from './server/server.component';
import {AppModule} from "../../app.module";
import {QRCodeModule} from "angularx-qrcode";
@NgModule({
imports: [
CommonModule,
ThemeModule,
FormsModule,
ReactiveFormsModule,
ComponentsModule,
QRCodeModule
],
declarations: [
DashboardComponent,
PeerComponent,
AddServerComponent,
ServerComponent,
],
exports: [
],
})
export class DashboardModule { }

2
wg_dashboard_frontend/src/app/pages/dashboard/index.ts

@ -0,0 +1,2 @@
export { DashboardComponent } from './dashboard.component';
export { DashboardModule } from './dashboard.module';

106
wg_dashboard_frontend/src/app/pages/dashboard/peer/peer.component.html

@ -0,0 +1,106 @@
<form ngForm #peerForm="ngForm">
<div class="mdl-grid peer-item" (click)="peer._expand = !peer._expand; fetchConfig()">
<div class="mdl-cell--2-col mdl-cell--12-col-phone" *ngIf="{ a:
(peer._stats && peer._stats.handshake && (peer._stats.handshake.split(' ')[1] === 'seconds' || pInt(peer._stats.handshake.split(' ')[0]) < 3))}; let isRunning">
<i class="material-icons" [ngClass]="{'text-success': isRunning.a, 'text-danger': !isRunning.a}">check_circle</i>
<span>{{peer.name}}</span>
</div>
<div class="mdl-cell--2-col mdl-cell--12-col-phone">{{peer.address}}</div>
<div class="mdl-cell--3-col mdl-cell--12-col-phone">{{peer.public_key | slice:0:20}}...</div>
<div class="mdl-cell--2-col mdl-cell--12-col-phone">{{peer._stats?.tx || '0 B' }}/{{peer._stats?.rx || '0 B'}}</div>
<div class="mdl-cell--2-col mdl-cell--12-col-phone">{{peer._stats?.handshake || "N/A"}}</div>
<div class="mdl-cell--2-col mdl-cell--12-col-phone">
<app-modal-confirm
[noConfirm]="true"
(onConfirm)="edit()"
icon="edit"
hover="Edit {{peer.name}}">
</app-modal-confirm>
<app-modal-confirm
[noConfirm]="false"
(onConfirm)="delete()"
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>
</div>
</div>
</form>
<!-- Expand Area -->
<div class="mdl-grid mdl-layout__header" *ngIf="peer._expand">
<div class="mdl-cell--5-col mdl-cell--12-col-phone mdl-cell--12-col-tablet">
<form #peerForm="ngForm" class="form" (ngSubmit)="peerForm.valid && edit()" >
<p style="font-size: 24px; margin-bottom: 10px;">Essentials</p>
<div class="mdl-grid">
<div class="mdl-cell mdl-cell--6-col ">
<label class="" for="name">Name</label>
<input [disabled]="!peer._edit" type="text" name="name" id="name" [(ngModel)]="peer.name" class="mdl-textfield__input" />
</div>
<div class="mdl-cell mdl-cell--6-col">
<label class="" for="address">Address</label>
<input [disabled]="!peer._edit" type="text" name="address" id="address" [(ngModel)]="peer.address" class="mdl-textfield__input"/>
</div>
<p style="width: 100%;margin-bottom: 15px;"></p>
<div class="mdl-cell mdl-cell--12-col">
<label class="" for="dns">DNS</label>
<input [disabled]="!peer._edit" type="text" name="dns" id="dns" [(ngModel)]="peer.dns" class="mdl-textfield__input"/>
</div>
<p style="width: 100%;margin-bottom: 15px;"></p>
<div class="mdl-cell mdl-cell--12-col">
<label class="" for="allowed_ips">Allowed IPs</label>
<input [disabled]="!peer._edit" type="text" name="allowed_ips" id="allowed_ips" [(ngModel)]="peer.allowed_ips" class="mdl-textfield__input"/>
</div>
</div>
<p style="width: 100%;margin-bottom: 15px;"></p>
<p style="font-size:24px; margin-bottom: 10px;">Keys</p>
<div class="mdl-grid">
<div class="mdl-cell mdl-cell--12-col">
<label class="" for="private_key">Private-Key</label>
<input [disabled]="!peer._edit" type="text" name="private_key" id="private_key" [(ngModel)]="peer.private_key" class="mdl-textfield__input"/>
</div>
<p style="width: 100%;margin-bottom: 15px;"></p>
<div class="mdl-cell mdl-cell--12-col">
<label class="" for="public_key">Public-Key</label>
<input [disabled]="!peer._edit" type="text" name="public_key" id="public_key" [(ngModel)]="peer.public_key" class="mdl-textfield__input"/>
</div>
</div>
<button
[hidden]="!peer._edit"
[disabled]="!peerForm.valid"
type="submit"
class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect button--colored-light-blue">
Submit changes
</button>
</form>
</div>
<div class="mdl-cell--5-col mdl-cell--12-col-phone mdl-cell--12-col-tablet">
<textarea readonly class="mdl-textfield--full-width"
style="min-height: 250px; height: 100%; background-color: #202020; color: #00bcd4;">{{config}}</textarea>
</div>
<div class="mdl-cell--2-col mdl-cell--12-col-phone mdl-cell--12-col-tablet">
<div style="text-align: center;">
<qrcode [qrdata]="config" [width]="256" [errorCorrectionLevel]="'M'"></qrcode>
</div>
</div>
</div>

16
wg_dashboard_frontend/src/app/pages/dashboard/peer/peer.component.scss

@ -0,0 +1,16 @@
.peer-item > div{
padding-top: 15px;
padding-bottom: 15px;
border-top: 1px solid black;
}
.peer-item-header > div{
font-weight: bold;
padding-top: 15px;
padding-bottom: 15px;
}
.no-padding{
padding: 0 !important;
}

61
wg_dashboard_frontend/src/app/pages/dashboard/peer/peer.component.ts

@ -0,0 +1,61 @@
import {Component, Input, OnInit} from '@angular/core';
import {ServerService} from "../../../services/server.service";
import {Peer} from "../../../interfaces/peer";
import {Server} from "../../../interfaces/server";
@Component({
selector: 'app-peer',
templateUrl: './peer.component.html',
styleUrls: ['./peer.component.scss']
})
export class PeerComponent implements OnInit {
@Input("peer") peer: Peer;
@Input("server") server: Server;
config: string = "Loading...";
constructor(public serverAPI: ServerService) { }
ngOnInit(): void {
}
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) => {
this.server.peers[idx] = newPeer;
});
} 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)
}
}

96
wg_dashboard_frontend/src/app/pages/dashboard/server/server.component.html

@ -0,0 +1,96 @@
<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>
<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>
</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>

0
wg_dashboard_frontend/src/app/pages/dashboard/server/server.component.scss

60
wg_dashboard_frontend/src/app/pages/dashboard/server/server.component.ts

@ -0,0 +1,60 @@
import {Component, Input, OnInit, Output} from '@angular/core';
import {Server} from "../../../interfaces/server";
import {ServerService} from "../../../services/server.service";
import {DataService} from "../../../services/data.service";
@Component({
selector: 'app-server',
templateUrl: './server.component.html',
styleUrls: ['./server.component.scss']
})
export class ServerComponent implements OnInit {
@Input() server: Server;
@Input() servers: Array<Server>;
serverConfig: string;
constructor(private serverAPI: ServerService, private comm: DataService) { }
ngOnInit(): void {
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);
})
}
}

20
wg_dashboard_frontend/src/app/pages/pages/error/error.component.html

@ -0,0 +1,20 @@
<div class="mdl-card mdl-card__blank-layout-card mdl-shadow--2dp">
<div class="mdl-card__supporting-text color--dark-gray">
<div class="mdl-grid">
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone">
<span class="mdl-card__title-text text-color--smooth-gray">DARKBOARD</span>
</div>
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone">
<span class="text--huge color-text--light-blue">404</span>
</div>
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone">
<span class="text--sorry text-color--white">Sorry, but there's nothing here</span>
</div>
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone">
<a routerLink="/" class="mdl-button mdl-js-button color-text--light-blue pull-right">
Go Back
</a>
</div>
</div>
</div>
</div>

10
wg_dashboard_frontend/src/app/pages/pages/error/error.component.ts

@ -0,0 +1,10 @@
import { Component, HostBinding } from '@angular/core';
import { BlankLayoutCardComponent } from 'app/components/blank-layout-card';
@Component({
selector: 'app-error',
styleUrls: ['../../../components/blank-layout-card/blank-layout-card.component.scss'],
templateUrl: './error.component.html',
})
export class ErrorComponent extends BlankLayoutCardComponent { }

1
wg_dashboard_frontend/src/app/pages/pages/error/index.ts

@ -0,0 +1 @@
export { ErrorComponent } from './error.component';

27
wg_dashboard_frontend/src/app/pages/pages/forgot-password/forgot-password.component.html

@ -0,0 +1,27 @@
<div class="mdl-card mdl-card__blank-layout-card mdl-shadow--2dp">
<div class="mdl-card__supporting-text color--dark-gray">
<div class="mdl-grid">
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone">
<span class="mdl-card__title-text text-color--smooth-gray">DARKBOARD</span>
</div>
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone">
<span class="blank-layout-card-name text-color--white">Forgot password?</span>
<span class="blank-layout-card-secondary-text text-color--smoke">Enter your email below to recieve your password</span>
</div>
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label full-size">
<input class="mdl-textfield__input" type="text" id="e-mail">
<label class="mdl-textfield__label" for="e-mail">Email</label>
</div>
</div>
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone submit-cell">
<div class="mdl-layout-spacer"></div>
<a routerLink="/app/dashboard">
<button class="mdl-button mdl-js-button mdl-button--raised color--light-blue">
SEND PASSWORD
</button>
</a>
</div>
</div>
</div>
</div>

10
wg_dashboard_frontend/src/app/pages/pages/forgot-password/forgot-password.component.ts

@ -0,0 +1,10 @@
import { Component, HostBinding } from '@angular/core';
import { BlankLayoutCardComponent } from 'app/components/blank-layout-card';
@Component({
selector: 'app-forgot-password',
styleUrls: ['../../../components/blank-layout-card/blank-layout-card.component.scss'],
templateUrl: './forgot-password.component.html',
})
export class ForgotPasswordComponent extends BlankLayoutCardComponent { }

1
wg_dashboard_frontend/src/app/pages/pages/forgot-password/index.ts

@ -0,0 +1 @@
export { ForgotPasswordComponent } from './forgot-password.component';

1
wg_dashboard_frontend/src/app/pages/pages/index.ts

@ -0,0 +1 @@
export { PagesModule } from './pages.module';

1
wg_dashboard_frontend/src/app/pages/pages/login/index.ts

@ -0,0 +1 @@
export { LoginComponent } from './login.component';

85
wg_dashboard_frontend/src/app/pages/pages/login/login.component.html

@ -0,0 +1,85 @@
<div class="mdl-card mdl-shadow--2dp">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">Login to Wireguard Manager</h2>
</div>
<div class="mdl-card__supporting-text">
<form class="login-form" [formGroup]="loginForm" (submit)="login()" autocomplete="off" novalidate>
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label full-size"
[class.is-invalid]="email.invalid && (email.dirty || email.touched)"
[class.is-valid]="email.valid && (email.dirty || email.touched)">
<input formControlName="email"
pattern="{{emailPattern}}"
(change)="onInputChange($event)"
class="mdl-textfield__input" type="text" id="email">
<label class="mdl-textfield__label" for="email">Email</label>
<div *ngIf="email.invalid && (email.dirty || email.touched)">
<span *ngIf="email.errors.required" class="mdl-textfield__error">
Email is required. <span class="color-text--orange"> Please, write any valid email.</span>
</span>
<span *ngIf="email.errors.pattern" class="mdl-textfield__error">
Email is invalid.
</span>
</div>
</div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label full-size"
[class.is-invalid]="password.invalid && (password.dirty || password.touched)"
[class.is-valid]="password.valid && (password.dirty || password.touched)"
id="forPass">
<input formControlName="password"
(change)="onInputChange($event)"
class="mdl-textfield__input" type="password" id="password">
<label class="mdl-textfield__label" for="password">Password</label>
<div *ngIf="password.invalid && (password.dirty || password.touched)">
<span *ngIf="password.errors.required" class="mdl-textfield__error">
Password is required. <span class="color-text--orange"> Please, write any password.</span>
</span>
</div>
</div>
<div class="full-size color-text--red" *ngIf="error"> {{ error }}</div>
</div>
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone submit-cell">
</div>
</form>
</div>
<div class="mdl-card__actions mdl-card--border">
<a class="mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect">
<a routerLink="/pages/forgot-password" class="blank-layout-card-link">Forgot password?</a>
<a routerLink="/pages/sign-up" class="blank-layout-card-link">Don't have account?</a>
<button class="mdl-button mdl-js-button mdl-button--raised color--light-blue"
type="submit" [disabled]="loginForm.invalid">
SIGN IN
</button>
</a>
</div>
<div class="mdl-card__menu">
<button class="mdl-button mdl-button--icon mdl-js-button mdl-js-ripple-effect">
<i class="material-icons">share</i>
</button>
</div>
</div>

57
wg_dashboard_frontend/src/app/pages/pages/login/login.component.ts

@ -0,0 +1,57 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { BlankLayoutCardComponent } from 'app/components/blank-layout-card';
import {AuthService} from "@services/*";
@Component({
selector: 'app-login',
styleUrls: ['../../../components/blank-layout-card/blank-layout-card.component.scss'],
templateUrl: './login.component.html',
})
export class LoginComponent extends BlankLayoutCardComponent implements OnInit {
public loginForm: FormGroup;
public email;
public password;
public emailPattern = '^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$';
public error: string;
constructor(private authService: AuthService,
private fb: FormBuilder,
private router: Router) {
super();
this.loginForm = this.fb.group({
password: new FormControl('', Validators.required),
email: new FormControl('', [
Validators.required,
Validators.pattern(this.emailPattern),
Validators.maxLength(20),
]),
});
this.email = this.loginForm.get('email');
this.password = this.loginForm.get('password');
}
public ngOnInit() {
this.authService.logout();
this.loginForm.valueChanges.subscribe(() => {
this.error = null;
});
}
public login() {
this.error = null;
if (this.loginForm.valid) {
this.authService.login(this.loginForm.getRawValue())
.subscribe(res => this.router.navigate(['/app/dashboard']),
error => this.error = error.message);
}
}
public onInputChange(event) {
event.target.required = true;
}
}

30
wg_dashboard_frontend/src/app/pages/pages/pages-routing.module.ts

@ -0,0 +1,30 @@
import { ModuleWithProviders, NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LayoutsModule } from 'app/layouts';
import { BlankLayoutComponent } from 'app/layouts/blank-layout';
import { ErrorComponent } from './error';
import { ForgotPasswordComponent } from './forgot-password';
import { LoginComponent } from './login';
import { SignUpComponent } from './sign-up';
@NgModule({
imports: [
RouterModule.forChild([
{
path: '',
component: BlankLayoutComponent,
children: [
{ path: '404', component: ErrorComponent, pathMatch: 'full' },
{ path: 'login', component: LoginComponent, pathMatch: 'full' },
{ path: 'sign-up', component: SignUpComponent, pathMatch: 'full' },
{ path: 'forgot-password', component: ForgotPasswordComponent, pathMatch: 'full' },
{ path: '**', redirectTo: '404' },
],
},
]),
LayoutsModule,
],
exports: [RouterModule],
})
export class PagesRoutingModule { }

30
wg_dashboard_frontend/src/app/pages/pages/pages.module.ts

@ -0,0 +1,30 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ThemeModule } from 'theme';
import { TooltipModule } from '../../../theme/directives/tooltip/tooltip.module';
import { ErrorComponent } from './error';
import { ForgotPasswordComponent } from './forgot-password';
import { LoginComponent } from './login';
import { PagesRoutingModule } from './pages-routing.module';
import { SignUpComponent } from './sign-up';
@NgModule({
imports: [
CommonModule,
ThemeModule,
PagesRoutingModule,
FormsModule,
ReactiveFormsModule,
TooltipModule,
],
declarations: [
ErrorComponent,
LoginComponent,
SignUpComponent,
ForgotPasswordComponent,
],
})
export class PagesModule { }

1
wg_dashboard_frontend/src/app/pages/pages/sign-up/index.ts

@ -0,0 +1 @@
export { SignUpComponent } from './sign-up.component';

73
wg_dashboard_frontend/src/app/pages/pages/sign-up/sign-up.component.html

@ -0,0 +1,73 @@
<div class="mdl-card mdl-card__blank-layout-card mdl-shadow--2dp">
<div class="mdl-card__supporting-text color--dark-gray">
<div class="mdl-grid">
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone">
<span class="mdl-card__title-text text-color--smooth-gray">DARKBOARD</span>
</div>
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone">
<span class="blank-layout-card-name text-color--white">Sign up</span>
</div>
<form class="login-form" [formGroup]="signupForm" (submit)="login()" novalidate autocomplete="off">
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label full-size"
[class.is-invalid]="username.invalid && (username.dirty || username.touched)"
[class.is-valid]="username.valid && (username.dirty || username.touched)">
<input formControlName="username"
(change)="onInputChange($event)"
class="mdl-textfield__input" type="text" id="username">
<label class="mdl-textfield__label" for="username">Name</label>
<div *ngIf="username.invalid && (username.dirty || username.touched)">
<span *ngIf="username.errors.required" class="mdl-textfield__error">
Name is required. <span class="color-text--orange"> Please, write any name.</span>
</span>
</div>
</div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label full-size"
[class.is-invalid]="password.invalid && (password.dirty || password.touched)"
[class.is-valid]="password.valid && (password.dirty || password.touched)" id="forRass">
<input formControlName="password"
(change)="onInputChange($event)"
class="mdl-textfield__input" type="password" id="password">
<label class="mdl-textfield__label" for="password">Password</label>
<div *ngIf="password.invalid && (password.dirty || password.touched)">
<span *ngIf="password.errors.required" class="mdl-textfield__error">
Password is required. <span class="color-text--orange"> Please, write any password.</span>
</span>
</div>
</div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label full-size"
[class.is-invalid]="email.invalid && (email.dirty || email.touched)"
[class.is-valid]="email.valid && (email.dirty || email.touched)">
<input formControlName="email"
pattern="{{emailPattern}}"
(change)="onInputChange($event)"
class="mdl-textfield__input" type="text" id="email">
<label class="mdl-textfield__label" for="email">Email</label>
<div *ngIf="email.invalid && (email.dirty || email.touched)">
<span *ngIf="email.errors.required" class="mdl-textfield__error">
Email is required. <span class="color-text--orange"> Please, write any valid email.</span>
</span>
<span *ngIf="email.errors.pattern" class="mdl-textfield__error">
Email is invalid.
</span>
</div>
</div>
<label baseCheckbox color="light-blue" class="checkbox--inline" inline></label>
<span class="blank-layout-card-link">I agree all statements in <a href="#"
class="underlined">terms of service</a></span>
</div>
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone submit-cell">
<a routerLink="/pages/login" class="blank-layout-card-link">I have already signed up</a>
<div class="mdl-layout-spacer"></div>
<button class="mdl-button mdl-js-button mdl-button--raised color--light-blue"
type="submit" [disabled]="signupForm.invalid">
SIGN UP
</button>
</div>
</form>
</div>
</div>
</div>

61
wg_dashboard_frontend/src/app/pages/pages/sign-up/sign-up.component.ts

@ -0,0 +1,61 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from '@services/*';
import { BlankLayoutCardComponent } from 'app/components/blank-layout-card';
@Component({
selector: 'app-sign-up',
styleUrls: ['../../../components/blank-layout-card/blank-layout-card.component.scss'],
templateUrl: './sign-up.component.html',
})
export class SignUpComponent extends BlankLayoutCardComponent implements OnInit {
public signupForm: FormGroup;
public email;
public password;
public username;
public emailPattern = '^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$';
public error: string;
constructor(private authService: AuthService,
private fb: FormBuilder,
private router: Router) {
super();
this.signupForm = this.fb.group({
password: new FormControl('', Validators.required),
email: new FormControl('', [
Validators.required,
Validators.pattern(this.emailPattern),
Validators.maxLength(20),
]),
username: new FormControl('', [Validators.required, Validators.maxLength(10)]),
});
this.email = this.signupForm.get('email');
this.password = this.signupForm.get('password');
this.username = this.signupForm.get('username');
}
public ngOnInit() {
this.authService.logout();
this.signupForm.valueChanges.subscribe(() => {
this.error = null;
});
}
public login() {
this.error = null;
if (this.signupForm.valid) {
this.authService.signup(this.signupForm.getRawValue())
.subscribe(res => this.router.navigate(['/app/dashboard']),
error => this.error = error.message);
}
}
public onInputChange(event) {
event.target.required = true;
}
}

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

Loading…
Cancel
Save