diff --git a/base/admin.py b/base/admin.py index 34ef61e..db6864e 100644 --- a/base/admin.py +++ b/base/admin.py @@ -1,3 +1,18 @@ from django.contrib import admin -# Register your models here. \ No newline at end of file +# Register your models here. +from .models import VendorsData, CryptoCoins, VendorsAddresses + +class VendorDetails(admin.ModelAdmin): + fields = ["vendor"] + list_display = ("vendorid_id", "vendor", "vendorWebName", "vendorWebAddr", "vendorAddDate", "vendorIsActive") + +class CryptoDetails(admin.ModelAdmin): + list_display = ("coinName", "coinSymbol", "coinAddDate", "coinIsActive" ) + +class VendorAddressDetails(admin.ModelAdmin): + list_display = ("coin", "address", "vendor_id", "vendorid_id", "addrIsActive") + +admin.site.register(VendorsData, VendorDetails) +admin.site.register(CryptoCoins, CryptoDetails) +admin.site.register(VendorsAddresses, VendorAddressDetails) \ No newline at end of file diff --git a/base/forms.py b/base/forms.py new file mode 100644 index 0000000..9a45957 --- /dev/null +++ b/base/forms.py @@ -0,0 +1,12 @@ +from django.forms import ModelForm +from .models import VendorsData, VendorsAddresses + +class VendorEditForm(ModelForm): + class Meta: + model = VendorsData + fields = ['vendorCoverAmount', 'vendorPaidNotification', 'vendorPayWindow', 'vendorWebName', 'vendorWebAddr', 'vendorNetworkFee'] + +class VendorAddrAddForm(ModelForm): + class Meta: + model = VendorsAddresses + fields = ['coin', 'address'] \ No newline at end of file diff --git a/base/functions.py b/base/functions.py new file mode 100644 index 0000000..ef5b68a --- /dev/null +++ b/base/functions.py @@ -0,0 +1,629 @@ +import string, hashlib, binascii, random, socket, base58, sys +from typing import Union +from urllib.parse import urlparse +from ipaddress import ip_address, ip_network, IPv4Address +from base58 import b58decode_check, b58encode_check +from enum import Enum + +def vendor_generator(size=6, chars=string.ascii_uppercase + string.digits): + return ''.join(random.choice(chars) for _ in range(size)) + + +def decodeBase58(address): + decoded = base58.b58decode(address).hex() + prefixAndHash = decoded[:len(decoded)-8] + checksum = decoded[len(decoded)-8:] + hash = prefixAndHash + for _ in range(1,3): + hash = hashlib.sha256(binascii.unhexlify(hash)).hexdigest() + if(checksum == hash[:8]): + return True + return False + +def decodeMonero(address): + # decode monero address + if len(address) == 95 or len(address) == 106: + # Decode the address from Base58 + decoded = decode(address) + + # Verify address type byte + if decoded[0:2] not in ["12", "2a", "13"]: + return False + + # Verify checksum + # later + return True + return False + +def checksumCheck(method, address): + match method.lower(): + case 'btc': + return decodeBase58(address) if address[0] == '1' or address[0] == '3' else True if address[0:3] == 'bc1' and decode("bc", address)[0] != None else False + case 'btct': + return decodeBase58(address) if address[0] == '2' else True if address[0:3] == 'tb1' and decode("tb", address)[0] != None else False + case 'ltc': + return decodeBase58(address) if address[0] == '3' or address[0] == 'M' or address[0] == 'L' else True if address[0:4] == 'ltc1' and decode("ltc", address)[0] != None else False + case 'bch': + return is_valid(address) if address[0] == '1' else True if is_valid('bitcoincash:'+address) == True else False + case 'zec': + return decodeBase58(address) if address[0] == 't' or address[0] == 'z' else False + case 'xmr': + #needs new function to check if address is valid, a decoder maybe + return decodeMonero(address) + case _: + return False + +######################################################################### +## UrlValidator +######################################################################### + +class UrlValidator: + @staticmethod + def is_internal_address(ip: Union[IPv4Address]) -> bool: + return any([ + ip.is_private, + ip.is_unspecified, + ip.is_reserved, + ip.is_loopback, + ip.is_multicast, + ip.is_link_local, + ]) + + @classmethod + def validate(cls, url: str): + DEFAULT_PORT_WHITELIST = {80, 81, 8080, 443, 8443, 8000} + DEFAULT_SCHEME_WHITELIST = {'http', 'https'} + DEFAULT_HOST_BLACKLIST = {'192.0.0.192', '169.254.169.254', '100.100.100.200', 'metadata.packet.net', 'metadata.google.internal'} + DEFAULT_CHARACTER_WHITELIST = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789:/-_.?&=' + + if url is None: + return False + + whitelist_set = set(DEFAULT_CHARACTER_WHITELIST) + if any(c not in whitelist_set for c in url): + return False + + try: + ip = ip_address(url) + except ValueError: + try: + host = urlparse(url).hostname + ip = ip_address(str(socket.gethostbyname(host))) + except: + return False + + port_whitelist = DEFAULT_PORT_WHITELIST.copy() + scheme_whitelist = DEFAULT_SCHEME_WHITELIST.copy() + host_blacklist = DEFAULT_HOST_BLACKLIST.copy() + + try: + port, scheme = urlparse(url).port, urlparse(url).scheme + except: + return False + + if scheme_whitelist and scheme is not None and scheme not in scheme_whitelist: + return False + + if host_blacklist and host is not None and host in host_blacklist: + return False + + if port_whitelist and port is not None and port not in port_whitelist: + return False + + if ip.version == 4: + if not ip.is_private: + # CGNAT IPs do not set `is_private` so `not is_global` added + if not ip_network(ip).is_global: + return False + else: + return False + + if cls.is_internal_address(ip): + return False + + return True + + +"""Reference implementation for Bech32/Bech32m and segwit addresses.""" +######################################################################### +## SEGWIT +######################################################################### + +class Encoding(Enum): + """Enumeration type to list the various supported encodings.""" + BECH32 = 1 + BECH32M = 2 + +CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" +BECH32M_CONST = 0x2bc830a3 + +def bech32_polymod(values): + """Internal function that computes the Bech32 checksum.""" + generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + chk = 1 + for value in values: + top = chk >> 25 + chk = (chk & 0x1ffffff) << 5 ^ value + for i in range(5): + chk ^= generator[i] if ((top >> i) & 1) else 0 + return chk + + +def bech32_hrp_expand(hrp): + """Expand the HRP into values for checksum computation.""" + return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] + + +def bech32_verify_checksum(hrp, data): + """Verify a checksum given HRP and converted data characters.""" + const = bech32_polymod(bech32_hrp_expand(hrp) + data) + if const == 1: + return Encoding.BECH32 + if const == BECH32M_CONST: + return Encoding.BECH32M + return None + +def bech32_create_checksum(hrp, data, spec): + """Compute the checksum values given HRP and data.""" + values = bech32_hrp_expand(hrp) + data + const = BECH32M_CONST if spec == Encoding.BECH32M else 1 + polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const + return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] + + +def bech32_encode(hrp, data, spec): + """Compute a Bech32 string given HRP and data values.""" + combined = data + bech32_create_checksum(hrp, data, spec) + return hrp + '1' + ''.join([CHARSET[d] for d in combined]) + +def bech32_decode(bech): + """Validate a Bech32/Bech32m string, and determine HRP and data.""" + if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or + (bech.lower() != bech and bech.upper() != bech)): + return (None, None, None) + bech = bech.lower() + pos = bech.rfind('1') + if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: + return (None, None, None) + if not all(x in CHARSET for x in bech[pos+1:]): + return (None, None, None) + hrp = bech[:pos] + data = [CHARSET.find(x) for x in bech[pos+1:]] + spec = bech32_verify_checksum(hrp, data) + if spec is None: + return (None, None, None) + return (hrp, data[:-6], spec) + +def convertbits(data, frombits, tobits, pad=True): + """General power-of-2 base conversion.""" + acc = 0 + bits = 0 + ret = [] + maxv = (1 << tobits) - 1 + max_acc = (1 << (frombits + tobits - 1)) - 1 + for value in data: + if value < 0 or (value >> frombits): + return None + acc = ((acc << frombits) | value) & max_acc + bits += frombits + while bits >= tobits: + bits -= tobits + ret.append((acc >> bits) & maxv) + if pad: + if bits: + ret.append((acc << (tobits - bits)) & maxv) + elif bits >= frombits or ((acc << (tobits - bits)) & maxv): + return None + return ret + + +def decode(hrp, addr): + """Decode a segwit address.""" + hrpgot, data, spec = bech32_decode(addr) + if hrpgot != hrp: + return (None, None) + decoded = convertbits(data[1:], 5, 8, False) + if decoded is None or len(decoded) < 2 or len(decoded) > 40: + return (None, None) + if data[0] > 16: + return (None, None) + if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: + return (None, None) + if data[0] == 0 and spec != Encoding.BECH32 or data[0] != 0 and spec != Encoding.BECH32M: + return (None, None) + return (data[0], decoded) + + +def encode(hrp, witver, witprog): + """Encode a segwit address.""" + spec = Encoding.BECH32 if witver == 0 else Encoding.BECH32M + ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5), spec) + if decode(hrp, ret) == (None, None): + return None + return ret + +######################################################################### +## CRYPTO +######################################################################### +CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l' + +def polymod(values): + chk = 1 + generator = [ + (0x01, 0x98f2bc8e61), + (0x02, 0x79b76d99e2), + (0x04, 0xf33e5fb3c4), + (0x08, 0xae2eabe2a8), + (0x10, 0x1e4f43e470)] + for value in values: + top = chk >> 35 + chk = ((chk & 0x07ffffffff) << 5) ^ value + for i in generator: + if top & i[0] != 0: + chk ^= i[1] + return chk ^ 1 + + +def prefix_expand(prefix): + return [ord(x) & 0x1f for x in prefix] + [0] + + +def calculate_checksum(prefix, payload): + poly = polymod(prefix_expand(prefix) + payload + [0, 0, 0, 0, 0, 0, 0, 0]) + out = list() + for i in range(8): + out.append((poly >> 5 * (7 - i)) & 0x1f) + return out + + +def verify_checksum(prefix, payload): + return polymod(prefix_expand(prefix) + payload) == 0 + + +def b32decode(inputs): + out = list() + for letter in inputs: + out.append(CHARSET.find(letter)) + return out + + +def b32encode(inputs): + out = '' + for char_code in inputs: + out += CHARSET[char_code] + return out + + +def convertbits(data, frombits, tobits, pad=True): + acc = 0 + bits = 0 + ret = [] + maxv = (1 << tobits) - 1 + max_acc = (1 << (frombits + tobits - 1)) - 1 + for value in data: + if value < 0 or (value >> frombits): + return None + acc = ((acc << frombits) | value) & max_acc + bits += frombits + while bits >= tobits: + bits -= tobits + ret.append((acc >> bits) & maxv) + if pad: + if bits: + ret.append((acc << (tobits - bits)) & maxv) + elif bits >= frombits or ((acc << (tobits - bits)) & maxv): + return None + return ret + + +######################################################################### +## BCH CONVERT +######################################################################### +class InvalidAddress(Exception): + pass + + +class Address: + VERSION_MAP = { + 'legacy': [ + ('P2SH', 5, False), + ('P2PKH', 0, False), + ('P2SH-TESTNET', 196, True), + ('P2PKH-TESTNET', 111, True) + ], + 'cash': [ + ('P2SH', 8, False), + ('P2PKH', 0, False), + ('P2SH-TESTNET', 8, True), + ('P2PKH-TESTNET', 0, True) + ] + } + MAINNET_PREFIX = 'bitcoincash' + TESTNET_PREFIX = 'bchtest' + + def __init__(self, version, payload, prefix=None): + self.version = version + self.payload = payload + if prefix: + self.prefix = prefix + else: + if Address._address_type('cash', self.version)[2]: + self.prefix = self.TESTNET_PREFIX + else: + self.prefix = self.MAINNET_PREFIX + + def __str__(self): + return 'version: {}\npayload: {}\nprefix: {}'.format(self.version, self.payload, self.prefix) + + def legacy_address(self): + version_int = Address._address_type('legacy', self.version)[1] + return b58encode_check(Address.code_list_to_string([version_int] + self.payload)) + + def cash_address(self): + version_int = Address._address_type('cash', self.version)[1] + payload = [version_int] + self.payload + payload = convertbits(payload, 8, 5) + checksum = calculate_checksum(self.prefix, payload) + return self.prefix + ':' + b32encode(payload + checksum) + + @staticmethod + def code_list_to_string(code_list): + if sys.version_info > (3, 0): + output = bytes() + for code in code_list: + output += bytes([code]) + else: + output = '' + for code in code_list: + output += chr(code) + return output + + @staticmethod + def _address_type(address_type, version): + for mapping in Address.VERSION_MAP[address_type]: + if mapping[0] == version or mapping[1] == version: + return mapping + raise InvalidAddress('Could not determine address version') + + @staticmethod + def from_string(address_string): + try: + address_string = str(address_string) + except Exception: + raise InvalidAddress('Expected string as input') + if ':' not in address_string: + return Address._legacy_string(address_string) + else: + return Address._cash_string(address_string) + + @staticmethod + def _legacy_string(address_string): + try: + decoded = bytearray(b58decode_check(address_string)) + except ValueError: + raise InvalidAddress('Could not decode legacy address') + version = Address._address_type('legacy', decoded[0])[0] + payload = list() + for letter in decoded[1:]: + payload.append(letter) + return Address(version, payload) + + @staticmethod + def _cash_string(address_string): + if address_string.upper() != address_string and address_string.lower() != address_string: + raise InvalidAddress('Cash address contains uppercase and lowercase characters') + address_string = address_string.lower() + colon_count = address_string.count(':') + if colon_count == 0: + address_string = Address.MAINNET_PREFIX + ':' + address_string + elif colon_count > 1: + raise InvalidAddress('Cash address contains more than one colon character') + prefix, base32string = address_string.split(':') + decoded = b32decode(base32string) + if not verify_checksum(prefix, decoded): + raise InvalidAddress('Bad cash address checksum') + converted = convertbits(decoded, 5, 8) + version = Address._address_type('cash', converted[0])[0] + if prefix == Address.TESTNET_PREFIX: + version += '-TESTNET' + payload = converted[1:-6] + return Address(version, payload, prefix) + + +def to_cash_address(address): + return Address.from_string(address).cash_address() + + +def to_legacy_address(address): + return Address.from_string(address).legacy_address() + + +def is_valid(address): + try: + Address.from_string(address) + return True + except InvalidAddress: + return False + +######################################################################### +## monero +######################################################################### + + +__alphabet = [ + ord(s) for s in "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" +] +__b58base = 58 +__UINT64MAX = 2 ** 64 +__encodedBlockSizes = [0, 2, 3, 5, 6, 7, 9, 10, 11] +__fullBlockSize = 8 +__fullEncodedBlockSize = 11 + + +def _hexToBin(hex_): + if len(hex_) % 2 != 0: + raise ValueError("Hex string has invalid length: %d" % len(hex_)) + return [int(hex_[i : i + 2], 16) for i in range(0, len(hex_), 2)] + + +def _binToHex(bin_): + return "".join("%02x" % int(b) for b in bin_) + + +def _uint8be_to_64(data): + if not (1 <= len(data) <= 8): + raise ValueError("Invalid input length: %d" % len(data)) + + res = 0 + for b in data: + res = res << 8 | b + return res + + +def _uint64_to_8be(num, size): + if size < 1 or size > 8: + raise ValueError("Invalid input length: %d" % size) + res = [0] * size + + twopow8 = 2 ** 8 + for i in range(size - 1, -1, -1): + res[i] = num % twopow8 + num = num // twopow8 + + return res + + +def encode_block(data, buf, index): + l_data = len(data) + + if l_data < 1 or l_data > __fullEncodedBlockSize: + raise ValueError("Invalid block length: %d" % l_data) + + num = _uint8be_to_64(data) + i = __encodedBlockSizes[l_data] - 1 + + while num > 0: + remainder = num % __b58base + num = num // __b58base + buf[index + i] = __alphabet[remainder] + i -= 1 + + return buf + + +def encode(hex): + """Encode hexadecimal string as base58 (ex: encoding a Monero address).""" + data = _hexToBin(hex) + l_data = len(data) + + if l_data == 0: + return "" + + full_block_count = l_data // __fullBlockSize + last_block_size = l_data % __fullBlockSize + res_size = ( + full_block_count * __fullEncodedBlockSize + __encodedBlockSizes[last_block_size] + ) + + res = bytearray([__alphabet[0]] * res_size) + + for i in range(full_block_count): + res = encode_block( + data[(i * __fullBlockSize) : (i * __fullBlockSize + __fullBlockSize)], + res, + i * __fullEncodedBlockSize, + ) + + if last_block_size > 0: + res = encode_block( + data[ + (full_block_count * __fullBlockSize) : ( + full_block_count * __fullBlockSize + last_block_size + ) + ], + res, + full_block_count * __fullEncodedBlockSize, + ) + + return bytes(res).decode("ascii") + + +def decode_block(data, buf, index): + l_data = len(data) + + if l_data < 1 or l_data > __fullEncodedBlockSize: + raise ValueError("Invalid block length: %d" % l_data) + + res_size = __encodedBlockSizes.index(l_data) + if res_size <= 0: + raise ValueError("Invalid block size: %d" % res_size) + + res_num = 0 + order = 1 + for i in range(l_data - 1, -1, -1): + digit = __alphabet.index(data[i]) + if digit < 0: + raise ValueError("Invalid symbol: %s" % data[i]) + + product = order * digit + res_num + if product > __UINT64MAX: + raise ValueError( + "Overflow: %d * %d + %d = %d" % (order, digit, res_num, product) + ) + + res_num = product + order = order * __b58base + + if res_size < __fullBlockSize and 2 ** (8 * res_size) <= res_num: + raise ValueError("Overflow: %d doesn't fit in %d bit(s)" % (res_num, res_size)) + + tmp_buf = _uint64_to_8be(res_num, res_size) + buf[index : index + len(tmp_buf)] = tmp_buf + + return buf + + +def decode(enc): + """Decode a base58 string (ex: a Monero address) into hexidecimal form.""" + enc = bytearray(enc, encoding="ascii") + l_enc = len(enc) + + if l_enc == 0: + return "" + + full_block_count = l_enc // __fullEncodedBlockSize + last_block_size = l_enc % __fullEncodedBlockSize + try: + last_block_decoded_size = __encodedBlockSizes.index(last_block_size) + except ValueError: + raise ValueError("Invalid encoded length: %d" % l_enc) + + data_size = full_block_count * __fullBlockSize + last_block_decoded_size + + data = bytearray(data_size) + for i in range(full_block_count): + data = decode_block( + enc[ + (i * __fullEncodedBlockSize) : ( + i * __fullEncodedBlockSize + __fullEncodedBlockSize + ) + ], + data, + i * __fullBlockSize, + ) + + if last_block_size > 0: + data = decode_block( + enc[ + (full_block_count * __fullEncodedBlockSize) : ( + full_block_count * __fullEncodedBlockSize + last_block_size + ) + ], + data, + full_block_count * __fullBlockSize, + ) + + return _binToHex(data) + +######################################################################### +######################################################################### \ No newline at end of file diff --git a/base/migrations/0005_alter_vendorsdata_vendorisactive.py b/base/migrations/0005_alter_vendorsdata_vendorisactive.py new file mode 100644 index 0000000..eda3647 --- /dev/null +++ b/base/migrations/0005_alter_vendorsdata_vendorisactive.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2 on 2023-04-29 14:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0004_vendorsdata_vendoruuid'), + ] + + operations = [ + migrations.AlterField( + model_name='vendorsdata', + name='vendorIsActive', + field=models.BooleanField(default=1), + ), + ] diff --git a/base/migrations/0006_alter_vendorsdata_vendordeleted_and_more.py b/base/migrations/0006_alter_vendorsdata_vendordeleted_and_more.py new file mode 100644 index 0000000..0abf473 --- /dev/null +++ b/base/migrations/0006_alter_vendorsdata_vendordeleted_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2 on 2023-04-29 14:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0005_alter_vendorsdata_vendorisactive'), + ] + + operations = [ + migrations.AlterField( + model_name='vendorsdata', + name='vendorDeleted', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='vendorsdata', + name='vendorPaidNotification', + field=models.BooleanField(default=True), + ), + migrations.AlterField( + model_name='vendorsdata', + name='vendorPayWindow', + field=models.BooleanField(default=True), + ), + migrations.AlterField( + model_name='vendorsdata', + name='vendorSkipScreen', + field=models.BooleanField(default=False), + ), + ] diff --git a/base/migrations/0007_vendorsdata_vendorupdated_and_more.py b/base/migrations/0007_vendorsdata_vendorupdated_and_more.py new file mode 100644 index 0000000..0cc85d7 --- /dev/null +++ b/base/migrations/0007_vendorsdata_vendorupdated_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2 on 2023-04-29 17:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0006_alter_vendorsdata_vendordeleted_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='vendorsdata', + name='vendorUpdated', + field=models.DateTimeField(auto_now=True), + ), + migrations.AlterField( + model_name='vendorsdata', + name='vendorAddDelete', + field=models.DateTimeField(auto_now_add=True), + ), + ] diff --git a/base/migrations/0008_alter_vendorsdata_vendorwebaddr.py b/base/migrations/0008_alter_vendorsdata_vendorwebaddr.py new file mode 100644 index 0000000..3815b02 --- /dev/null +++ b/base/migrations/0008_alter_vendorsdata_vendorwebaddr.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2 on 2023-05-02 12:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0007_vendorsdata_vendorupdated_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='vendorsdata', + name='vendorWebAddr', + field=models.URLField(max_length=250), + ), + ] diff --git a/base/migrations/0009_alter_vendorsdata_id.py b/base/migrations/0009_alter_vendorsdata_id.py new file mode 100644 index 0000000..3f52bd4 --- /dev/null +++ b/base/migrations/0009_alter_vendorsdata_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2 on 2023-05-02 15:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0008_alter_vendorsdata_vendorwebaddr'), + ] + + operations = [ + migrations.AlterField( + model_name='vendorsdata', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + ] diff --git a/base/models.py b/base/models.py index 4defafe..e8106a7 100644 --- a/base/models.py +++ b/base/models.py @@ -1,30 +1,74 @@ from django.db import models from users.models import User +from django.contrib import admin # Create your models here. + +class CryptoCoins(models.Model): + id = models.BigAutoField(primary_key=True) + coinName = models.CharField(max_length=250) + coinSymbol = models.CharField(max_length=10) + coinIsActive = models.BooleanField(default=True) + coinAddDate = models.DateTimeField(auto_now_add=True) + coinUpdated = models.DateTimeField(auto_now=True) + coinDelete = models.DateTimeField(auto_now_add=True, blank=True, null=True) + coinDeleted = models.BooleanField(default=False) + + class Meta: + verbose_name = "Crypto Coin" + verbose_name_plural = "Crypto Coins" + ordering = ["-id"] + class VendorsData(models.Model): + id = models.BigAutoField(primary_key=True) vendorid = models.ForeignKey(User, default=None, on_delete=models.CASCADE) vendorUUID = models.UUIDField() vendor = models.CharField(max_length=8, unique=True) - vendorSecretKey = models.UUIDField() + vendorSecretKey = models.CharField(max_length=250) vendorCoverAmount = models.IntegerField(default=0) - vendorPaidNotification = models.IntegerField(default=1) - vendorSkipScreen = models.IntegerField(default=0) - vendorPayWindow = models.IntegerField(default=0) + vendorNetworkFee = models.IntegerField(default=0) + vendorPaidNotification = models.BooleanField(default=True) + vendorSkipScreen = models.BooleanField(default=False) + vendorPayWindow = models.BooleanField(default=True) vendorWebName = models.CharField(max_length=250) - vendorWebAddr = models.CharField(max_length=250) + vendorWebAddr = models.URLField(max_length=250) vendorAddDate = models.DateTimeField(auto_now_add=True) - vendorDeleted = models.IntegerField(default=0) - vendorAddDelete = models.DateTimeField(auto_now=True) - vendorIsActive = models.IntegerField(default=1) + vendorDeleted = models.BooleanField(default=False) + vendorUpdated = models.DateTimeField(auto_now=True) + vendorAddDelete = models.DateTimeField(auto_now_add=True) + vendorIsActive = models.BooleanField(default=True) def __str__(self): return self.vendor + + def __repr__(self): + return self._repr() class Meta: verbose_name = "Vendor" verbose_name_plural = "Vendors" ordering = ["-id"] - - \ No newline at end of file + +class VendorsAddresses(models.Model): + id = models.BigAutoField(primary_key=True) + vendor = models.ForeignKey(VendorsData, default=None, on_delete=models.CASCADE) + vendorid = models.ForeignKey(User, default=None, on_delete=models.CASCADE) + vendorUUID = models.UUIDField() + coin = models.CharField(max_length=10) + address = models.CharField(max_length=250) + addrAddDate = models.DateTimeField(auto_now_add=True) + addrDeleted = models.BooleanField(default=False) + addrUpdated = models.DateTimeField(auto_now=True) + addrIsActive = models.BooleanField(default=True) + + def __str__(self): + return self.address + + class Meta: + verbose_name = "Address" + verbose_name_plural = "Addresses" + ordering = ["-addrAddDate"] + + + diff --git a/base/templates/base/dashboard.html b/base/templates/base/dashboard.html index 1ff54b3..b66ef5f 100644 --- a/base/templates/base/dashboard.html +++ b/base/templates/base/dashboard.html @@ -7,321 +7,349 @@
- -
-
-
-
-
- -

