]> git.ipfire.org Git - dbl.git/commitdiff
reports: Add a system to create a new report over the API
authorMichael Tremer <michael.tremer@ipfire.org>
Mon, 29 Dec 2025 20:28:55 +0000 (20:28 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Mon, 29 Dec 2025 20:28:55 +0000 (20:28 +0000)
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
Makefile.am
src/database.sql
src/dnsbl/__init__.py
src/dnsbl/api/lists.py
src/dnsbl/lists.py
src/dnsbl/reports.py [new file with mode: 0644]

index bf3c04ae02d7f7b8d007dfe71db6c9ad1ae39d9e..995d37c6ec4c57fd6d3330b77a8f52d07da076f9 100644 (file)
@@ -58,6 +58,7 @@ dist_pkgpython_PYTHON = \
        src/dnsbl/i18n.py \
        src/dnsbl/lists.py \
        src/dnsbl/logger.py \
+       src/dnsbl/reports.py \
        src/dnsbl/sources.py \
        src/dnsbl/util.py
 
index b87b7e8fc489cb1aa516c7f3837d9ab9e6860d31..1e9aff782365d975cef47ba848b0db03b9ff9c2a 100644 (file)
@@ -2,7 +2,7 @@
 -- PostgreSQL database dump
 --
 
-\restrict b3umosWsIJkpOpBVplcLzTd5va09HE84xdwzTROCiAHoMRKnogpbKgIksBp6ahf
+\restrict delNzbfbaFwfjowYSA0Hv8h5YKH5ddoC9grkkzzBvFQDPYVsiqb0QZFyQzEzwfL
 
 -- Dumped from database version 17.6 (Debian 17.6-0+deb13u1)
 -- Dumped by pg_dump version 17.6 (Debian 17.6-0+deb13u1)
@@ -139,6 +139,44 @@ CREATE SEQUENCE public.nameservers_id_seq
 ALTER SEQUENCE public.nameservers_id_seq OWNED BY public.nameservers.id;
 
 
+--
+-- Name: reports; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.reports (
+    id integer NOT NULL,
+    list_id integer NOT NULL,
+    name text NOT NULL,
+    reported_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
+    reported_by text NOT NULL,
+    closed_at timestamp with time zone,
+    closed_by text,
+    comment text DEFAULT ''::text NOT NULL,
+    block boolean DEFAULT true,
+    accepted boolean DEFAULT false
+);
+
+
+--
+-- Name: reports_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE public.reports_id_seq
+    AS integer
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+
+--
+-- Name: reports_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE public.reports_id_seq OWNED BY public.reports.id;
+
+
 --
 -- Name: source_domains; Type: TABLE; Schema: public; Owner: -
 --
@@ -236,6 +274,13 @@ ALTER TABLE ONLY public.lists ALTER COLUMN id SET DEFAULT nextval('public.lists_
 ALTER TABLE ONLY public.nameservers ALTER COLUMN id SET DEFAULT nextval('public.nameservers_id_seq'::regclass);
 
 
+--
+-- Name: reports id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.reports ALTER COLUMN id SET DEFAULT nextval('public.reports_id_seq'::regclass);
+
+
 --
 -- Name: source_domains id; Type: DEFAULT; Schema: public; Owner: -
 --
@@ -282,6 +327,14 @@ ALTER TABLE ONLY public.nameservers
     ADD CONSTRAINT nameservers_pkey PRIMARY KEY (id);
 
 
+--
+-- Name: reports reports_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.reports
+    ADD CONSTRAINT reports_pkey PRIMARY KEY (id);
+
+
 --
 -- Name: source_domains source_domains_pkey; Type: CONSTRAINT; Schema: public; Owner: -
 --
@@ -312,6 +365,13 @@ CREATE INDEX api_keys_prefix ON public.api_keys USING btree (prefix) WHERE (dele
 CREATE UNIQUE INDEX lists_unique ON public.lists USING btree (slug) WHERE (deleted_at IS NULL);
 
 
+--
+-- Name: reports_open; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX reports_open ON public.reports USING btree (name) WHERE (closed_at IS NULL);
+
+
 --
 -- Name: source_domains_unique; Type: INDEX; Schema: public; Owner: -
 --
@@ -333,6 +393,14 @@ CREATE INDEX source_domains_updated_at ON public.source_domains USING btree (sou
 CREATE UNIQUE INDEX sources_unique ON public.sources USING btree (list_id, url) WHERE (deleted_at IS NULL);
 
 
+--
+-- Name: reports reports_list_id; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.reports
+    ADD CONSTRAINT reports_list_id FOREIGN KEY (list_id) REFERENCES public.lists(id);
+
+
 --
 -- Name: source_domains source_domains_source_id; Type: FK CONSTRAINT; Schema: public; Owner: -
 --
@@ -353,5 +421,5 @@ ALTER TABLE ONLY public.sources
 -- PostgreSQL database dump complete
 --
 
-\unrestrict b3umosWsIJkpOpBVplcLzTd5va09HE84xdwzTROCiAHoMRKnogpbKgIksBp6ahf
+\unrestrict delNzbfbaFwfjowYSA0Hv8h5YKH5ddoC9grkkzzBvFQDPYVsiqb0QZFyQzEzwfL
 
index 668925e4535866bcfe205cf68527073c833a9d41..8a9a308c4f8081fad4b605b77ef4fbdb8eb6bc2b 100644 (file)
@@ -35,6 +35,7 @@ log = logging.getLogger(__name__)
 from . import auth
 from . import database
 from . import lists
+from . import reports
 from . import sources
 
 class Backend(object):
@@ -91,6 +92,10 @@ class Backend(object):
        def lists(self):
                return lists.Lists(self)
 
+       @functools.cached_property
+       def reports(self):
+               return reports.Reports(self)
+
        @functools.cached_property
        def sources(self):
                return sources.Sources(self)
index 7dc44390e3f07ebac3cb440bee72e24b1280dbe9..c2bf310bd8c66e56322356ce25b8de015cabbc45 100644 (file)
 ###############################################################################
 
 import fastapi
+import pydantic
 import typing
 
 from .. import lists
+from .. import reports
 from .. import sources
 
 # Import the main app
+from . import require_api_key
 from . import app
 from . import backend
 
@@ -55,5 +58,34 @@ def get_list(list = fastapi.Depends(get_list_from_path)) -> lists.List:
 def get_list_sources(list = fastapi.Depends(get_list_from_path)) -> typing.List[sources.Source]:
        return list.sources
 
+class CreateReport(pydantic.BaseModel):
+       # Domain
+       name : str
+
+       # Reported By
+       reported_by : str
+
+       # Comment
+       comment : str = ""
+
+       # Block?
+       block : bool = True
+
+
+@router.post("/{list}/reports")
+def list_report(
+       report: CreateReport,
+       auth = fastapi.Depends(require_api_key),
+       list = fastapi.Depends(get_list_from_path),
+) -> reports.Report:
+       # Create a new report
+       with backend.db:
+               return list.report(
+                       name        = report.name,
+                       reported_by = report.reported_by,
+                       comment     = report.comment,
+                       block       = report.block,
+               )
+
 # Include our endpoints
 app.include_router(router)
index 0fab807885a7a6cb248a5ff1d68e57281f2b46f3..276f6893e63055889ff30d376dbfff2aeea0d18f 100644 (file)
@@ -327,3 +327,14 @@ class List(sqlmodel.SQLModel, database.BackendMixin, table=True):
 
                # Run the export
                exporter(f, **kwargs)
+
+       # Reports
+       reports : typing.List["Report"] = sqlmodel.Relationship(back_populates="list")
+
+       # Report!
+
+       def report(self, **kwargs):
+               """
+                       Creates a new report for this list
+               """
+               return self.backend.reports.create(list=self, **kwargs)
diff --git a/src/dnsbl/reports.py b/src/dnsbl/reports.py
new file mode 100644 (file)
index 0000000..68bce05
--- /dev/null
@@ -0,0 +1,98 @@
+###############################################################################
+#                                                                             #
+# dnsbl - A DNS Blocklist Compositor For IPFire                               #
+# Copyright (C) 2025 IPFire Development Team                                  #
+#                                                                             #
+# This program is free software: you can redistribute it and/or modify        #
+# it under the terms of the GNU General Public License as published by        #
+# the Free Software Foundation, either version 3 of the License, or           #
+# (at your option) any later version.                                         #
+#                                                                             #
+# This program is distributed in the hope that it will be useful,             #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
+# GNU General Public License for more details.                                #
+#                                                                             #
+# You should have received a copy of the GNU General Public License           #
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
+#                                                                             #
+###############################################################################
+
+import datetime
+import sqlmodel
+
+from . import database
+
+class Reports(object):
+       def __init__(self, backend):
+               self.backend = backend
+
+       def __iter__(self):
+               stmt = (
+                       sqlmodel
+                       .select(
+                               Report,
+                       )
+                       .where(
+                               Report.closed_at == None,
+                       )
+                       .order_by(
+                               Report.created_at,
+                       )
+               )
+
+               return self.backend.db.fetch(stmt)
+
+       def create(self, **kwargs):
+               """
+                       Creates a new report
+               """
+               report = self.backend.db.insert(
+                       Report, **kwargs,
+               )
+
+               return report
+
+
+class Report(sqlmodel.SQLModel, database.BackendMixin, table=True):
+       __tablename__ = "reports"
+
+       # ID
+       id : int = sqlmodel.Field(primary_key=True)
+
+       # List ID
+       list_id : int = sqlmodel.Field(foreign_key="lists.id", exclude=True)
+
+       # List
+       list : "List" = sqlmodel.Relationship(back_populates="reports")
+
+       # Name
+       name : str
+
+       # Reported At
+       reported_at : datetime.datetime = sqlmodel.Field(
+               sa_column_kwargs = {"server_default" : sqlmodel.text("CURRENT_TIMESTAMP")}
+       )
+
+       # Reported By
+       reported_by : str
+
+       # Closed At
+       closed_at : datetime.datetime | None
+
+       # Closed By
+       closed_by : str | None
+
+       # Comment
+       comment : str = ""
+
+       # Block?
+       block : bool = True
+
+       # Accepted?
+       accepted : bool = False
+
+       # Close!
+
+       def close(self):
+               pass # XXX TODO