├── 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 |
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 |
71 |
72 |
73 |
74 |
75 |
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 |
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 |
27 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | #
42 | Name
43 | Renewal Interval
44 | Renewal Price
45 |
46 |
47 |
48 |
49 | {% for value in plans %}
50 |
51 | {{ plans.index(value) }}
52 | {{ value[0] }}
53 | {{ value[1] }} Days
54 | £{{ value[2] }}
55 |
56 |
57 | {% endfor %}
58 |
59 |
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 |
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 |
27 |
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 |
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 |
27 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | #
42 | License
43 | User
44 | Plan
45 | Next Renewal
46 | Bound to device
47 |
48 |
49 |
50 |
51 | {% for value in licenses %}
52 |
53 | {{ licenses.index(value) }}
54 | {{ value[0] }}
55 | {% if value[1] != None %}
56 | @{{ value[1] }}
57 | {% else %}
58 | {{ value[1] }}
59 | {% endif %}
60 | {{ value[7] }}
61 | {{ value[6] }}
62 | {{ value[5] }}
63 |
64 |
65 | {% endfor %}
66 |
67 |
68 |
69 |
70 |
71 |
Create Licenses
72 | Please be aware, page may need to be refreshed to show changes in table.
73 | Generate (file download indicated execution has finished)
74 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
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 |
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 |
--------------------------------------------------------------------------------