diff --git a/web/astro.config.mjs b/web/astro.config.mjs index de1a52d..7c50b13 100644 --- a/web/astro.config.mjs +++ b/web/astro.config.mjs @@ -107,6 +107,7 @@ export default defineConfig({ '/service/[...slug]/proof': '/service/[...slug]/#verification', '/attribute/[...slug]': '/attributes', '/attr/[...slug]': '/attributes', + '/service/[...slug]/review': '/service/[...slug]#comments', // #endregion }, env: { diff --git a/web/prisma/seed.ts b/web/prisma/seed.ts index 74edd41..84d5853 100755 --- a/web/prisma/seed.ts +++ b/web/prisma/seed.ts @@ -1143,7 +1143,7 @@ async function main() { } let users = await Promise.all( - Array.from({ length: 10 }, async () => { + Array.from({ length: 570 }, async () => { const { user } = await createAccount() return user }) diff --git a/web/src/components/CommentItem.astro b/web/src/components/CommentItem.astro index 46d23cb..90114c2 100644 --- a/web/src/components/CommentItem.astro +++ b/web/src/components/CommentItem.astro @@ -150,7 +150,7 @@ const commentUrl = makeCommentUrl({ serviceSlug, commentId: comment.id, origin: checked={comment.suspicious} /> -
+
diff --git a/web/src/components/InputSelect.astro b/web/src/components/InputSelect.astro index 6800c9e..e164b58 100644 --- a/web/src/components/InputSelect.astro +++ b/web/src/components/InputSelect.astro @@ -14,10 +14,11 @@ type Props = Omit, 'children' | 'inputId' | value: string disabled?: boolean }[] - selectProps?: Omit, 'name'> + selectProps?: Omit, 'name' | 'value'> + selectedValue?: string[] | string } -const { options, selectProps, ...wrapperProps } = Astro.props +const { options, selectProps, selectedValue, ...wrapperProps } = Astro.props const inputId = selectProps?.id ?? Astro.locals.makeId(`input-${wrapperProps.name}`) const hasError = !!wrapperProps.error && wrapperProps.error.length > 0 @@ -39,7 +40,15 @@ const hasError = !!wrapperProps.error && wrapperProps.error.length > 0 > { options.map((option) => ( - )) diff --git a/web/src/constants/attributeTypes.ts b/web/src/constants/attributeTypes.ts index d7b571e..ff06755 100644 --- a/web/src/constants/attributeTypes.ts +++ b/web/src/constants/attributeTypes.ts @@ -108,3 +108,19 @@ export const { }, ] as const satisfies AttributeTypeInfo[] ) + +export const baseScoreType = { + value: 'BASE_SCORE', + slug: 'base-score', + label: 'Base score', + icon: 'ri:information-line', + order: 5, + classNames: { + container: 'bg-night-500', + subcontainer: '', + text: 'text-day-200', + textLight: '', + icon: '', + button: '', + }, +} as const satisfies AttributeTypeInfo diff --git a/web/src/constants/contactMethods.ts b/web/src/constants/contactMethods.ts index 0f9eeda..e53c222 100644 --- a/web/src/constants/contactMethods.ts +++ b/web/src/constants/contactMethods.ts @@ -106,7 +106,7 @@ export const { type: 'matrix', label: 'Matrix', matcher: /^https?:\/\/(?:www\.)?matrix\.to\/#\/(.+)/, - formatter: ([, value]) => (value ? `#${value}` : 'Matrix'), + formatter: ([, value]) => value ?? 'Matrix', icon: 'ri:hashtag', urlType: 'url', }, @@ -121,7 +121,7 @@ export const { { type: 'simplex', label: 'SimpleX Chat', - matcher: /^https?:\/\/(?:www\.)?(simplex\.chat)\//, + matcher: /^https?:\/\/(?:www\.)?((?:simplex\.chat|smp\d+\.simplex\.im))\//, formatter: () => 'SimpleX Chat', icon: 'simplex', urlType: 'url', diff --git a/web/src/constants/kycLevels.ts b/web/src/constants/kycLevels.ts index 5f48e0c..ee68374 100644 --- a/web/src/constants/kycLevels.ts +++ b/web/src/constants/kycLevels.ts @@ -2,12 +2,16 @@ import { makeHelpersForOptions } from '../lib/makeHelpersForOptions' import { parseIntWithFallback } from '../lib/numbers' import { transformCase } from '../lib/strings' +import type { AttributeType } from '@prisma/client' + type KycLevelInfo = { id: T value: number icon: string name: string description: string + privacyPoints: number + attributeType: AttributeType } export const { @@ -22,6 +26,8 @@ export const { icon: 'diamond-question', name: `KYC ${id ? transformCase(id, 'title') : String(id)}`, description: '', + privacyPoints: 0, + attributeType: 'INFO', }), [ { @@ -30,6 +36,8 @@ export const { icon: 'anonymous-mask', name: 'Guaranteed no KYC', description: 'Terms explicitly state KYC will never be requested.', + privacyPoints: 25, + attributeType: 'GOOD', }, { id: '1', @@ -37,6 +45,8 @@ export const { icon: 'diamond-question', name: 'No KYC mention', description: 'No mention of current or future KYC requirements.', + privacyPoints: 15, + attributeType: 'GOOD', }, { id: '2', @@ -45,6 +55,8 @@ export const { name: 'KYC on authorities request', description: 'No routine KYC, but may cooperate with authorities, block funds or implement future KYC requirements.', + privacyPoints: -5, + attributeType: 'WARNING', }, { id: '3', @@ -52,6 +64,8 @@ export const { icon: 'gun', name: 'Shotgun KYC', description: 'May request KYC and block funds based on automated triggers.', + privacyPoints: -15, + attributeType: 'WARNING', }, { id: '4', @@ -59,6 +73,8 @@ export const { icon: 'fingerprint-detailed', name: 'Mandatory KYC', description: 'Required for key features and can be required arbitrarily at any time.', + privacyPoints: -25, + attributeType: 'BAD', }, ] as const satisfies KycLevelInfo<'0' | '1' | '2' | '3' | '4'>[] ) diff --git a/web/src/lib/attributes.ts b/web/src/lib/attributes.ts index f966d2f..c59fb09 100644 --- a/web/src/lib/attributes.ts +++ b/web/src/lib/attributes.ts @@ -2,6 +2,8 @@ import { orderBy } from 'lodash-es' import { getAttributeCategoryInfo } from '../constants/attributeCategories' import { getAttributeTypeInfo } from '../constants/attributeTypes' +import { getKycLevelClarificationInfo } from '../constants/kycLevelClarifications' +import { kycLevels } from '../constants/kycLevels' import { serviceVisibilitiesById } from '../constants/serviceVisibility' import { READ_MORE_SENTENCE_LINK, verificationStatusesByValue } from '../constants/verificationStatus' @@ -27,7 +29,7 @@ type NonDbAttribute = Prisma.AttributeGetPayload<{ }[] } -export const nonDbAttributes: (NonDbAttribute & { +type NonDbAttributeFull = NonDbAttribute & { customize: ( service: Prisma.ServiceGetPayload<{ select: { @@ -41,12 +43,16 @@ export const nonDbAttributes: (NonDbAttribute & { onionUrls: true i2pUrls: true acceptedCurrencies: true + kycLevel: true + kycLevelClarification: true } }> ) => Partial> & { show: boolean } -})[] = [ +} + +export const nonDbAttributes: NonDbAttributeFull[] = [ { slug: 'verification-verified', title: 'Verified', @@ -135,6 +141,31 @@ export const nonDbAttributes: (NonDbAttribute & { description: `${verificationStatusesByValue.VERIFICATION_FAILED.description} ${READ_MORE_SENTENCE_LINK}\n\nCheck out the [proof](#verification).`, }), }, + ...kycLevels.map((kycLevel) => ({ + slug: `kyc-level-${kycLevel.id}`, + title: kycLevel.name, + type: kycLevel.attributeType, + category: 'PRIVACY', + description: kycLevel.description, + privacyPoints: kycLevel.privacyPoints, + trustPoints: 0, + links: [ + { + url: `/?max-kyc=${kycLevel.id}`, + label: 'With this or better', + icon: 'ri:search-line', + }, + ], + customize: (service) => { + const clarification = getKycLevelClarificationInfo(service.kycLevelClarification) + return { + show: service.kycLevel === kycLevel.value, + title: kycLevel.name + (clarification.value !== 'NONE' ? ` (${clarification.label})` : ''), + description: + kycLevel.description + (clarification.value !== 'NONE' ? ` ${clarification.description}` : ''), + } + }, + })), { slug: 'archived', title: serviceVisibilitiesById.ARCHIVED.label, @@ -261,20 +292,7 @@ export function sortAttributes< } export function makeNonDbAttributes( - service: Prisma.ServiceGetPayload<{ - select: { - verificationStatus: true - serviceVisibility: true - isRecentlyListed: true - listedAt: true - createdAt: true - tosReviewAt: true - tosReview: true - onionUrls: true - i2pUrls: true - acceptedCurrencies: true - } - }>, + service: Parameters[0], { filter = false }: { filter?: boolean } = {} ) { const attributes = nonDbAttributes.map(({ customize, ...attribute }) => ({ diff --git a/web/src/pages/about.mdx b/web/src/pages/about.mdx index ee19be8..1065d51 100644 --- a/web/src/pages/about.mdx +++ b/web/src/pages/about.mdx @@ -153,15 +153,6 @@ Scores are calculated **automatically** using clear, fixed rules. We do not chan The privacy score measures how well a service protects user privacy, using a transparent, rules-based approach: 1. **Base Score:** Every service starts with a neutral score of 50 points. -1. **KYC Level:** Adjusts the score based on the level of identity verification required: - - KYC Level 0 (No KYC): **+25 points** - - KYC Level 1 (Minimal KYC): **+10 points** - - KYC Level 2 (Moderate KYC): **-5 points** - - KYC Level 3 (More KYC): **-15 points** - - KYC Level 4 (Full mandatory KYC): **-25 points** -1. **Onion URL:** **+5 points** if the service offers at least one Onion (Tor) URL. -1. **I2P URL:** **+5 points** if the service offers at least one I2P URL. -1. **Monero Acceptance:** **+5 points** if the service accepts Monero as a payment method. 1. **Privacy Attributes:** The sum of all privacy points from attributes categorized as 'PRIVACY' is added to the score. [See all attributes](/attributes). 1. **Final Score Range:** The final score is always kept between 0 and 100. @@ -170,13 +161,6 @@ The privacy score measures how well a service protects user privacy, using a tra The trust score represents how reliable and trustworthy a service is, based on objective, transparent criteria. 1. **Base Score:** Every service begins with a neutral score of 50 points. -1. **Verification Status:** - - **Verification Success:** +10 points - - **Approved:** +5 points - - **Community Contributed:** 0 points - - **Verification Failed (SCAM):** -50 points -1. **Recently Listed:** If a service was listed within the last 15 days and its status is `APPROVED`, a penalty of -10 points is applied to the trust score, and the service is flagged as recently listed. -1. **Can't Analyze ToS:** If a service's Terms of Service cannot be analyzed by our AI (usually due to captchas, client-side rendering, DDoS protections, or non-text format), a penalty of -3 points is applied to the trust score. 1. **Trust Attributes:** The total trust points from all attributes categorized as 'TRUST' are added to the score. [See all attributes](/attributes). 1. **Final Score Range:** The final score is always kept between 0 and 100. diff --git a/web/src/pages/admin/service-suggestions/[id].astro b/web/src/pages/admin/service-suggestions/[id].astro index 4e2b528..6cf3088 100644 --- a/web/src/pages/admin/service-suggestions/[id].astro +++ b/web/src/pages/admin/service-suggestions/[id].astro @@ -173,7 +173,7 @@ const typeInfo = getServiceSuggestionTypeInfo(serviceSuggestion.type) label: status.label, value: status.value, }))} - selectProps={{ value: serviceSuggestion.status }} + selectedValue={serviceSuggestion.status} class="flex-1" error={serviceSuggestionUpdateInputErrors.status} /> diff --git a/web/src/pages/admin/services/[slug]/edit.astro b/web/src/pages/admin/services/[slug]/edit.astro index f4e7285..d99d44d 100644 --- a/web/src/pages/admin/services/[slug]/edit.astro +++ b/web/src/pages/admin/services/[slug]/edit.astro @@ -801,7 +801,8 @@ const apiCalls = await Astro.locals.banners.try( label: type.label, value: type.id, }))} - selectProps={{ required: true, value: event.type }} + selectedValue={event.type} + selectProps={{ required: true }} error={eventUpdateInputErrors.type} /> @@ -982,7 +983,7 @@ const apiCalls = await Astro.locals.banners.try( label: status.label, value: status.value, }))} - selectProps={{ value: step.status }} + selectedValue={step.status} error={verificationStepUpdateInputErrors.status} /> diff --git a/web/src/pages/service/[slug].astro b/web/src/pages/service/[slug].astro index ec04618..7e0ee0a 100644 --- a/web/src/pages/service/[slug].astro +++ b/web/src/pages/service/[slug].astro @@ -27,7 +27,7 @@ import Tooltip from '../../components/Tooltip.astro' import UserBadge from '../../components/UserBadge.astro' import VerificationWarningBanner from '../../components/VerificationWarningBanner.astro' import { getAttributeCategoryInfo } from '../../constants/attributeCategories' -import { getAttributeTypeInfo } from '../../constants/attributeTypes' +import { baseScoreType, getAttributeTypeInfo } from '../../constants/attributeTypes' import { formatContactMethod } from '../../constants/contactMethods' import { currencies, getCurrencyInfo } from '../../constants/currencies' import { getEventTypeInfo } from '../../constants/eventTypes' @@ -1052,6 +1052,20 @@ const activeEventToShow = ) })} +
  • + + {baseScoreType.label} + +50 +
  • ) } @@ -1459,6 +1473,7 @@ const activeEventToShow = Comments