├── misc └── requirements.txt ├── temp └── gennedkeys.txt ├── licenses.db ├── static ├── favicon.ico ├── images │ ├── users.png │ └── licenses.png ├── js │ ├── redirect.js │ ├── dashboard.js │ ├── admindashboard.js │ └── tablefunction.js └── styles │ ├── home.css │ ├── login.css │ ├── dashboard.css │ ├── dashboardaccount.css │ ├── admindashboard.css │ ├── adminusers.css │ ├── adminlicenses.css │ ├── admindocs.css │ └── adminplans.css ├── config.json ├── graphinfo.csv ├── templates ├── redirect.html ├── login.html ├── home.html ├── adminusers.html ├── signup.html ├── admindash.html ├── dashboardaccount.html ├── adminplans.html ├── admindocs.html ├── adminlicenses.html └── dashboard.html ├── README.md ├── monitor.py ├── examplerequests.py ├── utils.py ├── mergesort.js ├── api.py └── main.py /misc/requirements.txt: -------------------------------------------------------------------------------- 1 | python3 2 | pip3 install flask 3 | -------------------------------------------------------------------------------- /temp/gennedkeys.txt: -------------------------------------------------------------------------------- 1 | deqf01iffxpp5nom 2 | umbhl9avs2dph00d 3 | o659vk2anyoinjk7 4 | -------------------------------------------------------------------------------- /licenses.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntax/saas-key-licensing-system/main/licenses.db -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntax/saas-key-licensing-system/main/static/favicon.ico -------------------------------------------------------------------------------- /static/images/users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntax/saas-key-licensing-system/main/static/images/users.png -------------------------------------------------------------------------------- /static/images/licenses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntax/saas-key-licensing-system/main/static/images/licenses.png -------------------------------------------------------------------------------- /static/js/redirect.js: -------------------------------------------------------------------------------- 1 | function redirectFunct() { 2 | location.replace("http://127.0.0.1:5000/login") 3 | } 4 | 5 | setTimeout(function(){ redirectFunct(); }, 3000); -------------------------------------------------------------------------------- /static/styles/home.css: -------------------------------------------------------------------------------- 1 | body,html{ 2 | height:100%; 3 | } 4 | 5 | .h-95{ 6 | height:95%; 7 | } 8 | .h-60{ 9 | height:60%; 10 | } 11 | 12 | .h-05{ 13 | height:5%; 14 | } -------------------------------------------------------------------------------- /static/styles/login.css: -------------------------------------------------------------------------------- 1 | body,html{ 2 | height:100%; 3 | } 4 | 5 | .h-90{ 6 | height:90%; 7 | } 8 | 9 | .h-80{ 10 | height:80%; 11 | } 12 | 13 | .h-10{ 14 | height:10%; 15 | } -------------------------------------------------------------------------------- /static/styles/dashboard.css: -------------------------------------------------------------------------------- 1 | body,html{ 2 | height:100%; 3 | } 4 | 5 | .h-90{ 6 | height:90%; 7 | } 8 | 9 | .h-80{ 10 | height:80%; 11 | } 12 | 13 | .h-10{ 14 | height:10%; 15 | } -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "UPLOAD_DIRECTORY_TEMP": "/Users/tomholland/OneDrive - Hampton School/mbp/coding/NEA/NEA-PROJECT/temp", 3 | "UPLOAD_DIRECTORY_MAIN": "/Users/tomholland/OneDrive - Hampton School/mbp/coding/NEA/NEA-PROJECT", 4 | "_comment": "DO NOT MANUALLY EDIT API KEY, REGEN THROUGH UTILS.PY FUNCTION", 5 | "api_key": "test" 6 | } -------------------------------------------------------------------------------- /graphinfo.csv: -------------------------------------------------------------------------------- 1 | date,Number of Licenses,Number of Users 2 | 19/03/2021,3,7 3 | 20/03/2021,2,4 4 | 21/03/2021,3,7 5 | 22/03/2021,2,4 6 | 23/03/2021,3,7 7 | 24/03/2021,2,4 8 | 25/03/2021,3,7 9 | 26/03/2021,2,4 10 | 27/03/2021,3,7 11 | 28/03/2021,4,3 12 | 29/03/2021,2,4 13 | 30/03/2021,3,7 14 | 31/03/2021,4,3 15 | 01/04/2021,5,6 16 | 02/04/2021,6,7 17 | 03/04/2021,27,3 18 | 04/04/2021,32,3 19 | 05/04/2021,32,3 20 | 06/04/2021,32,4 21 | 07/04/2021,25,3 22 | 08/04/2021,15,4 23 | 11/04/2021,15,4 24 | 12/04/2021,14,5 25 | 13/04/2021,16,5 -------------------------------------------------------------------------------- /static/styles/dashboardaccount.css: -------------------------------------------------------------------------------- 1 | body,html{ 2 | height:100%; 3 | } 4 | 5 | a:link { 6 | color: black; 7 | background-color: transparent; 8 | text-decoration: none; 9 | } 10 | 11 | a:visited { 12 | color: black; 13 | background-color: transparent; 14 | text-decoration: none; 15 | } 16 | 17 | a:hover { 18 | color: black; 19 | font-weight: bold; 20 | background-color: transparent; 21 | text-decoration: none; 22 | } 23 | 24 | a:active { 25 | color: black; 26 | background-color: transparent; 27 | text-decoration: none; 28 | } 29 | 30 | .h-90{ 31 | height:90%; 32 | } 33 | 34 | .h-80{ 35 | height:80%; 36 | } 37 | 38 | .h-10{ 39 | height:10%; 40 | } -------------------------------------------------------------------------------- /static/js/dashboard.js: -------------------------------------------------------------------------------- 1 | function relayUserTime(user){ 2 | // collects client side time as to show a representitive welcome message 3 | var time = new Date().getHours(); 4 | if (0<=time && time<12) { 5 | document.getElementById("time").innerHTML = "Good morning " + user + " !" ; 6 | } else if (12<= time && time<17) { 7 | document.getElementById("time").innerHTML = "Good afternoon " + user + " !" ; 8 | } else { 9 | document.getElementById("time").innerHTML = "Good evening " + user + " !" ; 10 | } 11 | } 12 | 13 | // ajax functions 14 | 15 | function unbind() { 16 | $.get('/unbindaccount'); 17 | setTimeout(function(){ location.reload(); }, 100); 18 | } 19 | function rescramble() { 20 | $.get('/rescramblelicense'); 21 | setTimeout(function(){ location.reload(); }, 100); 22 | } 23 | function unbinddevice() { 24 | $.get('/unbinddevice'); 25 | setTimeout(function(){ location.reload(); }, 100); 26 | } -------------------------------------------------------------------------------- /static/js/admindashboard.js: -------------------------------------------------------------------------------- 1 | // following selections set sidebar to be open or closed in correspodnging with localstorage (from last page visited) 2 | 3 | if (typeof(Storage) !== "undefined") { 4 | if(localStorage.getItem("sidebar") == "opened"){ 5 | document.getElementById("mySidenav").style.width = "250px"; 6 | document.getElementById("main").style.marginLeft = "250px"; 7 | } 8 | } 9 | function openNav() { 10 | // function for opening navigation panel, commits to local storage after change 11 | document.getElementById("mySidenav").style.width = "250px"; 12 | document.getElementById("main").style.marginLeft = "250px"; 13 | if (typeof(Storage) !== "undefined") { 14 | localStorage.setItem("sidebar", "opened"); 15 | } 16 | } 17 | function closeNav() { 18 | // function for closing navigation panel, commits to local storage after it has been changed 19 | document.getElementById("mySidenav").style.width = "0"; 20 | document.getElementById("main").style.marginLeft = "0"; 21 | if (typeof(Storage) !== "undefined") { 22 | localStorage.setItem("sidebar", "closed"); 23 | } 24 | } -------------------------------------------------------------------------------- /templates/redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Login 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 |
22 |

{{ reason}}

23 |

Redirecting! Please hold tight.

24 |
25 |
26 |
27 | 28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | SaaS distrubution system by Tom Holland :) 36 |
37 |
38 |
39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /static/styles/admindashboard.css: -------------------------------------------------------------------------------- 1 | body,html{ 2 | height:100%; 3 | } 4 | 5 | .h-90{ 6 | height:90%; 7 | } 8 | 9 | .h-80{ 10 | height:80%; 11 | } 12 | 13 | .h-40{ 14 | height:40%; 15 | } 16 | 17 | .h-20{ 18 | height:20%; 19 | } 20 | 21 | .h-10{ 22 | height:10%; 23 | } 24 | 25 | .h-5{ 26 | height:5%; 27 | } 28 | 29 | .h-95{ 30 | height:95%; 31 | } 32 | 33 | .sidenav { 34 | height: 100%; 35 | width: 0; 36 | position: fixed; 37 | z-index: 1; 38 | top: 0; 39 | left: 0; 40 | background-color: #111; 41 | overflow-x: hidden; 42 | padding-top: 60px; 43 | transition: 0.5s; 44 | } 45 | 46 | .sidenav a { 47 | padding: 8px 8px 8px 32px; 48 | text-decoration: none; 49 | font-size: 25px; 50 | color: #818181; 51 | display: block; 52 | transition: 0.3s; 53 | } 54 | 55 | .sidenav a:hover { 56 | color: #f1f1f1; 57 | } 58 | 59 | .sidenav .closebtn { 60 | position: absolute; 61 | top: 0; 62 | right: 25px; 63 | font-size: 36px; 64 | margin-left: 50px; 65 | } 66 | 67 | .footer { 68 | color: #f1f1f1; 69 | padding: 8px 8px 8px 32px; 70 | white-space: nowrap; 71 | } 72 | 73 | .navbarbutton { 74 | font-size: 150%; 75 | color: #E5D4C0; 76 | position: fixed; 77 | } 78 | 79 | .navbarbutton a { 80 | padding: 8px 8px 8px 32px; 81 | color: #818181; 82 | transition: 0.3s; 83 | } 84 | 85 | .navbarbutton a:hover { 86 | text-decoration: none; 87 | color: #f1f1f1 !important; 88 | } 89 | 90 | .navbarbutton a:visited { 91 | color: #818181; 92 | } 93 | 94 | .navbarbutton a:link { 95 | color: #818181; 96 | } 97 | 98 | #main { 99 | transition: margin-left .5s; 100 | padding: 20px; 101 | } 102 | 103 | @media screen and (max-height: 450px) { 104 | .sidenav {padding-top: 15px;} 105 | .sidenav a {font-size: 18px;} 106 | } -------------------------------------------------------------------------------- /static/styles/adminusers.css: -------------------------------------------------------------------------------- 1 | body,html{ 2 | height:100%; 3 | } 4 | 5 | .h-90{ 6 | height:90%; 7 | } 8 | 9 | .h-80{ 10 | height:80%; 11 | } 12 | 13 | .h-10{ 14 | height:10%; 15 | } 16 | 17 | .h-5{ 18 | height:5%; 19 | } 20 | 21 | .sidenav { 22 | height: 100%; 23 | width: 0; 24 | position: fixed; 25 | z-index: 1; 26 | top: 0; 27 | left: 0; 28 | background-color: #111; 29 | overflow-x: hidden; 30 | padding-top: 60px; 31 | transition: 0.5s; 32 | } 33 | 34 | .sidenav a { 35 | padding: 8px 8px 8px 32px; 36 | text-decoration: none; 37 | font-size: 25px; 38 | color: #818181; 39 | display: block; 40 | transition: 0.3s; 41 | } 42 | 43 | .sidenav a:hover { 44 | color: #f1f1f1; 45 | } 46 | 47 | .sidenav .closebtn { 48 | position: absolute; 49 | top: 0; 50 | right: 25px; 51 | font-size: 36px; 52 | margin-left: 50px; 53 | } 54 | 55 | .footer { 56 | color: #f1f1f1; 57 | padding: 8px 8px 8px 32px; 58 | white-space: nowrap; 59 | } 60 | 61 | .navbarbutton { 62 | font-size: 150%; 63 | color: #E5D4C0; 64 | position: fixed; 65 | } 66 | 67 | .navbarbutton a { 68 | padding: 8px 8px 8px 32px; 69 | color: #818181; 70 | transition: 0.3s; 71 | } 72 | 73 | .navbarbutton a:hover { 74 | text-decoration: none; 75 | color: #f1f1f1 !important; 76 | } 77 | 78 | .navbarbutton a:visited { 79 | color: #818181; 80 | } 81 | 82 | .navbarbutton a:link { 83 | color: #818181; 84 | } 85 | 86 | .invisblebutton { 87 | background: none!important; 88 | border: none; 89 | padding: 0!important; 90 | font-family: arial, sans-serif; 91 | color: #818181; 92 | text-decoration: underline; 93 | cursor: pointer; 94 | } 95 | 96 | .clickable { 97 | cursor: pointer; 98 | } 99 | 100 | 101 | table { 102 | width: 100%; 103 | } 104 | 105 | thead, tbody tr { 106 | display: table; 107 | width: 100%; 108 | table-layout: fixed; 109 | } 110 | 111 | tbody { 112 | display: block; 113 | overflow-y: auto; 114 | table-layout: fixed; 115 | max-height: 500px; 116 | } 117 | 118 | #main { 119 | transition: margin-left .5s; 120 | padding: 20px; 121 | } 122 | 123 | @media screen and (max-height: 450px) { 124 | .sidenav {padding-top: 15px;} 125 | .sidenav a {font-size: 18px;} 126 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HOW TO RUN PROGRAM 2 | 3 | ------ 4 | 5 | An example database has been set-up for the purpose of showcase. Use the following login details to access different aspects of the application. 6 | 7 | For the admin panel, use account 8 | user: admin 9 | password: test 10 | 11 | For the user's point of view, use accounts 12 | user: windows4life or appleman22 or johnsmith2 13 | password: ExamplePassword123!! 14 | 15 | ------ 16 | HOW TO RUN LOCALLY::: 17 | 18 | requirements; 19 | - Python 3.7, should work on Python 3.+ 20 | - the following python libs; Flask, flask-login, flask-limiter, requests, matplotlib 21 | 22 | following cmd commands will install required libraries: 23 | - pip install Flask 24 | - pip install flask-login 25 | - pip install Flask-Limiter 26 | - pip install requests 27 | - pip install matplotlib 28 | 29 | AFTER THIS, config.json needs to be edited in order to correspond with the directory the program is being run in. If running on windows, the absolute path's will likely contain backward slashes. Please change these backslashes to forward slashes before saving the config.json; if running on a unix-based OS ignore this step. 30 | 31 | After all requirements have been met, running main.py will run the host server on your localhost machine. 32 | 33 | ------ 34 | ALTERNATIVELY::: 35 | 36 | Can also be accessed remotely @ http://178.62.24.189:5000/, application is currently running on a ubunutu server at this address. 37 | 38 | Host application has the same example showcase database set up, so feel free to login with any of the accounts 39 | 40 | If you wish to run the example client described in the testing elements of the documentation, navigate to exampleclient/client.py in the code base and run the following file. A license key also needs to be entered into exampleclient/userconfig.json, this can be retrieved through login as one of the above provided accounts. 41 | 42 | This shows client-server model working etc. 43 | 44 | PLEASE NOTE: server has very little processing power, code has not been written for this server, and deployment on this server was not thorough. It should represent client-server model working, however for a real showcase of the web application and host program, please run locally. After brief testing on the server-hosted application, some functions like image rendering were not working as expected due to above reasons. 45 | 46 | ----- 47 | -------------------------------------------------------------------------------- /static/styles/adminlicenses.css: -------------------------------------------------------------------------------- 1 | body,html{ 2 | height:100%; 3 | } 4 | 5 | .h-90{ 6 | height:90%; 7 | } 8 | 9 | .h-30{ 10 | height:30%; 11 | } 12 | 13 | .h-80{ 14 | height:80%; 15 | } 16 | 17 | .h-60{ 18 | height: 60%; 19 | } 20 | 21 | .h-10{ 22 | height:10%; 23 | } 24 | 25 | .sidenav { 26 | height: 100%; 27 | width: 0; 28 | position: fixed; 29 | z-index: 1; 30 | top: 0; 31 | left: 0; 32 | background-color: #111; 33 | overflow-x: hidden; 34 | padding-top: 60px; 35 | transition: 0.5s; 36 | } 37 | 38 | .sidenav a { 39 | padding: 8px 8px 8px 32px; 40 | text-decoration: none; 41 | font-size: 25px; 42 | color: #818181; 43 | display: block; 44 | transition: 0.3s; 45 | } 46 | 47 | .sidenav a:hover { 48 | color: #f1f1f1; 49 | } 50 | 51 | .sidenav .closebtn { 52 | position: absolute; 53 | top: 0; 54 | right: 25px; 55 | font-size: 36px; 56 | margin-left: 50px; 57 | } 58 | 59 | .footer { 60 | color: #f1f1f1; 61 | padding: 8px 8px 8px 32px; 62 | white-space: nowrap; 63 | } 64 | 65 | .navbarbutton { 66 | font-size: 150%; 67 | color: #E5D4C0; 68 | position: fixed; 69 | } 70 | 71 | .navbarbutton a { 72 | padding: 8px 8px 8px 32px; 73 | color: #818181; 74 | transition: 0.3s; 75 | } 76 | 77 | .navbarbutton a:hover { 78 | text-decoration: none; 79 | color: #f1f1f1 !important; 80 | } 81 | 82 | .navbarbutton a:visited { 83 | color: #818181; 84 | } 85 | 86 | .navbarbutton a:link { 87 | color: #818181; 88 | } 89 | 90 | .invisblebutton { 91 | background: none!important; 92 | border: none; 93 | padding: 0!important; 94 | font-family: arial, sans-serif; 95 | color: #818181; 96 | text-decoration: underline; 97 | cursor: pointer; 98 | } 99 | 100 | .clickable { 101 | cursor: pointer; 102 | } 103 | 104 | 105 | table { 106 | width: 100%; 107 | } 108 | 109 | thead, tbody tr { 110 | display: table; 111 | width: 100%; 112 | table-layout: fixed; 113 | } 114 | 115 | tbody { 116 | display: block; 117 | overflow-y: auto; 118 | table-layout: fixed; 119 | max-height: 400px; 120 | } 121 | 122 | #main { 123 | transition: margin-left .5s; 124 | padding: 20px; 125 | } 126 | 127 | @media screen and (max-height: 450px) { 128 | .sidenav {padding-top: 15px;} 129 | .sidenav a {font-size: 18px;} 130 | } -------------------------------------------------------------------------------- /static/styles/admindocs.css: -------------------------------------------------------------------------------- 1 | form { display: table; } 2 | p { display: table-row; } 3 | label { display: table-cell; } 4 | input { display: table-cell; } 5 | 6 | body,html{ 7 | height:100%; 8 | } 9 | 10 | .h-90{ 11 | height:90%; 12 | } 13 | 14 | .h-80{ 15 | height:80%; 16 | } 17 | 18 | .h-10{ 19 | height:10%; 20 | } 21 | 22 | .h-5{ 23 | height:5%; 24 | } 25 | 26 | .sidenav { 27 | height: 100%; 28 | width: 0; 29 | position: fixed; 30 | z-index: 1; 31 | top: 0; 32 | left: 0; 33 | background-color: #111; 34 | overflow-x: hidden; 35 | padding-top: 60px; 36 | transition: 0.5s; 37 | } 38 | 39 | .sidenav a { 40 | padding: 8px 8px 8px 32px; 41 | text-decoration: none; 42 | font-size: 25px; 43 | color: #818181; 44 | display: block; 45 | transition: 0.3s; 46 | } 47 | 48 | .sidenav a:hover { 49 | color: #f1f1f1; 50 | } 51 | 52 | .sidenav .closebtn { 53 | position: absolute; 54 | top: 0; 55 | right: 25px; 56 | font-size: 36px; 57 | margin-left: 50px; 58 | } 59 | 60 | .footer { 61 | color: #f1f1f1; 62 | padding: 8px 8px 8px 32px; 63 | white-space: nowrap; 64 | } 65 | 66 | .navbarbutton { 67 | font-size: 150%; 68 | color: #E5D4C0; 69 | position: fixed; 70 | } 71 | 72 | .navbarbutton a { 73 | padding: 8px 8px 8px 32px; 74 | color: #818181; 75 | transition: 0.3s; 76 | } 77 | 78 | .navbarbutton a:hover { 79 | text-decoration: none; 80 | color: #f1f1f1 !important; 81 | } 82 | 83 | .navbarbutton a:visited { 84 | color: #818181; 85 | } 86 | 87 | .navbarbutton a:link { 88 | color: #818181; 89 | } 90 | 91 | .invisblebutton { 92 | background: none!important; 93 | border: none; 94 | padding: 0!important; 95 | font-family: arial, sans-serif; 96 | color: #818181; 97 | text-decoration: underline; 98 | cursor: pointer; 99 | } 100 | 101 | .clickable { 102 | cursor: pointer; 103 | } 104 | 105 | table { 106 | width: 100%; 107 | } 108 | 109 | thead, tbody tr { 110 | display: table; 111 | width: 100%; 112 | table-layout: fixed; 113 | } 114 | 115 | tbody { 116 | display: block; 117 | overflow-y: auto; 118 | table-layout: fixed; 119 | max-height: 200px; 120 | } 121 | 122 | 123 | #main { 124 | transition: margin-left .5s; 125 | padding: 20px; 126 | } 127 | 128 | @media screen and (max-height: 450px) { 129 | .sidenav {padding-top: 15px;} 130 | .sidenav a {font-size: 18px;} 131 | } -------------------------------------------------------------------------------- /static/styles/adminplans.css: -------------------------------------------------------------------------------- 1 | form { display: table; } 2 | p { display: table-row; } 3 | label { display: table-cell; } 4 | input { display: table-cell; } 5 | 6 | body,html{ 7 | height:100%; 8 | } 9 | 10 | .h-90{ 11 | height:90%; 12 | } 13 | 14 | .h-80{ 15 | height:80%; 16 | } 17 | 18 | .h-10{ 19 | height:10%; 20 | } 21 | 22 | .h-5{ 23 | height:5%; 24 | } 25 | 26 | .sidenav { 27 | height: 100%; 28 | width: 0; 29 | position: fixed; 30 | z-index: 1; 31 | top: 0; 32 | left: 0; 33 | background-color: #111; 34 | overflow-x: hidden; 35 | padding-top: 60px; 36 | transition: 0.5s; 37 | } 38 | 39 | .sidenav a { 40 | padding: 8px 8px 8px 32px; 41 | text-decoration: none; 42 | font-size: 25px; 43 | color: #818181; 44 | display: block; 45 | transition: 0.3s; 46 | } 47 | 48 | .sidenav a:hover { 49 | color: #f1f1f1; 50 | } 51 | 52 | .sidenav .closebtn { 53 | position: absolute; 54 | top: 0; 55 | right: 25px; 56 | font-size: 36px; 57 | margin-left: 50px; 58 | } 59 | 60 | .footer { 61 | color: #f1f1f1; 62 | padding: 8px 8px 8px 32px; 63 | white-space: nowrap; 64 | } 65 | 66 | .navbarbutton { 67 | font-size: 150%; 68 | color: #E5D4C0; 69 | position: fixed; 70 | } 71 | 72 | .navbarbutton a { 73 | padding: 8px 8px 8px 32px; 74 | color: #818181; 75 | transition: 0.3s; 76 | } 77 | 78 | .navbarbutton a:hover { 79 | text-decoration: none; 80 | color: #f1f1f1 !important; 81 | } 82 | 83 | .navbarbutton a:visited { 84 | color: #818181; 85 | } 86 | 87 | .navbarbutton a:link { 88 | color: #818181; 89 | } 90 | 91 | .invisblebutton { 92 | background: none!important; 93 | border: none; 94 | padding: 0!important; 95 | font-family: arial, sans-serif; 96 | color: #818181; 97 | text-decoration: underline; 98 | cursor: pointer; 99 | } 100 | 101 | .clickable { 102 | cursor: pointer; 103 | } 104 | 105 | table { 106 | width: 100%; 107 | } 108 | 109 | thead, tbody tr { 110 | display: table; 111 | width: 100%; 112 | table-layout: fixed; 113 | } 114 | 115 | tbody { 116 | display: block; 117 | overflow-y: auto; 118 | table-layout: fixed; 119 | max-height: 200px; 120 | } 121 | 122 | 123 | #main { 124 | transition: margin-left .5s; 125 | padding: 20px; 126 | } 127 | 128 | @media screen and (max-height: 450px) { 129 | .sidenav {padding-top: 15px;} 130 | .sidenav a {font-size: 18px;} 131 | } -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Login 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 |
22 |

Please login

23 |
or sign-up here
24 |
25 |
26 |
27 |
28 | 29 |
30 | 32 |
33 |
34 |
35 | 36 |
37 | 39 |
40 | 41 |
42 |
43 | {% if error %} 44 |

Error: {{ error }} 45 | {% endif %} 46 |

47 | 48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | SaaS distrubution system by Tom Holland :) 56 |
57 |
58 |
59 |
60 | 61 | -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Login 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |
21 | 22 |

