diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..d7b9b6c --- /dev/null +++ b/.env.sample @@ -0,0 +1 @@ +GITEA_BASE_URL=https://gitea.btcmap.org diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..9e36a64 --- /dev/null +++ b/.envrc @@ -0,0 +1,3 @@ +use flake + +printf 'GITEA_BASE_URL=%s\n' "${GITEA_BASE_URL-}" > .direnv/.env diff --git a/.gitignore b/.gitignore index 8fe2fe0..f39a15f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ __pycache__/ *.py[cod] *$py.class +venv # uv virtual environment .venv/ @@ -38,6 +39,5 @@ replit.nix # Others *.log *.sqlite -opencode.json +.direnv .env -users.json diff --git a/app.py b/app.py index 8089247..feacc4d 100644 --- a/app.py +++ b/app.py @@ -22,6 +22,7 @@ app.config['SESSION_COOKIE_SECURE'] = True app.config['SESSION_COOKIE_HTTPONLY'] = True app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' +app.config['GITEA_BASE_URL'] = os.environ.get('GITEA_BASE_URL', 'https://gitea.btcmap.org') # Configure server-side sessions (filesystem backend) app.config['SESSION_TYPE'] = 'filesystem' @@ -71,6 +72,9 @@ def inject_current_user(): }, 'url_alias': { 'required': True, + 'min_length': 2, + 'max_length': 100, + 'pattern': r'^[a-z0-9\-]+$', 'type': 'text' }, 'continent': { @@ -104,7 +108,8 @@ def inject_current_user(): }, 'contact:twitter': { 'required': False, - 'type': 'url' + 'type': 'url', + 'pattern': r'(?:x\.com|twitter\.com)\/[\w]+' }, 'contact:website': { 'required': False, @@ -116,47 +121,58 @@ def inject_current_user(): }, 'contact:telegram': { 'required': False, - 'type': 'url' + 'type': 'url', + 'pattern': r't\.me\/[\w]+' }, 'contact:signal': { 'required': False, - 'type': 'url' + 'type': 'url', + 'pattern': r'signal\.group\/' }, 'contact:whatsapp': { 'required': False, - 'type': 'url' + 'type': 'url', + 'pattern': r'chat\.whatsapp\.com\/' }, 'contact:nostr': { 'required': False, - 'type': 'text' + 'type': 'text', + 'pattern': r'^npub1[a-z0-9]{58}$' }, 'contact:meetup': { 'required': False, - 'type': 'url' + 'type': 'url', + 'pattern': r'meetup\.com\/' }, 'contact:discord': { 'required': False, - 'type': 'url' + 'type': 'url', + 'pattern': r'discord\.gg|discord\.com' }, 'contact:instagram': { 'required': False, - 'type': 'url' + 'type': 'url', + 'pattern': r'instagram\.com\/[\w._]+' }, 'contact:youtube': { 'required': False, - 'type': 'url' + 'type': 'url', + 'pattern': r'(?:youtu\.be|youtube\.com)\/' }, 'contact:facebook': { 'required': False, - 'type': 'url' + 'type': 'url', + 'pattern': r'facebook\.com\/[\w.]+' }, 'contact:linkedin': { 'required': False, - 'type': 'url' + 'type': 'url', + 'pattern': r'linkedin\.com\/' }, 'contact:rss': { 'required': False, - 'type': 'url' + 'type': 'url', + 'pattern': r'\/(?:feed|rss)' }, 'contact:phone': { 'required': False, @@ -164,15 +180,18 @@ def inject_current_user(): }, 'contact:github': { 'required': False, - 'type': 'url' + 'type': 'url', + 'pattern': r'github\.com\/[\w-]+' }, 'contact:matrix': { 'required': False, - 'type': 'url' + 'type': 'text', + 'pattern': r'@[\w._-]+:[\w.-]+' }, 'contact:geyser': { 'required': False, - 'type': 'url' + 'type': 'url', + 'pattern': r'geyser\.fund\/' }, 'contact:eventbrite': { 'required': False, @@ -192,11 +211,15 @@ def inject_current_user(): }, 'tips:lightning_address': { 'required': False, - 'type': 'text' + 'type': 'text', + 'min_length': 10, + 'pattern': r'^[\w\.\-]+@[\w\.\-]+\.[a-zA-Z]{2,}$' }, 'description': { 'required': False, - 'type': 'text' + 'type': 'text', + 'min_length': 10, + 'max_length': 500 } } } @@ -1154,19 +1177,25 @@ def lint_community_orgs(): @app.route('/api/gitea/get-issue/') @login_required def get_issue_data(issue_id): - try: - req_data = requests.get( - f"https://gitea.btcmap.org/api/v1/repos/teambtcmap/btcmap-data/issues/{issue_id}", - timeout=15, - ) - req_data.raise_for_status() - return jsonify({'data': req_data.json()}) - except requests.exceptions.Timeout: - return jsonify({'error': 'Request to Gitea timed out'}), 408 - except requests.exceptions.RequestException as exc: - app.logger.error(f"Error fetching Gitea issue {issue_id}: {exc}") - return jsonify({'error': 'Failed to fetch issue data'}), 502 - + try: + base_url = app.config['GITEA_BASE_URL'].rstrip('/') + req_data = requests.get( + f"{base_url}/api/v1/repos/teambtcmap/btcmap-data/issues/{issue_id}", + timeout=15, + ) + req_data.raise_for_status() + return jsonify({'data': req_data.json()}) + except requests.exceptions.HTTPError as exc: + status_code = exc.response.status_code if exc.response is not None else 502 + app.logger.error(f"Error fetching Gitea issue {issue_id}: {exc}") + if status_code == 404: + return jsonify({'error': 'Issue not found'}), 404 + return jsonify({'error': 'Failed to fetch issue data'}), 502 + except requests.exceptions.Timeout: + return jsonify({'error': 'Request to Gitea timed out'}), 408 + except requests.exceptions.RequestException as exc: + app.logger.error(f"Error fetching Gitea issue {issue_id}: {exc}") + return jsonify({'error': 'Failed to fetch issue data'}), 502 def get_area(area_id): result = rpc_call('get_area', {'id': area_id}) if 'error' not in result: diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..f428ce8 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1767313136, + "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..ce96d0e --- /dev/null +++ b/flake.nix @@ -0,0 +1,28 @@ +{ + description = "BTCMAP Admin page"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; + }; + + outputs = { self, nixpkgs }: + let + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + in { + devShells.${system}.default = pkgs.mkShell { + name = "btcmap-admin"; + + buildInputs = with pkgs; [ + python311 + python311Packages.virtualenv + geos + ]; + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ + pkgs.stdenv.cc.cc.lib + pkgs.zlib + ]; + }; + }; +} + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3452d41 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +flask +flask_session +Flask-Login +requests +geojson_rewind +shapely +pyproj +nostr-sdk +cryptography diff --git a/static/js/utils.js b/static/js/utils.js new file mode 100644 index 0000000..ac3c6b9 --- /dev/null +++ b/static/js/utils.js @@ -0,0 +1,19 @@ + /** + * Passing the name of URL parameter we get its value + * @param {string} sParam - The search string also known as URL parameter + * @returns {string} - The URL parameter content + * @throws {} + */ +function getURLParameter(sParam) { + const params = new URLSearchParams(window.location.search) + return params.get(sParam) +} + /** + * Passing a text without formatation, this text is normalized to get its main data + * @param {string} text - Unformated text + * @returns {string} - Formatted text + * @throws {} + */ +function cleanIssueText(text) { + return text?.replace(/\n/g, ' ').trim() || '' +} diff --git a/templates/add_area.html b/templates/add_area.html index 41be5c1..393c936 100644 --- a/templates/add_area.html +++ b/templates/add_area.html @@ -6,7 +6,8 @@
-

Add New Area

+

Add New Area

+

{% if template_area %}

Using deleted area "{{ template_area.tags.get('name', 'Unknown') }}" as template

{% endif %} @@ -145,13 +146,16 @@ {% endblock %} {% block scripts %} - - + + +