Dashboard

- -

Hello "user", welcome! Here's what's happening with your store today.

-
- - -
- - -
-
-
+ -
-
- -
-
-
-
-
-
- Earnings - $750.90 -
-
-
- -
-
-
-
- - 13% - - Since last month -
-
-
-
-
-
-
-
-
- Balance - 215 -
-
-
- -
-
-
-
- - 30% - - Since last month -
-
-
-
-
-
-
-
-
- Customers - 1.400 -
-
-
- -
-
-
-
- - -5% - - Since last month -
-
-
-
-
-
-
-
-
- Invoices - 150 -
-
-
- -
-
-
-
- - 10% - - Since last month -
-
-
-
+
+
+
+
+
+

Dashboard

+
+
- - - -
-
-
Last 5 paid invoices
+
+ +
+
+
+
+
+
+
+
+ ... +
USDT
+
+
+ Total balance +
+
+ 75.800,00 USDT +
+
+
+
+
+
+
+
+ ... + BTC +
+
+ 3.2893 USDT +
+
+ + + + +13.7% +
+
+
+
+
+
+
+
+ ... + ADA +
+
+ 10.745,49 ADA +
+
+ + + + -3.2% +
+
+
+
+
+
+
+
+ ... + EOS +
+
+ 7.890,00 EOS +
+
+ + + + -2.2% +
+
+
+
+
+
+ + + +
+
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameDateCompanyOfferMeeting
- ... - - Robert Fox - - - Feb 15, 2021 - - ... - - Dribbble - - - $3.500 - - - Scheduled - - - View - -
- ... - - Darlene Robertson - - - Apr 15, 2021 - - ... - - Netguru - - - $2.750 - - - Postponed - - - View - -
- ... - - Theresa Webb - - - Mar 20, 2021 - - ... - - Figma - - - $4.200 - - - Scheduled - - - View - -
- ... - - Kristin Watson - - - Feb 15, 2021 - - ... - - Mailchimp - - - $3.500 - - - Not discussed - - - View - -
- ... - - Cody Fisher - - - Apr 10, 2021 - - ... - - Webpixels - - - $1.500 - - - Canceled - - - View - -
-
-
-
-
+ +
+
+
- {% block footer %} - {% include "partials/footer.html" %} - {% endblock footer %} -
- - - + + {% endblock content %} \ No newline at end of file diff --git a/base/templates/base/vendor.html b/base/templates/base/vendor.html index f5ad1d9..dbd1c76 100644 --- a/base/templates/base/vendor.html +++ b/base/templates/base/vendor.html @@ -1,6 +1,6 @@ {% extends "partials/base.html" %} {% load static %} -{% block title %}Dashboard{% endblock title %} +{% block title %}Vendor Page{% endblock title %} {% block content %} @@ -8,24 +8,7 @@
-
-
-
-
-
- -

