├── .dockerignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── image.yml ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── back └── test.py ├── docker-compose.yaml ├── docker ├── gunicorn_config.py ├── push.sh └── start.py ├── docs ├── api.md ├── guides │ ├── docker_configuration.md │ ├── import_existing_server.md │ └── reverse_proxy.md ├── images │ ├── 0.png │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ └── 8.png ├── install.md ├── install_debian.md └── install_docker_github.md ├── package-lock.json ├── scripts └── ci.sh ├── wg-manager-backend ├── __init__.py ├── alembic.ini ├── const.py ├── database │ ├── __init__.py │ ├── database.py │ ├── models.py │ └── util.py ├── db │ ├── __init__.py │ ├── api_key.py │ ├── user.py │ └── wireguard.py ├── logger.py ├── logging.json ├── main.py ├── middleware.py ├── migrations │ ├── README │ ├── env.py │ ├── script.py.mako │ └── versions │ │ └── 4ac3e58519eb_base.py ├── requirements.txt ├── routers │ ├── __init__.py │ └── v1 │ │ ├── __init__.py │ │ ├── peer.py │ │ ├── server.py │ │ ├── user.py │ │ └── wg.py ├── schemas.py ├── script │ ├── __init__.py │ ├── obfuscate │ │ ├── __init__.py │ │ ├── obfs4.py │ │ ├── shapeshifter.py │ │ └── tor.py │ ├── wireguard.py │ └── wireguard_startup.py ├── templates │ ├── peer.j2 │ └── server.j2 ├── tests │ ├── __init__.py │ ├── database.db │ └── test_pytest.py └── util.py └── wg-manager-frontend ├── .editorconfig ├── .eslintrc.json ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .stylelintrc ├── LICENSE ├── angular.json ├── package.json ├── proxy.conf.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── directives │ │ └── var.directive.ts │ ├── index.ts │ ├── interfaces │ │ ├── peer.ts │ │ ├── server.ts │ │ └── user.ts │ ├── layout │ │ ├── layout.module.ts │ │ └── layout │ │ │ ├── layout.component.html │ │ │ ├── layout.component.scss │ │ │ ├── layout.component.spec.ts │ │ │ └── layout.component.ts │ ├── page │ │ ├── components │ │ │ ├── components.component.html │ │ │ ├── components.component.scss │ │ │ ├── components.component.ts │ │ │ ├── components.module.ts │ │ │ ├── index.ts │ │ │ └── modal-confirm │ │ │ │ ├── index.ts │ │ │ │ ├── modal-confirm.component.html │ │ │ │ ├── modal-confirm.component.scss │ │ │ │ └── modal-confirm.component.ts │ │ ├── dashboard │ │ │ ├── add-server │ │ │ │ ├── add-server.component.html │ │ │ │ ├── add-server.component.scss │ │ │ │ └── add-server.component.ts │ │ │ ├── dashboard.component.css │ │ │ ├── dashboard.component.html │ │ │ ├── dashboard.component.ts │ │ │ ├── dashboard.module.ts │ │ │ ├── peer │ │ │ │ ├── peer.component.html │ │ │ │ ├── peer.component.scss │ │ │ │ └── peer.component.ts │ │ │ └── server │ │ │ │ ├── server.component.html │ │ │ │ ├── server.component.scss │ │ │ │ └── server.component.ts │ │ ├── error │ │ │ ├── error.component.css │ │ │ ├── error.component.html │ │ │ ├── error.component.ts │ │ │ └── index.ts │ │ ├── page-routing.module.ts │ │ ├── page.module.ts │ │ └── user │ │ │ ├── edit │ │ │ ├── api-key │ │ │ │ ├── api-key.component.html │ │ │ │ ├── api-key.component.scss │ │ │ │ ├── api-key.component.spec.ts │ │ │ │ └── api-key.component.ts │ │ │ ├── edit.component.html │ │ │ ├── edit.component.scss │ │ │ └── edit.component.ts │ │ │ └── login │ │ │ ├── login.component.html │ │ │ ├── login.component.scss │ │ │ ├── login.component.spec.ts │ │ │ └── login.component.ts │ ├── services │ │ ├── auth │ │ │ ├── auth.guard.ts │ │ │ ├── auth.interceptor.ts │ │ │ ├── auth.service.ts │ │ │ └── index.ts │ │ ├── config.service.ts │ │ ├── data.service.ts │ │ ├── index.ts │ │ └── server.service.ts │ └── validators │ │ ├── ip-address.validator.ts │ │ └── number.validator.ts ├── assets │ ├── .gitkeep │ ├── fonts │ │ └── Roboto │ │ │ ├── Roboto-Black-Italic.ttf │ │ │ ├── Roboto-Black-Italic.woff │ │ │ ├── Roboto-Black-Italic.woff2 │ │ │ ├── Roboto-Black.ttf │ │ │ ├── Roboto-Black.woff │ │ │ ├── Roboto-Black.woff2 │ │ │ ├── Roboto-Bold-Italic.ttf │ │ │ ├── Roboto-Bold-Italic.woff │ │ │ ├── Roboto-Bold-Italic.woff2 │ │ │ ├── Roboto-Bold.ttf │ │ │ ├── Roboto-Bold.woff │ │ │ ├── Roboto-Bold.woff2 │ │ │ ├── Roboto-Italic.ttf │ │ │ ├── Roboto-Italic.woff │ │ │ ├── Roboto-Italic.woff2 │ │ │ ├── Roboto-Light-Italic.ttf │ │ │ ├── Roboto-Light-Italic.woff │ │ │ ├── Roboto-Light-Italic.woff2 │ │ │ ├── Roboto-Light.ttf │ │ │ ├── Roboto-Light.woff │ │ │ ├── Roboto-Light.woff2 │ │ │ ├── Roboto-Thin-Italic.ttf │ │ │ ├── Roboto-Thin-Italic.woff │ │ │ ├── Roboto-Thin-Italic.woff2 │ │ │ ├── Roboto-Thin.ttf │ │ │ ├── Roboto-Thin.woff │ │ │ ├── Roboto-Thin.woff2 │ │ │ ├── Roboto.eot │ │ │ ├── Roboto.svg │ │ │ ├── Roboto.ttf │ │ │ ├── Roboto.woff │ │ │ └── Roboto.woff2 │ ├── icons │ │ ├── favicon-16x16.png │ │ └── favicon-32x32.png │ └── images │ │ ├── 404.svg │ │ ├── Bobby.PNG │ │ ├── DB_16х16.png │ │ ├── Dark_background_1920x1080.png │ │ ├── Icon.png │ │ ├── Icon_header.png │ │ ├── active_marker.png │ │ ├── buffer.svg │ │ ├── card.jpg │ │ ├── cloudy_and_snow.svg │ │ ├── cotoneaster.jpg │ │ ├── gina-torres.png │ │ ├── imgo.png │ │ ├── istanbul.jpg │ │ ├── map_target_images_sprite.png │ │ ├── marker-advanced-active.svg │ │ ├── marker-advanced.svg │ │ ├── marker.png │ │ ├── nathan-fillion.png │ │ ├── robot.png │ │ ├── sao_paulo.jpg │ │ ├── skype.svg │ │ ├── tick-mask.svg │ │ ├── tick.svg │ │ ├── tick_dark.svg │ │ ├── tokyo.jpg │ │ ├── tudyk.png │ │ ├── watch_white.svg │ │ └── weather_bck.png ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── index.html ├── main.ts ├── polyfills.ts ├── theme │ ├── fonts │ │ └── font-roboto.css │ ├── index.ts │ ├── styles.scss │ └── theme.module.ts ├── tsconfig.app.json └── typings.d.ts ├── tsconfig.json └── widdershins.json /.dockerignore: -------------------------------------------------------------------------------- 1 | 2 | **/config 3 | **/build 4 | **/database.db 5 | **/node_modules 6 | **/.github 7 | **/dist 8 | 9 | 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [perara] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Platform:** 21 | - OS: RPI/Ubuntu/Windows/Docker ...etc 22 | - Browser [e.g. chrome, safari] 23 | - Version git-commit (If you updated and got error please try to record from -> to version 24 | 25 | **Stacktrace/error output** 26 | Add any other context about the problem here. 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | 31 | **Screenshots** 32 | If applicable, add screenshots to help explain your problem. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/image.yml: -------------------------------------------------------------------------------- 1 | name: build and publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | 8 | jobs: 9 | docker: 10 | runs-on: ubuntu-latest 11 | steps: 12 | 13 | - 14 | name: Checkout 15 | uses: actions/checkout@v2 16 | - 17 | name: Set up QEMU 18 | uses: docker/setup-qemu-action@v1 19 | - 20 | name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v1 22 | - 23 | name: Login to DockerHub 24 | uses: docker/login-action@v1 25 | with: 26 | username: ${{ secrets.DOCKER_USERNAME }} 27 | password: ${{ secrets.DOCKER_PASSWORD }} 28 | 29 | - 30 | name: Build and push main 31 | uses: docker/build-push-action@v2 32 | with: 33 | context: . 34 | platforms: linux/amd64,linux/arm64 35 | push: true 36 | tags: perara/wg-manager:latest 37 | if: github.ref == 'refs/heads/main' 38 | - 39 | name: Build and push dev 40 | uses: docker/build-push-action@v2 41 | with: 42 | context: . 43 | platforms: linux/amd64,linux/arm64,linux/arm/v7 44 | push: true 45 | tags: perara/wg-manager:dev 46 | if: github.ref == 'refs/heads/dev' 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /venv 8 | venv 9 | # dependencies 10 | /node_modules 11 | **node_modules 12 | **build 13 | **dist 14 | **__pycache__ 15 | 16 | # IDEs and editors 17 | /.idea 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | 25 | # IDE - VSCode 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | 32 | # misc 33 | /.sass-cache 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | npm-debug.log 38 | testem.log 39 | /typings 40 | 41 | # e2e 42 | /e2e/*.js 43 | /e2e/*.map 44 | 45 | # System Files 46 | .DS_Store 47 | Thumbs.db 48 | 49 | docker/__pycache__/ 50 | 51 | wg-manager-backend/__pycache__/ 52 | 53 | wg-manager-frontend/yarn.lock 54 | 55 | wg-manager-backend/config/server/wg0/clients/4.conf 56 | 57 | wg-manager-backend/config/server/wg0/clients/5.conf 58 | 59 | wg-manager-backend/config/server/wg0/clients/6.conf 60 | 61 | wg-manager-backend/config/server/wg0/clients/7.conf 62 | 63 | wg-manager-backend/config/server/wg0/wg0.conf 64 | 65 | wg-dashboard-py.iml 66 | 67 | docker/wg0.conf 68 | 69 | wg-manager-backend/database.db 70 | 71 | wg-manager-backend/logsaccess.log 72 | 73 | wg-manager-backend/database.db_initial_v1 copy 74 | 75 | wg-manager-backend/database.db_new_ver 76 | 77 | wg-manager-backend/script/obfuscate/binary/shapeshifter-dispatcher 78 | 79 | wg-manager/database.db 80 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: focal 2 | os: linux 3 | language: python 4 | env: 5 | global: 6 | - DOCKER_REPO=perara/wg-manager 7 | before_install: 8 | - curl -fsSL https://get.docker.com | sh 9 | - echo '{"experimental":"enabled"}' | sudo tee /etc/docker/daemon.json 10 | - mkdir -p $HOME/.docker 11 | - echo '{"experimental":"enabled"}' | sudo tee $HOME/.docker/config.json 12 | - sudo service docker start 13 | install: 14 | - docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 15 | - docker buildx create --name xbuilder --use 16 | script: bash scripts/ci.sh 17 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at per@sysx.no. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | ENV TZ=Europe/Minsk 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | 5 | #COPY ./wg-manager /tmp/build 6 | RUN mkdir -p /tmp/build 7 | WORKDIR /tmp/build 8 | 9 | RUN apt-get update && apt-get install -y \ 10 | nodejs \ 11 | npm \ 12 | && rm -rf /var/lib/apt/lists/* 13 | 14 | 15 | RUN npm cache clean --force 16 | RUN npm install 17 | RUN npm install @angular/cli 18 | RUN node_modules/@angular/cli/bin/ng build --configuration="production" 19 | RUN rm -rf node_modules 20 | RUN apt-get purge nodejs npm -y 21 | 22 | FROM ubuntu:20.04 23 | LABEL maintainer="per@sysx.no" 24 | ENV IS_DOCKER True 25 | WORKDIR /app 26 | ENV LIBRARY_PATH=/lib:/usr/lib 27 | ENV TZ=Europe/Oslo 28 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 29 | COPY wg-manager-backend /app 30 | 31 | # Install dependencies 32 | #RUN apk add --no-cache --update wireguard-tools py3-gunicorn python3 py3-pip ip6tables 33 | RUN apt-get update && apt-get install -y \ 34 | wireguard-tools \ 35 | iptables \ 36 | iproute2 \ 37 | python3 \ 38 | python3-pip \ 39 | python3-dev \ 40 | python3-gunicorn \ 41 | python3-uvicorn \ 42 | gunicorn \ 43 | && rm -rf /var/lib/apt/lists/* 44 | 45 | 46 | RUN pip3 install -r requirements.txt 47 | 48 | # Install dependencies 49 | #RUN apk add --no-cache build-base python3-dev libffi-dev jpeg-dev zlib-dev && \ 50 | #pip3 install uvicorn && \ 51 | #pip3 install -r requirements.txt && \ 52 | #apk del build-base python3-dev libffi-dev jpeg-dev zlib-dev 53 | 54 | # Copy startup scripts 55 | COPY docker/ ./startup 56 | RUN chmod 700 ./startup/start.py 57 | 58 | # Copy build files from previous step 59 | COPY --from=0 /tmp/build/dist /app/build 60 | 61 | ENTRYPOINT python3 startup/start.py 62 | 63 | 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Per-Arne Andersen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /back/test.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | if __name__ == "__main__": 4 | sess = requests.Session() 5 | 6 | resp = sess.post("http://localhost:8888/api/v1/login", data={ 7 | "username": "admin", 8 | "password": "admin" 9 | }) 10 | print(resp.json()) 11 | sess.headers.update({ 12 | "Authorization": f"Bearer {resp.json()['access_token']}" 13 | }) 14 | 15 | for _ in range(20): 16 | print(sess.get("http://localhost:8888/api/v1/wg/generate_psk").json()) 17 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "2.1" 2 | services: 3 | 4 | server: 5 | container_name: wg-manager 6 | build: . 7 | restart: always 8 | sysctls: 9 | net.ipv6.conf.all.disable_ipv6: 0 10 | cap_add: 11 | - NET_ADMIN 12 | #network_mode: host # Alternatively 13 | ports: 14 | - 11820:11820/udp 15 | - 51800-51900:51800-51900/udp 16 | - 8888:8888 17 | environment: 18 | HOST: 0.0.0.0 19 | PORT: 8888 20 | ADMIN_USERNAME: admin 21 | ADMIN_PASSWORD: admin 22 | WEB_CONCURRENCY: 2 23 | SERVER_INIT_INTERFACE_START: 1 24 | 25 | #endpoint dynamic variables: ||external|| , ||internal|| 26 | SERVER_INIT_INTERFACE: '{"address":"10.0.200.1","v6_address":"fd42:42:42::1","subnet":24,"v6_subnet":64,"interface":"wg0","listen_port":"51820","endpoint":"server","dns":"10.0.200.1,8.8.8.8","private_key":"","public_key":"","post_up":"","post_down":"","configuration":"","is_running":false,"peers":[]}' 27 | SERVER_STARTUP_API_KEY: thisisasecretkeythatnobodyknows 28 | networks: 29 | - wg-manager-net 30 | 31 | client: 32 | container_name: wg-manager-server-with-client 33 | build: . 34 | restart: always 35 | sysctls: 36 | net.ipv6.conf.all.disable_ipv6: 0 37 | cap_add: 38 | - NET_ADMIN 39 | ports: 40 | - 8889:8889 41 | privileged: true 42 | environment: 43 | HOST: 0.0.0.0 # Optional (For Accessing WEB-Gui) 44 | PORT: 8889 # Optional (Web-GUI Listen Port) 45 | WEB_CONCURRENCY: 1 # Optional 46 | ADMIN_USERNAME: admin 47 | ADMIN_PASSWORD: admin 48 | INIT_SLEEP: 5 # If you run into concurrency issues 49 | SERVER: 0 # If you want to host a server as well 50 | CLIENT: 1 # If you want to connect to servers 51 | CLIENT_START_AUTOMATICALLY: 1 # If you want the client to start automatically 52 | CLIENT_1_NAME: "client-1" # Name of first client 53 | CLIENT_1_ROUTES: "10.0.200.0/24" 54 | CLIENT_1_SERVER_HOST: "http://server:8888" # Endpoint of first server 55 | CLIENT_1_SERVER_INTERFACE: "wg0" # Interface of first server (to get config) 56 | CLIENT_1_API_KEY: "thisisasecretkeythatnobodyknows" # API-Key of first server (to get config) 57 | networks: 58 | - wg-manager-net 59 | 60 | networks: 61 | wg-manager-net: 62 | driver: bridge -------------------------------------------------------------------------------- /docker/gunicorn_config.py: -------------------------------------------------------------------------------- 1 | import json 2 | import multiprocessing 3 | import os 4 | 5 | workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1") 6 | web_concurrency_str = os.getenv("WEB_CONCURRENCY", None) 7 | host = os.getenv("HOST", "unix:/tmp/gunicorn.sock") 8 | port = os.getenv("PORT", "80") 9 | use_loglevel = os.getenv("LOG_LEVEL", "info") 10 | use_bind = host if "unix" in host else f"{host}:{port}" 11 | 12 | cores = multiprocessing.cpu_count() 13 | workers_per_core = float(workers_per_core_str) 14 | default_web_concurrency = workers_per_core * cores 15 | if web_concurrency_str: 16 | web_concurrency = int(web_concurrency_str) 17 | assert web_concurrency > 0 18 | else: 19 | web_concurrency = max(int(default_web_concurrency), 2) 20 | 21 | # Gunicorn config variables 22 | loglevel = use_loglevel 23 | workers = web_concurrency 24 | bind = use_bind 25 | keepalive = 120 26 | errorlog = "-" 27 | 28 | # For debugging and testing 29 | log_data = { 30 | "loglevel": loglevel, 31 | "workers": workers, 32 | "bind": bind, 33 | "worker-tmp-dir": "/dev/shm", 34 | # Additional, non-gunicorn variables 35 | "workers_per_core": workers_per_core, 36 | "host": host, 37 | "port": port, 38 | } 39 | print(json.dumps(log_data)) 40 | -------------------------------------------------------------------------------- /docker/push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd .. 3 | docker login 4 | 5 | docker build -t perara/wg-manager:dev . 6 | docker push perara/wg-manager:dev 7 | -------------------------------------------------------------------------------- /docker/start.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import isdir 3 | DEFAULT_MODULE_LOCATIONS = [("app.main", "/app/app/main.py"), ("main", "/app/main.py")] 4 | DEFAULT_GUNICORN_CONF = [(None, "/app/gunicorn_config.py"), (None, "/app/startup/gunicorn_config.py")] 5 | 6 | 7 | def get_location(pot): 8 | for i in pot: 9 | if not isdir(i[1]): 10 | continue 11 | # Last record will be "defauilt" 12 | return i[0] if i[0] else i[1] 13 | 14 | 15 | if __name__ == "__main__": 16 | MODULE_NAME = os.getenv("MODULE_LOCATION", get_location(DEFAULT_MODULE_LOCATIONS)) 17 | VARIABLE_NAME = os.getenv("VARIABLE_NAME", "app") 18 | APP_MODULE = os.getenv("APP_MODULE", f"{MODULE_NAME}:{VARIABLE_NAME}") 19 | GUNICORN_CONF = os.getenv("GUNICORN_CONF", get_location(DEFAULT_GUNICORN_CONF)) 20 | OPTIONS = [ 21 | "--preload", 22 | "-k", 23 | "uvicorn.workers.UvicornWorker", 24 | "-c", 25 | f"{GUNICORN_CONF} {APP_MODULE}" 26 | ] 27 | 28 | # Set envs 29 | os.putenv("VARIABLE_NAME", VARIABLE_NAME) 30 | os.putenv("APP_MODULE", APP_MODULE) 31 | os.putenv("GUNICORN_CONF", GUNICORN_CONF) 32 | 33 | os.system(f"gunicorn -k uvicorn.workers.UvicornWorker -c {GUNICORN_CONF} {APP_MODULE}") 34 | -------------------------------------------------------------------------------- /docs/guides/docker_configuration.md: -------------------------------------------------------------------------------- 1 | # Docker Configuration 2 | ```bash 3 | docker run -d \ 4 | --sysctl net.ipv6.conf.all.disable_ipv6=0 \ 5 | --cap-add NET_ADMIN \ 6 | --name wg-manager \ 7 | #--net host \ 8 | -p "51800-51900:51800-51900/udp" \ 9 | -p "8888:8888" \ 10 | -v wg-manager:/config \ 11 | -e HOST="0.0.0.0" \ 12 | -e PORT="8888" \ 13 | -e ADMIN_USERNAME="admin" \ 14 | -e ADMIN_PASSWORD="admin" \ 15 | perara/wg-manager 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/guides/import_existing_server.md: -------------------------------------------------------------------------------- 1 | # Importing existing configuration 2 | It is not unusual to have existing WireGuard configurations in production, and for this reason, we support to import these in full or partial form. 3 | 4 | It is possible to: 5 | 1. Import server configuration only 6 | * Peer export will not work due to impartial information, such as missing private-key 7 | 2. Import peer configuration only, 8 | * Server configuration will only be partial. e.g private-key must be manually entered 9 | 3. Import server **and** peer configuration. 10 | * Given compliant configuration (see assumptions), everything should be fully imported. 11 | 12 | ## Configuration assumptions 13 | There are a few assumptions made for the configuration to be successfully imported. 14 | 1. Generally, any file that **does not contain** **Endpoint** key in the Peer sections are servers. The import will fail 15 | if multiple files is without Endpoint 16 | 2. All files that **have Endpoint defined** are considered peers of the server 17 | 3. All files should be imported at the **same time** 18 | 19 | ### Server 20 | The format of the server should look similar to this: 21 | ``` 22 | [Interface] 23 | Address = 10.0.92.1/24 24 | PrivateKey = 0MHXsC4zJrDZA5MpZZKQiS/j5srAvSC9bJx7Igtq1FE= 25 | ListenPort = 56944 26 | PostUp = 27 | PostDown = 28 | 29 | [Peer] 30 | PublicKey = XNRPEweV3guSis3YRHDBldizn6xivv+2Tug0G/BM6gE= 31 | AllowedIPs = 10.0.92.2/32 32 | 33 | [Peer] 34 | PublicKey = XNRPEweV3guSis3YRHDBldizn6xivv+2Tug0G/BM6gE= 35 | AllowedIPs = 10.0.92.3/32 36 | ``` 37 | 38 | ### Peer 39 | ``` 40 | [Interface] 41 | Address = 10.0.92.2/24 42 | PrivateKey = aN08xqUVOEAoc74e2yzvN/yOtXJgtISru7mjrPFhlUY 43 | DNS="8.8.8.8" 44 | 45 | [Peer] 46 | PublicKey = gybMBZBfwsmsXBl8bG2ZobGiG77aGdxOoyQsjTzrKkY= 47 | AllowedIPs = 0.0.0.0/0 48 | Endpoint = my-address.com:5455 49 | 50 | 51 | [Peer] 52 | PublicKey = gybMBZBfwsmsXBl8bG2ZobGiG77aGdxOoyQsjTzrKkY= 53 | AllowedIPs = 0.0.0.0/0 54 | Endpoint = my-address.com:5455 55 | ``` 56 | Note that we do **not** support importing peers with multiple peer sections. 57 | 58 | ## How to 59 | 1. Click the **Import Configuration** button in the right pane 60 | 2. Select all relevant server and client files and submit. 61 | 3. If successfully, the server configuration should now be filled and a indicator on how many peers added is shown at the bottom of the form. 62 | -------------------------------------------------------------------------------- /docs/guides/reverse_proxy.md: -------------------------------------------------------------------------------- 1 | # Reverse Proxy 2 | Use jwilder/nginx-proxy or similar. 3 | -------------------------------------------------------------------------------- /docs/images/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/docs/images/0.png -------------------------------------------------------------------------------- /docs/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/docs/images/1.png -------------------------------------------------------------------------------- /docs/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/docs/images/2.png -------------------------------------------------------------------------------- /docs/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/docs/images/3.png -------------------------------------------------------------------------------- /docs/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/docs/images/4.png -------------------------------------------------------------------------------- /docs/images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/docs/images/5.png -------------------------------------------------------------------------------- /docs/images/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/docs/images/6.png -------------------------------------------------------------------------------- /docs/images/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/docs/images/7.png -------------------------------------------------------------------------------- /docs/images/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/docs/images/8.png -------------------------------------------------------------------------------- /docs/install_debian.md: -------------------------------------------------------------------------------- 1 | # Installing on Debian/Ubuntu 2 | -------------------------------------------------------------------------------- /docs/install_docker_github.md: -------------------------------------------------------------------------------- 1 | # Build Docker Image from Github Repo 2 | The steps below will walk you through installing the application in a docker container from the latest github version. 3 | 4 | --- 5 | 6 | First thing we need to do is clone the github repository 7 | ```bash 8 | git clone https://github.com/perara/wg-manager.git 9 | ``` 10 | 11 | Next we need to build the image. This will take some time. 12 | ```bash 13 | docker build -t wg-manager . 14 | ``` 15 | 16 | Now that our image is built, we can either launch the container via _docker-compose_ or through the _docker CLI_. 17 | 18 | ## Docker Compose 19 | ```yaml 20 | version: "2.1" 21 | services: 22 | wireguard: 23 | container_name: wg-manager 24 | image: wg-manager 25 | restart: always 26 | sysctls: 27 | net.ipv6.conf.all.disable_ipv6: 0 # Required for IPV6 28 | cap_add: 29 | - NET_ADMIN 30 | network_mode: host 31 | ports: 32 | - 51802:51802/udp 33 | - 8888:8888 34 | volumes: 35 | - ./wg-manager:/config 36 | environment: 37 | HOST: 0.0.0.0 38 | PORT: 8888 39 | ADMIN_USERNAME: admin 40 | ADMIN_PASSWORD: admin 41 | WEB_CONCURRENCY: 1 42 | ``` 43 | 44 | ## Docker CLI 45 | ```bash 46 | docker run -d \ 47 | --sysctl net.ipv6.conf.all.disable_ipv6=0 \ 48 | --cap-add NET_ADMIN \ 49 | --name wg-manager \ 50 | --net host \ 51 | -p "51802:51802/udp" \ 52 | -p "8888:8888" \ 53 | -v wg-manager:/config \ 54 | -e HOST="0.0.0.0" \ 55 | -e PORT="8888" \ 56 | -e ADMIN_USERNAME="admin" \ 57 | -e ADMIN_PASSWORD="admin" \ 58 | wg-manager 59 | ``` 60 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "@angular/cdk": { 6 | "version": "9.2.0", 7 | "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-9.2.0.tgz", 8 | "integrity": "sha512-jeeznvNDpR9POuxzz8Y0zFvMynG9HCJo3ZPTqOjlOq8Lj8876+rLsHDvKEMeLdwlkdi1EweYJW1CLQzI+TwqDA==", 9 | "requires": { 10 | "parse5": "^5.0.0" 11 | } 12 | }, 13 | "@angular/flex-layout": { 14 | "version": "9.0.0-beta.29", 15 | "resolved": "https://registry.npmjs.org/@angular/flex-layout/-/flex-layout-9.0.0-beta.29.tgz", 16 | "integrity": "sha512-93sxR+kYfYMOdnlWL0Q77FZ428gg8XnBu0YZm6GsCdkw/vLggIT/G1ZAqHlCPIODt6pxmCJ5KXh4ShvniIYDsA==" 17 | }, 18 | "parse5": { 19 | "version": "5.1.1", 20 | "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", 21 | "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", 22 | "optional": true 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /scripts/ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$TRAVIS_PULL_REQUEST" = "true" ] || [ "$TRAVIS_BRANCH" != "master" ]; then 4 | docker buildx build \ 5 | --progress plain \ 6 | --platform=linux/amd64,linux/arm64/v8,linux/arm64,linux/arm/v7,linux/arm/v6 \ 7 | . 8 | exit $? 9 | fi 10 | echo $DOCKER_PASSWORD | docker login -u qmcgaw --password-stdin &> /dev/null 11 | TAG="${TRAVIS_TAG:-latest}" 12 | docker buildx build \ 13 | --progress plain \ 14 | --platform=linux/amd64,linux/arm64/v8,linux/arm64,linux/arm/v7,linux/arm/v6 \ 15 | -t $DOCKER_REPO:$TAG \ 16 | --push \ 17 | . 18 | -------------------------------------------------------------------------------- /wg-manager-backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-backend/__init__.py -------------------------------------------------------------------------------- /wg-manager-backend/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = migrations 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # sys.path path, will be prepended to sys.path if present. 11 | # defaults to the current working directory. 12 | prepend_sys_path = . 13 | 14 | # timezone to use when rendering the date 15 | # within the migration file as well as the filename. 16 | # string value is passed to dateutil.tz.gettz() 17 | # leave blank for localtime 18 | # timezone = 19 | 20 | # max length of characters to apply to the 21 | # "slug" field 22 | # truncate_slug_length = 40 23 | 24 | # set to 'true' to run the environment during 25 | # the 'revision' command, regardless of autogenerate 26 | # revision_environment = false 27 | 28 | # set to 'true' to allow .pyc and .pyo files without 29 | # a source .py file to be detected as revisions in the 30 | # versions/ directory 31 | # sourceless = false 32 | 33 | # version location specification; this defaults 34 | # to alembic/versions. When using multiple version 35 | # directories, initial revisions must be specified with --version-path 36 | # version_locations = %(here)s/bar %(here)s/bat alembic/versions 37 | 38 | # the output encoding used when revision files 39 | # are written from script.py.mako 40 | # output_encoding = utf-8 41 | 42 | sqlalchemy.url = sqlite:///database.db 43 | 44 | 45 | [post_write_hooks] 46 | # post_write_hooks defines scripts or Python functions that are run 47 | # on newly generated revision scripts. See the documentation for further 48 | # detail and examples 49 | 50 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 51 | # hooks=black 52 | # black.type=console_scripts 53 | # black.entrypoint=black 54 | # black.options=-l 79 55 | 56 | # Logging configuration 57 | [loggers] 58 | keys = root,sqlalchemy,alembic 59 | 60 | [handlers] 61 | keys = console 62 | 63 | [formatters] 64 | keys = generic 65 | 66 | [logger_root] 67 | level = WARN 68 | handlers = console 69 | qualname = 70 | 71 | [logger_sqlalchemy] 72 | level = WARN 73 | handlers = 74 | qualname = sqlalchemy.engine 75 | 76 | [logger_alembic] 77 | level = INFO 78 | handlers = 79 | qualname = alembic 80 | 81 | [handler_console] 82 | class = StreamHandler 83 | args = (sys.stderr,) 84 | level = NOTSET 85 | formatter = generic 86 | 87 | [formatter_generic] 88 | format = %(levelname)-5.5s [%(name)s] %(message)s 89 | datefmt = %H:%M:%S 90 | -------------------------------------------------------------------------------- /wg-manager-backend/const.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import string 4 | IS_DOCKER = os.getenv("IS_DOCKER", "False") == "True" 5 | DATABASE_FILE = "/config/database.db" if IS_DOCKER else "./database.db" 6 | DATABASE_URL = f"sqlite:///{DATABASE_FILE}" 7 | 8 | OBFUSCATE_ENABLED = os.getenv("OBFUSCATION", "True") == "True" # TODO should be false by default 9 | OBFUSCATE_MODE = os.getenv("OBFUSCATION_MODE", "obfs4") 10 | OBFUSCATE_SOCKS_TOR_PORT = int(os.getenv("OBFUSCATE_SOCKS_TOR_PORT", "5555")) 11 | OBFUSCATE_TOR_LISTEN_ADDR = int(os.getenv("OBFUSCATE_TOR_LISTEN_ADDR", "5556")) 12 | OBFUSCATE_SUPPORTED = ["obfs4"] 13 | assert OBFUSCATE_MODE in OBFUSCATE_SUPPORTED, "Invalid OBFUSCATE_MODE=%s, Valid options are: %s" % (OBFUSCATE_MODE, 14 | OBFUSCATE_SUPPORTED) 15 | 16 | os.makedirs("build", exist_ok=True) 17 | DEFAULT_POST_UP = os.getenv("POST_UP", "iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;") 18 | DEFAULT_POST_DOWN = os.getenv("POST_DOWN", "iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE;") 19 | DEFAULT_POST_UP_v6 = os.getenv("POST_UP_V6", "ip6tables -A FORWARD -i %i -j ACCEPT; ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;") 20 | DEFAULT_POST_DOWN_v6 = os.getenv("POST_DOWN_V6", "ip6tables -D FORWARD -i %i -j ACCEPT; ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE;") 21 | 22 | SECRET_KEY = ''.join(random.choices(string.ascii_uppercase + string.digits, k=64)) 23 | ALGORITHM = "HS256" 24 | 25 | API_KEY_LENGTH = 32 26 | ACCESS_TOKEN_EXPIRE_MINUTES = 30 27 | CMD_WG_COMMAND = ["wg"] 28 | CMD_WG_QUICK = ["wg-quick"] 29 | 30 | INIT_SLEEP = int(os.getenv("INIT_SLEEP", "0")) 31 | SERVER_STARTUP_API_KEY = os.getenv("SERVER_STARTUP_API_KEY", None) 32 | SERVER_INIT_INTERFACE = os.getenv("SERVER_INIT_INTERFACE", None) 33 | SERVER_INIT_INTERFACE_START = os.getenv("SERVER_INIT_INTERFACE_START", "1") == "1" 34 | SERVER = os.getenv("SERVER", "1") == "1" 35 | CLIENT = os.getenv("CLIENT", "0") == "1" 36 | CLIENT_START_AUTOMATICALLY = os.getenv("CLIENT_START_AUTOMATICALLY", "1") == "1" 37 | 38 | if not IS_DOCKER: 39 | CMD_WG_COMMAND = ["sudo"] + CMD_WG_COMMAND 40 | CMD_WG_QUICK = ["sudo"] + CMD_WG_QUICK 41 | DEFAULT_CONFIG_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "config") 42 | else: 43 | DEFAULT_CONFIG_DIR = "/config" 44 | os.makedirs(DEFAULT_CONFIG_DIR, exist_ok=True) 45 | 46 | 47 | ENV_CONFIG_DIR = os.getenv("ENV_CONFIG_DIR", DEFAULT_CONFIG_DIR) 48 | os.makedirs(ENV_CONFIG_DIR, exist_ok=True) 49 | 50 | 51 | def _server_dir(interface): 52 | s_dir = os.path.join(ENV_CONFIG_DIR, "server", interface) 53 | os.makedirs(s_dir, exist_ok=True) 54 | return s_dir 55 | 56 | 57 | SERVER_DIR = _server_dir 58 | 59 | 60 | def _client_dir(interface): 61 | c_dir = os.path.join(ENV_CONFIG_DIR, "server", interface, "clients") 62 | os.makedirs(c_dir, exist_ok=True) 63 | return c_dir 64 | 65 | 66 | CLIENT_DIR = _client_dir 67 | 68 | PEER_FILE = lambda db_peer: os.path.join(CLIENT_DIR(db_peer.server_ref.interface), str(db_peer.id) + ".conf") 69 | 70 | SERVER_FILE = lambda interface: os.path.join(SERVER_DIR(interface), interface + ".conf") 71 | -------------------------------------------------------------------------------- /wg-manager-backend/database/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-backend/database/__init__.py -------------------------------------------------------------------------------- /wg-manager-backend/database/database.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy 2 | from sqlalchemy import MetaData 3 | from sqlalchemy.ext.declarative import declarative_base 4 | from sqlalchemy.orm import sessionmaker 5 | import const 6 | 7 | engine = sqlalchemy.create_engine( 8 | const.DATABASE_URL, connect_args={"check_same_thread": False} 9 | ) 10 | 11 | 12 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 13 | 14 | meta = MetaData(naming_convention={ 15 | "ix": "ix_%(column_0_label)s", 16 | "uq": "uq_%(table_name)s_%(column_0_name)s", 17 | "ck": "ck_%(table_name)s_%(column_0_name)s", 18 | "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", 19 | "pk": "pk_%(table_name)s" 20 | }) 21 | Base = declarative_base(metadata=meta) 22 | -------------------------------------------------------------------------------- /wg-manager-backend/database/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import sqlalchemy 4 | 5 | from sqlalchemy import Integer, Column, DateTime 6 | from sqlalchemy.orm import relationship, backref 7 | from database.database import Base 8 | 9 | 10 | class User(Base): 11 | __tablename__ = "users" 12 | 13 | id = Column(Integer, primary_key=True, index=True) 14 | email = Column(sqlalchemy.String, unique=True, index=True) 15 | password = Column(sqlalchemy.String) 16 | username = Column(sqlalchemy.String, unique=True) 17 | full_name = Column(sqlalchemy.String) 18 | role = Column(sqlalchemy.String) 19 | 20 | 21 | class UserAPIKey(Base): 22 | __tablename__ = "api_keys" 23 | id = Column(Integer, primary_key=True, autoincrement=True) 24 | key = Column(sqlalchemy.String, unique=True) 25 | user_id = Column(Integer, sqlalchemy.ForeignKey('users.id', ondelete="CASCADE", onupdate="CASCADE")) 26 | user = relationship("User", foreign_keys=[user_id]) 27 | created_date = Column(DateTime, default=datetime.datetime.utcnow) 28 | 29 | 30 | class WGServer(Base): 31 | __tablename__ = "server" 32 | 33 | id = Column(Integer, primary_key=True, index=True) 34 | interface = Column(sqlalchemy.String, unique=True, index=True) 35 | subnet = Column(sqlalchemy.Integer, nullable=False) 36 | address = Column(sqlalchemy.String, unique=True) 37 | v6_address = Column(sqlalchemy.String, unique=True) 38 | v6_subnet = Column(sqlalchemy.Integer, nullable=False) 39 | listen_port = Column(sqlalchemy.String, unique=True) 40 | private_key = Column(sqlalchemy.String) 41 | public_key = Column(sqlalchemy.String) 42 | endpoint = Column(sqlalchemy.String) 43 | dns = Column(sqlalchemy.String) 44 | allowed_ips = Column(sqlalchemy.String) 45 | keep_alive = Column(sqlalchemy.Integer, default=0) 46 | read_only = Column(sqlalchemy.Integer, default=0) 47 | 48 | post_up = Column(sqlalchemy.String) 49 | post_down = Column(sqlalchemy.String) 50 | is_running = Column(sqlalchemy.Boolean) 51 | configuration = Column(sqlalchemy.Text) 52 | 53 | peers = relationship("WGPeer", cascade="all, delete", passive_deletes=True, lazy="joined") 54 | 55 | 56 | class WGPeer(Base): 57 | __tablename__ = "peer" 58 | 59 | id = Column(Integer, primary_key=True, index=True) 60 | name = Column(sqlalchemy.String, default="Unnamed") 61 | address = Column(sqlalchemy.String) 62 | v6_address = Column(sqlalchemy.String) 63 | public_key = Column(sqlalchemy.String) 64 | private_key = Column(sqlalchemy.String) 65 | shared_key = Column(sqlalchemy.Text) 66 | dns = Column(sqlalchemy.Text) 67 | allowed_ips = Column(sqlalchemy.String) 68 | keep_alive = Column(sqlalchemy.Integer, default=0) 69 | read_only = Column(sqlalchemy.Integer, default=0) 70 | 71 | server_id = Column(Integer, sqlalchemy.ForeignKey('server.id', ondelete="CASCADE", onupdate="CASCADE")) 72 | server = relationship("WGServer", backref=backref("server")) 73 | configuration = Column(sqlalchemy.Text) 74 | -------------------------------------------------------------------------------- /wg-manager-backend/database/util.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import alembic.command 4 | from alembic.config import Config 5 | from sqlalchemy.orm import Session 6 | from sqlalchemy_utils import database_exists 7 | 8 | import middleware 9 | from database.database import engine, Base, SessionLocal 10 | from database import models 11 | from loguru import logger 12 | 13 | 14 | def perform_migrations(): 15 | logger.info("Performing migrations...") 16 | alembic_cfg = Config("alembic.ini") 17 | alembic_cfg.attributes['configure_logger'] = False 18 | 19 | alembic_cfg.set_main_option('script_location', "migrations") 20 | alembic_cfg.set_main_option('sqlalchemy.url', str(engine.url)) 21 | 22 | alembic.command.upgrade(alembic_cfg, 'head') 23 | logger.info("Migrations done!") 24 | 25 | 26 | def setup_initial_database(): 27 | if not database_exists(engine.url): 28 | logger.info("Database does not exists. Creating initial database...") 29 | 30 | # Create database from metadata 31 | Base.metadata.create_all(engine) 32 | logger.info("Database creation done!") 33 | 34 | # Create default user 35 | _db: Session = SessionLocal() 36 | 37 | # Retrieve user with admin role 38 | admin_exists = _db.query(models.User.id).filter_by(role="admin").first() is not None 39 | 40 | if not admin_exists: 41 | logger.info("Admin user does not exists. Creating with env variables ADMIN_USERNAME, ADMIN_PASSWORD") 42 | env_admin_username = os.getenv("ADMIN_USERNAME") 43 | env_admin_password = os.getenv("ADMIN_PASSWORD") 44 | 45 | if not env_admin_username: 46 | raise RuntimeError("Database does not exist and the environment variable ADMIN_USERNAME is set") 47 | 48 | if not env_admin_password: 49 | raise RuntimeError("Database does not exist and the environment variable ADMIN_PASSWORD is set") 50 | 51 | _db.merge(models.User( 52 | username=env_admin_username, 53 | password=middleware.get_password_hash(env_admin_password), 54 | full_name="Admin", 55 | role="admin", 56 | email="" 57 | )) 58 | 59 | _db.commit() 60 | _db.close() 61 | -------------------------------------------------------------------------------- /wg-manager-backend/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-backend/db/__init__.py -------------------------------------------------------------------------------- /wg-manager-backend/db/api_key.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from database import models 4 | 5 | 6 | def add_initial_api_key_for_admin(sess: Session, api_key, ADMIN_USERNAME): 7 | 8 | db_user = sess.query(models.User)\ 9 | .filter_by(username=ADMIN_USERNAME)\ 10 | .one() 11 | 12 | exists_api_key = sess.query(models.UserAPIKey)\ 13 | .filter_by( 14 | user_id=db_user.id, 15 | key=api_key 16 | )\ 17 | .count() 18 | 19 | if exists_api_key == 0: 20 | db_api_key = models.UserAPIKey() 21 | db_api_key.key = api_key 22 | db_api_key.user_id = db_user.id 23 | 24 | sess.add(db_api_key) 25 | sess.commit() 26 | 27 | return True 28 | -------------------------------------------------------------------------------- /wg-manager-backend/db/user.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from sqlalchemy.orm import Session 4 | from database import models 5 | from middleware import get_password_hash 6 | 7 | import schemas 8 | 9 | 10 | def update_user(sess: Session, form_data: schemas.UserInDB): 11 | user = get_user_by_name(sess, form_data.username) 12 | user.password = form_data.password 13 | user.full_name = form_data.full_name 14 | user.email = form_data.email # TODO this section should be updated 15 | 16 | sess.add(user) 17 | sess.commit() 18 | return get_user_by_name(sess, form_data.username) 19 | 20 | 21 | def authenticate_user(sess, username: str, password: str) -> Optional[models.User]: 22 | user = get_user_by_name(sess, username) 23 | if user and verify_password(password, user.password): 24 | return user 25 | return None 26 | 27 | 28 | def get_user_by_name(db: Session, username: str) -> models.User: 29 | return db.query(models.User).filter(models.User.username == username).first() 30 | 31 | 32 | def get_user_by_username_and_password(db: Session, username: str, password: str) -> models.User: 33 | return db.query(models.User).filter((models.User.username == username) & (models.User.password == password)).first() 34 | 35 | 36 | def create_user(sess: Session, user: models.User): 37 | user.password = get_password_hash(user.password) 38 | sess.add(user) 39 | sess.commit() 40 | 41 | return user.id is not None 42 | -------------------------------------------------------------------------------- /wg-manager-backend/logger.py: -------------------------------------------------------------------------------- 1 | 2 | def setup_logging(): 3 | import logging 4 | from loguru import logger 5 | class InterceptHandler(logging.Handler): 6 | def emit(self, record): 7 | # Get corresponding Loguru level if it exists 8 | try: 9 | level = logger.level(record.levelname).name 10 | except ValueError: 11 | level = record.levelno 12 | 13 | # Find caller from where originated the logged message 14 | frame, depth = logging.currentframe(), 2 15 | while frame.f_code.co_filename == logging.__file__: 16 | frame = frame.f_back 17 | depth += 1 18 | 19 | logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage()) 20 | 21 | logging.basicConfig(handlers=[InterceptHandler()], level=1) 22 | -------------------------------------------------------------------------------- /wg-manager-backend/logging.json: -------------------------------------------------------------------------------- 1 | { 2 | "logger": { 3 | "path": "./logs", 4 | "filename": "access.log", 5 | "level": "info", 6 | "rotation": "20 days", 7 | "retention": "1 months", 8 | "format": "{level: <8} {time:YYYY-MM-DD HH:mm:ss.SSS} {extra[request_id]} - {name}:{function} - {message}" 9 | 10 | } 11 | } -------------------------------------------------------------------------------- /wg-manager-backend/main.py: -------------------------------------------------------------------------------- 1 | from logger import setup_logging 2 | setup_logging() 3 | 4 | import const 5 | from uvicorn_loguru_integration import run_uvicorn_loguru 6 | 7 | import time 8 | from starlette.middleware.base import BaseHTTPMiddleware 9 | 10 | import middleware 11 | 12 | from routers.v1 import user, server, peer, wg 13 | import script.wireguard_startup 14 | import pkg_resources 15 | import uvicorn as uvicorn 16 | from fastapi.staticfiles import StaticFiles 17 | 18 | from starlette.responses import FileResponse 19 | from fastapi import Depends, FastAPI 20 | import database.util 21 | 22 | app = FastAPI() 23 | app.add_middleware(BaseHTTPMiddleware, dispatch=middleware.db_session_middleware) 24 | app.add_middleware(BaseHTTPMiddleware, dispatch=middleware.logging_middleware) 25 | 26 | app.include_router( 27 | user.router, 28 | prefix="/api/v1", 29 | tags=["user"], 30 | dependencies=[], 31 | responses={404: {"description": "Not found"}} 32 | ) 33 | 34 | 35 | app.include_router( 36 | server.router, 37 | prefix="/api/v1/server", 38 | tags=["server"], 39 | dependencies=[Depends(middleware.auth)], 40 | responses={404: {"description": "Not found"}} 41 | ) 42 | 43 | 44 | app.include_router( 45 | peer.router, 46 | prefix="/api/v1/peer", 47 | tags=["peer"], 48 | dependencies=[Depends(middleware.auth)], 49 | responses={404: {"description": "Not found"}} 50 | ) 51 | 52 | 53 | app.include_router( 54 | wg.router, 55 | prefix="/api/v1/wg", 56 | tags=["wg"], 57 | dependencies=[Depends(middleware.auth)], 58 | responses={404: {"description": "Not found"}} 59 | ) 60 | 61 | 62 | @app.get("/", include_in_schema=True) 63 | def root(): 64 | return FileResponse('build/index.html') 65 | 66 | 67 | app.mount("/", StaticFiles(directory=pkg_resources.resource_filename(__name__, 'build')), name="static") 68 | 69 | 70 | def main(): 71 | # Sleep the wait timer. 72 | time.sleep(const.INIT_SLEEP) 73 | 74 | # Ensure database existence 75 | database.util.setup_initial_database() 76 | 77 | # Perform Migrations 78 | database.util.perform_migrations() 79 | 80 | # Configure wireguard 81 | script.wireguard_startup.setup_on_start() 82 | 83 | 84 | @app.on_event("startup") 85 | async def startup(): 86 | if __name__ != "__main__": 87 | main() 88 | 89 | 90 | @app.on_event("shutdown") 91 | async def shutdown(): 92 | pass 93 | 94 | 95 | if __name__ == "__main__": 96 | 97 | main() 98 | 99 | run_uvicorn_loguru( 100 | uvicorn.Config( 101 | "__main__:app", 102 | host="0.0.0.0", 103 | port=8000, 104 | log_level="warning", 105 | reload=True, 106 | workers=1 107 | ) 108 | ) 109 | -------------------------------------------------------------------------------- /wg-manager-backend/middleware.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta, datetime 2 | 3 | import jwt 4 | from fastapi import Depends, HTTPException 5 | from fastapi.security import OAuth2PasswordBearer 6 | from jwt import PyJWTError 7 | from loguru import logger 8 | from passlib.context import CryptContext 9 | from sqlalchemy.orm import Session 10 | from starlette import status 11 | from starlette.requests import Request 12 | from starlette.responses import Response 13 | 14 | import const 15 | import schemas 16 | from database import models 17 | from database.database import SessionLocal 18 | 19 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/login", auto_error=False) 20 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 21 | 22 | 23 | def get_password_hash(password): 24 | return pwd_context.hash(password) 25 | 26 | 27 | def verify_password(plain_password, hashed_password): 28 | return pwd_context.verify(plain_password, hashed_password) 29 | 30 | 31 | async def logging_middleware(request: Request, call_next): 32 | response = await call_next(request) 33 | logger.opt(depth=2).info(f"{request.method} {request.url} - Code: {response.status_code}") 34 | return response 35 | 36 | 37 | async def db_session_middleware(request: Request, call_next): 38 | response = Response("Internal server error (Database error)", status_code=500) 39 | try: 40 | request.state.db = SessionLocal() 41 | response = await call_next(request) 42 | finally: 43 | request.state.db.close() 44 | return response 45 | 46 | 47 | # NON MIDDLEWARE MIDDLEWARISH THING 48 | 49 | 50 | # Dependency 51 | def get_db(request: Request): 52 | return request.state.db 53 | 54 | 55 | def create_access_token(*, data: dict, expires_delta: timedelta = None): 56 | to_encode = data.copy() 57 | if expires_delta: 58 | expire = datetime.utcnow() + expires_delta 59 | else: 60 | expire = datetime.utcnow() + timedelta(minutes=15) 61 | to_encode.update({"exp": expire}) 62 | encoded_jwt = jwt.encode(to_encode, const.SECRET_KEY, algorithm=const.ALGORITHM) 63 | return encoded_jwt 64 | 65 | 66 | def retrieve_api_key(request: Request): 67 | return request.headers.get("X-API-Key", None) 68 | 69 | 70 | def auth(token: str = Depends(oauth2_scheme), api_key: str = Depends(retrieve_api_key), sess: Session = Depends(get_db)): 71 | 72 | username = None 73 | 74 | credentials_exception = HTTPException( 75 | status_code=status.HTTP_401_UNAUTHORIZED, 76 | detail="Could not validate credentials", 77 | headers={"WWW-Authenticate": "Bearer"}, 78 | ) 79 | 80 | # Attempt to authenticate using JWT 81 | try: 82 | payload = jwt.decode(token, const.SECRET_KEY, algorithms=[const.ALGORITHM]) 83 | username: str = payload.get("sub") 84 | except PyJWTError: 85 | pass 86 | 87 | try: 88 | db_user_api_key = sess.query(models.UserAPIKey).filter_by(key=api_key).one() 89 | username = db_user_api_key.user.username 90 | except Exception: 91 | pass 92 | 93 | if username is None: 94 | raise credentials_exception 95 | 96 | user = schemas.User.from_orm( 97 | schemas.UserInDB(username=username, password="").from_db(sess) 98 | ) 99 | if user is None: 100 | raise credentials_exception 101 | return user 102 | 103 | -------------------------------------------------------------------------------- /wg-manager-backend/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /wg-manager-backend/migrations/env.py: -------------------------------------------------------------------------------- 1 | from logging.config import fileConfig 2 | 3 | from sqlalchemy import engine_from_config 4 | from sqlalchemy import pool 5 | 6 | from alembic import context 7 | 8 | # this is the Alembic Config object, which provides 9 | # access to the values within the .ini file in use. 10 | import database.models 11 | import database.database 12 | 13 | config = context.config 14 | 15 | # Interpret the config file for Python logging. 16 | # This line sets up loggers basically. 17 | if config.attributes.get('configure_logger', True): 18 | fileConfig(config.config_file_name) 19 | 20 | # add your model's MetaData object here 21 | # for 'autogenerate' support 22 | # from myapp import mymodel 23 | # target_metadata = mymodel.Base.metadata 24 | target_metadata = database.database.Base.metadata 25 | 26 | # other values from the config, defined by the needs of env.py, 27 | # can be acquired: 28 | # my_important_option = config.get_main_option("my_important_option") 29 | # ... etc. 30 | 31 | 32 | def run_migrations_offline(): 33 | """Run migrations in 'offline' mode. 34 | 35 | This configures the context with just a URL 36 | and not an Engine, though an Engine is acceptable 37 | here as well. By skipping the Engine creation 38 | we don't even need a DBAPI to be available. 39 | 40 | Calls to context.execute() here emit the given string to the 41 | script output. 42 | 43 | """ 44 | url = config.get_main_option("sqlalchemy.url") 45 | context.configure( 46 | url=url, 47 | target_metadata=target_metadata, 48 | literal_binds=True, 49 | dialect_opts={"paramstyle": "named"}, 50 | render_as_batch=False 51 | ) 52 | 53 | with context.begin_transaction(): 54 | context.run_migrations() 55 | 56 | 57 | def run_migrations_online(): 58 | """Run migrations in 'online' mode. 59 | 60 | In this scenario we need to create an Engine 61 | and associate a connection with the context. 62 | 63 | """ 64 | connectable = engine_from_config( 65 | config.get_section(config.config_ini_section), 66 | prefix="sqlalchemy.", 67 | poolclass=pool.NullPool, 68 | ) 69 | 70 | with connectable.connect() as connection: 71 | context.configure( 72 | connection=connection, 73 | target_metadata=target_metadata, 74 | render_as_batch=False 75 | ) 76 | 77 | with context.begin_transaction(): 78 | context.run_migrations() 79 | 80 | 81 | if context.is_offline_mode(): 82 | run_migrations_offline() 83 | else: 84 | run_migrations_online() 85 | -------------------------------------------------------------------------------- /wg-manager-backend/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /wg-manager-backend/migrations/versions/4ac3e58519eb_base.py: -------------------------------------------------------------------------------- 1 | """base 2 | 3 | Revision ID: 4ac3e58519eb 4 | Revises: 5 | Create Date: 2021-03-13 20:29:10.062757 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | # revision identifiers, used by Alembic. 12 | from sqlalchemy.exc import OperationalError 13 | 14 | revision = '4ac3e58519eb' 15 | down_revision = None 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def ignore_duplicate(fn): 21 | try: 22 | fn() 23 | except OperationalError as e: 24 | if "duplicate" in str(e): 25 | pass 26 | 27 | 28 | def upgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | try: 31 | op.create_table('api_keys', 32 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 33 | sa.Column('key', sa.String(), nullable=True), 34 | sa.Column('user_id', sa.Integer(), nullable=True), 35 | sa.Column('created_date', sa.DateTime(), nullable=True), 36 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='fk_user_api_key_user_id', 37 | onupdate='CASCADE', ondelete='CASCADE'), 38 | sa.PrimaryKeyConstraint('id'), 39 | sa.UniqueConstraint('key') 40 | ) 41 | except OperationalError: 42 | pass 43 | 44 | try: 45 | op.drop_table('migrate_version') 46 | except OperationalError: 47 | pass 48 | 49 | naming_convention = { 50 | "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", 51 | } 52 | with op.batch_alter_table("peer", naming_convention=naming_convention) as batch_op: 53 | batch_op.drop_constraint("fk_peer_server_id_server", type_="foreignkey") 54 | 55 | with op.batch_alter_table('peer', schema=None) as batch_op: 56 | batch_op.create_foreign_key('fk_peer_server_id_server', 'server', ['server_id'], ['id'], onupdate='CASCADE', 57 | ondelete='CASCADE') 58 | 59 | ignore_duplicate(lambda: op.add_column('peer', sa.Column('configuration', sa.Text(), nullable=True))) 60 | ignore_duplicate(lambda: op.add_column('peer', sa.Column('keep_alive', sa.Integer(), nullable=True))) 61 | ignore_duplicate(lambda: op.add_column('peer', sa.Column('read_only', sa.Integer(), nullable=True))) 62 | ignore_duplicate(lambda: op.add_column('peer', sa.Column('server_id', sa.Integer(), nullable=True))) 63 | ignore_duplicate(lambda: op.add_column('peer', sa.Column('shared_key', sa.Text(), nullable=True))) 64 | ignore_duplicate(lambda: op.add_column('peer', sa.Column('v6_address', sa.String(), nullable=True))) 65 | 66 | # op.drop_constraint(None, 'peer', type_='foreignkey') 67 | # 68 | # op.drop_column('peer', 'server') 69 | 70 | try: 71 | with op.batch_alter_table('peer', schema=None) as batch_op: 72 | batch_op.drop_column("server") 73 | except KeyError: 74 | pass 75 | 76 | ignore_duplicate(lambda: op.add_column('server', sa.Column('allowed_ips', sa.String(), nullable=True))) 77 | ignore_duplicate(lambda: op.add_column('server', sa.Column('configuration', sa.Text(), nullable=True))) 78 | ignore_duplicate(lambda: op.add_column('server', sa.Column('dns', sa.String(), nullable=True))) 79 | ignore_duplicate(lambda: op.add_column('server', sa.Column('keep_alive', sa.Integer(), nullable=True))) 80 | ignore_duplicate(lambda: op.add_column('server', sa.Column('read_only', sa.Integer(), nullable=True))) 81 | ignore_duplicate(lambda: op.add_column('server', sa.Column('subnet', sa.Integer(), nullable=False))) 82 | ignore_duplicate(lambda: op.add_column('server', sa.Column('v6_address', sa.String(), nullable=True))) 83 | ignore_duplicate(lambda: op.add_column('server', sa.Column('v6_subnet', sa.Integer(), nullable=False))) 84 | # op.create_unique_constraint(None, 'server', ['v6_address']) 85 | 86 | try: 87 | with op.batch_alter_table('server', schema=None) as batch_op: 88 | batch_op.drop_column("shared_key") 89 | except KeyError: 90 | pass 91 | 92 | # ### end Alembic commands ### 93 | 94 | 95 | def downgrade(): 96 | # ### commands auto generated by Alembic - please adjust! ### 97 | op.add_column('server', sa.Column('shared_key', sa.VARCHAR(), nullable=True)) 98 | op.drop_constraint(None, 'server', type_='unique') 99 | op.drop_column('server', 'v6_subnet') 100 | op.drop_column('server', 'v6_address') 101 | op.drop_column('server', 'subnet') 102 | op.drop_column('server', 'read_only') 103 | op.drop_column('server', 'keep_alive') 104 | op.drop_column('server', 'dns') 105 | op.drop_column('server', 'configuration') 106 | op.drop_column('server', 'allowed_ips') 107 | op.add_column('peer', sa.Column('server', sa.INTEGER(), nullable=True)) 108 | op.drop_constraint('fk_wg_peer_server_id', 'peer', type_='foreignkey') 109 | op.create_foreign_key(None, 'peer', 'server', ['server'], ['interface']) 110 | op.drop_column('peer', 'v6_address') 111 | op.drop_column('peer', 'shared_key') 112 | op.drop_column('peer', 'server_id') 113 | op.drop_column('peer', 'read_only') 114 | op.drop_column('peer', 'keep_alive') 115 | op.drop_column('peer', 'configuration') 116 | op.drop_table('api_keys') 117 | # ### end Alembic commands ### 118 | -------------------------------------------------------------------------------- /wg-manager-backend/requirements.txt: -------------------------------------------------------------------------------- 1 | pydantic 2 | fastapi 3 | aiofiles 4 | aiosqlite 5 | sqlalchemy < 1.4.0 6 | databases 7 | PyJWT 8 | passlib 9 | bcrypt 10 | python-multipart 11 | jinja2 12 | sqlalchemy_utils 13 | sqlalchemy-migrate 14 | requests 15 | uvicorn 16 | uvicorn-loguru-integration 17 | uvloop 18 | httptools 19 | qrcode[pil] 20 | alembic 21 | loguru -------------------------------------------------------------------------------- /wg-manager-backend/routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-backend/routers/__init__.py -------------------------------------------------------------------------------- /wg-manager-backend/routers/v1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-backend/routers/v1/__init__.py -------------------------------------------------------------------------------- /wg-manager-backend/routers/v1/peer.py: -------------------------------------------------------------------------------- 1 | import ipaddress 2 | import itertools 3 | from fastapi import APIRouter, Depends, HTTPException 4 | from sqlalchemy.orm import Session 5 | from starlette.responses import PlainTextResponse 6 | 7 | from database import models 8 | import schemas 9 | import middleware 10 | import db.wireguard 11 | import script.wireguard 12 | 13 | router = APIRouter() 14 | 15 | 16 | def generate_ip_address(server: schemas.WGServer, v6): 17 | if v6: 18 | address_space = set( 19 | itertools.islice(ipaddress.ip_network("fd42:42:42::1/64", strict=False).hosts(), 1, 1024) 20 | ) 21 | else: 22 | address_space = set(ipaddress.ip_network(f"{server.address}/{server.subnet}", strict=False).hosts()) 23 | occupied_space = set() 24 | 25 | # Try add server IP to list. 26 | try: 27 | occupied_space.add(ipaddress.ip_address(server.v6_address if v6 else server.address)) 28 | except ValueError: 29 | pass 30 | 31 | for p in server.peers: 32 | 33 | # Try add peer ip to list. 34 | try: 35 | occupied_space.add(ipaddress.ip_address(p.v6_address if v6 else p.address)) 36 | except ValueError as e: 37 | pass # Ignore invalid addresses. These are out of address_space 38 | 39 | address_space -= occupied_space 40 | 41 | # Select first available address 42 | return str(list(sorted(address_space)).pop(0)) 43 | 44 | 45 | @router.post("/add", response_model=schemas.WGPeer) 46 | def add_peer( 47 | peer_add: schemas.WGPeerConfigAdd, 48 | sess: Session = Depends(middleware.get_db) 49 | ): 50 | server = schemas.WGServer(interface=peer_add.server_interface).from_db(sess) 51 | 52 | if server is None: 53 | raise HTTPException(500, detail="The server-interface '%s' does not exist!" % peer_add.server_interface) 54 | 55 | peer = schemas.WGPeer(server_id=server.id) 56 | 57 | if server.v6_address: 58 | peer.v6_address = generate_ip_address(server, v6=True) 59 | peer.address = generate_ip_address(server, v6=False) 60 | 61 | # Private public key generation 62 | keys = script.wireguard.generate_keys() 63 | peer.private_key = keys["private_key"] 64 | peer.public_key = keys["public_key"] 65 | 66 | peer.allowed_ips = server.allowed_ips 67 | peer.keep_alive = server.keep_alive 68 | 69 | # Set unnamed 70 | peer.name = "Unnamed" if not peer_add.name else peer_add.name 71 | 72 | peer.dns = server.dns 73 | 74 | peer.configuration = script.wireguard.generate_config(dict( 75 | peer=peer, 76 | server=server 77 | )) 78 | 79 | db_peer = models.WGPeer(**peer.dict()) 80 | sess.add(db_peer) 81 | sess.commit() 82 | 83 | # If server is running. Add peer 84 | if script.wireguard.is_running(server): 85 | script.wireguard.add_peer(server, peer) 86 | 87 | # Update server configuration 88 | db.wireguard.server_update_configuration(sess, db_peer.server_id) 89 | 90 | return schemas.WGPeer.from_orm(db_peer) 91 | 92 | 93 | @router.post("/configuration/get_or_add") 94 | def get_or_add_peer_return_config(peer_get: schemas.WGPeerConfigGetByName, 95 | sess: Session = Depends(middleware.get_db) 96 | ): 97 | server = sess.query(models.WGServer).filter_by(interface=peer_get.server_interface).one() 98 | peer = sess.query(models.WGPeer).filter_by(name=peer_get.name, server_id=server.id).all() 99 | 100 | if not peer: 101 | return add_peer_get_config(schemas.WGPeerConfigAdd( 102 | name=peer_get.name, 103 | server_interface=peer_get.server_interface 104 | ), sess=sess) 105 | 106 | peer = peer[0] 107 | 108 | return PlainTextResponse(peer.configuration) 109 | 110 | 111 | @router.post("/configuration/add") 112 | def add_peer_get_config(peer_add: schemas.WGPeerConfigAdd, 113 | sess: Session = Depends(middleware.get_db) 114 | ): 115 | wg_peer: schemas.WGPeer = add_peer(peer_add, sess) 116 | 117 | return PlainTextResponse(wg_peer.configuration) 118 | 119 | 120 | @router.post("/delete", response_model=schemas.WGPeer) 121 | def delete_peer( 122 | peer: schemas.WGPeer, 123 | sess: Session = Depends(middleware.get_db) 124 | ): 125 | 126 | server = sess.query(models.WGServer).filter_by(id=peer.server_id).one() 127 | 128 | if not db.wireguard.peer_remove(sess, peer): 129 | raise HTTPException(400, detail="Were not able to delete peer %s (%s)" % (peer.name, peer.public_key)) 130 | 131 | if script.wireguard.is_running(schemas.WGServer(interface=server.interface)): 132 | script.wireguard.remove_peer(server, peer) 133 | 134 | return peer 135 | 136 | 137 | @router.post("/edit") 138 | def edit_peer( 139 | peer: schemas.WGPeer, 140 | sess: Session = Depends(middleware.get_db) 141 | ): 142 | 143 | peer = db.wireguard.peer_edit(sess, peer) 144 | 145 | return peer 146 | -------------------------------------------------------------------------------- /wg-manager-backend/routers/v1/server.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, HTTPException 2 | from sqlalchemy.orm import Session 3 | from starlette.responses import JSONResponse 4 | 5 | from database import models 6 | import schemas 7 | import middleware 8 | import db.wireguard 9 | import script.wireguard 10 | import typing 11 | 12 | router = APIRouter() 13 | 14 | 15 | @router.get("/all", response_model=typing.List[schemas.WGServer]) 16 | def servers_all( 17 | sess: Session = Depends(middleware.get_db) 18 | ): 19 | interfaces = db.wireguard.server_get_all(sess) 20 | for iface in interfaces: 21 | iface.is_running = script.wireguard.is_running(iface) 22 | 23 | return interfaces 24 | 25 | 26 | @router.post("/add", response_model=schemas.WGServer) 27 | def add_interface( 28 | server: schemas.WGServerAdd, 29 | sess: Session = Depends(middleware.get_db) 30 | ): 31 | 32 | return db.wireguard.server_add(server, sess) 33 | 34 | 35 | @router.post("/stop", response_model=schemas.WGServer) 36 | def stop_server( 37 | server: schemas.WGServer, 38 | sess: Session = Depends(middleware.get_db) 39 | ): 40 | script.wireguard.stop_interface(server) 41 | server.is_running = script.wireguard.is_running(server) 42 | server.sync(sess) 43 | return server 44 | 45 | 46 | @router.post("/start", response_model=schemas.WGServer) 47 | def start_server( 48 | server: schemas.WGServer, 49 | sess: Session = Depends(middleware.get_db) 50 | ): 51 | script.wireguard.start_interface(server) 52 | server.is_running = script.wireguard.is_running(server) 53 | server.sync(sess) 54 | return server 55 | 56 | 57 | @router.post("/restart", response_model=schemas.WGServer) 58 | def restart_server( 59 | server: schemas.WGServer, 60 | sess: Session = Depends(middleware.get_db) 61 | ): 62 | script.wireguard.restart_interface(server) 63 | server.is_running = script.wireguard.is_running(server) 64 | server.sync(sess) 65 | 66 | return server 67 | 68 | 69 | @router.post("/delete", response_model=schemas.WGServer) 70 | def delete_server( 71 | form_data: schemas.WGServer, 72 | sess: Session = Depends(middleware.get_db) 73 | ): 74 | # Stop if running 75 | if script.wireguard.is_running(form_data): 76 | script.wireguard.stop_interface(form_data) 77 | 78 | if not db.wireguard.server_remove(sess, form_data): 79 | raise HTTPException(400, detail="Were not able to delete %s" % form_data.interface) 80 | return form_data 81 | 82 | 83 | @router.post("/stats", dependencies=[Depends(middleware.auth)]) 84 | def stats_server(server: schemas.WGServer): 85 | if script.wireguard.is_running(server): 86 | stats = script.wireguard.get_stats(server) 87 | else: 88 | stats = [] 89 | 90 | return JSONResponse(content=stats) 91 | 92 | 93 | @router.post("/edit", response_model=schemas.WGServer) 94 | def edit_server( 95 | data: dict, sess: Session = Depends(middleware.get_db) 96 | ): 97 | interface = data["interface"] 98 | old = schemas.WGServer(interface=interface).from_db(sess) 99 | 100 | # Stop if running 101 | if script.wireguard.is_running(old): 102 | script.wireguard.stop_interface(old) 103 | 104 | # Update server 105 | server = schemas.WGServer(**data["server"]) 106 | server.configuration = script.wireguard.generate_config(server) 107 | server = old.update(sess, new=server) 108 | 109 | # Update peers 110 | for peer_data in server.peers: 111 | peer = schemas.WGPeer(**peer_data) 112 | peer.configuration = script.wireguard.generate_config(dict( 113 | peer=peer, 114 | server=server 115 | )) 116 | 117 | db_peer = models.WGPeer(**peer.dict()) 118 | sess.merge(db_peer) 119 | sess.commit() 120 | 121 | script.wireguard.start_interface(server) 122 | server.is_running = script.wireguard.is_running(server) 123 | server.sync(sess) # TODO - fix this sync mess. 124 | server.from_db(sess) 125 | 126 | return server 127 | 128 | 129 | @router.get("/config/{server_id}", response_model=str) 130 | def server_config( 131 | server_id: int, 132 | sess: Session = Depends(middleware.get_db) 133 | ): 134 | 135 | return db.wireguard.get_server_by_id(sess, server_id=server_id).configuration 136 | -------------------------------------------------------------------------------- /wg-manager-backend/routers/v1/user.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import timedelta 3 | 4 | from fastapi import APIRouter, HTTPException, Depends, Form, Body 5 | from fastapi.responses import JSONResponse 6 | import typing 7 | from sqlalchemy.orm import Session 8 | from starlette import status 9 | from binascii import hexlify 10 | import const 11 | import db.user 12 | import middleware 13 | from database import models 14 | import schemas 15 | 16 | router = APIRouter() 17 | 18 | 19 | @router.get("/logout") 20 | def logout(user: schemas.User = Depends(middleware.auth)): 21 | return dict(message="ok") 22 | 23 | 24 | @router.post("/user/edit", response_model=schemas.User) 25 | def edit(form_data: schemas.UserInDB, 26 | user: schemas.UserInDB = Depends(middleware.auth), 27 | sess: Session = Depends(middleware.get_db) 28 | ): 29 | form_data.password = middleware.get_password_hash(form_data.password) 30 | form_data.sync(sess) 31 | return form_data 32 | 33 | 34 | @router.get("/users/api-key/add", response_model=schemas.UserAPIKeyFull) 35 | def add_api_key( 36 | user: schemas.UserInDB = Depends(middleware.auth), 37 | sess: Session = Depends(middleware.get_db) 38 | ): 39 | key = hexlify(os.urandom(const.API_KEY_LENGTH)).decode() 40 | 41 | api_key = models.UserAPIKey( 42 | user_id=user.id, 43 | key=key, 44 | ) 45 | sess.add(api_key) 46 | sess.commit() 47 | 48 | return schemas.UserAPIKeyFull.from_orm(api_key) 49 | 50 | 51 | @router.post("/users/api-key/delete") 52 | def delete_api_keys( 53 | key_id: int = Body(None, embed=True), 54 | user: schemas.UserInDB = Depends(middleware.auth), 55 | sess: Session = Depends(middleware.get_db) 56 | ): 57 | 58 | count = sess.query(models.UserAPIKey)\ 59 | .filter_by(id=key_id)\ 60 | .delete() 61 | sess.commit() 62 | 63 | return JSONResponse({ 64 | "message": "Key deleted OK" if count == 1 else "There was an error while deleting the api-key" 65 | }) 66 | 67 | 68 | @router.get("/users/api-key/list", response_model=typing.List[schemas.UserAPIKey]) 69 | def get_api_keys( 70 | user: schemas.UserInDB = Depends(middleware.auth), 71 | sess: Session = Depends(middleware.get_db) 72 | ): 73 | keys = [schemas.UserAPIKey.from_orm(x) for x in sess.query(models.UserAPIKey) 74 | .filter(models.UserAPIKey.user_id == user.id).all()] 75 | 76 | return keys 77 | 78 | 79 | @router.post("/login", response_model=schemas.Token) 80 | def login(*, username: str = Form(...), password: str = Form(...), sess: Session = Depends(middleware.get_db)): 81 | user: schemas.UserInDB = schemas.UserInDB(username=username, password="").from_db(sess) 82 | 83 | # Verify password 84 | if not user or not middleware.verify_password(password, user.password): 85 | raise HTTPException( 86 | status_code=status.HTTP_401_UNAUTHORIZED, 87 | detail="Incorrect username or password", 88 | headers={"WWW-Authenticate": "Bearer"}, 89 | ) 90 | 91 | # Create token 92 | access_token_expires = timedelta(minutes=const.ACCESS_TOKEN_EXPIRE_MINUTES) 93 | access_token = middleware.create_access_token( 94 | data={"sub": user.username}, expires_delta=access_token_expires 95 | ) 96 | 97 | return schemas.Token( 98 | access_token=access_token, 99 | token_type="bearer", 100 | user=schemas.User(**user.dict()) 101 | ) 102 | 103 | 104 | @router.post("/users/create/") 105 | def create_user( 106 | form_data: schemas.UserInDB, 107 | sess: Session = Depends(middleware.get_db), 108 | user: schemas.User = Depends(middleware.auth) 109 | ): 110 | user = db.user.get_user_by_name(sess, form_data.username) 111 | 112 | # User already exists 113 | if user: 114 | if not db.user.authenticate_user(sess, form_data.username, form_data.password): 115 | raise HTTPException(status_code=401, detail="Incorrect password") 116 | 117 | else: 118 | 119 | # Create the user 120 | if not db.user.create_user(sess, models.User( 121 | username=form_data.username, 122 | password=form_data.password, 123 | full_name=form_data.full_name, 124 | email=form_data.email, 125 | role=form_data.role, 126 | )): 127 | raise HTTPException(status_code=400, detail="Could not create user") 128 | -------------------------------------------------------------------------------- /wg-manager-backend/routers/v1/wg.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from fastapi import APIRouter, Depends 4 | from sqlalchemy.orm import Session 5 | from zipfile import ZipFile 6 | from io import BytesIO 7 | import middleware 8 | from starlette.responses import StreamingResponse 9 | import schemas 10 | import script.wireguard 11 | import db.wireguard 12 | 13 | router = APIRouter() 14 | 15 | 16 | @router.get("/generate_psk", response_model=schemas.PSK) 17 | def generate_psk(): 18 | return schemas.PSK( 19 | psk=script.wireguard.generate_psk() 20 | ) 21 | 22 | 23 | @router.get("/generate_keypair", response_model=schemas.KeyPair) 24 | def generate_key_pair(): 25 | keys = script.wireguard.generate_keys() 26 | private_key = keys["private_key"] 27 | public_key = keys["public_key"] 28 | return schemas.KeyPair( 29 | private_key=private_key, 30 | public_key=public_key 31 | ) 32 | 33 | 34 | @router.get("/dump") 35 | def dump_database( 36 | sess: Session = Depends(middleware.get_db) 37 | ): 38 | in_memory = BytesIO() 39 | zf = ZipFile(in_memory, mode="w") 40 | for server in db.wireguard.server_get_all(sess): 41 | zf.writestr(f"{server.interface}/{server.interface}.conf", server.configuration) 42 | 43 | for peer in server.peers: 44 | zf.writestr(f"{server.interface}/peers/{peer.name}_{peer.address.replace('.','-')}.conf", server.configuration) 45 | 46 | zf.close() 47 | in_memory.seek(0) 48 | 49 | now = datetime.now().strftime("%m-%d-%Y-%H:%M:%S") 50 | return StreamingResponse(in_memory, media_type="application/zip", headers={ 51 | "Content-Disposition": f'attachment; filename="wg-manager-dump-{now}.zip"' 52 | }) 53 | -------------------------------------------------------------------------------- /wg-manager-backend/schemas.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import pydantic 4 | from pydantic import BaseModel, typing 5 | from sqlalchemy.orm import Session, Query 6 | from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound 7 | import logging 8 | from database import models 9 | 10 | _LOGGER = logging.getLogger(__name__) 11 | 12 | 13 | class GenericModel(BaseModel): 14 | 15 | class Meta: 16 | model = None 17 | key = None 18 | excludes = {"id"} 19 | 20 | class Config: 21 | orm_mode = True 22 | 23 | def _ensure_orm(self): 24 | if not self.Config and not self.Config.orm_mode and not self.Meta.model and not self.Meta.key: 25 | raise NotImplementedError("Incorrect configuration Config.orm_mode must be enabled and Meta.model must be " 26 | "set to a sqlalchemy model. Additional Meta.key must be set to bind model and schema") 27 | 28 | def filter_query(self, sess) -> Query: 29 | query = sess.query(self.Meta.model).filter_by(**{ 30 | self.Meta.key: getattr(self, self.Meta.key) 31 | }) 32 | 33 | return query 34 | 35 | def update(self, sess: Session, new): 36 | self._ensure_orm() 37 | 38 | self.filter_query(sess).update(new.dict(include=self.columns())) 39 | 40 | sess.commit() 41 | 42 | for k, v in new.dict().items(): 43 | try: 44 | setattr(self, k, v) 45 | except ValueError: 46 | pass 47 | 48 | return self 49 | 50 | def columns(self, no_exclude=False): 51 | cols = set([x for x in dir(self.Meta.model) if not x.startswith("_")]) 52 | #cols = set([str(x).replace(f"{self.Meta.model.__table__.name}.", "") for x in self.Meta.model.__table__.columns]) 53 | return cols if no_exclude else cols - self.Meta.excludes 54 | 55 | def sync(self, sess: Session): 56 | self._ensure_orm() 57 | 58 | # Count existing 59 | n_results = self.filter_query(sess).count() 60 | if n_results == 0: 61 | # Insert, does not exists at all. 62 | # Convert from schema to model 63 | dbm = self.Meta.model(**self.dict(exclude=self.Meta.excludes)) # TODO. added exclude here. this might mess stuff? 64 | sess.add(dbm) 65 | else: 66 | self.filter_query(sess).update(self.dict(include=self.columns())) 67 | 68 | sess.commit() 69 | 70 | def from_db(self, sess: Session): 71 | self._ensure_orm() 72 | 73 | try: 74 | db_item = self.filter_query(sess).one() 75 | 76 | for c in self.columns(no_exclude=True): 77 | try: 78 | setattr(self, c, getattr(db_item, c)) 79 | except ValueError as e: 80 | pass 81 | return self 82 | except MultipleResultsFound as e: 83 | _LOGGER.exception(e) 84 | except NoResultFound as e: 85 | _LOGGER.exception(e) 86 | 87 | _LOGGER.warning("We did not find any records in the database that corresponds to the model. This means you " 88 | "are trying to fetch a unsaved schema!") 89 | return None 90 | 91 | 92 | class User(GenericModel): 93 | id: int = None 94 | username: str 95 | email: str = None 96 | full_name: str = None 97 | role: str = None 98 | 99 | class Meta: 100 | model = models.User 101 | key = "username" 102 | excludes = {"id"} 103 | 104 | 105 | class UserAPIKey(GenericModel): 106 | id: int 107 | created_date: datetime 108 | 109 | 110 | class UserAPIKeyFull(UserAPIKey): 111 | key: str 112 | 113 | 114 | class UserInDB(User): 115 | password: str 116 | 117 | 118 | class Token(GenericModel): 119 | access_token: str 120 | token_type: str 121 | user: User 122 | 123 | 124 | class WGPeer(GenericModel): 125 | id: int = None 126 | name: str = None 127 | address: str = None 128 | v6_address: str = None 129 | private_key: str = None 130 | public_key: str = None 131 | shared_key: str = None 132 | server_id: str 133 | dns: str = None 134 | allowed_ips: str = None 135 | keep_alive: int = None 136 | configuration: str = None 137 | 138 | class Meta: 139 | model = models.WGPeer 140 | key = "id" 141 | excludes = {} 142 | 143 | 144 | class WGPeerConfig(GenericModel): 145 | config: str 146 | 147 | 148 | class KeyPair(GenericModel): 149 | public_key: str 150 | private_key: str 151 | 152 | 153 | class PSK(GenericModel): 154 | psk: str 155 | 156 | 157 | class WGServer(GenericModel): 158 | id: int = None 159 | address: str = None 160 | v6_address: str = None 161 | subnet: int = None 162 | v6_subnet: int = None 163 | interface: str 164 | listen_port: int = None 165 | endpoint: str = None 166 | private_key: str = None 167 | public_key: str = None 168 | is_running: bool = None 169 | configuration: str = None 170 | post_up: str = None 171 | post_down: str = None 172 | dns: str = None 173 | allowed_ips: str = None 174 | keep_alive: int = None 175 | read_only: int = None 176 | 177 | peers: pydantic.typing.List['WGPeer'] = [] 178 | 179 | class Meta: 180 | model = models.WGServer 181 | key = "interface" 182 | excludes = {"id", "peers", "v6_support"} 183 | 184 | def convert(self): 185 | self.peers = [] if not self.peers else self.peers 186 | return models.WGServer(**self.dict(exclude={"is_running"})) 187 | 188 | 189 | class WGServerAdd(WGServer): 190 | address: str 191 | interface: str 192 | listen_port: int 193 | v6_support: bool 194 | 195 | 196 | class WGPeerConfigAdd(GenericModel): 197 | server_interface: str 198 | name: str = None 199 | 200 | 201 | class WGPeerConfigGetByName(GenericModel): 202 | server_interface: str 203 | name: str = None -------------------------------------------------------------------------------- /wg-manager-backend/script/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-backend/script/__init__.py -------------------------------------------------------------------------------- /wg-manager-backend/script/obfuscate/__init__.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from pathlib import Path 3 | import subprocess 4 | import shlex 5 | 6 | 7 | class NotInstalledError(Exception): 8 | pass 9 | 10 | 11 | class BaseObfuscation(abc.ABC): 12 | 13 | def __init__(self, binary_name=None, binary_path=None, algorithm=None): 14 | 15 | assert binary_name is not None or binary_path is not None 16 | self.binary_name = binary_name if binary_name is not None else Path(binary_path).name 17 | self.binary_path = binary_path if binary_path else "" 18 | self.algorithm = algorithm 19 | 20 | def ensure_installed(self): 21 | 22 | # Attempt to find process by path 23 | binary = Path(self.binary_path) 24 | if not binary.is_file(): 25 | # Did not find by path, attempt to find using which 26 | proc_which = subprocess.Popen(["which", self.binary_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 27 | data = [x.decode().strip() for x in proc_which.communicate() if x != b''][0] 28 | 29 | if proc_which.returncode != 0: 30 | raise NotInstalledError("Could not find binary '%s'" % data) 31 | 32 | self.binary_path = data 33 | 34 | def execute(self, *args, kill_first=False, override_command=None, stream=False, prefix=""): 35 | if prefix != "": 36 | prefix += ": " 37 | 38 | if kill_first: 39 | # TODO try to delete by full name as we dont want to kill other processes. 40 | pattern = self.binary_name 41 | self.execute(*[pattern], override_command="pkill") 42 | #pattern = self.binary_path + " " + ' '.join(args) 43 | #print(pattern) 44 | #kill_output, kill_code = self.execute(*[pattern], override_command="pkill") 45 | 46 | command = override_command if override_command is not None else self.binary_path 47 | proc_which = subprocess.Popen([command] + list(args), stdout=subprocess.PIPE, stderr=subprocess.PIPE) 48 | 49 | if not stream: 50 | raw_data = proc_which.communicate() 51 | 52 | data = [x.decode().strip() for x in raw_data if x != b''] 53 | if len(data) == 0: 54 | data = "" 55 | else: 56 | data = data[0] 57 | return data, proc_which.returncode 58 | 59 | else: 60 | while True: 61 | output = proc_which.stdout.readline() 62 | if output == '' and proc_which.poll() is not None: 63 | break 64 | if output: 65 | print(prefix + output.strip().decode()) 66 | rc = proc_which.poll() 67 | return rc 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /wg-manager-backend/script/obfuscate/obfs4.py: -------------------------------------------------------------------------------- 1 | from script.obfuscate import BaseObfuscation 2 | import re 3 | 4 | 5 | class ObfuscateOBFS4(BaseObfuscation): 6 | 7 | def __init__(self): 8 | super().__init__( 9 | binary_name="obfs4proxy", 10 | binary_path="/usr/bin/obfs4proxy", 11 | algorithm="obfs4" 12 | ) 13 | 14 | self.ensure_installed() 15 | 16 | def ensure_installed(self): 17 | super().ensure_installed() 18 | 19 | output, code = self.execute("-version") 20 | 21 | if re.match(f'{self.binary_name}-[0-9]+.[0-9]+.[0-9]+', output) and code == 0: 22 | return True 23 | else: 24 | raise RuntimeError(f"Could not verify that {self.binary_name} is installed correctly.") 25 | 26 | 27 | if __name__ == "__main__": 28 | 29 | x = ObfuscateOBFS4() 30 | x.ensure_installed() -------------------------------------------------------------------------------- /wg-manager-backend/script/obfuscate/tor.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import requests 4 | 5 | import const 6 | from script.obfuscate import BaseObfuscation 7 | import re 8 | import os 9 | import qrcode 10 | import socket 11 | 12 | from script.obfuscate.obfs4 import ObfuscateOBFS4 13 | 14 | 15 | class ObfuscationViaTOR(BaseObfuscation): 16 | 17 | def __init__(self, algorithm: BaseObfuscation): 18 | super().__init__( 19 | binary_name="tor" 20 | ) 21 | self.algorithm = algorithm 22 | self.tor_data_dir = "/tmp/wg-manager-tor-proxy" 23 | self.tor_config_file = "/tmp/wg-manager-tor-proxy/torrc" 24 | self.tor_fingerprint_file = f"{self.tor_data_dir}/fingerprint" 25 | self.tor_bridge_file = f"{self.tor_data_dir}/pt_state/obfs4_bridgeline.txt" 26 | 27 | Path(self.tor_config_file).touch() 28 | os.makedirs(self.tor_data_dir, exist_ok=True) 29 | 30 | def __del__(self): 31 | pass 32 | 33 | def ensure_installed(self): 34 | super().ensure_installed() 35 | output, code = self.execute("--version") 36 | 37 | if re.match(f'Tor version .*', output) and code == 0: 38 | return True 39 | else: 40 | raise RuntimeError(f"Could not verify that {self.binary_name} is installed correctly.") 41 | 42 | def start(self): 43 | 44 | output, code = self.execute( 45 | "-f", self.tor_config_file, 46 | "--DataDirectory", self.tor_data_dir, 47 | "--RunAsDaemon", "1", 48 | "--ExitPolicy", "reject *:*", 49 | "--ORPort", str(const.OBFUSCATE_SOCKS_TOR_PORT), 50 | "--BridgeRelay", "1", 51 | "--PublishServerDescriptor", "0", 52 | "--ServerTransportPlugin", f"{self.algorithm.algorithm} exec {self.algorithm.binary_path}", 53 | "--ServerTransportListenAddr", f"{self.algorithm.algorithm} 0.0.0.0:{const.OBFUSCATE_TOR_LISTEN_ADDR}", 54 | "--ExtORPort", "auto", 55 | "--ContactInfo", "wg-manager@github.com", 56 | "--Nickname", "wgmanager", 57 | kill_first=True 58 | ) 59 | 60 | print(output) 61 | 62 | def generate_bridge_line(self, local=False): 63 | 64 | if local: 65 | ip_address = socket.gethostbyname(socket.gethostname()) 66 | else: 67 | ip_address = requests.get("https://api.ipify.org").text 68 | 69 | with open(self.tor_fingerprint_file, "r") as f: 70 | fingerprint = f.read().split(" ") 71 | assert len(fingerprint) == 2, "Could not load fingerprint correctly. " \ 72 | "Should be a list of 2 items (name, fingerprint)" 73 | fingerprint = fingerprint[1] 74 | 75 | with open(self.tor_bridge_file, "r") as f: 76 | bridge_line_raw = f.read() 77 | 78 | bridge_line = re.search(r"^Bridge .*", bridge_line_raw, re.MULTILINE).group(0) 79 | bridge_line = bridge_line\ 80 | .replace("", ip_address)\ 81 | .replace("", str(const.OBFUSCATE_TOR_LISTEN_ADDR))\ 82 | .replace("", fingerprint)\ 83 | .replace("Bridge ", "bridge://")\ 84 | .replace("\n", "") 85 | #bridge_line = f"bridge://{self.algorithm.algorithm} {ip_address}:{const.OBFUSCATE_SOCKS_TOR_PORT} {fingerprint}" 86 | print(bridge_line) 87 | return bridge_line 88 | 89 | def output_qr(self, text, image=False): 90 | 91 | qr = qrcode.QRCode( 92 | version=10, 93 | error_correction=qrcode.constants.ERROR_CORRECT_L, 94 | box_size=10, 95 | border=4, 96 | ) 97 | qr.add_data(text) 98 | qr.make(fit=True) 99 | 100 | if image: 101 | img = qr.make_image(fill_color="black", back_color="white") 102 | img.show() 103 | else: 104 | try: 105 | qr.print_tty() 106 | except: 107 | qr.print_ascii() 108 | 109 | 110 | if __name__ == "__main__": 111 | 112 | x = ObfuscationViaTOR( 113 | algorithm=ObfuscateOBFS4() 114 | ) 115 | x.ensure_installed() 116 | x.start() 117 | bridge_line = x.generate_bridge_line(local=False) 118 | x.output_qr(bridge_line, image=True) 119 | #x.generate_bridge_line(local=False) -------------------------------------------------------------------------------- /wg-manager-backend/script/wireguard_startup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import typing 3 | 4 | from sqlalchemy.orm import Session 5 | 6 | import const 7 | from database import models 8 | from database.database import SessionLocal 9 | from db.api_key import add_initial_api_key_for_admin 10 | from db.wireguard import server_add_on_init 11 | from script.wireguard import is_installed, start_interface, is_running, load_environment_clients 12 | 13 | 14 | def setup_on_start(): 15 | _db: Session = SessionLocal() 16 | servers: typing.List[models.WGServer] = _db.query(models.WGServer).all() 17 | for s in servers: 18 | try: 19 | last_state = s.is_running 20 | if is_installed() and last_state and is_running(s): 21 | start_interface(s) 22 | except Exception as e: 23 | print(e) 24 | 25 | if const.CLIENT: 26 | load_environment_clients(_db) 27 | 28 | if const.SERVER_INIT_INTERFACE is not None: 29 | server_add_on_init(_db) 30 | 31 | if const.SERVER_STARTUP_API_KEY is not None: 32 | ADMIN_USERNAME = os.getenv("ADMIN_USERNAME") 33 | add_initial_api_key_for_admin(_db, const.SERVER_STARTUP_API_KEY, ADMIN_USERNAME) 34 | _db.close() 35 | -------------------------------------------------------------------------------- /wg-manager-backend/templates/peer.j2: -------------------------------------------------------------------------------- 1 | [Interface] 2 | Address = {{ data.peer.address }}/{{ data.server.subnet }}{%- if is_ipv6 -%},{{ data.peer.v6_address }}/{{ data.server.v6_subnet }}{%- endif %} 3 | PrivateKey = {{ data.peer.private_key }} 4 | {% if data.peer.dns %} 5 | DNS = {{ data.peer.dns }} 6 | {% endif %} 7 | 8 | [Peer] 9 | PublicKey = {{ data.server.public_key }} 10 | AllowedIPs = {{ data.peer.allowed_ips }} 11 | Endpoint = {{ data.server.endpoint }}:{{ data.server.listen_port }} 12 | {% if data.peer.shared_key %}PresharedKey = {{ data.peer.shared_key }}{% endif %} 13 | {% if data.peer.keep_alive %}PersistentKeepalive = {{data.peer.keep_alive}}{% endif %} 14 | -------------------------------------------------------------------------------- /wg-manager-backend/templates/server.j2: -------------------------------------------------------------------------------- 1 | [Interface] 2 | Address = {{ data.address }}/{{ data.subnet }}{%- if is_ipv6 -%},{{ data.v6_address }}/{{ data.v6_subnet }}{%- endif %} 3 | ListenPort = {{ data.listen_port }} 4 | PrivateKey = {{ data.private_key }} 5 | 6 | {% if data.post_up or data.v6_post_up %} 7 | PostUp = {{ data.post_up }}{%- if is_ipv6 -%} {{ data.v6_post_up }}{%- endif %} 8 | {%- endif %} 9 | {% if data.post_down or data.v6_post_down %} 10 | PostDown = {{ data.post_down }}{%- if is_ipv6 -%} {{ data.v6_post_down }}{%- endif %} 11 | {%- endif %} 12 | 13 | {% for peer in data.peers %} 14 | [Peer] 15 | # Client Name: {{ peer.name }} 16 | PublicKey = {{ peer.public_key }} 17 | {%- if peer.shared_key %} 18 | PresharedKey = {{ peer.shared_key }} 19 | {%- endif %} 20 | AllowedIPs = {{ peer.address }}/32{%- if is_ipv6 -%},{{ peer.v6_address }}/128{%- endif %} 21 | {% endfor %} 22 | -------------------------------------------------------------------------------- /wg-manager-backend/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-backend/tests/__init__.py -------------------------------------------------------------------------------- /wg-manager-backend/tests/database.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-backend/tests/database.db -------------------------------------------------------------------------------- /wg-manager-backend/tests/test_pytest.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | import schemas 4 | from database import SessionLocal 5 | 6 | with warnings.catch_warnings(): 7 | warnings.filterwarnings("ignore",category=DeprecationWarning) 8 | 9 | 10 | from main import app 11 | from fastapi.testclient import TestClient 12 | 13 | 14 | client = TestClient(app) 15 | 16 | sess = SessionLocal() 17 | 18 | username = "admin" 19 | password = "admin" 20 | token_headers = {} 21 | 22 | def test_logout_without_auth(): 23 | response = client.get("/api/logout") 24 | assert response.status_code == 401 25 | #assert response.json() == dict(message="ok") 26 | 27 | 28 | def test_login_missing_username(): 29 | response = client.post("/api/login", json=dict( 30 | password=password 31 | )) 32 | assert response.status_code == 422 33 | 34 | 35 | def test_login_missing_password(): 36 | 37 | response = client.post("/api/login", json=dict( 38 | password=password 39 | )) 40 | assert response.status_code == 422 41 | 42 | 43 | def test_login(): 44 | 45 | response = client.post("/api/login", json=dict( 46 | username=username, 47 | password=password 48 | ) 49 | ) 50 | assert response.status_code == 200 # Must have status code 200 51 | assert "user" in response.json() 52 | assert "token_type" in response.json() 53 | assert "access_token" in response.json() 54 | token_headers["Authorization"] = response.json()["token_type"] + " " + response.json()["access_token"] 55 | return response 56 | 57 | 58 | def test_logout_with_auth(): 59 | response = client.get("/api/logout", headers=token_headers) 60 | assert response.status_code == 200 61 | 62 | 63 | def test_user_edit(): 64 | 65 | user = schemas.UserInDB( 66 | username="test", 67 | password="test", 68 | full_name="test", 69 | email="test", 70 | role="test" 71 | ) 72 | 73 | user.sync(sess=sess) 74 | 75 | db_user = user.from_db(sess) 76 | #print(db_user.username) 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /wg-manager-backend/util.py: -------------------------------------------------------------------------------- 1 | from loguru import logger 2 | from fastapi import HTTPException 3 | import os 4 | from jinja2 import Environment, FileSystemLoader 5 | 6 | templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates') 7 | jinja_env = Environment(loader=FileSystemLoader(templates_path)) 8 | 9 | 10 | class WGMHTTPException(HTTPException): 11 | 12 | def __init__(self, status_code: int, detail: str = None): 13 | HTTPException.__init__(self, status_code, detail) 14 | logger.opt(depth=1).error(detail) 15 | -------------------------------------------------------------------------------- /wg-manager-frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /wg-manager-frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "projects/**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "parserOptions": { 12 | "project": [ 13 | "tsconfig.json", 14 | "e2e/tsconfig.json" 15 | ], 16 | "createDefaultProgram": true 17 | }, 18 | "extends": [ 19 | "plugin:@angular-eslint/recommended", 20 | "plugin:@angular-eslint/template/process-inline-templates" 21 | ], 22 | "rules": { 23 | "@angular-eslint/component-selector": [ 24 | "error", 25 | { 26 | "prefix": "app", 27 | "style": "kebab-case", 28 | "type": "element" 29 | } 30 | ], 31 | "@angular-eslint/directive-selector": [ 32 | "error", 33 | { 34 | "prefix": "app", 35 | "style": "camelCase", 36 | "type": "attribute" 37 | } 38 | ] 39 | } 40 | }, 41 | { 42 | "files": [ 43 | "*.html" 44 | ], 45 | "extends": [ 46 | "plugin:@angular-eslint/template/recommended" 47 | ], 48 | "rules": {} 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /wg-manager-frontend/.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute to wg-manager 2 | 3 | #### **Did you find a bug?** 4 | 5 | * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/perara/wg-manager/issues). 6 | 7 | * If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/perara/wg-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. 8 | 9 | * If possible, use the [bug report template](.github/ISSUE_TEMPLATE.md) to create the issue. 10 | 11 | #### **Did you write a patch that fixes a bug?** 12 | 13 | * Open a new GitHub pull request with the patch. 14 | 15 | * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 16 | 17 | #### **Do you intend to add a new feature or change an existing one?** 18 | 19 | * Send a [GitHub Pull Request](https://github.com/perara/wg-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). 20 | 21 | * We use [Karma commit message convention](http://karma-runner.github.io/0.13/dev/git-commit-msg.html). Please follow it. 22 | 23 | * 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). 24 | 25 | #### **Do you have questions about the source code?** 26 | 27 | * Ask any question about how to use wg-manager in the [Issues](https://github.com/perara/wg-manager/issues). 28 | 29 | Thanks! :heart: :heart: :heart: 30 | 31 | Per-Arne Andersen 32 | -------------------------------------------------------------------------------- /wg-manager-frontend/.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### Expected behavior 2 | 3 | 4 | 5 | #### Actual behavior 6 | 7 | 8 | 9 | #### Steps to reproduce the behavior 10 | 11 | 12 | 13 | #### Relevant code 14 | 15 | ``` 16 | 17 | ``` 18 | 19 | #### Environment description 20 | 21 | -------------------------------------------------------------------------------- /wg-manager-frontend/.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### Related issues 2 | 3 | 4 | 5 | #### Changes proposed in this pull request 6 | 7 | -------------------------------------------------------------------------------- /wg-manager-frontend/.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-recommended-scss", 4 | "stylelint-config-standard" 5 | ], 6 | "plugins": [ 7 | "stylelint-scss" 8 | ], 9 | "rules": { 10 | "at-rule-no-unknown": [ 11 | true, 12 | { 13 | ignoreAtRules: [ 14 | 'extend', 15 | 'at-root', 16 | 'debug', 17 | 'warn', 18 | 'error', 19 | 'if', 20 | 'else', 21 | 'for', 22 | 'each', 23 | 'while', 24 | 'mixin', 25 | 'include', 26 | 'content', 27 | 'return', 28 | 'function' 29 | ] 30 | } 31 | ], 32 | "selector-type-no-unknown": [ 33 | true, 34 | { 35 | "ignore": [ 36 | "custom-elements" 37 | ] 38 | } 39 | ], 40 | "no-descending-specificity": null 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /wg-manager-frontend/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Per-Arne Andersen 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /wg-manager-frontend/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "wg-manager": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "aot": true, 15 | "outputPath": "dist", 16 | "index": "src/index.html", 17 | "main": "src/main.ts", 18 | "tsConfig": "src/tsconfig.app.json", 19 | "polyfills": "src/polyfills.ts", 20 | "assets": [ 21 | "src/assets" 22 | ], 23 | "styles": [ 24 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 25 | "src/theme/styles.scss" 26 | ], 27 | "scripts": [] 28 | }, 29 | "configurations": { 30 | "production": { 31 | "budgets": [ 32 | { 33 | "type": "anyComponentStyle", 34 | "maximumWarning": "6kb" 35 | } 36 | ], 37 | "optimization": true, 38 | "outputHashing": "all", 39 | "sourceMap": false, 40 | "namedChunks": false, 41 | "aot": true, 42 | "extractLicenses": true, 43 | "vendorChunk": false, 44 | "buildOptimizer": true, 45 | "fileReplacements": [ 46 | { 47 | "replace": "src/environments/environment.ts", 48 | "with": "src/environments/environment.prod.ts" 49 | } 50 | ] 51 | } 52 | } 53 | }, 54 | "serve": { 55 | "builder": "@angular-devkit/build-angular:dev-server", 56 | "options": { 57 | "browserTarget": "wg-manager:build", 58 | "proxyConfig": "proxy.conf.json" 59 | }, 60 | "configurations": { 61 | "production": { 62 | "browserTarget": "wg-manager:build:production" 63 | } 64 | } 65 | }, 66 | "extract-i18n": { 67 | "builder": "@angular-devkit/build-angular:extract-i18n", 68 | "options": { 69 | "browserTarget": "wg-manager:build" 70 | } 71 | }, 72 | "lint": { 73 | "builder": "@angular-eslint/builder:lint", 74 | "options": { 75 | "lintFilePatterns": [ 76 | "src/**/*.ts", 77 | "src/**/*.html" 78 | ] 79 | } 80 | } 81 | } 82 | }, 83 | "wg-manager-e2e": { 84 | "root": "", 85 | "sourceRoot": "", 86 | "projectType": "application" 87 | } 88 | }, 89 | "defaultProject": "wg-manager", 90 | "cli": { 91 | "packageManager": "yarn", 92 | "defaultCollection": "@angular-eslint/schematics" 93 | }, 94 | "schematics": { 95 | "@schematics/angular:component": { 96 | "prefix": "app", 97 | "style": "scss" 98 | }, 99 | "@schematics/angular:directive": { 100 | "prefix": "app" 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /wg-manager-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wg-manager", 3 | "version": "1.1.0", 4 | "description": "WG Dashboard", 5 | "keywords": [ 6 | "dashboard" 7 | ], 8 | "license": "MIT", 9 | "scripts": { 10 | "ng": "ng", 11 | "md-doc": "npm install widdershins && wget http://127.0.0.1:8000/openapi.json -O /tmp/wg-openapi.json && widdershins /tmp/wg-openapi.json --environment widdershins.json --resolve=true --omitHeader=true --language_tabs 'python:Python' -o ../docs/api.md", 12 | "start": "ng serve --host 0.0.0.0 --disable-host-check", 13 | "build": "ng build", 14 | "buildwatch": "ng build --watch --aot --outputPath=../wg-manager-backend/build/ --host 0.0.0.0 --disable-host-check", 15 | "tlint": "ng lint", 16 | "tlint:fix": "ng lint --fix", 17 | "slint": "stylelint --syntax scss ./**/*.scss", 18 | "slint:fix": "stylelint --syntax scss --fix ./**/*.scss", 19 | "lint": "ng lint && stylelint --syntax scss ./**/*.scss", 20 | "postinstall": "ngcc" 21 | }, 22 | "pre-commit": "lint", 23 | "private": false, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/perara/wg-manager" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/perara/wg-manager/issues" 30 | }, 31 | "homepage": "https://github.com/perara/wg-manager", 32 | "dependencies": { 33 | "@angular/animations": "11.2.10", 34 | "@angular/cdk": "11.2.9", 35 | "@angular/common": "11.2.10", 36 | "@angular/compiler": "11.2.10", 37 | "@angular/core": "11.2.10", 38 | "@angular/flex-layout": "11.0.0-beta.33", 39 | "@angular/forms": "11.2.10", 40 | "@angular/localize": "11.2.10", 41 | "@angular/material": "11.2.9", 42 | "@angular/platform-browser": "11.2.10", 43 | "@angular/platform-browser-dynamic": "11.2.10", 44 | "@angular/router": "11.2.10", 45 | "ngx-cookie-service": "11.0.2", 46 | "angularx-qrcode": "11.0.0", 47 | "angular-material-dynamic-themes": "1.0.4", 48 | "angular-notifier": "9.0.1", 49 | "classlist.js": "1.1.20150312", 50 | "core-js": "3.10.2", 51 | "tslib": "2.2.0", 52 | "web-animations-js": "2.3.2", 53 | "widdershins": "4.0.1", 54 | "zone.js": "0.11.4", 55 | "material-icons": "0.6.1", 56 | "@jedmao/ini-parser": "0.2.4", 57 | "file-saver": "2.0.5", 58 | "hammerjs": "2.0.8", 59 | "ip-address": "^7.1.0", 60 | "ip-cidr": "2.1.4", 61 | "js-file-download": "0.4.12", 62 | "jszip": "3.6.0", 63 | "rxjs": "6.6.7" 64 | 65 | }, 66 | "devDependencies": { 67 | "@angular-devkit/build-angular": "~0.1102.9", 68 | "@angular-devkit/schematics": "^11.0.5", 69 | "@angular-eslint/builder": "4.0.0", 70 | "@angular-eslint/eslint-plugin": "4.0.0", 71 | "@angular-eslint/eslint-plugin-template": "4.0.0", 72 | "@angular-eslint/schematics": "4.0.0", 73 | "@angular-eslint/template-parser": "4.0.0", 74 | "@angular/cli": "11.2.9", 75 | "@angular/compiler-cli": "11.2.10", 76 | "@angular/language-service": "11.2.10", 77 | "@types/node": "^14.14.41", 78 | "@typescript-eslint/eslint-plugin": "4.16.1", 79 | "@typescript-eslint/parser": "4.16.1", 80 | "eslint": "^7.6.0", 81 | "pre-commit": "1.2.2", 82 | "sass": "1.32.11", 83 | "stylelint": "13.12.0", 84 | "stylelint-config-recommended-scss": "4.2.0", 85 | "stylelint-config-standard": "21.0.0", 86 | "stylelint-scss": "3.19.0", 87 | "ts-node": "9.1.1", 88 | "tslint-angular": "3.0.3", 89 | "tslint-config-airbnb": "5.11.2", 90 | "typescript": "4.0.5" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /wg-manager-frontend/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:8000", 4 | "secure": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | import { LayoutModule } from './layout/layout.module'; 4 | 5 | import { ErrorComponent } from './page/error'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | RouterModule.forRoot( 10 | [ 11 | { path: '', redirectTo: 'page/dashboard', pathMatch: 'full' }, 12 | { path: 'page', loadChildren: () => import('./page/page.module').then(m => m.PageModule) }, 13 | 14 | /*{ path: 'app', component: LayoutComponent, children: 15 | [ 16 | //{ path: 'dashboard', component: DashboardComponent, pathMatch: 'full', canActivate: [AuthGuard]}, 17 | { path: 'dashboard2', component: Dashboard2Component, pathMatch: 'full'}, 18 | 19 | { path: '**', redirectTo: '/pages/404'}, 20 | ] 21 | },*/ 22 | 23 | /*{ path: 'user', component: LayoutComponent, children: 24 | [ 25 | { path: 'login', component: LoginComponent, pathMatch: 'full'}, 26 | { path: 'edit', component: EditComponent, pathMatch: 'full', canActivate: [AuthGuard]}, 27 | ] 28 | },*/ 29 | { path: '**', redirectTo: '/page/404' }, 30 | 31 | ], 32 | { useHash: true, relativeLinkResolution: 'legacy' }, 33 | ), 34 | LayoutModule, 35 | ], 36 | exports: [RouterModule], 37 | }) 38 | export class AppRoutingModule {} 39 | 40 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, HostBinding} from '@angular/core'; 2 | import { AuthService } from '@services/*'; 3 | import {OverlayContainer} from "@angular/cdk/overlay"; 4 | import {DataService} from "./services/data.service"; 5 | import {CookieService} from "ngx-cookie-service"; 6 | 7 | const THEME_DARKNESS_SUFFIX = `-dark`; 8 | 9 | @Component({ 10 | selector: 'app-root', 11 | template: ``, 12 | }) 13 | export class AppComponent { 14 | @HostBinding('class') activeThemeCssClass: string; 15 | isThemeDark = false; 16 | activeTheme: string; 17 | 18 | constructor( 19 | private auth: 20 | AuthService, 21 | private overlayContainer: OverlayContainer, 22 | private comm: DataService, 23 | private cookieService: CookieService 24 | ) { 25 | auth.init(); 26 | 27 | this.comm.on("changeTheme").subscribe( (data: { 28 | theme: any, 29 | darkMode: boolean 30 | }) => { 31 | this.setActiveTheme(data.theme.theme, /* darkness: */ data.darkMode) 32 | }); 33 | 34 | if(this.cookieService.check("currentTheme")){ 35 | this.setActiveTheme( 36 | JSON.parse(this.cookieService.get("currentTheme")).theme, 37 | (this.cookieService.get("darkMode") === 'true') 38 | ); 39 | 40 | } 41 | 42 | 43 | 44 | 45 | 46 | } 47 | 48 | setActiveTheme(theme: string, darkness: boolean = null) { 49 | if (darkness === null) 50 | darkness = this.isThemeDark; 51 | else if (this.isThemeDark === darkness) { 52 | if (this.activeTheme === theme) return 53 | } else 54 | this.isThemeDark = darkness; 55 | 56 | this.activeTheme = theme; 57 | 58 | const cssClass = darkness === true ? theme + THEME_DARKNESS_SUFFIX : theme; 59 | 60 | const classList = this.overlayContainer.getContainerElement().classList; 61 | if (classList.contains(this.activeThemeCssClass)) 62 | classList.replace(this.activeThemeCssClass, cssClass); 63 | else 64 | classList.add(cssClass); 65 | 66 | this.activeThemeCssClass = cssClass 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; 2 | import { NgModule } from '@angular/core'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { AuthInterceptor, AuthService } from '@services/*'; 5 | import { AppRoutingModule } from './app-routing.module'; 6 | import { AppComponent } from './app.component'; 7 | import { VarDirective } from './directives/var.directive'; 8 | import { QRCodeModule } from 'angularx-qrcode'; 9 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 10 | import { MatGridListModule } from '@angular/material/grid-list'; 11 | import { MatCardModule } from '@angular/material/card'; 12 | import { MatMenuModule } from '@angular/material/menu'; 13 | import { MatIconModule } from '@angular/material/icon'; 14 | import { MatButtonModule } from '@angular/material/button'; 15 | 16 | import { MatToolbarModule } from '@angular/material/toolbar'; 17 | import { MatSidenavModule } from '@angular/material/sidenav'; 18 | import { MatListModule } from '@angular/material/list'; 19 | import { FlexLayoutModule } from '@angular/flex-layout'; 20 | import {CookieService} from "ngx-cookie-service"; 21 | import {NotifierModule} from "angular-notifier"; 22 | 23 | @NgModule({ 24 | declarations: [ 25 | AppComponent, 26 | VarDirective, 27 | ], 28 | imports: [ 29 | BrowserModule, 30 | AppRoutingModule, 31 | HttpClientModule, 32 | QRCodeModule, 33 | BrowserAnimationsModule, 34 | MatGridListModule, 35 | MatCardModule, 36 | MatMenuModule, 37 | MatIconModule, 38 | MatButtonModule, 39 | MatToolbarModule, 40 | MatSidenavModule, 41 | MatListModule, 42 | FlexLayoutModule, 43 | NotifierModule 44 | 45 | ], 46 | providers: [ 47 | CookieService, 48 | AuthService, 49 | { 50 | provide: HTTP_INTERCEPTORS, 51 | useClass: AuthInterceptor, 52 | multi: true, 53 | }, 54 | ], 55 | bootstrap: [AppComponent], 56 | exports: [ 57 | VarDirective, 58 | ], 59 | }) 60 | export class AppModule {} 61 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/directives/var.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[ngVar]', 5 | }) 6 | export class VarDirective { 7 | @Input() 8 | set ngVar(context: any) { 9 | console.log(context); 10 | this.context.$implicit = this.context.ngVar = context; 11 | this.updateView(); 12 | } 13 | 14 | context: any = {}; 15 | 16 | constructor(private vcRef: ViewContainerRef, private templateRef: TemplateRef) {} 17 | 18 | updateView() { 19 | this.vcRef.clear(); 20 | this.vcRef.createEmbeddedView(this.templateRef, this.context); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/index.ts: -------------------------------------------------------------------------------- 1 | export { AppComponent } from './app.component'; 2 | export { AppModule } from './app.module'; 3 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/interfaces/peer.ts: -------------------------------------------------------------------------------- 1 | export interface Peer { 2 | _stats: any; 3 | address: string; 4 | v6_address: string; 5 | public_key: string; 6 | private_key: string; 7 | shared_key: string; 8 | dns: string; 9 | allowed_ips: string; 10 | keep_alive: number; 11 | name: string; 12 | configuration: string; 13 | stats: { 14 | sent: string, 15 | received: string, 16 | handshake: string, 17 | }; 18 | 19 | _expand?: boolean; 20 | _edit?: boolean; 21 | } 22 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/interfaces/server.ts: -------------------------------------------------------------------------------- 1 | import { Peer } from './peer'; 2 | 3 | export interface Server { 4 | id: number; 5 | address: string; 6 | interface: string; 7 | listen_port: string; 8 | endpoint: string; 9 | private_key: string; 10 | public_key: string; 11 | shared_key: string; 12 | is_running: boolean; 13 | post_up: string; 14 | post_down: string; 15 | configuration: string; 16 | subnet: number; 17 | read_only: number; 18 | peers: Peer[]; 19 | } 20 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/interfaces/user.ts: -------------------------------------------------------------------------------- 1 | import { Peer } from './peer'; 2 | 3 | export interface User { 4 | full_name: string; 5 | email: string; 6 | role: string; 7 | username: string; 8 | access_token: string; 9 | token_type: string; 10 | 11 | } 12 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/layout/layout.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { LayoutComponent } from './layout/layout.component'; 5 | import { MatSidenavModule } from '@angular/material/sidenav'; 6 | import { MatToolbarModule } from '@angular/material/toolbar'; 7 | import { MatListModule } from '@angular/material/list'; 8 | import { MatIconModule } from '@angular/material/icon'; 9 | import { MatButtonModule } from '@angular/material/button'; 10 | import { FlexLayoutModule } from '@angular/flex-layout'; 11 | import { RouterModule } from '@angular/router'; 12 | import { MatMenuModule } from '@angular/material/menu'; 13 | import {MatSlideToggleModule} from "@angular/material/slide-toggle"; 14 | import {NotifierModule} from "angular-notifier"; 15 | import {FormsModule} from "@angular/forms"; 16 | 17 | @NgModule({ 18 | declarations: [LayoutComponent], 19 | imports: [ 20 | CommonModule, 21 | MatSidenavModule, 22 | MatToolbarModule, 23 | MatListModule, 24 | MatIconModule, 25 | MatButtonModule, 26 | FlexLayoutModule, 27 | RouterModule, 28 | MatMenuModule, 29 | MatSlideToggleModule, 30 | NotifierModule, 31 | FormsModule, 32 | ], 33 | exports: [ 34 | 35 | ], 36 | }) 37 | export class LayoutModule { } 38 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/layout/layout/layout.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | menu 8 | 9 | {{config.applicationName}} 10 | 11 | 12 | 13 | {{item.icon}} 14 | {{item.text}} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Logged in as {{auth.user.username}} 23 | 24 | 25 | Edit User 26 | 27 | 28 | Logout 29 | 30 | 31 | 32 | Themes 33 | 34 | 35 | 36 | 37 | {{theme.name}} 38 | 44 | Dark 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | notifications 60 | Notifications 61 | 62 | 63 | message 64 | Messages 65 | 66 | account_box 67 | My Account 68 | 69 | 70 | lock 71 | My Account 72 | 73 | 74 | close Close 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/layout/layout/layout.component.scss: -------------------------------------------------------------------------------- 1 | .sidenav-container { 2 | height: 100%; 3 | } 4 | 5 | .sidenav { 6 | width: 200px; 7 | } 8 | 9 | .sidenav .mat-toolbar { 10 | background: inherit; 11 | } 12 | 13 | .mat-toolbar.mat-primary { 14 | position: sticky; 15 | top: 0; 16 | z-index: 1; 17 | } 18 | 19 | .menu-spacer { 20 | flex: 1 1 auto; 21 | } 22 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/layout/layout/layout.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LayoutComponent } from './layout.component'; 4 | 5 | describe('LayoutComponent', () => { 6 | let component: LayoutComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ LayoutComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LayoutComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/layout/layout/layout.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; 4 | import { map, shareReplay } from 'rxjs/operators'; 5 | import { ConfigService } from '../../services/config.service'; 6 | import { AuthService } from '@services/*'; 7 | import {OverlayContainer} from "@angular/cdk/overlay"; 8 | import {DataService} from "../../services/data.service"; 9 | import {CookieService} from "ngx-cookie-service"; 10 | 11 | @Component({ 12 | selector: 'app-layout', 13 | templateUrl: './layout.component.html', 14 | styleUrls: ['./layout.component.scss'], 15 | }) 16 | export class LayoutComponent implements OnInit { 17 | 18 | isHandset$: Observable = this.breakpointObserver.observe(Breakpoints.Handset) 19 | .pipe( 20 | map(result => result.matches), 21 | shareReplay(), 22 | ); 23 | 24 | menu: {link: string[], icon: string, text: string}[] = [ 25 | { link: ['/page/dashboard'], icon: 'home', text: 'Dashboard' }, 26 | ]; 27 | 28 | themes = [ 29 | {theme: "indigo-pink", name: "Blue"}, 30 | {theme: "deeppurple-amber", name: "Purple"}, 31 | {theme: "pink-bluegrey", name: "Pink"}, 32 | {theme: "purple-green", name: "Purple-Green"}, 33 | ]; 34 | currentTheme = null; 35 | darkMode = false; 36 | 37 | constructor( 38 | private breakpointObserver: BreakpointObserver, 39 | public config: ConfigService, 40 | public auth: AuthService, 41 | private comm: DataService, 42 | private cookieService: CookieService 43 | ) { 44 | this.darkMode = (this.cookieService.get("darkMode") === 'true'); 45 | 46 | if(this.cookieService.check("currentTheme")){ 47 | this.currentTheme = JSON.parse(this.cookieService.get("currentTheme")); 48 | }else { 49 | this.currentTheme = { ... this.themes[0]} 50 | } 51 | this.sendData() 52 | 53 | } 54 | ngOnInit(): void { 55 | console.log('Layout'); 56 | } 57 | 58 | toggleDarkMode(){ 59 | this.cookieService.set("darkMode", String(this.darkMode)); 60 | this.sendData(); 61 | } 62 | 63 | setCurrentTheme(theme){ 64 | this.cookieService.set("currentTheme", JSON.stringify(theme)); 65 | this.currentTheme = theme; 66 | this.sendData(); 67 | } 68 | 69 | sendData(){ 70 | const send = { 71 | theme: this.currentTheme, 72 | darkMode: this.darkMode 73 | }; 74 | 75 | this.comm.emit('changeTheme', send); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/components/components.component.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/app/page/components/components.component.html -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/components/components.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/app/page/components/components.component.scss -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/components/components.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-components', 5 | template: '', 6 | styles: [''], 7 | }) 8 | export class ComponentsComponent { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/components/components.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { ThemeModule } from 'theme'; 6 | import { ComponentsComponent } from './components.component'; 7 | 8 | import { ModalConfirmComponent } from './modal-confirm'; 9 | import { QRCodeModule } from 'angularx-qrcode'; 10 | import {MatButtonModule} from "@angular/material/button"; 11 | import {MatTooltipModule} from "@angular/material/tooltip"; 12 | import {MatCardModule} from "@angular/material/card"; 13 | import {MatIconModule} from "@angular/material/icon"; 14 | import {FlexModule} from "@angular/flex-layout"; 15 | 16 | @NgModule({ 17 | imports: [ 18 | CommonModule, 19 | ThemeModule, 20 | FormsModule, 21 | QRCodeModule, 22 | MatButtonModule, 23 | MatTooltipModule, 24 | MatCardModule, 25 | MatIconModule, 26 | FlexModule, 27 | ], 28 | providers: [ 29 | ], 30 | exports: [ 31 | ComponentsComponent, 32 | ModalConfirmComponent, 33 | ], 34 | declarations: [ 35 | ComponentsComponent, 36 | ModalConfirmComponent, 37 | ], 38 | }) 39 | export class ComponentsModule { } 40 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/components/index.ts: -------------------------------------------------------------------------------- 1 | export { ComponentsComponent } from './components.component'; 2 | export { ComponentsModule } from './components.module'; 3 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/components/modal-confirm/index.ts: -------------------------------------------------------------------------------- 1 | export { ModalConfirmComponent } from './modal-confirm.component'; 2 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/components/modal-confirm/modal-confirm.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | {{title || "No 'title' defined" }} 12 | 13 | 14 | 15 | close 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {{text || "No Text Defined" }} 27 | {{text || "No Text Defined" }} 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Confirm 38 | Cancel 39 | 40 | 41 | 42 | 43 | 44 | 45 | 50 | {{icon}} 51 | 52 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/components/modal-confirm/modal-confirm.component.scss: -------------------------------------------------------------------------------- 1 | .dark-modal .modal-content { 2 | background-color: #292b2c; 3 | color: white; 4 | } 5 | 6 | .dark-modal .close { 7 | color: white; 8 | } 9 | 10 | .light-blue-backdrop { 11 | background-color: #5cb3fd; 12 | } 13 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/components/modal-confirm/modal-confirm.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ContentChild, 4 | EventEmitter, 5 | Input, 6 | OnInit, 7 | Output, 8 | TemplateRef, ViewChild, ViewContainerRef, 9 | ViewEncapsulation 10 | } from '@angular/core'; 11 | import {NgForOfContext} from "@angular/common"; 12 | 13 | @Component({ 14 | selector: 'app-modal-confirm', 15 | templateUrl: './modal-confirm.component.html', 16 | encapsulation: ViewEncapsulation.Emulated, 17 | styleUrls: ['./modal-confirm.component.scss'], 18 | }) 19 | export class ModalConfirmComponent implements OnInit { 20 | @Input() noConfirm = false; 21 | @Input() qrCode = false; 22 | @Input() icon: string; 23 | @Input() hover: string; 24 | @Input() title: string; 25 | @Input() text: string; 26 | @Input() area: boolean; 27 | @Output() onCancel: EventEmitter = new EventEmitter(); 28 | @Output() onConfirm: EventEmitter = new EventEmitter(); 29 | 30 | @ViewChild('modal', { read: TemplateRef }) _template: TemplateRef; 31 | @ViewChild('vc', {read: ViewContainerRef}) vc: ViewContainerRef; 32 | shown = false; 33 | 34 | constructor() { 35 | 36 | 37 | } 38 | 39 | open($event){ 40 | $event.stopPropagation(); 41 | if (this.noConfirm) { 42 | this.onConfirm.emit($event); 43 | return true; 44 | } 45 | 46 | this.shown = true; 47 | //this.vc.createEmbeddedView(this._template, {fromContext: 'John'}); 48 | 49 | } 50 | confirm($event){ 51 | $event.stopPropagation(); 52 | this.onConfirm.emit($event); 53 | this.shown= false; 54 | 55 | } 56 | 57 | cancel($event){ 58 | $event.stopPropagation(); 59 | this.onCancel.emit($event); 60 | this.shown = false 61 | } 62 | 63 | ngOnInit(): void { 64 | 65 | this.area = this.area || false; 66 | this.area = !!this.area; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/dashboard/add-server/add-server.component.scss: -------------------------------------------------------------------------------- 1 | .add-server-form { 2 | min-width: 150px; 3 | //max-width: 500px; 4 | width: 100%; 5 | } 6 | 7 | .add-server-full-width { 8 | width: 100%; 9 | } 10 | 11 | td { 12 | padding-right: 8px; 13 | } 14 | 15 | 16 | 17 | :host { 18 | width: 100%; 19 | } 20 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/dashboard/dashboard.component.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { BreakpointObserver } from '@angular/cdk/layout'; 3 | import { Server } from '../../interfaces/server'; 4 | import { ServerService } from '../../services/server.service'; 5 | import { Peer } from '../../interfaces/peer'; 6 | 7 | @Component({ 8 | selector: 'dashboard', 9 | templateUrl: './dashboard.component.html', 10 | styleUrls: ['./dashboard.component.css'], 11 | }) 12 | export class DashboardComponent implements OnInit { 13 | servers: Server[] = []; 14 | 15 | constructor(private breakpointObserver: BreakpointObserver, private serverAPI: ServerService) { 16 | 17 | } 18 | 19 | ngOnInit(): void { 20 | this.serverAPI.getServers() 21 | .subscribe((servers: Server[]) => { 22 | this.servers.push(...servers); 23 | servers.forEach((server) => { 24 | 25 | this.serverAPI.serverStats(server).subscribe((stats: Peer[]) => { 26 | stats.forEach(item => { 27 | const peer = server.peers.find(x => x.public_key == item.public_key); 28 | peer._stats = item; 29 | }); 30 | 31 | }); 32 | 33 | }); 34 | 35 | }); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { DashboardComponent } from './dashboard.component'; 4 | import { MatGridListModule } from '@angular/material/grid-list'; 5 | import { MatCardModule } from '@angular/material/card'; 6 | import { MatMenuModule } from '@angular/material/menu'; 7 | import { MatIconModule } from '@angular/material/icon'; 8 | import { MatButtonModule } from '@angular/material/button'; 9 | import { ServerComponent } from './server/server.component'; 10 | import { MatExpansionModule } from '@angular/material/expansion'; 11 | import { AddServerComponent } from './add-server/add-server.component'; 12 | import { MatFormFieldModule } from '@angular/material/form-field'; 13 | import { MatInputModule } from '@angular/material/input'; 14 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 15 | import { ComponentsModule } from '../components'; 16 | import { FlexModule } from '@angular/flex-layout'; 17 | import { MatTableModule } from '@angular/material/table'; 18 | import { PeerComponent } from './peer/peer.component'; 19 | import { QRCodeModule } from 'angularx-qrcode'; 20 | import {MatTooltipModule} from "@angular/material/tooltip"; 21 | import {MatSelectModule} from "@angular/material/select"; 22 | import {MatCheckboxModule} from "@angular/material/checkbox"; 23 | 24 | @NgModule({ 25 | declarations: [ 26 | DashboardComponent, 27 | ServerComponent, 28 | AddServerComponent, 29 | PeerComponent, 30 | ], 31 | imports: [ 32 | CommonModule, 33 | MatGridListModule, 34 | MatCardModule, 35 | MatMenuModule, 36 | MatIconModule, 37 | MatButtonModule, 38 | MatExpansionModule, 39 | MatFormFieldModule, 40 | MatInputModule, 41 | ReactiveFormsModule, 42 | ComponentsModule, 43 | FlexModule, 44 | MatTableModule, 45 | FormsModule, 46 | QRCodeModule, 47 | MatTooltipModule, 48 | MatSelectModule, 49 | MatCheckboxModule, 50 | 51 | ], 52 | }) 53 | export class DashboardModule { } 54 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/dashboard/peer/peer.component.html: -------------------------------------------------------------------------------- 1 | 5 | 8 | 9 | 10 | 11 | 12 | Essentials 13 | 14 | 15 | 16 | Name 17 | 18 | 19 | 20 | 21 | 22 | Address 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | DNS 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Allowed IPs 39 | 40 | 41 | 42 | 43 | 44 | 45 | PersistentKeepalive interval 46 | 47 | 48 | 49 | 50 | 51 | Keys 52 | 53 | 54 | Private-Key 55 | 56 | 57 | 58 | 59 | 60 | 61 | Public-Key 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | PreShared-Key 71 | 72 | 73 | 74 | 75 | 76 | share 77 | Generate PSK 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 94 | Submit changes 95 | 96 | 97 | 98 | 99 | 100 | 101 | 105 | {{peer.configuration || "Error fetching configuration..."}} 106 | 107 | 108 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/dashboard/peer/peer.component.scss: -------------------------------------------------------------------------------- 1 | .peer-edit-form { 2 | min-width: 150px; 3 | //max-width: 500px; 4 | width: 100%; 5 | text-align: left; 6 | } 7 | 8 | td { 9 | padding-left: 0 !important; 10 | padding-right: 8px; 11 | text-align: left !important; 12 | } 13 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/dashboard/peer/peer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnInit, ViewEncapsulation } from '@angular/core'; 2 | import { ServerService } from '../../../services/server.service'; 3 | import { Peer } from '../../../interfaces/peer'; 4 | import { Server } from '../../../interfaces/server'; 5 | import { FormControl, FormGroup } from '@angular/forms'; 6 | 7 | @Component({ 8 | selector: 'app-peer', 9 | templateUrl: './peer.component.html', 10 | encapsulation: ViewEncapsulation.None, 11 | styleUrls: ['./peer.component.scss'], 12 | }) 13 | export class PeerComponent implements OnInit { 14 | 15 | @Input('peer') peer: Peer; 16 | @Input('server') server: Server; 17 | @Input('selectedPeer') selectedPeer: Peer; 18 | @Input('onEvent') editPeerEmitter: EventEmitter = new EventEmitter(); 19 | @Input('cbOnPeerUpdate') cbOnPeerUpdate: Function; 20 | 21 | constructor(public serverAPI: ServerService) { } 22 | 23 | ngOnInit(): void { 24 | 25 | this.editPeerEmitter.subscribe((msg) => { 26 | if (msg.peer !== this.peer) { 27 | return; 28 | } 29 | if (msg.type === 'edit') { 30 | this.edit(); 31 | 32 | 33 | } else if (msg.type == 'delete') { 34 | this.delete(); 35 | } 36 | }); 37 | 38 | } 39 | 40 | edit() { 41 | if (this.peer._edit) { 42 | 43 | 44 | // Submit the edit (True -> False) 45 | const idx = this.server.peers.indexOf(this.peer); 46 | this.serverAPI.editPeer(this.peer).subscribe((peer) => { 47 | Object.keys(peer).forEach(k => { 48 | this.server.peers[idx][k] = peer[k]; 49 | }); 50 | 51 | // Query server for server configuration update 52 | this.cbOnPeerUpdate(); 53 | }); 54 | 55 | } else if (!this.peer._edit) { 56 | this.peer._expand = true; 57 | 58 | // Open for edit. aka do nothing (False -> True 59 | 60 | } 61 | 62 | this.peer._edit = !this.peer._edit; 63 | 64 | } 65 | 66 | delete() { 67 | const idx = this.server.peers.indexOf(this.peer); 68 | this.serverAPI.deletePeer(this.peer).subscribe((apiServer) => { 69 | this.server.peers.splice(idx, 1); 70 | 71 | // Query server for server configuration update 72 | this.cbOnPeerUpdate(); 73 | }); 74 | } 75 | 76 | getPSK() { 77 | this.serverAPI.getPSK().subscribe((psk: any) => { 78 | this.peer.shared_key = psk.psk; 79 | }); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/dashboard/server/server.component.scss: -------------------------------------------------------------------------------- 1 | 2 | table { 3 | width: 100%; 4 | } 5 | 6 | :host { 7 | width: 100%; 8 | } 9 | 10 | .table-icon{ 11 | font-size: 20px; 12 | } 13 | 14 | .dashboard-card{ 15 | margin-bottom: 10px; 16 | } 17 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/dashboard/server/server.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnInit } from '@angular/core'; 2 | import { Server } from '../../../interfaces/server'; 3 | import { ServerService } from '../../../services/server.service'; 4 | import { DataService } from '../../../services/data.service'; 5 | import { Peer } from '../../../interfaces/peer'; 6 | import * as JSZip from 'jszip'; 7 | import { saveAs } from 'file-saver'; 8 | 9 | @Component({ 10 | selector: 'app-server', 11 | templateUrl: './server.component.html', 12 | 13 | styleUrls: ['./server.component.scss', '../dashboard.component.css'], 14 | }) 15 | export class ServerComponent implements OnInit { 16 | @Input() server: Server; 17 | @Input() servers: Server[]; 18 | public editPeerEmitter: EventEmitter = new EventEmitter(); 19 | 20 | selectedPeer: Peer | null; 21 | 22 | constructor(private serverAPI: ServerService, private comm: DataService) { } 23 | 24 | ngOnInit(): void { 25 | console.log('Server'); 26 | } 27 | 28 | edit() { 29 | 30 | this.comm.emit('server-edit', this.server); 31 | } 32 | 33 | stop() { 34 | this.serverAPI.stopServer(this.server).subscribe((apiServer) => { 35 | this.server.is_running = apiServer.is_running; 36 | }); 37 | } 38 | 39 | start() { 40 | this.serverAPI.startServer(this.server).subscribe((apiServer) => { 41 | this.server.is_running = apiServer.is_running; 42 | }); 43 | } 44 | 45 | addPeer() { 46 | this.serverAPI.addPeer({ 47 | server_interface: this.server.interface 48 | }).subscribe((peer) => { 49 | this.server.peers.push(peer); 50 | 51 | // Query server for server configuration update 52 | this.onPeerUpdate(); 53 | }); 54 | } 55 | 56 | restart() { 57 | this.serverAPI.restartServer(this.server).subscribe((apiServer) => { 58 | this.server.is_running = apiServer.is_running; 59 | }); 60 | } 61 | 62 | delete() { 63 | const index = this.servers.indexOf(this.server); 64 | this.serverAPI.deleteServer(this.server).subscribe(() => { 65 | this.servers.splice(index, 1); 66 | }); 67 | } 68 | 69 | openPeer(peer: Peer) { 70 | if (this.selectedPeer == peer) { 71 | this.selectedPeer = null; 72 | return; 73 | } 74 | this.selectedPeer = peer; 75 | this.editPeerEmitter.emit({ type: 'open', peer }); 76 | } 77 | 78 | onPeerUpdate(){ 79 | this.serverAPI.serverConfig(this.server).subscribe((configuration) => { 80 | this.server.configuration = configuration 81 | }) 82 | } 83 | 84 | pInt(string: string) { 85 | return parseInt(string); 86 | } 87 | 88 | downloadPeerConfig(peer: Peer){ 89 | const blob = new Blob([peer.configuration], {type: "text/plain;charset=utf-8"}); 90 | saveAs(blob, `${peer.name}_${peer.address}.conf`); 91 | } 92 | 93 | downloadServerConfig(){ 94 | const zip = new JSZip(); 95 | zip.file(`${this.server.interface}.conf`, this.server.configuration) 96 | this.server.peers.forEach( peer => { 97 | zip.file(`clients/${peer.name}_${peer.address}.conf`, peer.configuration) 98 | }) 99 | 100 | zip.generateAsync({type:"blob"}).then((content) => { 101 | saveAs(content, `${this.server.interface}_${this.server.address}.zip`); 102 | }); 103 | } 104 | 105 | getEndpointFromConfig(config){ 106 | console.log(config) 107 | let res = config.match("Endpoint = (.*)") // TODO handle whitespace 108 | return res[1] 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/error/error.component.css: -------------------------------------------------------------------------------- 1 | *{-webkit-box-sizing:border-box;box-sizing:border-box}body{padding:0;margin:0}#notfound{position:relative;height:100vh}#notfound .notfound{position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.notfound{max-width:520px;width:100%;line-height:1.4;text-align:center}.notfound .notfound-404{position:relative;height:240px}.notfound .notfound-404 h1{font-family:montserrat,sans-serif;position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%);font-size:252px;font-weight:900;margin:0;color:#262626;text-transform:uppercase;letter-spacing:-40px;margin-left:-20px}.notfound .notfound-404 h1>span{text-shadow:-8px 0 0 #fff}.notfound .notfound-404 h3{font-family:cabin,sans-serif;position:relative;font-size:16px;font-weight:700;text-transform:uppercase;color:#262626;margin:0;letter-spacing:3px;padding-left:6px}.notfound h2{font-family:cabin,sans-serif;font-size:20px;font-weight:400;text-transform:uppercase;color:#000;margin-top:0;margin-bottom:25px}@media only screen and (max-width:767px){.notfound .notfound-404{height:200px}.notfound .notfound-404 h1{font-size:200px}}@media only screen and (max-width:480px){.notfound .notfound-404{height:162px}.notfound .notfound-404 h1{font-size:162px;height:150px;line-height:162px}.notfound h2{font-size:16px}} 2 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/error/error.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Oops! Page not found 5 | 404 6 | 7 | we are sorry, but the page you requested was not found 8 | 9 | 10 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/error/error.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-error', 5 | styleUrls: ['error.component.css'], 6 | templateUrl: './error.component.html', 7 | }) 8 | export class ErrorComponent { } 9 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/error/index.ts: -------------------------------------------------------------------------------- 1 | export { ErrorComponent } from './error.component'; 2 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/page-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { DashboardComponent } from './dashboard/dashboard.component'; 4 | import { LayoutComponent } from '../layout/layout/layout.component'; 5 | import { ErrorComponent } from './error'; 6 | import { LoginComponent } from './user/login/login.component'; 7 | import { AuthGuard } from '@services/*'; 8 | import { EditComponent } from './user/edit/edit.component'; 9 | 10 | const routes: Routes = [ 11 | { path: '', component: LayoutComponent, children: 12 | [ 13 | { path: 'dashboard', component: DashboardComponent, pathMatch: 'full', canActivate: [AuthGuard] }, 14 | { path: '404', component: ErrorComponent, pathMatch: 'full' }, 15 | ], 16 | }, 17 | { path: 'user', component: LayoutComponent, children: 18 | [ 19 | { path: 'edit', component: EditComponent, pathMatch: 'full', canActivate: [AuthGuard]}, 20 | { path: 'login', component: LoginComponent, pathMatch: 'full' }, 21 | ], 22 | }, 23 | ]; 24 | 25 | @NgModule({ 26 | imports: [RouterModule.forChild(routes)], 27 | exports: [RouterModule], 28 | }) 29 | export class PageRoutingModule { } 30 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/page.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { PageRoutingModule } from './page-routing.module'; 4 | import { DashboardModule } from './dashboard/dashboard.module'; 5 | import { LoginComponent } from './user/login/login.component'; 6 | import { MatCardModule } from '@angular/material/card'; 7 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 8 | import { MatInputModule } from '@angular/material/input'; 9 | import { FlexModule } from '@angular/flex-layout'; 10 | import { EditComponent } from './user/edit/edit.component'; 11 | import { MatButtonModule } from '@angular/material/button'; 12 | import {MatTableModule} from "@angular/material/table"; 13 | import { ApiKeyComponent } from './user/edit/api-key/api-key.component'; 14 | 15 | @NgModule({ 16 | declarations: [LoginComponent, EditComponent, ApiKeyComponent], 17 | imports: [ 18 | CommonModule, 19 | PageRoutingModule, 20 | FormsModule, 21 | DashboardModule, 22 | MatCardModule, 23 | ReactiveFormsModule, 24 | MatInputModule, 25 | FlexModule, 26 | MatButtonModule, 27 | MatTableModule, 28 | ], 29 | 30 | }) 31 | export class PageModule { } 32 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/user/edit/api-key/api-key.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | API Keys 4 | 5 | 6 | 7 | You can use API-Keys to perform authenticated actions. These are less secure than using OAuth2, but at the gain for increased convenience. 8 | Note: A newly created API Key will only show once. This means that you have to take note of the key and safe it somewhere safe. 9 | 10 | 11 | 12 | 13 | 14 | ID. 15 | {{element.id}} 16 | 17 | 18 | 19 | 20 | API-Key 21 | {{(element.key) ? element.key : "[HIDDEN]"}} 22 | 23 | 24 | 25 | 26 | Creation Date 27 | {{element.created_date | date:'medium'}} 28 | 29 | 30 | 31 | 32 | Delete 33 | Delete 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | New Key 42 | 43 | 44 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/user/edit/api-key/api-key.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/app/page/user/edit/api-key/api-key.component.scss -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/user/edit/api-key/api-key.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ApiKeyComponent } from './api-key.component'; 4 | 5 | describe('ApiKeyComponent', () => { 6 | let component: ApiKeyComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ApiKeyComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ApiKeyComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/user/edit/api-key/api-key.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {ServerService} from "../../../../services/server.service"; 3 | 4 | @Component({ 5 | selector: 'app-api-key', 6 | templateUrl: './api-key.component.html', 7 | styleUrls: ['./api-key.component.scss'] 8 | }) 9 | export class ApiKeyComponent implements OnInit { 10 | 11 | displayedColumns: string[] = ['id', 'key', 'created_at', 'delete']; 12 | dataSource = []; 13 | 14 | constructor(private serverService: ServerService 15 | ) { } 16 | 17 | ngOnInit(): void { 18 | 19 | 20 | this.serverService.getAPIKeys().subscribe((apiKeys: Array) => { 21 | this.dataSource = [...apiKeys] 22 | 23 | console.log(this.dataSource) 24 | }) 25 | } 26 | 27 | deleteAPIKey(elem){ 28 | let idx = this.dataSource.indexOf(elem); 29 | this.serverService.deleteAPIKey(elem.id).subscribe(x => { 30 | this.dataSource.splice(idx, 1); 31 | this.dataSource = [...this.dataSource] 32 | }) 33 | } 34 | 35 | createAPIKey(){ 36 | 37 | this.serverService.addAPIKey().subscribe(key => { 38 | this.dataSource.push(key) 39 | this.dataSource = [...this.dataSource] 40 | 41 | }) 42 | 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/user/edit/edit.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Edit User 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Full Name 15 | 16 | 17 | 18 | 19 | 20 | 21 | Username 22 | 23 | 24 | 25 | 26 | 27 | 28 | E-Mail 29 | 30 | 31 | 32 | 33 | 34 | 35 | Password 36 | 37 | 38 | 39 | 40 | 41 | Edit User 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/user/edit/edit.component.scss: -------------------------------------------------------------------------------- 1 | .user-edit-component{ 2 | padding: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/user/edit/edit.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormControl, FormGroup, Validators } from '@angular/forms'; 3 | import { AuthService } from '@services/*'; 4 | import { Router } from '@angular/router'; 5 | import {ServerService} from "../../../services/server.service"; 6 | 7 | @Component({ 8 | selector: 'app-edit', 9 | templateUrl: './edit.component.html', 10 | styleUrls: ['./edit.component.scss'], 11 | }) 12 | export class EditComponent implements OnInit { 13 | 14 | public editForm: FormGroup = new FormGroup({ 15 | full_name: new FormControl(''), 16 | password: new FormControl('', Validators.required), 17 | email: new FormControl('', [ 18 | Validators.required, 19 | Validators.pattern('^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$'), 20 | Validators.maxLength(20), 21 | ]), 22 | username: new FormControl('', [Validators.required, Validators.maxLength(20)]), 23 | }); 24 | public user: any; 25 | public error: string; 26 | 27 | constructor( 28 | private authService: AuthService, 29 | private router: Router 30 | ) { 31 | 32 | } 33 | 34 | public ngOnInit() { 35 | this.user = this.authService.user; 36 | 37 | this.editForm.setValue({ 38 | full_name: this.user.full_name, 39 | password: '', 40 | email: this.user.email, 41 | username: this.user.username, 42 | }); 43 | } 44 | 45 | public edit() { 46 | if (this.editForm.valid) { 47 | this.authService.edit(this.editForm.getRawValue()) 48 | .subscribe(res => this.router.navigate(['/app/dashboard']), 49 | error => this.error = error.message); 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/user/login/login.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Authenticate to Wireguard Management 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Username 15 | 16 | 17 | 18 | 19 | 20 | 21 | Password 22 | 23 | 24 | 25 | 26 | 27 | 28 | SIGN IN 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/user/login/login.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/app/page/user/login/login.component.scss -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/user/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginComponent } from './login.component'; 4 | 5 | describe('LoginComponent', () => { 6 | let component: LoginComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ LoginComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LoginComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/page/user/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewEncapsulation } from '@angular/core'; 2 | import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; 3 | import { AuthService } from '@services/*'; 4 | import { Router } from '@angular/router'; 5 | 6 | @Component({ 7 | selector: 'app-login', 8 | templateUrl: './login.component.html', 9 | encapsulation: ViewEncapsulation.None, 10 | styleUrls: ['./login.component.scss'], 11 | }) 12 | export class LoginComponent implements OnInit { 13 | 14 | public loginForm: FormGroup; 15 | public username; 16 | public password; 17 | public error: string; 18 | 19 | constructor(private authService: AuthService, 20 | private fb: FormBuilder, 21 | private router: Router) { 22 | 23 | this.loginForm = this.fb.group({ 24 | password: new FormControl('', Validators.required), 25 | username: new FormControl('', [ 26 | Validators.required, 27 | ]), 28 | }); 29 | this.username = this.loginForm.get('username'); 30 | this.password = this.loginForm.get('password'); 31 | } 32 | 33 | public ngOnInit() { 34 | 35 | this.loginForm.valueChanges.subscribe(() => { 36 | this.error = null; 37 | }); 38 | } 39 | 40 | public login() { 41 | this.error = null; 42 | if (this.loginForm.valid) { 43 | this.authService.login(this.loginForm.getRawValue()) 44 | .subscribe(res => this.router.navigate(['/page/dashboard']), 45 | error => this.error = error.message); 46 | } 47 | } 48 | 49 | public onInputChange(event) { 50 | event.target.required = true; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/services/auth/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router'; 3 | 4 | import { AuthService } from './auth.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class AuthGuard implements CanActivate { 10 | constructor(private authService: AuthService, 11 | private router: Router) { 12 | } 13 | 14 | canActivate(next: ActivatedRouteSnapshot): boolean { 15 | if (this.authService.isLoggedIn) { 16 | return true; 17 | } 18 | // Navigate to the login page with extras 19 | this.router.navigate(['page/user/login']); 20 | return false; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/services/auth/auth.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { 5 | HttpErrorResponse, 6 | HttpEvent, 7 | HttpHandler, 8 | HttpInterceptor, 9 | HttpRequest, 10 | } from '@angular/common/http'; 11 | 12 | import { AuthService } from './auth.service'; 13 | import { tap } from 'rxjs/operators'; 14 | import { Router } from '@angular/router'; 15 | 16 | @Injectable() 17 | export class AuthInterceptor implements HttpInterceptor { 18 | constructor(private auth: AuthService, private router: Router) {} 19 | 20 | public intercept(request: HttpRequest, next: HttpHandler): Observable> { 21 | // add authorization token for full api requests 22 | if (request.url.includes('api') && this.auth.isLoggedIn) { 23 | request = request.clone({ 24 | setHeaders: { Authorization: `Bearer ${this.auth.user.access_token}` }, 25 | }); 26 | } 27 | 28 | return next.handle(request).pipe(tap(() => {}, 29 | (err: any) => { 30 | if (err instanceof HttpErrorResponse) { 31 | if (err.status !== 401 && err.status !== 403) { 32 | return; 33 | } 34 | 35 | this.auth.clearData(); 36 | this.router.navigate(['/page/user/login']); 37 | } 38 | })); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/services/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { BehaviorSubject, Observable, of } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | 6 | import { environment } from '../../../environments/environment'; 7 | import { User } from '../../interfaces/user'; 8 | import { Router } from '@angular/router'; 9 | 10 | const tokenName = 'token'; 11 | 12 | @Injectable({ 13 | providedIn: 'root', 14 | }) 15 | export class AuthService { 16 | 17 | public user: User = null; 18 | private url = `${environment.apiBaseUrl}/api/v1`; 19 | 20 | constructor(private http: HttpClient, private router: Router) {} 21 | 22 | public get isLoggedIn(): boolean { 23 | return !!this.user?.access_token; 24 | } 25 | 26 | public login(data): Observable { 27 | // Create form 28 | const formData: FormData = new FormData(); 29 | formData.append('username', data.username); 30 | formData.append('password', data.password); 31 | 32 | return this.http.post(`${this.url}/login`, formData) 33 | .pipe( 34 | map((res: any) => { 35 | this._handleUser(res); 36 | })); 37 | } 38 | 39 | public edit(formData: any) { 40 | return this.http.post(`${this.url}/user/edit`, formData) 41 | .pipe(map((res: any) => { 42 | this._handleUser(res); 43 | })); 44 | } 45 | 46 | _handleUser(res: any) { 47 | const user: any = res.user; 48 | user.access_token = res.access_token; 49 | user.token_type = res.token_type; 50 | localStorage.setItem('session', JSON.stringify(user)); 51 | this.init(); 52 | } 53 | 54 | public logout() { 55 | return this.http.get(`${this.url}/logout`) 56 | .pipe(map((data) => { 57 | this.clearData(); 58 | this.router.navigate(['/page/user/login']); 59 | return of(false); 60 | })); 61 | } 62 | 63 | public clearData() { 64 | this.user = null; 65 | localStorage.clear(); 66 | 67 | } 68 | 69 | public get authToken(): string { 70 | return localStorage.getItem(tokenName); 71 | } 72 | 73 | public init() { 74 | this.user = JSON.parse(localStorage.getItem('session')); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/services/auth/index.ts: -------------------------------------------------------------------------------- 1 | export { AuthGuard } from './auth.guard'; 2 | export { AuthService } from './auth.service'; 3 | export { AuthInterceptor } from './auth.interceptor'; 4 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/services/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpErrorResponse } from '@angular/common/http'; 3 | import { throwError } from 'rxjs'; 4 | import {NotifierService} from "angular-notifier"; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class ConfigService { 10 | 11 | public applicationName = 'WireGuard Manager'; 12 | 13 | constructor(private notify: NotifierService) { 14 | } 15 | 16 | public handleError(error: HttpErrorResponse) { 17 | if (error.error instanceof ErrorEvent) { 18 | // A client-side or network error occurred. Handle it accordingly. 19 | console.error('An error occurred:', error.error.message); 20 | this.notify.notify("error", error.error.message) 21 | } else { 22 | // The backend returned an unsuccessful response code. 23 | // The response body may contain clues as to what went wrong, 24 | console.error( 25 | `Backend returned code ${error.status}, ` + 26 | `body was: ${error.error}`); 27 | } 28 | this.notify.notify("error", JSON.stringify(error.error.detail)) 29 | // return an observable with a user-facing error message 30 | return throwError('Something bad happened; please try again later.'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/services/data.service.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class DataService { 8 | 9 | _observables: any = {}; 10 | constructor() {} 11 | 12 | emit(event: string, value: any): void { 13 | if (this._observables.hasOwnProperty(event)) { 14 | this._observables[event].emit(value); 15 | } 16 | } 17 | 18 | on(event: string): Observable { 19 | if (!this._observables.hasOwnProperty(event)) { 20 | this._observables[event] = new EventEmitter(); 21 | } 22 | 23 | return this._observables[event].asObservable(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth'; 2 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/services/server.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ConfigService } from './config.service'; 3 | import { HttpClient } from '@angular/common/http'; 4 | 5 | import { catchError } from 'rxjs/operators'; 6 | import { Server } from '../interfaces/server'; 7 | import { Peer } from '../interfaces/peer'; 8 | import { Observable, Subscribable } from 'rxjs'; 9 | import {NotifierService} from "angular-notifier"; 10 | 11 | @Injectable({ 12 | providedIn: 'root', 13 | }) 14 | export class ServerService { 15 | public base = '/api/v1/'; 16 | public serverURL = this.base + "server"; 17 | public peerURL = this.base + "peer"; 18 | public wgURL = this.base + "wg"; 19 | public userURL = this.base + "users"; 20 | public apiKeyURL = this.userURL + "/api-key" 21 | 22 | 23 | 24 | constructor(private config: ConfigService, private http: HttpClient, private notify: NotifierService) { 25 | 26 | } 27 | 28 | public deletePeer(peer: Peer): Subscribable { 29 | return this.http.post(this.peerURL + '/delete', peer); 30 | } 31 | 32 | public serverPerformAction(action: string, item: any): Subscribable { 33 | return this.http.post(this.serverURL + '/' + action, item) 34 | .pipe(catchError(this.config.handleError.bind(this))); 35 | } 36 | 37 | public addPeer(server_interface: any): Subscribable { 38 | return this.http.post(this.peerURL + '/add', server_interface) 39 | .pipe(catchError(this.config.handleError.bind(this))); 40 | } 41 | 42 | public editPeer(peer: Peer): Subscribable { 43 | return this.http.post(this.peerURL + '/edit', peer) 44 | .pipe(catchError(this.config.handleError.bind(this))); 45 | } 46 | 47 | public getServers(): Observable { 48 | return this.http.get(this.serverURL + '/all') 49 | .pipe(catchError(this.config.handleError.bind(this))); 50 | } 51 | 52 | public addServer(item: Server): Subscribable { 53 | return this.http.post(this.serverURL + '/add', item) 54 | .pipe(catchError(this.config.handleError.bind(this))); 55 | } 56 | 57 | public startServer(item: Server): Subscribable { 58 | return this.serverPerformAction('start', item) 59 | } 60 | 61 | public stopServer(item: Server): Subscribable { 62 | return this.serverPerformAction('stop', item); 63 | } 64 | 65 | public restartServer(item: Server): Subscribable { 66 | return this.serverPerformAction('restart', item); 67 | } 68 | 69 | public deleteServer(item: Server): Subscribable { 70 | return this.serverPerformAction('delete', item); 71 | } 72 | 73 | public editServer(oldServer: Server, newServer: Server): Subscribable { 74 | return this.serverPerformAction('edit', { 75 | interface: oldServer.interface, 76 | server: newServer, 77 | }); 78 | } 79 | 80 | public getKeyPair() { 81 | return this.http.get(this.wgURL + '/generate_keypair') 82 | .pipe(catchError(this.config.handleError.bind(this))); 83 | } 84 | 85 | public getPSK() { 86 | return this.http.get(this.wgURL + '/generate_psk') 87 | .pipe(catchError(this.config.handleError.bind(this))); 88 | } 89 | 90 | public peerConfig(peer: Peer) { 91 | return this.http.post(this.peerURL + '/config', peer) 92 | .pipe(catchError(this.config.handleError.bind(this))); 93 | } 94 | 95 | public serverConfig(server: Server): Subscribable { 96 | return this.http.get(this.serverURL + '/config/' + server.id.toString()) 97 | .pipe(catchError(this.config.handleError.bind(this))); 98 | } 99 | 100 | public serverStats(server: Server) { 101 | return this.http.post(this.serverURL + '/stats', server) 102 | .pipe(catchError(this.config.handleError.bind(this))); 103 | } 104 | 105 | public addAPIKey() { 106 | return this.http.get(this.apiKeyURL + '/add') 107 | .pipe(catchError(this.config.handleError.bind(this))); 108 | } 109 | 110 | public getAPIKeys() { 111 | return this.http.get(this.apiKeyURL + '/list') 112 | .pipe(catchError(this.config.handleError.bind(this))); 113 | } 114 | 115 | public deleteAPIKey(api_key_id: { id: number }) { 116 | return this.http.post(this.apiKeyURL + '/delete', { 117 | key_id: api_key_id 118 | }) 119 | .pipe(catchError(this.config.handleError.bind(this))); 120 | 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/validators/ip-address.validator.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl, ValidationErrors } from '@angular/forms'; 2 | import * as IPCIDR from 'ip-cidr'; 3 | import {Address4, Address6} from 'ip-address' 4 | 5 | export class IPValidator { 6 | 7 | static isIPAddress(control: AbstractControl): ValidationErrors | null { 8 | if (Address4.isValid(control.value) || Address6.isValid(control.value)) 9 | return null; 10 | else 11 | return { validIP: true }; 12 | } 13 | 14 | 15 | 16 | } 17 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/app/validators/number.validator.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl, ValidationErrors } from '@angular/forms'; 2 | import * as IPCIDR from 'ip-cidr'; 3 | 4 | export class NumberValidator { 5 | 6 | static stringIsNumber(control: AbstractControl): ValidationErrors | null { 7 | if (isNaN(control.value)) { 8 | return { validNumber: true }; 9 | } 10 | return null; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/.gitkeep -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Black-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Black-Italic.ttf -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Black-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Black-Italic.woff -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Black-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Black-Italic.woff2 -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Black.ttf -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Black.woff -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Black.woff2 -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Bold-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Bold-Italic.ttf -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Bold-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Bold-Italic.woff -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Bold-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Bold-Italic.woff2 -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Bold.ttf -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Bold.woff -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Bold.woff2 -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Italic.ttf -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Italic.woff -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Italic.woff2 -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Light-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Light-Italic.ttf -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Light-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Light-Italic.woff -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Light-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Light-Italic.woff2 -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Light.ttf -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Light.woff -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Light.woff2 -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Thin-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Thin-Italic.ttf -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Thin-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Thin-Italic.woff -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Thin-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Thin-Italic.woff2 -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Thin.ttf -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Thin.woff -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto-Thin.woff2 -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto.eot -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto.svg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto.svg -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto.ttf -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto.woff -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/fonts/Roboto/Roboto.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/fonts/Roboto/Roboto.woff2 -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/icons/favicon-16x16.png -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/icons/favicon-32x32.png -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/Bobby.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/images/Bobby.PNG -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/DB_16х16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/images/DB_16х16.png -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/Dark_background_1920x1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/images/Dark_background_1920x1080.png -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/images/Icon.png -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/Icon_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/images/Icon_header.png -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/active_marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/images/active_marker.png -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/buffer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/images/card.jpg -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/cloudy_and_snow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/cotoneaster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/images/cotoneaster.jpg -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/gina-torres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/images/gina-torres.png -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/imgo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/images/imgo.png -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/istanbul.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/images/istanbul.jpg -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/map_target_images_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/images/map_target_images_sprite.png -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/marker-advanced-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Circle 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/marker-advanced.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Circle 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/images/marker.png -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/nathan-fillion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/images/nathan-fillion.png -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/robot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/images/robot.png -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/sao_paulo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/images/sao_paulo.jpg -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/skype.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 17 | 18 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/tick-mask.svg: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 16 | 17 | 18 | 21 | 22 | 23 | 30 | 31 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/tick.svg: -------------------------------------------------------------------------------- 1 | 2 | 11 | 15 | 16 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/tick_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 11 | 15 | 16 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/tokyo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/images/tokyo.jpg -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/tudyk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/images/tudyk.png -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/watch_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/assets/images/weather_bck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perara/wg-manager/e70ba4bdc288318bc164fe696750a0c40ed1fac6/wg-manager-frontend/src/assets/images/weather_bck.png -------------------------------------------------------------------------------- /wg-manager-frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | apiBaseUrl: '', 4 | }; 5 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false, 8 | apiBaseUrl: '', 9 | }; 10 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Wireguard Manager 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode, ViewEncapsulation } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app'; 5 | import { environment } from './environments/environment'; 6 | import 'hammerjs'; 7 | 8 | (function () { 9 | if (typeof EventTarget !== 'undefined') { 10 | const func = EventTarget.prototype.addEventListener; 11 | EventTarget.prototype.addEventListener = function (type, fn, capture) { 12 | this.func = func; 13 | if (typeof capture !== 'boolean') { 14 | capture = capture || {}; 15 | capture.passive = false; 16 | } 17 | this.func(type, fn, capture); 18 | }; 19 | } 20 | }()); 21 | if (environment.production) { 22 | enableProdMode(); 23 | } 24 | 25 | platformBrowserDynamic().bootstrapModule(AppModule, { defaultEncapsulation: ViewEncapsulation.None }); 26 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************************** 2 | * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates. 3 | */ 4 | import '@angular/localize/init'; 5 | /** 6 | * This file includes polyfills needed by Angular and is loaded before the app. 7 | * You can add your own extra polyfills to this file. 8 | * 9 | * This file is divided into 2 sections: 10 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 11 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 12 | * file. 13 | * 14 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 15 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 16 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 17 | * 18 | * Learn more in https://angular.io/guide/browser-support 19 | */ 20 | 21 | /*************************************************************************************************** 22 | * BROWSER POLYFILLS 23 | */ 24 | 25 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 26 | import 'classlist.js'; // Run `npm install --save classlist.js`. 27 | 28 | /** IE10 and IE11 requires the following for the Reflect API. */ 29 | // import 'core-js/es6/reflect'; 30 | 31 | /** 32 | * Evergreen browsers require these. 33 | */ 34 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 35 | // import 'core-js/es7/reflect'; 36 | 37 | /** 38 | * Required to support Web Animations `@angular/platform-browser/animations`. 39 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 40 | */ 41 | import 'web-animations-js'; // Run `npm install --save web-animations-js`. 42 | 43 | /*************************************************************************************************** 44 | * Zone JS is required by default for Angular itself. 45 | */ 46 | import 'zone.js/dist/zone'; // Included with Angular CLI. 47 | 48 | /*************************************************************************************************** 49 | * APPLICATION IMPORTS 50 | */ 51 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/theme/fonts/font-roboto.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Roboto'; 3 | font-style: italic; 4 | font-weight: 100; 5 | src: local('Roboto Thin Italic'), local('Roboto-ThinItalic'), url(assets/fonts/Roboto/Roboto-Thin-Italic.ttf) format('truetype'), url(assets/fonts/Roboto/Roboto-Thin-Italic.woff2) format('woff2'), url(assets/fonts/Roboto/Roboto-Thin-Italic.woff) format('woff'); 6 | } 7 | 8 | @font-face { 9 | font-family: 'Roboto'; 10 | font-style: italic; 11 | font-weight: 300; 12 | src: local('Roboto Light Italic'), local('Roboto-LightItalic'), url(assets/fonts/Roboto/Roboto-Light-Italic.ttf) format('truetype'), url(assets/fonts/Roboto/Roboto-Light-Italic.woff2) format('woff2'), url(assets/fonts/Roboto/Roboto-Light-Italic.woff) format('woff'); 13 | } 14 | 15 | @font-face { 16 | font-family: 'Roboto'; 17 | font-style: italic; 18 | font-weight: 400; 19 | src: local('Roboto Italic'), local('Roboto-Italic'), url(assets/fonts/Roboto/Roboto-Italic.ttf) format('truetype'), url(assets/fonts/Roboto/Roboto-Italic.woff2) format('woff2'), url(assets/fonts/Roboto/Roboto-Italic.woff) format('woff'); 20 | } 21 | 22 | @font-face { 23 | font-family: 'Roboto'; 24 | font-style: italic; 25 | font-weight: 700; 26 | src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'), url(assets/fonts/Roboto/Roboto-Bold-Italic.ttf) format('truetype'), url(assets/fonts/Roboto/Roboto-Bold-Italic.woff2) format('woff2'), url(assets/fonts/Roboto/Roboto-Bold-Italic.woff) format('woff'); 27 | } 28 | 29 | @font-face { 30 | font-family: 'Roboto'; 31 | font-style: italic; 32 | font-weight: 900; 33 | src: local('Roboto Black Italic'), local('Roboto-BlackItalic'), url(assets/fonts/Roboto/Roboto-Black-Italic.ttf) format('truetype'), url(assets/fonts/Roboto/Roboto-Black-Italic.woff2) format('woff2'), url(assets/fonts/Roboto/Roboto-Black-Italic.woff) format('woff'); 34 | } 35 | 36 | @font-face { 37 | font-family: 'Roboto'; 38 | font-style: normal; 39 | font-weight: 100; 40 | src: local('Roboto Thin'), local('Roboto-Thin'), url(assets/fonts/Roboto/Roboto-Thin.ttf) format('truetype'), url(assets/fonts/Roboto/Roboto-Thin.woff2) format('woff2'), url(assets/fonts/Roboto/Roboto-Thin.woff) format('woff'); 41 | } 42 | 43 | @font-face { 44 | font-family: 'Roboto'; 45 | font-style: normal; 46 | font-weight: 300; 47 | src: local('Roboto Light'), local('Roboto-Light'), url(assets/fonts/Roboto/Roboto-Light.ttf) format('truetype'), url(assets/fonts/Roboto/Roboto-Light.woff2) format('woff2'), url(assets/fonts/Roboto/Roboto-Light.woff) format('woff'); 48 | } 49 | 50 | @font-face { 51 | font-family: 'Roboto'; 52 | font-style: normal; 53 | font-weight: 400; 54 | src: url(assets/fonts/Roboto/Roboto.eot); 55 | src: local('Roboto'), local('Roboto-Regular'), url(assets/fonts/Roboto/Roboto.ttf) format('truetype'), url(assets/fonts/Roboto/Roboto.eot?#iefix) format('embedded-opentype'), url(assets/fonts/Roboto/Roboto.svg#Roboto) format('svg'), url(assets/fonts/Roboto/Roboto.woff2) format('woff2'), url(assets/fonts/Roboto/Roboto.woff) format('woff'); 56 | } 57 | 58 | @font-face { 59 | font-family: 'Roboto'; 60 | font-style: normal; 61 | font-weight: 700; 62 | src: local('Roboto Bold'), local('Roboto-Bold'), url(assets/fonts/Roboto/Roboto-Bold.ttf) format('truetype'), url(assets/fonts/Roboto/Roboto-Bold.woff2) format('woff2'), url(assets/fonts/Roboto/Roboto-Bold.woff) format('woff'); 63 | } 64 | 65 | @font-face { 66 | font-family: 'Roboto'; 67 | font-style: normal; 68 | font-weight: 900; 69 | src: local('Roboto Black'), local('Roboto-Black'), url(assets/fonts/Roboto/Roboto-Black.ttf) format('truetype'), url(assets/fonts/Roboto/Roboto-Black.woff2) format('woff2'), url(assets/fonts/Roboto/Roboto-Black.woff) format('woff'); 70 | } 71 | 72 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/theme/index.ts: -------------------------------------------------------------------------------- 1 | export { ThemeModule } from './theme.module'; 2 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/theme/styles.scss: -------------------------------------------------------------------------------- 1 | @import "./fonts/font-roboto.css"; 2 | $material-icons-font-path: '~material-icons/iconfont/'; 3 | @import '~material-icons/iconfont/material-icons.scss'; 4 | 5 | @import "~angular-notifier/styles"; 6 | @import '~@angular/material/theming'; 7 | 8 | @include mat-core(); 9 | 10 | // Add your desired themes to this map. 11 | $themes-map: ( 12 | indigo-pink: ( 13 | primary-base: $mat-indigo, 14 | accent-base: $mat-pink, 15 | ), 16 | 17 | deeppurple-amber: ( 18 | primary-base: $mat-deep-purple, 19 | accent-base: $mat-amber, 20 | ), 21 | 22 | pink-bluegrey: ( 23 | primary-base: $mat-pink, 24 | accent-base: $mat-blue-gray, 25 | ), 26 | 27 | purple-green: ( 28 | primary-base: $mat-purple, 29 | accent-base: $mat-green, 30 | ), 31 | ); 32 | 33 | 34 | // Import the module and do the job: 35 | @import '~angular-material-dynamic-themes/themes-core'; 36 | @include make-stylesheets($themes-map); 37 | 38 | //html, body { height: 100%; } 39 | //body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 40 | html, 41 | body { 42 | box-sizing: border-box; 43 | height: 100%; 44 | margin: 0; 45 | } 46 | 47 | .full-width { 48 | width: 100%; 49 | } 50 | 51 | .app-material-icon-valign { 52 | display: inline-flex; 53 | vertical-align: middle; 54 | } 55 | 56 | .mat-card-header-text{ 57 | width: 100% !important; 58 | } 59 | 60 | .button-row button, 61 | .button-row a{ 62 | margin-right: 8px; 63 | } 64 | 65 | .card-container-right{ 66 | display: inline; 67 | float: right; 68 | } 69 | 70 | .card-container-left{ 71 | display: inline !important; 72 | } 73 | 74 | .green{ 75 | color:green;; 76 | 77 | } 78 | .red{ 79 | color:red; 80 | } 81 | 82 | .mat-form-field{ 83 | // TODO this is a sleezy fix for cutting text in input boxes. need to figure out the root cause eventually. 84 | line-height: 1.5; 85 | } 86 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/theme/theme.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | 5 | const BASE_COMPONENTS = [ 6 | 7 | ]; 8 | 9 | const BASE_DIRECTIVES = []; 10 | 11 | const BASE_PIPES = []; 12 | 13 | @NgModule({ 14 | declarations: [ 15 | ...BASE_PIPES, 16 | ...BASE_DIRECTIVES, 17 | ...BASE_COMPONENTS, 18 | ], 19 | imports: [ 20 | CommonModule, 21 | FormsModule, 22 | ReactiveFormsModule, 23 | 24 | ], 25 | exports: [ 26 | ...BASE_PIPES, 27 | ...BASE_DIRECTIVES, 28 | ...BASE_COMPONENTS, 29 | 30 | ], 31 | }) 32 | export class ThemeModule { } 33 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "main.ts", 10 | "polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /wg-manager-frontend/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /wg-manager-frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./src", 5 | "module": "es2020", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "downlevelIteration": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2017", 19 | "dom", 20 | "es2015.generator", 21 | "es2015.iterable" 22 | ], 23 | "paths": { 24 | "@components/*": ["app/components"], 25 | "@services/*": ["app/services/"] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /wg-manager-frontend/widdershins.json: -------------------------------------------------------------------------------- 1 | { 2 | "language_tabs": [{ "python": "Python" }] 3 | } 4 | --------------------------------------------------------------------------------
29 | 30 | DNS 31 | 32 | 33 |
37 | 38 | Allowed IPs 39 | 40 | 41 |
44 | 45 | PersistentKeepalive interval 46 | 47 | 48 |
Keys
53 | 54 | Private-Key 55 | 56 | 57 |
60 | 61 | Public-Key 62 | 63 | 64 |
83 | 84 |
13 | 14 | Full Name 15 | 16 | 17 |
20 | 21 | Username 22 | 23 | 24 |
27 | 28 | E-Mail 29 | 30 | 31 |
34 | 35 | Password 36 | 37 | 38 |
13 | 14 | Username 15 | 16 | 17 |
20 | 21 | Password 22 | 23 | 24 |