23 | A Software-as-a-Service means of distributing your project 24 |

By Tom • Showcase Version

25 | 26 |
27 |
28 |
29 |

New or exisiting user?

30 | Login 32 | or 33 | Sign-up 34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | or scroll down 42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
Account login details to showcase certain features of the application: 51 |

52 | For the admin panel, use account
53 | user: admin
password: test 54 |

55 | For the user's point of view, use accounts
56 | user: windows4life or appleman22 or johnsmith2
password: ExamplePassword123!!
57 |
58 |
59 |
60 |
61 | 62 | -------------------------------------------------------------------------------- /monitor.py: -------------------------------------------------------------------------------- 1 | from main import Renewal 2 | from api import Database 3 | import utils 4 | import time 5 | import datetime 6 | import csv 7 | 8 | 9 | def monitorRenewals(): 10 | # function that repetitively monitors for when current time becomes equal to the users license date 11 | # whent this event occurs, it attempts to charge the user (stub function as explained in design documentation) 12 | # upon success renewal date is incremeneted, upon failure key is destroyed 13 | while True: 14 | 15 | def charge(session): 16 | # placeholder function 17 | 18 | # sing session ID, makes get request to status webhook 19 | # creates communction with stripe API, which will return one of the following 20 | if session: 21 | event_type = "checkout.session.completed" # response from stripe API, example. 22 | if event_type == "checkout.session.completed" or "invoice.paid": 23 | return True 24 | else: 25 | return False 26 | 27 | db = Database() 28 | rendict = db.getAllLicenseWithRenewal() 29 | db.closeConnection() 30 | now = datetime.datetime.now() 31 | for value in rendict: 32 | if (rendict[value] - now).total_seconds() <= 0: 33 | 34 | db = Database() 35 | session = db.getLicenseStripeSessionID(value) 36 | attempt = charge(session) 37 | 38 | if attempt: 39 | renewal = Renewal(value) 40 | renewal.incrementRenewalDate() 41 | renewal.commitRenewdatetoDatabase(value) 42 | else: 43 | print('failed to charge') 44 | db = Database() 45 | db.deleteLicense(value) 46 | db.closeConnection() 47 | else: 48 | pass 49 | 50 | time.sleep(5) 51 | 52 | 53 | def monitorGraphs(): 54 | # function that monitors for when it becomes a new day 55 | # when this event occurs, it recollects data relating to number of users/ licenses against time 56 | # then polts new graphs for the admin user to see on their dashboard 57 | while True: 58 | 59 | with open('graphinfo.csv', 'r') as current_file: 60 | latestdate = list(csv.reader(current_file, delimiter=','))[-1][0] 61 | 62 | latestdate = datetime.datetime.strptime(latestdate, '%d/%m/%Y').date() 63 | nowdate = datetime.date.today() 64 | 65 | if nowdate > latestdate: 66 | with open('graphinfo.csv', 'a') as current_file: 67 | db = Database() 68 | current_file.write( 69 | f'''\n{datetime.datetime.strftime(nowdate, '%d/%m/%Y')},{db.getCountofTable('licenses')},{db.getCountofTable('users')}''') 70 | db.closeConnection() 71 | 72 | # generate a new set of graphs for the new day 73 | utils.generateGraph() 74 | 75 | # only checks to refesh once every 10 minutes, as generating graphs is relatively processer intensive 76 | time.sleep(10 * 60) 77 | -------------------------------------------------------------------------------- /templates/adminusers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Dashboard: {{ current_user.id }} 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 |
19 | × 20 | overview 21 | users 22 | licenses 23 | plans 24 | api 25 | 26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {% for value in users %} 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {% if value[5] == "FALSE" %} 60 | 61 | {% else %} 62 | 63 | {% endif %} 64 | 65 | {% endfor %} 66 | 67 |
#UsernameFirstLastEmailLicense
{{ users.index(value) }}@{{ value[0] }}{{ value[1] }}{{ value[2] }}{{ value[3] }}{{ value[6] }}
68 |
69 |
70 |
71 |
72 |
73 |
74 | 75 | 76 | -------------------------------------------------------------------------------- /templates/signup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Login 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 |