Vendor Page

- - -
- - -
- - -
-
-
+
@@ -39,9 +22,9 @@ VendorID - Creation Date Website Name - + Created + Updated Status @@ -50,22 +33,18 @@ - ... - - {{vendor.vendor}} - + {{vendor.vendor}} - {{vendor.vendorAddDate}} - - - ... - - {{vendor.vendorWebName}} - + {{vendor.vendorWebName|truncatechars:20}} + {{vendor.vendorAddDate}} + + + + {{vendor.vendorUpdated|timesince}} ago {% if vendor.vendorIsActive == 0 %} @@ -79,8 +58,7 @@ {% endif %} - Edit - Addresses + Manage @@ -90,7 +68,7 @@
@@ -99,13 +77,5 @@ -
- {% block footer %} - {% include "partials/footer.html" %} - {% endblock footer %} -
- - - {% endblock content %} \ No newline at end of file diff --git a/base/templates/base/vendorEdit.html b/base/templates/base/vendorEdit.html new file mode 100644 index 0000000..9dd4ddb --- /dev/null +++ b/base/templates/base/vendorEdit.html @@ -0,0 +1,184 @@ +{% extends "partials/base.html" %} +{% load static %} +{% block title %}Vendor Edit Page{% endblock title %} +{% block content %} + + + + + + + + +
+
+
+
+
+

