Per-Arne
5 years ago
commit
a33a1a8661
268 changed files with 23212 additions and 0 deletions
@ -0,0 +1,9 @@ |
|||||
|
|
||||
|
**/config |
||||
|
**/build |
||||
|
**/database.db |
||||
|
**/node_modules |
||||
|
**/.github |
||||
|
**/dist |
||||
|
|
||||
|
|
@ -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 |
@ -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 |
||||
|
|
@ -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,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,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 |
@ -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"}) |
@ -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) |
@ -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") |
@ -0,0 +1,11 @@ |
|||||
|
pydantic |
||||
|
fastapi |
||||
|
aiofiles |
||||
|
aiosqlite |
||||
|
sqlalchemy |
||||
|
databases |
||||
|
PyJWT |
||||
|
passlib |
||||
|
bcrypt |
||||
|
python-multipart |
||||
|
jinja2 |
@ -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,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 |
@ -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 %} |
@ -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 %} |
@ -0,0 +1,2 @@ |
|||||
|
from jinja2 import Environment, PackageLoader |
||||
|
jinja_env = Environment(loader=PackageLoader(__name__, 'templates')) |
@ -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 |
@ -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 |
@ -0,0 +1,20 @@ |
|||||
|
#### Expected behavior |
||||
|
|
||||
|
|
||||
|
|
||||
|
#### Actual behavior |
||||
|
|
||||
|
|
||||
|
|
||||
|
#### Steps to reproduce the behavior |
||||
|
|
||||
|
|
||||
|
|
||||
|
#### Relevant code |
||||
|
|
||||
|
``` |
||||
|
|
||||
|
``` |
||||
|
|
||||
|
#### Environment description |
||||
|
|
@ -0,0 +1,6 @@ |
|||||
|
#### Related issues |
||||
|
|
||||
|
|
||||
|
|
||||
|
#### Changes proposed in this pull request |
||||
|
|
@ -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 |
||||
|
} |
||||
|
} |
@ -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. |
@ -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" |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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'. |
File diff suppressed because it is too large
@ -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" |
||||
|
} |
||||
|
} |
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"/api": { |
||||
|
"target": "http://localhost:8000", |
||||
|
"secure": false |
||||
|
} |
||||
|
} |
@ -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 {} |
@ -0,0 +1,7 @@ |
|||||
|
import { Component } from '@angular/core'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-root', |
||||
|
template: `<router-outlet></router-outlet>`, |
||||
|
}) |
||||
|
export class AppComponent { } |
@ -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 {} |
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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; |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
export { BlankLayoutCardComponent } from './blank-layout-card.component'; |
@ -0,0 +1,2 @@ |
|||||
|
export { MessageMenuComponent } from './message-menu.component'; |
||||
|
export { MessageMenuService } from './message-menu.service'; |
@ -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> |
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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(); |
||||
|
} |
||||
|
} |
@ -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', |
||||
|
}, |
||||
|
]; |
||||
|
} |
||||
|
} |
@ -0,0 +1,2 @@ |
|||||
|
export { NotificationMenuComponent } from './notification-menu.component'; |
||||
|
export { NotificationMenuService } from './notification-menu.service'; |
@ -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> |
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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(); |
||||
|
} |
||||
|
} |
@ -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', |
||||
|
}, |
||||
|
]; |
||||
|
} |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
export { SidebarComponent } from './sidebar.component'; |
@ -0,0 +1,3 @@ |
|||||
|
.mdl-navigation base-menu-item:nth-child(2) i.material-icons { |
||||
|
transform: rotate(180deg); |
||||
|
} |
@ -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' }, |
||||
|
]; |
||||
|
} |
@ -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); |
||||
|
} |
||||
|
} |
@ -0,0 +1,2 @@ |
|||||
|
export { AppComponent } from './app.component'; |
||||
|
export { AppModule } from './app.module'; |
@ -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 |
||||
|
} |
@ -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> |
||||
|
} |
@ -0,0 +1,5 @@ |
|||||
|
<div class="mdl-layout mdl-js-layout"> |
||||
|
<main class="mdl-layout__content"> |
||||
|
<router-outlet></router-outlet> |
||||
|
</main> |
||||
|
</div> |
@ -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'); |
||||
|
} |
@ -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, |
||||
|
) { } |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
export { BlankLayoutComponent } from './blank-layout.component'; |
@ -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> |
@ -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'])); |
||||
|
} |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
export { CommonLayoutComponent } from './common-layout.component'; |
@ -0,0 +1 @@ |
|||||
|
export { LayoutsModule } from './layouts.module'; |
@ -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,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', |
||||
|
}, |
||||
|
]; |
||||
|
} |
@ -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 { } |
@ -0,0 +1,2 @@ |
|||||
|
export { ComponentsComponent } from './components.component'; |
||||
|
export { ComponentsModule } from './components.module'; |
@ -0,0 +1 @@ |
|||||
|
export { ModalConfirmComponent } from './modal-confirm.component'; |
@ -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">×</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> |
@ -0,0 +1,11 @@ |
|||||
|
.dark-modal .modal-content { |
||||
|
background-color: #292b2c; |
||||
|
color: white; |
||||
|
} |
||||
|
|
||||
|
.dark-modal .close { |
||||
|
color: white; |
||||
|
} |
||||
|
.light-blue-backdrop { |
||||
|
background-color: #5cb3fd; |
||||
|
} |
@ -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 |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
} |
@ -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; |
||||
|
} |
@ -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,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 |
||||
|
}) |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -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> |
@ -0,0 +1,2 @@ |
|||||
|
|
||||
|
|
@ -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 |
||||
|
}); |
||||
|
|
||||
|
|
||||
|
}); |
||||
|
|
||||
|
|
||||
|
}); |
||||
|
|
||||
|
|
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
} |
@ -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 { } |
@ -0,0 +1,2 @@ |
|||||
|
export { DashboardComponent } from './dashboard.component'; |
||||
|
export { DashboardModule } from './dashboard.module'; |
@ -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> |
@ -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; |
||||
|
} |
@ -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) |
||||
|
} |
||||
|
} |
@ -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,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); |
||||
|
}) |
||||
|
} |
||||
|
} |
@ -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> |
@ -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 { } |
@ -0,0 +1 @@ |
|||||
|
export { ErrorComponent } from './error.component'; |
@ -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> |
@ -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 { } |
@ -0,0 +1 @@ |
|||||
|
export { ForgotPasswordComponent } from './forgot-password.component'; |
@ -0,0 +1 @@ |
|||||
|
export { PagesModule } from './pages.module'; |
@ -0,0 +1 @@ |
|||||
|
export { LoginComponent } from './login.component'; |
@ -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> |
||||
|
|
||||
|
|
@ -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; |
||||
|
} |
||||
|
} |
@ -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 { } |
@ -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 { } |
@ -0,0 +1 @@ |
|||||
|
export { SignUpComponent } from './sign-up.component'; |
@ -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> |
@ -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…
Reference in new issue