Please sign-up

24 |
or login here
25 |
26 |
27 |
28 |
29 | 30 |
31 | 33 |
34 |
35 |
36 | 37 |
38 | 40 |
41 |
42 |
43 | 44 |
45 | 47 |
48 |
49 |
50 | 51 |
52 | 54 |
55 |
56 |
57 | 58 |
59 | 61 |
62 | 63 |
64 |
65 | {% if error %} 66 |

Error: {{ error }} 67 | {% endif %} 68 |

69 | 70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | SaaS distrubution system by Tom Holland :) 78 |
79 |
80 |
81 |
82 | 83 | -------------------------------------------------------------------------------- /static/js/tablefunction.js: -------------------------------------------------------------------------------- 1 | // following selections set sidebar to be open or closed in correspodnging with localstorage (from last page visited) 2 | 3 | if (typeof(Storage) !== "undefined") { 4 | if(localStorage.getItem("sidebar") == "opened"){ 5 | document.querySelector("#mySidenav").style.width = "250px"; 6 | document.querySelector("#main").style.marginLeft = "250px"; 7 | } 8 | } 9 | function openNav() { 10 | // function for opening navigation panel, commits to local storage after change 11 | document.querySelector("#mySidenav").style.width = "250px"; 12 | document.querySelector("#main").style.marginLeft = "250px"; 13 | if (typeof(Storage) !== "undefined") { 14 | localStorage.setItem("sidebar", "opened"); 15 | } 16 | } 17 | function closeNav() { 18 | // function for closing navigation panel, commits to local storage after change 19 | document.querySelector("#mySidenav").style.width = "0"; 20 | document.querySelector("#main").style.marginLeft = "0"; 21 | if (typeof(Storage) !== "undefined") { 22 | localStorage.setItem("sidebar", "closed"); 23 | } 24 | } 25 | 26 | var lastCol = undefined; 27 | var dir = undefined; 28 | 29 | function sortTable2(col, isInteger = false) { 30 | // entire function has a time complexity of O(nlogn) due to merge sort being least time efficient sub function 31 | if (lastCol == undefined) lastCol = col; 32 | if (dir == undefined) dir = "asc"; 33 | if (lastCol != col) { 34 | dir = "asc"; 35 | lastCol = col; 36 | } 37 | let table, rows, preswitch = [], 38 | switching, i, x, y, shouldSwitch, switchcount = 0; 39 | table = document.querySelector("#dbtable"); 40 | rows = table.rows; 41 | for (heading of document.querySelectorAll("TH")) { 42 | heading.style.textDecoration = "none"; 43 | } 44 | // performs visual change, setting sorted column to be underlined 45 | rows[0].querySelectorAll("TH")[col].style.textDecoration = "underline"; 46 | let tbody = document.querySelector("#dbtable > tbody"); 47 | let new_tbody = tbody.cloneNode(); 48 | //set direction to ascending initially 49 | if (dir == "asc") { 50 | dir = "desc" 51 | } else { 52 | dir = "asc" 53 | } 54 | // collects data from that column with its associated index number to be sorted 55 | if (isInteger) { 56 | for (i = 1; i < (rows.length); i++) { 57 | preswitch.push([rows[i].querySelectorAll("TD")[0].innerHTML,parseInt(rows[i].querySelectorAll("TD")[col].innerHTML)]) 58 | } 59 | } else { 60 | for (i = 1; i < (rows.length); i++) { 61 | preswitch.push([rows[i].querySelectorAll("TD")[0].innerHTML,rows[i].querySelectorAll("TD")[col].innerHTML]) 62 | } 63 | } 64 | //copy of preswitch 65 | let preswitch_copy = [...preswitch]; 66 | 67 | function merge(left, right) { 68 | let array = [] 69 | // in the case where any one of the arrays become empty, break out of the loop 70 | while (left.length && right.length) { 71 | // selects the smaller among the smallest element of the sub arrays (left and right) 72 | if (left[0][1] < right[0][1]) { 73 | array.push(left.shift()) 74 | } else { 75 | array.push(right.shift()) 76 | } 77 | } 78 | 79 | // concatenating the leftover elements 80 | // (in case we didn't go through the entire left or right array) 81 | // ... acts as a spread operator 82 | return [...array, ...left, ...right] 83 | } 84 | 85 | function mergeSort(array) { 86 | // performs the actual merge sort 87 | const halflength = array.length / 2 88 | 89 | // recursion base case 90 | if (array.length < 2) { 91 | return array 92 | } 93 | 94 | const left = array.splice(0, halflength) 95 | return merge(mergeSort(left), mergeSort(array)) 96 | } 97 | 98 | sorted = mergeSort(preswitch); 99 | preswitch = preswitch_copy; 100 | let index_array = []; 101 | // get the order of the sorted array based on the preswitch array 102 | sorted.forEach(value => { 103 | index_array.push(preswitch.indexOf(value)); 104 | }) 105 | // simply reverses the array in the case it needs to be descending, to avoid having to merge sort again 106 | if(dir=="desc")index_array.reverse(); 107 | index_array.forEach(index => { 108 | new_tbody.appendChild(tbody.children[index].cloneNode(true)); 109 | }) 110 | // performs visual change on the body of the table 111 | tbody.parentElement.replaceChild(new_tbody, tbody); 112 | } -------------------------------------------------------------------------------- /templates/admindash.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Dashboard: {{ current_user.id }} 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 |
19 | × 20 | overview 21 | users 22 | licenses 23 | plans 24 | api 25 | 26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |

