diff --git a/src/frontend/nginx-security-headers.conf b/src/frontend/nginx-security-headers.conf index 78a08a4..648ff29 100644 --- a/src/frontend/nginx-security-headers.conf +++ b/src/frontend/nginx-security-headers.conf @@ -5,4 +5,4 @@ add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()" always; # Script 'unsafe-inline' is needed by the inline theme script and Angular's inline loaders. -add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://cloudflareinsights.com https://www.googletagmanager.com https://www.google-analytics.com https://*.google-analytics.com https://*.analytics.google.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; +add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://cloudflareinsights.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; diff --git a/src/frontend/src/app/app.ts b/src/frontend/src/app/app.ts index 5a61a47..a25ddb3 100644 --- a/src/frontend/src/app/app.ts +++ b/src/frontend/src/app/app.ts @@ -2,7 +2,6 @@ import { ChangeDetectionStrategy, Component, afterNextRender, effect, inject, un import { RouterOutlet } from '@angular/router'; import { ToastComponent } from './shared/components/toast/toast'; import { ConfirmModalComponent } from './shared/components/confirm-modal/confirm-modal.component'; -import { AnalyticsService } from './core/services/analytics.service'; import { RouteLoaderComponent } from './shared/components/route-loader/route-loader'; import { PackageService } from './core/services/package.service'; import { SignalrService } from './core/services/signalr.service'; @@ -22,8 +21,6 @@ export class App { private liveUpdates = inject(LiveUpdatesService); constructor() { - inject(AnalyticsService).init(); - afterNextRender(() => { this.loadLiveUpdates(); this.signalr.startConnection().catch(() => undefined); diff --git a/src/frontend/src/app/core/services/analytics.service.ts b/src/frontend/src/app/core/services/analytics.service.ts deleted file mode 100644 index 8d59fd7..0000000 --- a/src/frontend/src/app/core/services/analytics.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Injectable, PLATFORM_ID, inject } from '@angular/core'; -import { isPlatformBrowser } from '@angular/common'; -import { Router, NavigationEnd } from '@angular/router'; -import { filter } from 'rxjs/operators'; -import { environment } from '../../../environments/environment'; - -type GtagArguments = [command: string, ...parameters: unknown[]]; - -interface AnalyticsWindow { - dataLayer?: unknown[]; - gtag?: (...args: GtagArguments) => void; -} - -@Injectable({ providedIn: 'root' }) -export class AnalyticsService { - private router = inject(Router); - private isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); - - // Lives as long as the application, so the subscription is never torn down - init(): void { - if (!this.isBrowser || !environment.gaId) return; - - const gtag = this.loadScript(environment.gaId); - - this.router.events.pipe( - filter((event): event is NavigationEnd => event instanceof NavigationEnd) - ).subscribe(event => { - if (event.urlAfterRedirects.startsWith('/admin')) return; - - gtag('event', 'page_view', { - page_path: event.urlAfterRedirects, - page_location: window.location.href, - page_title: document.title - }); - }); - } - - private loadScript(gaId: string): (...args: GtagArguments) => void { - const script = document.createElement('script'); - script.async = true; - script.src = `https://www.googletagmanager.com/gtag/js?id=${gaId}`; - document.head.appendChild(script); - - const analytics = window as unknown as AnalyticsWindow; - const dataLayer = analytics.dataLayer ?? (analytics.dataLayer = []); - // gtag.js reads the queue entries as the arguments objects the snippet pushes, not as arrays - const gtag = function (..._args: GtagArguments) { - dataLayer.push(arguments); - }; - analytics.gtag = gtag; - - gtag('js', new Date()); - gtag('config', gaId, { send_page_view: false }); - return gtag; - } -} diff --git a/src/frontend/src/app/features/cookies/cookies.html b/src/frontend/src/app/features/cookies/cookies.html index 8a1f375..7100cec 100644 --- a/src/frontend/src/app/features/cookies/cookies.html +++ b/src/frontend/src/app/features/cookies/cookies.html @@ -11,17 +11,17 @@
- {{ appName }} uses browser local storage to enhance your user experience. We do not use - third-party cookies or tracking technologies. This policy explains what we store locally - in your browser and why. + {{ appName }} sets no cookies and uses no tracking technologies. It keeps a few preferences in + your browser's local storage so the site remembers them. This policy lists exactly what is + stored and why.
- All data is stored locally in your browser using localStorage. Nothing is sent to - external servers except when explicitly requested by your actions (e.g., package searches). + These entries live only in your browser (localStorage, or sessionStorage where noted). They are + never sent to our servers or to anyone else.
- All items marked as "Functional" or "Essential" are necessary for the Service to operate - correctly. We do not use analytics, advertising, or tracking cookies. + "Essential" entries are needed for the Service to work; "Functional" entries only remember your + choices. None of them identify or track you, so they need no consent.
+ We count visits with Cloudflare Web Analytics, which sets no cookies, stores nothing in your + browser and does not track you across sites or visits. +
- {{ appName }} does not integrate with any third-party services that set cookies or track - users. All functionality is self-contained within our application. + No third-party service sets cookies on {{ appName }}. Your browser does contact Google Fonts and + GitHub to load fonts and images; see the + Privacy Policy + for what they receive.
- Clearing local storage will reset your theme preference and consent choice. Essential - functional data (like auth tokens) will also be removed, logging you out if applicable. + Clearing it resets your theme and registry choices and shows the privacy notice again. + Administrators are also signed out.
@@ -97,8 +115,7 @@We may update this Cookie Policy from time to time. Changes will be posted on this page - with an updated "Last updated" date. Continued use of {{ appName }} after changes - constitutes acceptance of the updated policy. + with an updated "Last updated" date.
diff --git a/src/frontend/src/app/features/cookies/cookies.ts b/src/frontend/src/app/features/cookies/cookies.ts index 0d60ce9..99d4fcf 100644 --- a/src/frontend/src/app/features/cookies/cookies.ts +++ b/src/frontend/src/app/features/cookies/cookies.ts @@ -12,7 +12,7 @@ export class CookiesComponent implements OnInit { private seo = inject(SeoService); appName = APP_CONFIG.name; contactEmail = APP_CONFIG.contact.email; - lastUpdated = 'February 9, 2026'; + lastUpdated = 'October 19, 2026'; ngOnInit() { this.seo.updateMeta({ diff --git a/src/frontend/src/app/features/privacy/privacy.html b/src/frontend/src/app/features/privacy/privacy.html index 92170b0..2852742 100644 --- a/src/frontend/src/app/features/privacy/privacy.html +++ b/src/frontend/src/app/features/privacy/privacy.html @@ -11,111 +11,216 @@- {{ appName }} is committed to protecting your privacy. This policy explains what information we - collect, how we use it, and your rights regarding that information. We designed the Service to - minimize data collection — we only collect what is necessary to operate and improve {{ appName }}. + This policy explains what personal data {{ appName }} ("the Service", "we") processes when you + visit {{ siteUrl }}, why, for how long, and which rights you have. {{ appName }} needs no account + to use, sets no tracking cookies and shows no ads. We keep only what is needed to run the Service + and protect it from abuse.
+ The data controller is {{ controllerName }}, the individual developer who operates {{ appName }} + (based in {{ controllerCountry }}). You can reach the controller about anything in this policy at + {{ contactEmail }}. +
++ For every request to our API we record your IP address, your browser's user agent string, the + requested path, the HTTP method, the response status and the time. We use these logs to detect + abuse, enforce rate limits and diagnose errors. +
+ ++ When you compare package versions, we store the package name, the selected versions, + whether prereleases were included, the time, how long the analysis took and your IP address. + Package names and versions feed the public "recent" and "popular" lists; your IP address is never + shown publicly and is used only to spot automated abuse. +
+ ++ Short-lived counters keyed by IP address enforce rate limits and expire on their own. An IP address + that attacks or overloads the Service can be blocked, temporarily or permanently, together with the + reason for the block. +
-- When you search for a package, we store the package name, selected versions, and a timestamp - of the search. This data is used to display recent searches and improve the Service. + We measure traffic with Cloudflare Web Analytics. It sets no cookies, uses no browser storage and + does not fingerprint or follow individual visitors. It reports aggregated data such as the page + visited, the referring site, browser and device type, country and page load performance.
-- We may collect your IP address, browser type, and general usage patterns through server logs. - This information is used for security monitoring, rate limiting, and diagnosing technical issues. + There is no public sign-up. The only accounts are those of the Service's administrators, for whom + we store a username, an email address and a hashed password, never the password itself.
-- If you create an account, we store your email address and an encrypted password. We never store - passwords in plain text. + If you write to us, we use your email address and message only to answer you.
+ We do not sell your data, use it for advertising, build profiles of you, or make automated + decisions about you that have legal or similarly significant effects. +
| Data | +Retention | +
|---|---|
| Request logs | +Deleted automatically after 30 days | +
| Search data | +Deleted automatically after 90 days | +
| Rate-limit counters | +Expire on their own within minutes | +
| Temporary IP blocks | +Removed once the block expires | +
| Permanent IP blocks | +Kept until an administrator lifts the block or you ask us to review it | +
| Database backups | +Overwritten after 14 days | +
| Server and container logs | +Rotated automatically and overwritten after a short period | +
| Administrator accounts | +Until the account is removed | +
| Email correspondence | +As long as needed to handle your request | +
- Data is stored on secured servers. Cached package data is automatically purged after 30 days - and refreshed from upstream sources. Search history is retained for operational purposes and may - be periodically cleaned. You can request deletion of your data by contacting us. +
We share personal data only with the providers needed to run the Service:
++ Cloudflare, Google and GitHub may process data outside your country, including in the United + States. Cloudflare processes it under its Data Processing Addendum, which includes the European + Commission's Standard Contractual Clauses.
-- {{ appName }} retrieves data from NuGet.org, npmjs.com, and GitHub to display package information. - When you search for a package, we make requests to these services on your behalf. These services - have their own privacy policies that govern how they handle data. +
+ To build release notes, our server fetches public data from NuGet.org, npmjs.com, PyPI, pub.dev and + GitHub, and security data from sources such as OSV and deps.dev. These requests come from our server + and contain package names and versions only — never your IP address or any other data about you.
+ Under the GDPR, the KVKK (Article 11) and similar laws, you can: +
- To exercise any of these rights, contact us at + To use any of these rights, email {{ contactEmail }}. + Most of our records are keyed only by IP address, so please include the IP address(es) and the + approximate dates concerned. We answer within 30 days.
- We may update this Privacy Policy from time to time. Changes will be posted on this page with - an updated revision date. Continued use of the Service after changes constitutes acceptance - of the revised policy. + Traffic is encrypted with TLS, the database is reachable only from inside the server, administrative + areas sit behind additional access controls, and backups are readable only by the server's owner.
- For complete transparency, please also review: +
+ {{ appName }} is a developer tool and is not directed at children under 16. +
++ We may update this policy when the Service changes. The new version is posted on this page with + an updated date.
+- If you have questions about this Privacy Policy, please contact us at + Questions about this policy or your data: {{ contactEmail }}.
{{ appName }} is a free, open-source tool that allows users to compare package versions across - registries (such as NuGet and npm), view release notes, and analyze differences between versions. + registries (NuGet, npm, PyPI and pub.dev), view release notes, and analyze differences between versions. The Service retrieves publicly available data from package registries and GitHub repositories.
@@ -42,16 +42,17 @@The release notes, version information, and package metadata displayed on {{ appName }} are sourced - from third-party registries (NuGet.org, npmjs.com) and GitHub. We do not claim ownership of this - content. The accuracy, completeness, and timeliness of this data depend on the upstream sources. - {{ appName }} is not responsible for errors or omissions in third-party data. + from third-party registries (NuGet.org, npmjs.com, PyPI and pub.dev) and GitHub. We do not claim + ownership of this content; it belongs to its authors and is shown with a link to its source. The accuracy, completeness, and timeliness of this data depend on the upstream sources. + {{ appName }} is not responsible for errors or omissions in third-party data. If you own content + shown here and want it corrected or removed, contact us and we will act on it promptly.
- The {{ appName }} source code is available under its respective open-source license. The Service's + The {{ appName }} source code is available under the MIT License. The Service's branding, design, and original content are the property of the {{ appName }} project. Third-party package names, logos, and trademarks belong to their respective owners.
diff --git a/src/frontend/src/app/features/terms/terms.ts b/src/frontend/src/app/features/terms/terms.ts index c07e431..17e24ab 100644 --- a/src/frontend/src/app/features/terms/terms.ts +++ b/src/frontend/src/app/features/terms/terms.ts @@ -13,7 +13,7 @@ export class TermsComponent implements OnInit { private seo = inject(SeoService); appName = APP_CONFIG.name; contactEmail = APP_CONFIG.contact.email; - lastUpdated = 'February 8, 2026'; + lastUpdated = 'October 19, 2026'; ngOnInit(): void { this.seo.updateMeta({ diff --git a/src/frontend/src/app/shared/components/cookie-consent/cookie-consent.html b/src/frontend/src/app/shared/components/cookie-consent/cookie-consent.html index 19f406c..6b6031d 100644 --- a/src/frontend/src/app/shared/components/cookie-consent/cookie-consent.html +++ b/src/frontend/src/app/shared/components/cookie-consent/cookie-consent.html @@ -6,30 +6,24 @@- We use local storage to enhance your experience. No third-party cookies or tracking. - - Learn more + Privacy Policy