+ {% block vendor_title %} {{vendor_title}} {% endblock vendor_title %} +

+
+
+ +
+ + +
+ {% csrf_token %} + + {{ form.non_field_errors }} + +
+
+ +
+
+
+
+ + +
+ Looks good! +
+
+
+

+

+
+ +
+
+
+
+ + +
+ {% if form.vendorWebAddr.errors %} + {% for error in form.vendorWebAddr.errors %} + {{ error|escape }} + {% endfor %} + {% endif %} +
+
+
+

+

+
+ +
+
+ + {% if form.vendorNetworkFee.errors %} + {% for error in form.vendorNetworkFee.errors %} + {{ error|escape }} + {% endfor %} + {% endif %} +
+
+

+

+
+ +
+
+ +
+ {% if form.vendorPaidNotification.errors %} + {% for error in form.vendorPaidNotification.errors %} + {{ error|escape }} + {% endfor %} + {% endif %} +
+

+ +

+
+ +
+
+ + {% if form.vendorCoverAmount.errors %} + {% for error in form.vendorCoverAmount.errors %} + {{ error|escape }} + {% endfor %} + {% endif %} +
+
+

+

+
+ +
+
+ + {% if form.vendorPayWindow.errors %} + {% for error in form.vendorPayWindow.errors %} + {{ error|escape }} + {% endfor %} + {% endif %} +
+
+
+ +
+
+ +
+
+
+
+ + {% if messages %} + + {% endif %} + + + + + + + + + + + + +{% endblock content %} \ No newline at end of file diff --git a/base/urls.py b/base/urls.py index f83b06d..8ce6f3b 100644 --- a/base/urls.py +++ b/base/urls.py @@ -5,7 +5,14 @@ from django.urls import path urlpatterns = [ path('login/', views.LoginPage, name="login"), path('logout/', views.LogoutPage, name="logout"), + path('', views.index, name="dashboard"), path('vendor/', views.VendorPage, name="vendor"), + path('vendor/edit//', views.VendorEditPage, name="edit-vendor"), + path('vendor/create', views.VendorCreatePage, name="create-vendor"), + path('vendor/address//', views.VendorAddrPage, name="add-address"), + + + #path('vendor-success', views.VendorSuccess, name="vendor-success") ] \ No newline at end of file diff --git a/base/views.py b/base/views.py index 43fefbd..a89c1b9 100644 --- a/base/views.py +++ b/base/views.py @@ -1,13 +1,21 @@ from django.shortcuts import render, redirect from django.contrib import messages -from users.models import User -from .models import VendorsData from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required - +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ +from django.db.models import Q +from django_otp.decorators import otp_required +# +from users.models import User +from .models import VendorsData, CryptoCoins, VendorsAddresses +from .forms import VendorEditForm, VendorAddrAddForm +from .functions import vendor_generator, checksumCheck +import uuid, hashlib # Create your views here. @login_required(login_url='login') +#@otp_required() def index(request): context = {} return render(request, 'base/dashboard.html', context) @@ -33,7 +41,7 @@ def LoginPage(request): context = {} - return render(request, 'base/login.html', context) + return render(request, 'base/loogin2.html', context) def LogoutPage(request): logout(request) @@ -41,6 +49,136 @@ def LogoutPage(request): @login_required(login_url='login') def VendorPage(request): - vendors = VendorsData.objects.filter(vendorid_id=request.user.id) + vendors = VendorsData.objects.filter(Q(vendorid_id=request.user.id) & Q(vendorDeleted=False)) context = {'vendors': vendors} - return render(request, 'base/vendor.html', context) \ No newline at end of file + return render(request, 'base/vendor.html', context) + +@login_required(login_url='login') +#@otp_required(login_url='two_factor:login') +def VendorEditPage(request, vdr ): + try: + vendors = VendorsData.objects.get(Q(vendorid_id=request.user.id) & Q(vendorUUID=vdr)) + form = VendorEditForm() + vendor_title = "Edit Vendor" + if request.method == 'POST': + if request.POST.get('submit') == 'Submit': + try: + vendors = VendorsData.objects.get(Q(vendorid_id=request.user.id) & Q(vendorUUID=vdr)) + form = VendorEditForm(request.POST, instance=vendors) + if form.is_valid(): + form.save() + messages.success(request, "Vendor information saved") + return redirect('vendor') + else: + messages.error(request, "Something wrong happened, try again.") + except Exception as e: + messages.error(request, "Something wrong happened, try again.") + context = {'vendors': vendors, 'form': form, 'vendor_title': vendor_title} + return render(request, 'base/vendorEdit.html', context) + except Exception as e: + messages.error(request, e) + return redirect('vendor') + +@login_required(login_url='login') +def VendorCreatePage(request): + vendor_title = "Create Vendor" + context = {'vendor_title': vendor_title} + if request.method == 'POST': + try: + try: + vendor_accounts = len(VendorsData.objects.filter(vendorid_id=request.user.id)) + if vendor_accounts > request.user.vendornr: + messages.error(request, "You have reached the maximum number of vendor accounts") + return redirect('vendor') + except: pass + + form = VendorEditForm(request.POST) + if form.is_valid(): + # Generate vendor + new_vendor = form.save(commit=False) + try: + vdr = vendor_generator() + vendor_check = VendorsData.objects.get(vendorUUID=vdr) + while vendor_check is vdr: + vdr = vendor_generator() + vendor_check = VendorsData.objects.get(vendorUUID=vdr) + except: + pass + + secretKey = str(uuid.uuid4()) + md5s = hashlib.md5(secretKey.encode()).hexdigest() + new_vendor.vendor = vdr + new_vendor.vendorSecretKey = md5s + new_vendor.vendorSkipScreen = False + new_vendor.vendorUUID = uuid.uuid4() + new_vendor.vendorid_id = request.user.id + # Save vendor + try: + new_vendor.save() + vendor_title = "Congrats!" + context = {'vendor_title': vendor_title, "vendor": new_vendor, 'secret': secretKey} + return render(request, 'base/vendorSuccess.html', context) + except Exception: + messages.error(request, "Unable to create vendor") # + else: + messages.error(request, "The information submited is incomplete") # + except Exception as e: + messages.error(request, "Something wrong happened, try again.") + return render(request, 'base/vendorCreate.html', context) + +@login_required(login_url='login') +def VendorAddrPage(request,vdr): + try: + coins = CryptoCoins.objects.filter(coinIsActive=True) + try: + vendors = VendorsData.objects.get(Q(vendorid_id=request.user.id) & Q(vendorUUID=vdr)) + except: + messages.error(request, "Vendor not found") + return redirect('vendor') + addresses = VendorsAddresses.objects.filter(Q(vendorUUID=vdr) & Q(vendorid_id=request.user.id)) + form = VendorAddrAddForm() + vendor_title = "Add a crypto address" + if request.method == 'POST': + if request.POST.get('submit') == 'Submit': + try: + if checksumCheck(request.POST.get('coin').lower(), request.POST.get('address')) == False: + messages.error(request, "Invalid address") + context = {'vendor_title': vendor_title, 'coins': coins, 'vendors': vendors, "addresses": addresses} + return render(request, 'base/vendorAddr.html', context) + except: + # stop here, return nothing, notify us. + return redirect('vendorAddr', vdr) + ##check if duplicate + + + try: + form = VendorAddrAddForm(request.POST) + if form.is_valid(): + new_address = form.save(commit=False) + new_address.vendorUUID = vdr + new_address.addrDeleted = False + new_address.addrIsActive = True + new_address.vendor_id = vendors.id + new_address.vendorid_id = request.user.id + try: + for obj in VendorsAddresses.objects.filter(Q(coin=request.POST.get('coin')) & Q(addrIsActive=True) & Q(vendorid_id=request.user.id)): + obj.addrIsActive = False + obj.save() + new_address.save() + messages.success(request, "Vendor address saved") + #add email notification + context = {'vendor_title': vendor_title, 'coins': coins, 'vendors': vendors, "addresses": addresses} + return render(request, 'base/vendorAddr.html', context) + except Exception as e: + messages.error(request, e) # + else: + messages.error(request, "Something wrong happened, try again!") + except Exception as e: + messages.error(request, "Something wrong happened, try again!") + + context = {'vendor_title': vendor_title, 'coins': coins, 'vendors': vendors, "addresses": addresses} + return render(request, 'base/vendorAddr.html', context) + except Exception as e: + messages.error(request, "Something wrong happened, try again!") + return redirect('vendor') + diff --git a/dLitepay/settings.py b/dLitepay/settings.py index 69f47b7..3d42183 100644 --- a/dLitepay/settings.py +++ b/dLitepay/settings.py @@ -26,7 +26,7 @@ SECRET_KEY = 'django-insecure-z#_ppfgs06)e4v18t!970-&-&jkxht!tw&ms#u5n_m-fbxulwa # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['10.0.70.5', '127.0.0.1'] # User substitution # https://docs.djangoproject.com/en/1.11/topics/auth/customizing/#auth-custom-user @@ -43,6 +43,13 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'django_bcrypt', + 'django_otp', + 'django_otp.plugins.otp_static', + 'django_otp.plugins.otp_totp', + 'django_otp.plugins.otp_email', # <- if you want email capability. + 'two_factor', + 'two_factor.plugins.phonenumber', # <- if you want phone number capability. + 'two_factor.plugins.email', # <- if you want email capability. # 'base.apps.BaseConfig', 'users.apps.UsersConfig', @@ -54,6 +61,7 @@ MIDDLEWARE = [ 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django_otp.middleware.OTPMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] @@ -113,13 +121,13 @@ AUTH_PASSWORD_VALIDATORS = [ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] - +''' PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', 'django.contrib.auth.hashers.PBKDF2PasswordHasher', 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', ] - +''' # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ @@ -143,23 +151,44 @@ STATICFILES_DIRS = [ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + }, + }, + 'loggers': { + 'two_factor': { + 'handlers': ['console'], + 'level': 'INFO', + } + } +} + # Messages customize MESSAGE_TAGS = { - messages.DEBUG: "alert-info", - messages.INFO: "alert-info", - messages.SUCCESS: "alert-success", - messages.WARNING: "alert-warning", - messages.ERROR: "alert-danger", + messages.DEBUG: "text-info", + messages.INFO: "text-info", + messages.SUCCESS: "text-success", + messages.WARNING: "text-warning", + messages.ERROR: "text-danger", } - BCRYPT_ROUNDS = 11 PASSWORD_RESET_TIMEOUT = 3600 # +#LOGIN_URL = 'two_factor:login' +#OTP_LOGIN_URL = 'account:login' SESSION_EXPIRE_AT_BROWSER_CLOSE = True SESSION_COOKIE_SECURE = False #to be switched to true when in prod CSRF_COOKIE_SECURE = False #to be switched to true when in prod SECURE_SSL_REDIRECT = False #to be switched to true when in prod SESSION_COOKIE_AGE = 3600 -SERVER_EMAIL = 'info@litepay.ch' \ No newline at end of file +TWO_FACTOR_REMEMBER_COOKIE_AGE = 3600 +SERVER_EMAIL = 'info@litepay.ch' +DEFAULT_FROM_EMAIL = 'info@litepay.ch' \ No newline at end of file diff --git a/dLitepay/urls.py b/dLitepay/urls.py index 9738c13..5c97f9d 100644 --- a/dLitepay/urls.py +++ b/dLitepay/urls.py @@ -16,9 +16,15 @@ Including another URLconf """ from django.contrib import admin from django.urls import path, include +from two_factor.urls import urlpatterns as tf_urls +urlpatterns = [ + #path('', include(tf_urls)), + path('', include('base.urls')) +] - +''' urlpatterns = [ path('admin/', admin.site.urls), - path('', include('base.urls')) + ] +''' \ No newline at end of file diff --git a/src/.DS_Store b/src/.DS_Store index fb7f9d8..965bed2 100644 Binary files a/src/.DS_Store and b/src/.DS_Store differ diff --git a/templates/main.html b/templates/main.html index 4323902..05d96ea 100644 --- a/templates/main.html +++ b/templates/main.html @@ -18,9 +18,7 @@ - {% block footer %} - {% include "partials/footer.html" %} - {% endblock footer %} + {% endblock content %} \ No newline at end of file diff --git a/templates/partials/base.html b/templates/partials/base.html index 75d3431..1e9d852 100755 --- a/templates/partials/base.html +++ b/templates/partials/base.html @@ -1,23 +1,27 @@ {% load static %} - - + + + - - {% block title %}{% endblock title %} | - - - - - - {% block css %} - - - - - {% endblock css %} + + + + {% block title %}{% endblock title %} | + + + {% block css %} + + + + + + + + {% endblock css %} - + +
{% block header %} @@ -29,8 +33,6 @@ {% block content %} {% block pagetitle %} {% endblock pagetitle %} - {% block footer %} - {% endblock footer %} {% endblock content %}
diff --git a/templates/partials/footer.html b/templates/partials/footer.html index e1e6407..73f30da 100755 --- a/templates/partials/footer.html +++ b/templates/partials/footer.html @@ -1,10 +1,9 @@ {% block footer %} -