Welcome to the administrator panel!

39 |
Your statistics at a glance...
40 |
41 |
42 |
43 |
44 |
45 |
{{ stats[0][0] }}
46 |
{{ stats[0][1] }}
47 |
48 |
49 |
50 |
51 |
52 |
53 |
{{ stats[1][0] }}
54 |
{{ stats[1][1] }}
55 |
56 |
57 |
58 |
59 |
60 |
61 |
{{ stats[2][0] }}
62 |
{{ stats[2][1] }}
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | Image 71 |
72 |
73 |
74 |
75 | Image 76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | SaaS distrubution system by Tom Holland :) 89 |
90 |
91 |
92 |
93 | 94 | 95 | -------------------------------------------------------------------------------- /examplerequests.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from uuid import getnode as get_mac 3 | import socket 4 | import platform 5 | 6 | API_KEY = '' 7 | 8 | 9 | # Authentication class acts as a framework for building your authnetication system off 10 | # this is a guide and does not to be adheard to exactly, the API functionality should allow for multiple authentication 11 | # flows to exist, whilst all are secure and suffice. 12 | 13 | class Authentication(): 14 | def __init__(self, licenseid): 15 | self.license = licenseid 16 | self.hwid = None 17 | self.devicename = None 18 | self.isBoundToUser = False 19 | 20 | self.headers = {'api_key': API_KEY} 21 | 22 | self.getLicenseInfo() 23 | 24 | def getLicenseInfo(self): 25 | # performs GET request given a license key and attempts to return its attributes 26 | try: 27 | resp = requests.get(f'http://127.0.0.1:5000/api/v1/licenses/{self.license}', headers=self.headers).json() 28 | self.hwid = resp['license']['HWID'] 29 | self.devicename = resp['license']["device"] 30 | self.isBoundToUser = bool(resp['license']["boundToUser"]) 31 | return resp['license'] 32 | except: 33 | self.license = None 34 | return None 35 | 36 | def setToBound(self, hwid, devicename): 37 | # performs POST request given a license key sets it to bound, given appropriate request data 38 | payloadjson = { 39 | "HWID": f"{hwid}", 40 | "device": f"{devicename}" 41 | } 42 | resp = requests.post(f'http://127.0.0.1:5000/api/v1/licenses/{self.license}', headers=self.headers, 43 | json=payloadjson).json() 44 | return resp 45 | 46 | def setToUnbound(self): 47 | # performs POST request given a license key sets it to unbound 48 | payloadjson = { 49 | "HWID": None, 50 | "device": None 51 | } 52 | resp = requests.post(f'http://127.0.0.1:5000/api/v1/licenses/{self.license}', headers=self.headers, 53 | json=payloadjson).json() 54 | return resp 55 | 56 | 57 | # Local functions (to follow) should be built in whatever way the developer sees fit, i.e. should derive the "HWID" element in a unique way, not necessarily the example shown. 58 | # more device related data can be collected using external libararys like psutil, which can be isntalled via pip, however for the sake of example the libraries used are preinstalled with py 59 | # These functions should be implemented around the developers software they are wanting to distrubute in order to validate users. 60 | 61 | def collectLocalData(): 62 | # this function will end up being called often for comparison, and could be written in a variety of ways 63 | 64 | def deriveHWID(): 65 | # gets device MAC address 66 | mac_address = get_mac() 67 | # gets name of local microprocessor 68 | processor_arch = platform.uname().processor 69 | # gets instruction set architecture 70 | machine = platform.uname().machine 71 | 72 | # any convolution of relevant data would be valid, could be hashed, hashed with a pepper, etc. 73 | # how this value is derived should be kept unkown to the user of the application 74 | return str(mac_address) + processor_arch + machine 75 | 76 | hwid = deriveHWID() 77 | # gets name of local node 78 | devicename = socket.gethostname() 79 | 80 | return hwid, devicename 81 | 82 | 83 | def validateUser(license): 84 | # this function will end uo being called often, ideally at key function within the developers program, and could be written in a variety of ways 85 | # this authenticates a users license to be valid, and not currently 86 | 87 | auth = Authentication(license) 88 | localhwid, localdevname = collectLocalData() 89 | 90 | if auth.license and auth.isBoundToUser: 91 | # checks license key is still valid 92 | if not (auth.hwid and auth.devicename): 93 | # in the case where license is currently unbound 94 | auth.setToBound(localhwid, localdevname) 95 | else: 96 | if auth.hwid == localhwid and auth.devicename == localdevname: 97 | # proceed with operation, license still valid 98 | pass 99 | else: 100 | # license is bound to another machine, not the one it is attempting to be used on, hence quit program 101 | quit() 102 | else: 103 | # license key is invalid, hence quit progam 104 | quit() 105 | -------------------------------------------------------------------------------- /templates/dashboardaccount.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Dashboard: {{ current_user.id }} 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 |
22 |
23 | 24 |

Edit your account (logged in as {{ current_user.id }})

25 |
26 |
27 |
28 | 29 |
30 | 32 |
33 |
34 |
35 | 36 |
37 | 39 |
40 |
41 |
42 | 43 |
44 | 46 |
47 |
48 |
49 | 50 |
51 | 53 |
54 |
55 |
56 | 57 |
58 | 60 |
61 | {% if error %} 62 | {{error}} 63 | {% else %} 64 | 65 | {% endif%} 66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | SaaS distrubution system by Tom Holland :) 77 |
78 |
79 |
80 |
81 | 82 | -------------------------------------------------------------------------------- /templates/adminplans.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Dashboard: {{ current_user.id }} 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 |
19 | × 20 | overview 21 | users 22 | licenses 23 | plans 24 | api 25 | 26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {% for value in plans %} 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | {% endfor %} 58 | 59 |
#NameRenewal IntervalRenewal Price
{{ plans.index(value) }}{{ value[0] }}{{ value[1] }} Days£{{ value[2] }}
60 | {% if reason %} 61 |
{{ reason }}
62 | {% endif %} 63 |
64 |
65 |
66 |
Create Plan
67 |
Please be aware, page may need to be refreshed to show 68 | changes in table.
69 |
70 |

71 | 72 | 73 |

74 |

75 | 76 | 77 |

78 |

79 | 80 | 81 |

82 | 83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | 92 | 93 | -------------------------------------------------------------------------------- /templates/admindocs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Dashboard: {{ current_user.id }} 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 |
19 | × 20 | overview 21 | users 22 | licenses 23 | plans 24 | api 25 | 26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
API documentation
40 |
For all requests, headers must be parsed in the format {"api_key": 41 | YOUR_API_KEY_HERE} for authentication.
42 |
43 |
GET 44 | http://127.0.0.1:5000/api/v1/licenses/{license} 45 |
46 | ------------------ 47 |
48 | Retrieves information relating to the license parsed into the parameters of the request, 49 | denoted by {license}. If the request authenticates successfully, a JSON object of the specfic 50 | license will be returned. 51 |
52 |
53 | POST http://127.0.0.1:5000/api/v1/licenses/{license} 54 |
55 | REQUIRES body paramaters of "HWID" and "device" 56 |
57 | ------------------ 58 |
59 | Updates the HWID and devicename for a given license, specified in the request. Both can be set 60 | to None in the case where the user is attempting to log out (unbind) of their device. If the request 61 | is valid 62 | and authenticated successfully, it will return a JSON object of the updated license and a 63 | suitable status. 64 |
65 |
66 |
67 |
68 |
69 |
API Wrappers for quick implentation
70 |
Files include an example implementation of the authentication mechanism and provide an API wrapper 71 | class for the language of your project. Click to download.
72 |
73 |

74 | 75 |
76 |
77 |
78 |
79 |
80 |
81 | Your personal API key is {{ api_key }} 82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | 90 | 91 | -------------------------------------------------------------------------------- /templates/adminlicenses.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Dashboard: {{ current_user.id }} 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 |
19 | × 20 | overview 21 | users 22 | licenses 23 | plans 24 | api 25 | 26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {% for value in licenses %} 52 | 53 | 54 | 55 | {% if value[1] != None %} 56 | 57 | {% else %} 58 | 59 | {% endif %} 60 | 61 | 62 | 63 | 64 | 65 | {% endfor %} 66 | 67 |
#LicenseUserPlanNext RenewalBound to device
{{ licenses.index(value) }}{{ value[0] }}@{{ value[1] }}{{ value[1] }}{{ value[7] }}{{ value[6] }}{{ value[5] }}
68 |
69 |
70 |
71 |
Create Licenses
72 |
Please be aware, page may need to be refreshed to show changes in table.
73 | 74 |
75 | 81 | with amount: 82 | 83 |
84 | 85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | 97 | 98 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | from api import Database 2 | import random 3 | import hashlib 4 | import csv 5 | import matplotlib 6 | 7 | #necessary in relation to imports as to stop 8 | matplotlib.use('Agg') 9 | 10 | import matplotlib.pyplot as plt 11 | 12 | def generatekey(random_chars, alphabet="abcdefghijklmnopqrstuvwxyz1234567890"): 13 | # generates a license key from a pseudorandom mechanism 14 | r = random.SystemRandom() 15 | return ''.join([r.choice(alphabet) for _ in range(random_chars)]) 16 | 17 | 18 | def createLicense(planname): 19 | # creates a license using generatekey function, commiting to database after suitable logic 20 | initialconn = Database() 21 | if initialconn.getPlanInfo(planname): 22 | initialconn.closeConnection() 23 | while True: 24 | license = generatekey(random_chars=16) 25 | # 79586 6110994640 0884391936 combinations 26 | conn = Database() 27 | if conn.checkIfLicenseExists(license): 28 | # in the event the license identifier generated is already in use, retry 29 | continue 30 | else: 31 | print(f'created license {license}') 32 | conn.commitLicense(license, planname) 33 | conn.closeConnection() 34 | return license 35 | else: 36 | initialconn.closeConnection() 37 | return 'Plan does not exist' 38 | 39 | 40 | def gensalt(username): 41 | # a strange algorithm to create larger salts, as well as remove the predictability vulnerability for as long as this algorithm is kept secret 42 | # uses a combination of ceaser shifts 43 | # goal of this is not to provide a secure cipher, simply to act as a string manipulation algorithm 44 | def ceaser(shift, string): 45 | alphabet = '0abcdefghijkl1234mnopqrst567uvwx89yz' 46 | output = [] 47 | 48 | for letter in string: 49 | if letter.strip() and letter in alphabet: 50 | output.append(alphabet[(alphabet.index(letter) + shift) % 36]) 51 | 52 | return ''.join(output) 53 | 54 | def manipulationalgo(inputstr): 55 | doubled = ''.join([element * 2 for element in inputstr]) 56 | output = [] 57 | count = 1 58 | for letter in doubled: 59 | # cant use .index() as letter appears multiple times! 60 | if count % 2 == 0: 61 | output.append(ceaser(count, letter)) 62 | else: 63 | output.append(ceaser(-1, letter)) 64 | count += 1 65 | 66 | return ''.join(output) 67 | 68 | salt = manipulationalgo(username) 69 | pepper = '3UwF4zVIB2CkF3uOMkmAifCMjO+88RKNfL4u6EXifPQ=' 70 | 71 | return salt + pepper 72 | 73 | 74 | def hash(username, password): 75 | # performs hashing function given the salt 76 | hashdpw = hashlib.pbkdf2_hmac( 77 | hash_name='sha256', # The hash digest algorithm for HMAC 78 | password=password.encode('utf-8'), 79 | salt=gensalt(username).encode('utf-8'), 80 | iterations=100000 # 100,0 00 iterations of SHA-256 81 | ) 82 | 83 | return hashdpw.hex() 84 | 85 | 86 | def createAdminUser(values): 87 | # needs username,fName,sName,emailAddress,password passed into it as a comma seperated str, where pw is pre hashed 88 | values += ',TRUE' 89 | db = Database() 90 | db.addToUsers(values) 91 | return 'success' 92 | 93 | 94 | def gatherStatistics(): 95 | # function to perform database calls to collect 'insight' statistics for the admin overview page 96 | db = Database() 97 | 98 | # this dict can easily be added to due to modular programming design 99 | try: 100 | dict = { 101 | "Licenses": db.getCountofTable('licenses'), 102 | "Users": db.getCountofTable('users'), 103 | "Plans": db.getCountofTable('plans'), 104 | "Users with a License Bound": db.getConditionalCountofTable('licenses','boundToUser','1'), 105 | "Licenses Bound to a User's Device": db.getConditionalCountofTable('licenses', 'boundToDevice', '1'), 106 | "Most Popular Plan": db.getMostPopular('licenses','plan')[0], 107 | "Percentages of Licenses bound to a User": f'''{round((db.getConditionalCountofTable('licenses','boundToUser','1')/ db.getCountofTable('licenses'))*100, 2)}%''' 108 | } 109 | except: 110 | # in the event where tables are not populated enough 111 | dict = { 112 | "Licenses": '', 113 | "Users": '', 114 | "Plans":'', 115 | "Users with a License Bound": '', 116 | "Licenses Bound to a User's Device": '', 117 | "Most Popular Plan": '', 118 | "Percentages of Licenses bound to a User": '', 119 | } 120 | 121 | db.closeConnection() 122 | return dict 123 | 124 | def generateGraph(): 125 | # function to collect information, generate graphs and convert to png form for the admin dashboard 126 | with open('graphinfo.csv','r') as graphdata: 127 | graphpoints = csv.reader(graphdata, delimiter=',') 128 | rows = list(graphpoints) 129 | 130 | # get licenses graph 131 | fig, ax = plt.subplots() 132 | if len(rows) > 15: 133 | plt.plot([value[0] for value in rows[1:][-15:]], [int(value[1]) for value in rows[1:][-15:]]) 134 | else: 135 | plt.plot([value[0] for value in rows[1:]], [int(value[1]) for value in rows[1:]]) 136 | plt.ylabel(rows[0][1]) 137 | fig.autofmt_xdate() 138 | plt.savefig('static/images/licenses.png',dpi=300) 139 | 140 | # get users graph 141 | fig, ax = plt.subplots() 142 | if len(rows) > 15: 143 | plt.plot([value[0] for value in rows[1:][-15:]], [int(value[2]) for value in rows[1:][-15:]]) 144 | else: 145 | plt.plot([value[0] for value in rows[1:]], [int(value[2]) for value in rows[1:]]) 146 | plt.ylabel(rows[0][2]) 147 | fig.autofmt_xdate() 148 | plt.savefig('static/images/users.png', dpi=300) 149 | 150 | 151 | if __name__ == '__main__': 152 | createAdminUser(f'''admin,test,account,admin@gmail.com,{hash('admin', 'test')}''') 153 | 154 | -------------------------------------------------------------------------------- /mergesort.js: -------------------------------------------------------------------------------- 1 | function merge(left, right) { 2 | let array = [] 3 | // Break out of loop if any one of the array gets empty 4 | while (left.length && right.length) { 5 | // Pick the smaller among the smallest element of left and right sub arrays 6 | if (left[0] < right[0]) { 7 | array.push(left.shift()) 8 | } else { 9 | array.push(right.shift()) 10 | } 11 | } 12 | 13 | // Concatenating the leftover elements 14 | // (in case we didn't go through the entire left or right array) 15 | // ... acts as a spread operator 16 | return [ ...array, ...left, ...right ] 17 | } 18 | 19 | function mergeSort(array) { 20 | const halflength = array.length / 2 21 | 22 | // Base case 23 | if(array.length < 2){ 24 | return array 25 | } 26 | 27 | const left = array.splice(0, halflength) 28 | return merge(mergeSort(left),mergeSort(array)) 29 | } 30 | 31 | //ONE WAY SORT 32 | function sortTable2(col) { 33 | let table, rows, preswitch = [], switching, i, x, y, shouldSwitch, dir, switchcount = 0; 34 | table = document.querySelector("#dbtable"); 35 | rows = table.rows; 36 | let tbody = document.querySelector("#dbtable > tbody"); 37 | let new_tbody = tbody.cloneNode(); 38 | 39 | for (i = 1; i < (rows.length); i++) { 40 | preswitch.push(rows[i].querySelectorAll("TD")[col].innerHTML) 41 | } 42 | //copy of preswitch 43 | let preswitch_copy = [...preswitch]; 44 | 45 | function merge(left, right) { 46 | let array = [] 47 | // Break out of loop if any one of the array gets empty 48 | while (left.length && right.length) { 49 | // Pick the smaller among the smallest element of left and right sub arrays 50 | if (left[0] < right[0]) { 51 | array.push(left.shift()) 52 | } else { 53 | array.push(right.shift()) 54 | } 55 | } 56 | 57 | // Concatenating the leftover elements 58 | // (in case we didn't go through the entire left or right array) 59 | // ... acts as a spread operator 60 | return [ ...array, ...left, ...right ] 61 | } 62 | 63 | function mergeSort(array) { 64 | const halflength = array.length / 2 65 | 66 | // Base case 67 | if(array.length < 2){ 68 | return array 69 | } 70 | 71 | const left = array.splice(0, halflength) 72 | return merge(mergeSort(left),mergeSort(array)) 73 | } 74 | 75 | sorted = mergeSort(preswitch); 76 | preswitch = preswitch_copy; 77 | console.log(preswitch); 78 | console.log(sorted); 79 | let index_array = []; 80 | // get the order of the sorted array based on the preswitch array 81 | sorted.forEach(value => { 82 | index_array.push(preswitch.indexOf(value)); 83 | // console.log(index_array); 84 | }) 85 | console.log(index_array); 86 | index_array.forEach(index => { 87 | // console.log(tbody.children[index].children[1].innerHTML); 88 | new_tbody.appendChild(tbody.children[index].cloneNode(true)); 89 | }) 90 | tbody.parentElement.replaceChild(new_tbody, tbody); 91 | } 92 | 93 | // TWO WAY SORT WITH UNDERLINE 94 | function sortTable2(col) { 95 | //console.log(lastCol); 96 | //console.log(dir); 97 | if (lastCol == undefined) lastCol = col; 98 | if (dir == undefined) dir = "asc"; 99 | if (lastCol != col) { 100 | dir = "asc"; 101 | lastCol = col; 102 | } 103 | let table, rows, preswitch = [], 104 | switching, i, x, y, shouldSwitch, switchcount = 0; 105 | table = document.querySelector("#dbtable"); 106 | rows = table.rows; 107 | console.log(document.querySelectorAll("TH")); 108 | for (heading of document.querySelectorAll("TH")) { 109 | console.log(heading) 110 | heading.style.textDecoration = "none"; 111 | } 112 | rows[0].querySelectorAll("TH")[col].style.textDecoration = "underline"; 113 | let tbody = document.querySelector("#dbtable > tbody"); 114 | let new_tbody = tbody.cloneNode(); 115 | //set direction to ascending initially 116 | if (dir == "asc") { 117 | dir = "desc" 118 | } else { 119 | dir = "asc" 120 | } 121 | 122 | for (i = 1; i < (rows.length); i++) { 123 | preswitch.push([rows[i].querySelectorAll("TD")[0].innerHTML,rows[i].querySelectorAll("TD")[col].innerHTML]) 124 | } 125 | //copy of preswitch 126 | //console.log(preswitch) 127 | let preswitch_copy = [...preswitch]; 128 | 129 | function merge(left, right) { 130 | let array = [] 131 | // Break out of loop if any one of the array gets empty 132 | while (left.length && right.length) { 133 | // Pick the smaller among the smallest element of left and right sub arrays 134 | if (left[0][1] < right[0][1]) { 135 | array.push(left.shift()) 136 | } else { 137 | array.push(right.shift()) 138 | } 139 | } 140 | 141 | // Concatenating the leftover elements 142 | // (in case we didn't go through the entire left or right array) 143 | // ... acts as a spread operator 144 | return [...array, ...left, ...right] 145 | } 146 | 147 | function mergeSort(array) { 148 | const halflength = array.length / 2 149 | 150 | // Base case 151 | if (array.length < 2) { 152 | return array 153 | } 154 | 155 | const left = array.splice(0, halflength) 156 | return merge(mergeSort(left), mergeSort(array)) 157 | } 158 | 159 | sorted = mergeSort(preswitch); 160 | preswitch = preswitch_copy; 161 | // console.log(preswitch); 162 | //console.log(sorted); 163 | let index_array = []; 164 | // get the order of the sorted array based on the preswitch array 165 | sorted.forEach(value => { 166 | index_array.push(preswitch.indexOf(value)); 167 | // console.log(index_array); 168 | }) 169 | //console.log(index_array); 170 | if(dir=="desc")index_array.reverse(); 171 | index_array.forEach(index => { 172 | // console.log(tbody.children[index].children[1].innerHTML); 173 | new_tbody.appendChild(tbody.children[index].cloneNode(true)); 174 | }) 175 | tbody.parentElement.replaceChild(new_tbody, tbody); 176 | } -------------------------------------------------------------------------------- /templates/dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Dashboard: {{ current_user.id }} 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 |
24 |

25 | {% if current_user.license.exists %} 26 |
27 | email: {{ current_user.email }} 28 |
29 | license: {{ current_user.license }} 30 |
31 | {% else %} 32 |
33 | email: {{ current_user.email }} 34 |
35 |
36 |
37 | {% endif %} 38 |
39 |
40 |
41 |
42 | 43 | 44 |
45 | {% if not current_user.license.exists %} 46 |
47 |
No license currently bound
48 |
49 | {% if lerror %} 50 |

Error: {{ lerror }} 51 | {% endif %} 52 |

53 | 55 | 57 |
58 |
59 |
60 | {% else %} 61 | {% if not current_user.license.boundtodevice %} 62 |
63 |
License Information
64 |

key: {{ current_user.license }}
click to unbind from account 66 | {{current_user.id}}

67 |

status: not currently bound

68 | Re-scramble Key 69 |
70 | {% else %} 71 |
72 |
License Information
73 |

key: {{ current_user.license }}
click to unbind from account 75 | {{current_user.id}}

76 |

status: bound to device {{ 77 | current_user.license.devicename}}

78 | Unbind from {{ 79 | current_user.license.devicename}} 80 | Re-scramble Key 81 |
82 | {%endif%} 83 | {%endif%} 84 |
85 |
86 |
87 |
88 | {% if not current_user.license.exists %} 89 |
90 |
Account and Billing
91 |

No impending charge as no license is bound.

92 | Edit Account Settings 93 | 94 | Edit Billing Settings 95 |
96 | {% else %} 97 |
98 |
Account and Billing
99 |

Next Charge of ${{ current_user.license.renewal.renewamount }} 100 | due on {{ current_user.license.renewal.renewdate }}

101 | Edit Account Settings 102 | 103 | Edit Billing Settings 104 |
105 | {% endif %} 106 |
107 |
108 |
109 |
110 |
111 | 116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 | SaaS distrubution system by Tom Holland :) 128 |
129 |
130 |
131 |
132 | 133 | 134 | -------------------------------------------------------------------------------- /api.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sqlite3 3 | from datetime import datetime 4 | 5 | 6 | class Database(): 7 | def __init__(self): 8 | self.conn = sqlite3.connect('./licenses.db') 9 | self.c = self.conn.cursor() 10 | 11 | def create(self): 12 | # creates and configues the database in the case it is empty or does not exist 13 | if os.path.getsize('licenses.db') != 0: 14 | return 'DB File already exists and has already been created.' 15 | else: 16 | self.c.execute('CREATE TABLE users (username text PRIMARY KEY, fName text, sName text, emailAddress text, password text, admin bool)') 17 | self.c.execute('CREATE TABLE licenses (license text PRIMARY KEY, username text, boundToUser boolean, boundToDevice boolean, HWID string, devicename string, nextrenewal string, plan string, stripeSessionID string)') 18 | self.c.execute('CREATE TABLE plans (name text PRIMARY KEY, interval integer, amount float)') 19 | self.conn.commit() 20 | return 'Created DB file' 21 | 22 | def closeConnection(self): 23 | # closes connection 24 | # necessary to be called as it is important to refrain from concurrent database connections where possible 25 | self.conn.close() 26 | return 27 | 28 | # --- non specific functions --- 29 | 30 | def getCountofTable(self, table): 31 | self.c.execute(f'''SELECT COUNT(*) FROM {table}''') 32 | result = self.c.fetchone() 33 | return result[0] 34 | 35 | def getConditionalCountofTable(self, table, column, condition): 36 | self.c.execute(f'''SELECT COUNT(*) FROM {table} WHERE {column} = {condition}''') 37 | result = self.c.fetchone() 38 | return result[0] 39 | 40 | def getMostPopular(self,table, column): 41 | self.c.execute(f'''SELECT {column}, COUNT({column}) AS value_occurance FROM {table} GROUP BY {column} ORDER BY value_occurance DESC LIMIT 1;''') 42 | result = self.c.fetchone() 43 | return result 44 | 45 | # --- user related functions --- 46 | 47 | def getAll(self, dbname): 48 | self.c.execute(f'''SELECT * FROM {dbname}''') 49 | result = self.c.fetchall() 50 | return result 51 | 52 | def searchUsersByUsername(self, user): 53 | self.c.execute(f'''SELECT * FROM users WHERE username = ?''', (user,)) 54 | result = self.c.fetchone() 55 | return result 56 | 57 | def addToUsers(self, values): 58 | if not self.searchUsersByUsername(tuple(values.split(','))[0]): 59 | self.c.execute(f'''INSERT INTO users(username,fName,sName,emailAddress,password,admin) 60 | VALUES(?, ?, ?, ?, ?, ?)''', tuple(values.split(','))) 61 | self.conn.commit() 62 | return 63 | else: 64 | return 'user already exists' 65 | 66 | def updateUser(self, param, value, username): 67 | self.c.execute(f'''UPDATE users SET {param} = "{value}" WHERE username = "{username}";''') 68 | self.conn.commit() 69 | return 70 | 71 | def searchUsers(self, email, user): 72 | self.c.execute(f'''SELECT username, emailAddress FROM users WHERE emailAddress = ? OR username = ?''', 73 | (email, user)) 74 | result = self.c.fetchone() 75 | return result 76 | 77 | def deleteUser(self, user): 78 | self.c.execute('''DELETE FROM users WHERE username = ?;''', (user,)) 79 | self.conn.commit() 80 | return 81 | 82 | # --- license related functions --- 83 | 84 | def getLicenseInfo(self, license): 85 | self.c.execute(f'''SELECT * FROM licenses WHERE license = ?''', 86 | (license,)) 87 | result = self.c.fetchone() 88 | return result 89 | 90 | def checkIfLicenseExists(self, license): 91 | self.c.execute(f'''SELECT * FROM licenses WHERE license = ?''', 92 | (license,)) 93 | result = self.c.fetchone() 94 | return bool(result) 95 | 96 | def checkIfLicenseBound(self, license): 97 | self.c.execute(f'''SELECT boundToUser FROM licenses WHERE license = ?''', 98 | (license,)) 99 | result = self.c.fetchone() 100 | if not result[0]: 101 | return False 102 | else: 103 | return True 104 | 105 | def checkIfUserHasLicense(self, username): 106 | self.c.execute(f'''SELECT license FROM licenses WHERE username = ?''', 107 | (username,)) 108 | result = self.c.fetchone() 109 | if result: 110 | return result[0] 111 | else: 112 | return False 113 | 114 | def getUserbyLicense(self, license): 115 | self.c.execute(f'''SELECT username FROM licenses WHERE license = ?''', 116 | (license,)) 117 | result = self.c.fetchone() 118 | if result: 119 | return result[0] 120 | else: 121 | return None 122 | 123 | def commitLicense(self, license, plan): 124 | self.c.execute( 125 | f'''INSERT INTO licenses(license,username,boundtoUser,boundtoDevice,HWID,devicename,nextrenewal,plan,stripeSessionID) 126 | VALUES(?, NULL, FALSE, FALSE, NULL, NULL, NULL, ?, "placeholder")''', 127 | (license, plan)) # needs to be updated to include plan, and validate that plane xists etc. 128 | self.conn.commit() 129 | return 130 | 131 | def setLicenseToUnbound(self, license): 132 | self.c.execute( 133 | f'''UPDATE licenses SET username = NULL, boundtoUser = False, boundtoDevice = False, HWID = NULL, devicename = NULL WHERE license = ?;''', 134 | (license,)) 135 | self.conn.commit() 136 | return 137 | 138 | def setLicenseToUnboundDEVICE(self, license): 139 | self.c.execute( 140 | f'''UPDATE licenses SET boundtoDevice = False, HWID = NULL, devicename = NULL WHERE license = ?;''', 141 | (license,)) 142 | self.conn.commit() 143 | return 144 | 145 | def setLicenseHWIDandDevice(self, license, hwid, devicename): 146 | self.c.execute( 147 | f'''UPDATE licenses SET boundtoDevice = True, HWID = ?, devicename = ? WHERE license = ?;''', 148 | (hwid, devicename, license,)) 149 | self.conn.commit() 150 | return 151 | 152 | def updateNextRenewal(self, license, date): 153 | self.c.execute(f'''UPDATE licenses SET nextrenewal = ? WHERE license = ?;''', 154 | (date, license)) 155 | self.conn.commit() 156 | return 157 | 158 | def updateLicenseKey(self, newlicense, oldlicense): 159 | self.c.execute(f'''UPDATE licenses SET license = ? WHERE license = ?;''', 160 | (newlicense, oldlicense)) 161 | self.conn.commit() 162 | return 163 | 164 | def getNextRenewal(self, license): 165 | self.c.execute(f'''SELECT nextrenewal FROM licenses WHERE license = ?''', (license,)) 166 | result = self.c.fetchone()[0] 167 | if not result or result == "NULL": 168 | return None 169 | else: 170 | return datetime.strptime(result, '%Y-%m-%d %H:%M:%S.%f') 171 | 172 | def getLicenseStripeSessionID(self, license): 173 | self.c.execute(f'''SELECT stripeSessionID FROM licenses WHERE license = ?''', (license,)) 174 | result = self.c.fetchone()[0] 175 | return result 176 | 177 | def getAllLicenseWithRenewal(self): 178 | self.c.execute(f'''SELECT license, nextrenewal FROM licenses WHERE nextrenewal != "None"''') 179 | result = self.c.fetchall() 180 | renewaldict = {} 181 | for value in result: 182 | renewaldict[value[0]] = datetime.strptime(value[1], '%Y-%m-%d %H:%M:%S.%f') 183 | return renewaldict 184 | 185 | def bindUsertoLicense(self, license, username): 186 | # attemps to bind a license to a certain user given the license is valid. 187 | if self.checkIfLicenseExists(license): 188 | if not self.checkIfLicenseBound(license): 189 | if not self.checkIfUserHasLicense(username): 190 | self.c.execute(f'''UPDATE licenses SET boundtoUser = TRUE, username = ? WHERE license = ?;''', 191 | (username, license)) 192 | print(f'bound {license} to {username}') 193 | self.conn.commit() 194 | return 'success' 195 | else: 196 | return f'User already has license {self.checkIfUserHasLicense(username)}' 197 | else: 198 | return f'That license is already bound to another user' 199 | else: 200 | return 'License doesnt exist' 201 | 202 | def getPlanfromLicense(self, license): 203 | self.c.execute( 204 | '''SELECT plans.* FROM plans JOIN licenses ON licenses.plan = plans.name WHERE licenses.license = ?;''', 205 | (license,)) 206 | result = self.c.fetchone() 207 | resultdict = {"name": result[0], 208 | "renewalinterval": result[1], 209 | "renewalprice": result[2]} 210 | return resultdict 211 | 212 | def getLicensesfromPlan(self, plan): 213 | self.c.execute( 214 | '''SELECT license FROM licenses where plan =?;''', 215 | (plan,)) 216 | result = self.c.fetchall() 217 | return result 218 | 219 | def findBoundLicensesOfGivenPlan(self, plan): 220 | self.c.execute( 221 | '''SELECT license FROM licenses where plan =? and boundToUser = 1;''', 222 | (plan,)) 223 | result = self.c.fetchall() 224 | return result 225 | 226 | def deleteLicensesOfGivenPlan(self, plan): 227 | self.c.execute('''DELETE FROM licenses WHERE plan = ?;''', (plan,)) 228 | self.conn.commit() 229 | return 230 | 231 | def deleteLicense(self, license): 232 | self.c.execute('''DELETE FROM licenses WHERE license = ?;''', (license,)) 233 | self.conn.commit() 234 | return 235 | 236 | # --- plan related functions --- 237 | 238 | def getPlanInfo(self, name): 239 | self.c.execute(f'''SELECT * FROM plans WHERE name = "{name}";''') 240 | result = self.c.fetchone() 241 | if not result: 242 | return None 243 | else: 244 | return result 245 | 246 | def createPlan(self, name, interval, amount): 247 | if not self.getPlanInfo(name): 248 | self.c.execute(f'''INSERT INTO plans(name,interval,amount) 249 | VALUES(?, ?, ?)''', (name, interval, amount)) 250 | self.conn.commit() 251 | return 252 | else: 253 | return 'Plan already exists' 254 | 255 | def deletePlan(self, plan): 256 | self.c.execute('''DELETE FROM plans WHERE name = ?;''', (plan,)) 257 | self.conn.commit() 258 | return 259 | 260 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, redirect, url_for, request, abort, jsonify, make_response, send_from_directory 2 | from flask_login import LoginManager, UserMixin, login_required, login_user, logout_user, current_user 3 | from flask_limiter import Limiter 4 | from flask_limiter.util import get_remote_address 5 | import re 6 | from api import Database 7 | import monitor 8 | import utils 9 | import os 10 | import time 11 | import datetime 12 | import json 13 | import threading 14 | import random 15 | import csv 16 | 17 | 18 | class Renewal: 19 | def __init__(self, key): 20 | self.renewdate = self.getRenewalDate(key) 21 | self.renewamount = None 22 | self.renewinterval = None 23 | 24 | # running of this function should result in the last two being defined. 25 | self.getRenewalInfoFromPlan(key) 26 | 27 | def getRenewalDate(self, key): 28 | # gets date of the next renewal given a key 29 | db = Database() 30 | dbdate = db.getNextRenewal(key) 31 | return dbdate 32 | 33 | def commitRenewdatetoDatabase(self, key): 34 | # commits a renewal date to db following a change within the class 35 | dbconn = Database() 36 | dbconn.updateNextRenewal(key, self.renewdate) 37 | dbconn.closeConnection() 38 | return 39 | 40 | def getRenewalInfoFromPlan(self, key): 41 | # gets all information regarding a licenses renewal through its plan attribute 42 | db = Database() 43 | planinfo = db.getPlanfromLicense(key) 44 | self.renewamount = float(planinfo['renewalprice']) 45 | self.renewinterval = int(planinfo['renewalinterval']) 46 | return planinfo 47 | 48 | def incrementRenewalDate(self): 49 | # increments the renewal date the correct period in the case the license is renewed 50 | if self.renewdate and self.renewdate != 'Error reading DB': 51 | self.renewdate = self.renewdate + datetime.timedelta(days=self.renewinterval) 52 | return self.renewdate 53 | 54 | def initalRenewalIncrement(self, key): 55 | # performs the inital renewal increment, adding correct period of days on its first bind 56 | if not self.renewdate: 57 | self.renewdate = datetime.datetime.now() 58 | self.incrementRenewalDate() 59 | self.commitRenewdatetoDatabase(key) 60 | return 61 | else: 62 | return 'Not inital' 63 | 64 | 65 | class License: 66 | # this is a class that describes the license in context of the user its bound to, only. 67 | def __init__(self, owner): 68 | self.owner = owner 69 | self.hwid = None 70 | self.boundtodevice = False 71 | self.devicename = None 72 | self.renewal = None 73 | 74 | self.key = self.loadUserLicense() 75 | # self.exists is necessary as self.key being None cannot necessarily be represented in conitional statements (due to str dunder), otherwise. 76 | if self.key: 77 | self.exists = True 78 | self.renewal = Renewal(self.key) 79 | else: 80 | self.exists = False 81 | 82 | def __str__(self): 83 | return str(self.key) 84 | 85 | def __repr__(self): 86 | return self.__str__() 87 | 88 | def loadUserLicense(self): 89 | # loads all license related information in relation to a user, when given a user. 90 | db = Database() 91 | license = db.checkIfUserHasLicense(self.owner) 92 | if not license: 93 | db.closeConnection() 94 | self.key = None 95 | self.exists = False 96 | return None 97 | else: 98 | self.key = license 99 | self.exists = True 100 | self.renewal = Renewal(self.key) 101 | 102 | licenseinfo = db.getLicenseInfo(license) 103 | if licenseinfo[3] == 1: 104 | self.boundtodevice = True 105 | else: 106 | self.boundtodevice = False 107 | self.hwid = licenseinfo[4] 108 | self.devicename = licenseinfo[5] 109 | return license 110 | 111 | def unbindDevice(self): 112 | # sets the device the license is bound to to none 113 | if not self.key: 114 | return 'No license currently bound to account' 115 | else: 116 | if not self.boundtodevice: 117 | return 'License not currently bound to a device to unbind from' 118 | else: 119 | db = Database() 120 | db.setLicenseToUnboundDEVICE(self.key) 121 | db.closeConnection() 122 | self.hwid = None 123 | self.devicename = None 124 | self.boundtodevice = False 125 | return 126 | 127 | def rescramble(self): 128 | # rescrambles the key identifier to a unique value 129 | if not self.key: 130 | return 'No license currently bound to account' 131 | else: 132 | while True: 133 | license = utils.generatekey(random_chars=16) 134 | # 79586 6110994640 0884391936 combinations 135 | conn = Database() 136 | if conn.checkIfLicenseExists(license): 137 | continue 138 | else: 139 | print(f'found unused license value {license}') 140 | self.unbindDevice() 141 | conn.updateLicenseKey(license, self.key) 142 | self.loadUserLicense() 143 | conn.closeConnection() 144 | return license 145 | return 146 | 147 | 148 | class User(UserMixin): 149 | def __init__(self, username, fname, sname, email, password, couldHaveLicense=True): 150 | self.id = username 151 | self.fname = fname 152 | self.sname = sname 153 | self.email = email 154 | self.hashdpassword = password 155 | self.authenticated = False 156 | self.isadmin = False 157 | 158 | if couldHaveLicense: 159 | self.license = License(self.id) 160 | 161 | def __str__(self): 162 | return self.id 163 | 164 | def unbindLicense(self): 165 | # unbinds a license from a user's account, called by an ajax function. 166 | if self.license: 167 | db = Database() 168 | db.setLicenseToUnbound(self.license.key) 169 | db.closeConnection() 170 | self.license = None 171 | return 172 | else: 173 | return 'No License bound previously' 174 | 175 | def getAdminPerms(self): 176 | return self.isadmin 177 | 178 | 179 | class AdministativeUser(User): 180 | def __init__(self, username, fname, sname, email, password): 181 | super().__init__(username, fname, sname, email, password, couldHaveLicense=False) 182 | self.isadmin = True 183 | 184 | 185 | app = Flask(__name__) 186 | # secret key for encoding of session on the webapp 187 | app.secret_key = os.urandom(24) 188 | 189 | login_manager = LoginManager(app) 190 | login_manager.login_view = "login" 191 | 192 | limiter = Limiter( 193 | app=app, 194 | key_func=get_remote_address, 195 | default_limits=["20 per second"], 196 | ) 197 | 198 | 199 | @login_manager.user_loader 200 | def load_user(username): 201 | # function for loading all appropriate user data, and the specific user type, on login 202 | dbconnection = Database() 203 | result = dbconnection.searchUsersByUsername(username) 204 | dbconnection.closeConnection() 205 | if result: 206 | if result[5] == "FALSE": 207 | return User(result[0], result[1], result[2], result[3], result[4]) 208 | else: 209 | return AdministativeUser(result[0], result[1], result[2], result[3], result[4]) 210 | else: 211 | return None 212 | 213 | 214 | # api based functs 215 | 216 | @app.route("/unbindaccount") 217 | @login_required 218 | def unbindkey(): 219 | # ajax-called function for unbinding key from a account 220 | current_user.unbindLicense() 221 | return redirect(url_for('dashboard')) 222 | 223 | 224 | @app.route("/unbinddevice") 225 | @login_required 226 | def unbinddevice(): 227 | # ajax-called function for unbinding device from a key 228 | current_user.license.unbindDevice() 229 | return redirect(url_for('dashboard')) 230 | 231 | 232 | @app.route("/rescramblelicense") 233 | @login_required 234 | def rescramblelicense(): 235 | # ajax-called function for rescrambling license identifier 236 | current_user.license.rescramble() 237 | return redirect(url_for('dashboard')) 238 | 239 | 240 | # front end webapp endpoints 241 | 242 | @app.route('/favicon.ico') 243 | def getfavicon(): 244 | # returns favicon for any page on the domain 245 | # otherwise risks returning 500s if this endpoint is not present. 246 | return send_from_directory(os.path.join(app.root_path, 'static'), 247 | 'favicon.ico', mimetype='image/vnd.microsoft.icon') 248 | 249 | 250 | @app.route('/') 251 | def index(): 252 | # front page of website 253 | return render_template('home.html') 254 | 255 | 256 | @app.route('/login', methods=['GET', 'POST']) 257 | def login(): 258 | # validates inputs and compares with database 259 | error = None 260 | if current_user.is_authenticated: 261 | return redirect(url_for('dashboard')) 262 | if request.method == 'POST': 263 | temp = Database() 264 | result = temp.searchUsersByUsername(request.form['username']) 265 | temp.closeConnection() 266 | if not result: 267 | error = 'No account with that username.\nIf you do not yet have an account, you can sign up with the above link.' 268 | else: 269 | hashdpw = utils.hash(request.form['username'], request.form['password']) 270 | if hashdpw == result[4]: 271 | user = load_user(request.form['username']) 272 | login_user(user) 273 | if current_user.getAdminPerms(): 274 | return redirect(url_for('admindash')) 275 | else: 276 | return redirect(url_for('dashboard')) 277 | else: 278 | error = 'Invalid password!' 279 | return render_template('login.html', error=error) 280 | 281 | 282 | @app.route('/signup', methods=['GET', 'POST']) 283 | def signup(): 284 | # validates inputs and commits to database 285 | mailregex = r"^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w{2,3}$" 286 | pwregex = r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$" 287 | unameregex = r"^[A-Za-z0-9]+$" 288 | error = None 289 | 290 | if current_user.is_authenticated: 291 | return redirect(url_for('dashboard')) 292 | if request.method == 'POST': 293 | temp = Database() 294 | if ' ' not in request.form['name'] or len(request.form['name'].split(' ')) != 2: 295 | error = 'We require your first and surname, with a space inbetween!' 296 | elif not re.search(unameregex, request.form['username']): 297 | error = 'Your username cannot contain any spaces!' 298 | elif not re.search(mailregex, request.form['email']): 299 | error = 'Email Invalid' 300 | elif not re.search(pwregex, request.form['password']): 301 | error = 'Password invalid. Must be 8+ characters, including at least one upper-case letter, lower-case letter, number and special character.' 302 | elif request.form['password'] != request.form['confirmpassword']: 303 | error = 'Your passwords do not match.' 304 | elif temp.searchUsers(request.form['email'], 305 | request.form['username']): # checks if this returns anythng other than NONE 306 | error = 'An account using that email or username already exists!' 307 | else: 308 | hashdpw = utils.hash(request.form['username'], request.form['password']) 309 | temp.addToUsers( 310 | f'''{request.form['username']},{request.form['name'].split()[0]},{request.form['name'].split()[1]},{request.form['email']},{hashdpw},FALSE''') 311 | user = load_user(request.form['username']) 312 | login_user(user) 313 | temp.closeConnection() 314 | return redirect(url_for('dashboard')) 315 | 316 | return render_template('signup.html', error=error) 317 | 318 | 319 | @app.route("/logout") 320 | @login_required 321 | def logout(): 322 | # logs out of the application 323 | reason = f'logging out of account {current_user}!' 324 | logout_user() 325 | return render_template('redirect.html', reason=reason) 326 | 327 | 328 | @app.route("/dashboard", methods=['GET', 'POST']) 329 | @login_required 330 | def dashboard(): 331 | # returns user based dashboad 332 | if not current_user.getAdminPerms(): 333 | lerror = None 334 | if request.method == 'POST' and request.form['licenseid'] != '': 335 | temp = Database() 336 | result = temp.bindUsertoLicense(request.form['licenseid'], current_user.id) 337 | if result == "success": 338 | current_user.license.loadUserLicense() 339 | if not current_user.license.renewal.getRenewalDate(current_user.license.key): 340 | current_user.license.renewal.initalRenewalIncrement(current_user.license.key) 341 | else: 342 | lerror = result 343 | print(f'ERROR: {lerror}') 344 | 345 | # redirect appropriate as to avoid POST callbacks 346 | # explained beautifully here: https://www.youtube.com/watch?v=JQFeEscCvTg&ab_channel=DaveHollingworth 347 | return redirect(url_for('dashboard')) 348 | 349 | return render_template('dashboard.html', lerror=lerror) 350 | else: 351 | return redirect(url_for('admindash')) 352 | 353 | 354 | @app.route("/dashboard/account", methods=['GET', 'POST']) 355 | @login_required 356 | def dashboardaccount(): 357 | # allows for user account settings to be edited following their arrival at their dashboard 358 | if not current_user.getAdminPerms(): 359 | error = None 360 | mailregex = r"^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w{2,3}$" 361 | namesregex = r"[a-zA-Z]+" 362 | pwregex = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$" 363 | 364 | if request.method == 'POST': 365 | form = request.form.to_dict() 366 | if utils.hash(current_user.id, form['cpassword']) == current_user.hashdpassword: 367 | for regex, value, potentialerror in zip([namesregex, namesregex, mailregex], 368 | [form['fname'], form['sname'], form['email']], 369 | ["Invalid first name", "Invalid surname", "Invalid email"]): 370 | if not re.fullmatch(regex, value): 371 | return render_template('dashboardaccount.html', error=potentialerror) 372 | if request.form['newpassword']: 373 | if not re.fullmatch(pwregex, form['newpassword']): 374 | return render_template('dashboardaccount.html', error='Not a valid password!') 375 | else: 376 | form['newpassword'] = utils.hash(current_user.id, request.form['newpassword']) 377 | 378 | db = Database() 379 | 380 | keymap = {"newpassword": "password", "fname": "fName", "sname": "sName", "email": "emailAddress"} 381 | for k, v in form.items(): 382 | if v != "" and k != "cpassword": 383 | db.updateUser(keymap[k], v, current_user.id) 384 | 385 | db.closeConnection() 386 | return render_template('redirect.html', reason='Successfully commited changes!') 387 | else: 388 | error = 'Current password is needed to commit changes and is incorrect/missing' 389 | 390 | return render_template('dashboardaccount.html', error=error) 391 | else: 392 | return redirect(url_for('admindash')) 393 | 394 | 395 | @app.route("/admin/dashboard", methods=['GET', 'POST']) 396 | @login_required 397 | def admindash(): 398 | # shows overview page with a selection of interesting stats 399 | if current_user.getAdminPerms(): 400 | statsdict = utils.gatherStatistics() 401 | randomstats = [[statsdict[value], value] for value in random.sample(list(statsdict), 3)] 402 | return render_template('admindash.html', stats=randomstats) 403 | else: 404 | reason = f'Insufficient permissions.' 405 | return render_template('redirect.html', reason=reason) 406 | 407 | 408 | @app.route("/admin/dashboard/users", methods=['GET', 'POST']) 409 | @login_required 410 | def adminusers(): 411 | # endpoint for reaching users table 412 | if current_user.getAdminPerms(): 413 | # in the case where users are attempting to be deleted (POST) 414 | if request.method == "POST": 415 | try: 416 | db = Database() 417 | if db.checkIfUserHasLicense(request.form['delete']): 418 | db.setLicenseToUnbound(db.checkIfUserHasLicense(request.form['delete'])) 419 | db.deleteUser(request.form['delete']) 420 | db.closeConnection() 421 | 422 | return redirect(url_for('adminusers')) 423 | 424 | except: 425 | return redirect(url_for("adminusers")) 426 | 427 | db = Database() 428 | tempusers = db.getAll('users') 429 | users = [] 430 | for user in tempusers: 431 | user = list(user) 432 | dbattempt = db.checkIfUserHasLicense(user[0]) 433 | if dbattempt: 434 | user.append(dbattempt) 435 | else: 436 | user.append(None) 437 | users.append(user) 438 | db.closeConnection() 439 | return render_template('adminusers.html', users=users) 440 | else: 441 | reason = f'Insufficient permissions.' 442 | return render_template('redirect.html', reason=reason) 443 | 444 | 445 | @app.route("/admin/dashboard/licenses", methods=['GET', 'POST']) 446 | @login_required 447 | def adminlicenses(): 448 | # endpoint for reaching licenses table 449 | if current_user.getAdminPerms(): 450 | # in the case where licenses are attempting to be made / deleted (POST) 451 | if request.method == 'POST': 452 | try: 453 | if not 'delete' in request.form: 454 | arr = [] 455 | for _ in range(int(request.form['amount'])): 456 | key = utils.createLicense(request.form['plans']) 457 | arr.append(key) 458 | 459 | filename = 'gennedkeys.txt' 460 | with open(f'temp/{filename}', 'w') as output: 461 | for key in arr: 462 | output.write("%s\n" % key) 463 | 464 | # return redirect(url_for('dashboard')) 465 | return send_from_directory(directory=app_config['UPLOAD_DIRECTORY_TEMP'], filename=filename, 466 | as_attachment=True) 467 | else: 468 | db = Database() 469 | db.deleteLicense(request.form['delete']) 470 | db.closeConnection() 471 | return redirect(url_for('adminlicenses')) 472 | except: 473 | return redirect(url_for('adminlicenses')) 474 | 475 | db = Database() 476 | licenses = db.getAll('licenses') 477 | plans = db.getAll('plans') 478 | db.closeConnection() 479 | return render_template('adminlicenses.html', licenses=licenses, plans=plans) 480 | else: 481 | reason = f'Insufficient permissions.' 482 | return render_template('redirect.html', reason=reason) 483 | 484 | 485 | @app.route("/admin/dashboard/plans", methods=['GET', 'POST']) 486 | @login_required 487 | def adminplans(): 488 | # endpoint for reaching plans table 489 | if current_user.getAdminPerms(): 490 | # in the case where plans are attempting to be made / deleted (POST) 491 | if request.method == 'POST': 492 | try: 493 | if not 'delete' in request.form: 494 | db = Database() 495 | db.createPlan(request.form['name'], request.form['days'], request.form['price']) 496 | db.closeConnection() 497 | 498 | return redirect(url_for('adminplans')) 499 | else: 500 | db = Database() 501 | if db.findBoundLicensesOfGivenPlan(request.form['delete']): 502 | print('cannot delete') 503 | db = Database() 504 | plans = list(db.getAll('plans')) 505 | db.closeConnection() 506 | return render_template('adminplans.html', plans=plans, 507 | reason='Cannot delete as a user(s) currently has a license of this plan type bound, delete this license first.') 508 | else: 509 | db.deleteLicensesOfGivenPlan(request.form['delete']) 510 | db.deletePlan(request.form['delete']) 511 | db.closeConnection() 512 | return redirect(url_for('adminplans')) 513 | except: 514 | return redirect(url_for('adminplans')) 515 | 516 | db = Database() 517 | plans = list(db.getAll('plans')) 518 | db.closeConnection() 519 | return render_template('adminplans.html', plans=plans, reason=None) 520 | else: 521 | reason = f'Insufficient permissions.' 522 | return render_template('redirect.html', reason=reason) 523 | 524 | 525 | @app.route("/admin/dashboard/documentation", methods=['GET', 'POST']) 526 | @login_required 527 | def admindocs(): 528 | # endpoint for accessing documentation 529 | if current_user.getAdminPerms(): 530 | if request.method == 'POST': 531 | # in the case where user attempts to download a API wrapper file 532 | return send_from_directory(directory=app_config['UPLOAD_DIRECTORY_MAIN'], filename='examplerequests.py', 533 | as_attachment=True) 534 | return render_template('admindocs.html', api_key=app_config['api_key']) 535 | else: 536 | reason = f'Insufficient permissions.' 537 | return render_template('redirect.html', reason=reason) 538 | 539 | 540 | # API speicifc functions 541 | 542 | @app.errorhandler(404) 543 | def not_found(e): 544 | # error handler in the case of unrecognised endpoint 545 | return render_template('redirect.html', reason='Unrecognised endpoint.') 546 | 547 | 548 | @app.errorhandler(400) 549 | def bad_syntax(e): 550 | # error handler in the case of unrecognised request body, through API 551 | return make_response(jsonify({'error': 'malformed syntax, seek docs'}), 400) 552 | 553 | 554 | @app.route('/api/v1/licenses/', methods=['GET', 'POST']) 555 | @limiter.limit("2 per second") 556 | def get_specific_license(licenseid): 557 | # entire API endpoint which accepts both GET and POST requests 558 | try: 559 | if request.headers['api_key'] == app_config['api_key']: 560 | if request.method == "GET": 561 | try: 562 | db = Database() 563 | result = db.getLicenseInfo(licenseid) 564 | print(result) 565 | licensedict = { 566 | "lickey": result[0], 567 | "user": result[1], 568 | "boundToUser": result[2], 569 | "boundToDevice": result[3], 570 | "HWID": result[4], 571 | "device": result[5], 572 | "nextRen": result[6], 573 | "planName": result[7] 574 | } 575 | 576 | return jsonify({"license": licensedict}) 577 | except: 578 | return jsonify({"license": "could not find license"}) 579 | elif request.method == "POST": 580 | print(request.json) 581 | if "HWID" and "device" in request.json: 582 | db = Database() 583 | if not request.json["HWID"] and not request.json["device"]: 584 | db.setLicenseToUnboundDEVICE(licenseid) 585 | else: 586 | db.setLicenseHWIDandDevice(licenseid, request.json["HWID"], request.json["device"]) 587 | 588 | result = db.getLicenseInfo(licenseid) 589 | licensedict = { 590 | "lickey": result[0], 591 | "user": result[1], 592 | "boundToUser": result[2], 593 | "boundToDevice": result[3], 594 | "HWID": result[4], 595 | "device": result[5], 596 | "nextRen": result[6], 597 | "planName": result[7] 598 | } 599 | 600 | return jsonify({"status": "updated", "license": licensedict}) 601 | else: 602 | return jsonify({"status": "malformed request in post, needs HWID and device"}) 603 | 604 | else: 605 | return jsonify({"status": "unauthorised"}) 606 | except: 607 | return jsonify({"status": "fatal error, perhaps malformed request"}) 608 | 609 | 610 | if __name__ == '__main__': 611 | with open('config.json', 'r') as configfile: 612 | # open and read config file 613 | app_config = json.load(configfile) 614 | 615 | # initalistaion of the database 616 | db = Database() 617 | db.create() 618 | 619 | # creates and runs monitor renewal function on a secondary daemon thread 620 | monitorfunct = threading.Thread(name='monitor', target=monitor.monitorRenewals, daemon=True) 621 | monitorfunct.start() 622 | 623 | # creates and runs monitor stats function on a secondary daemon thread 624 | monitorstats = threading.Thread(name='monitorstats', target=monitor.monitorGraphs, daemon=True) 625 | monitorstats.start() 626 | 627 | # runs flask application on main thread 628 | app.run() 629 | --------------------------------------------------------------------------------