initial commit

This commit is contained in:
pluja 2023-10-30 22:41:30 +01:00
commit b8564a4af7
41 changed files with 7076 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
dev.sh
node_modules
*.fiber.gz
style.css
src/ent/*
!src/ent/schema/
!src/ent/generate.go

3
.opencommitignore Normal file
View File

@ -0,0 +1,3 @@
src/ent/*
!src/ent/schema/
!src/ent/generate.go

11
README.md Normal file
View File

@ -0,0 +1,11 @@
# KYCNOT.ME OFFICIAL REPOSITORY
This is the official repository of the KYCNOT.ME project.
## Comments
Comments are based on [Disgus](). I'm using a slightly modified version of the [Disgus]() project, to fit the KYCNOT.me theme and needs.
## ToS Checker
The ToS checker is an automated ai-driven tool that checks the ToS of a website.

4
TODO.md Normal file
View File

@ -0,0 +1,4 @@
# Todo
- [ ] Add simple points (features) system
- [ ]

1054
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

7
package.json Normal file
View File

@ -0,0 +1,7 @@
{
"devDependencies": {
"@tailwindcss/forms": "^0.5.6",
"@tailwindcss/typography": "^0.5.10",
"tailwindcss": "^3.3.3"
}
}

5
src/.env Normal file
View File

@ -0,0 +1,5 @@
DB_TYPE="sqlite"
ROOT_DIR="./"
LISTEN_ADDR="127.0.0.1:4488"
POW_DIFFICULTY=4
POW_INCREASE_EVERY_CHALLENGES=20

174
src/database/attributes.go Normal file
View File

@ -0,0 +1,174 @@
package database
type Attribute struct {
Title string `json:"title"`
Description string `json:"description"`
Rating string `json:"rating"`
}
const (
AttributeRatingInfo = "info"
AttributeRatingGood = "good"
AttributeRatingWarning = "warning"
AttributeRatingBad = "bad"
AttributeRiskPreventionSystem = "auripr"
AttributeKYCForSomeFeatures = "kmausfe"
AttributeKYCIfObligedByLaw = "koblainpo"
AttributeCustodialWallet = "cuwa"
AttributeAccountNeeded = "acneus"
AttributePrivateSourceCode = "socopr"
AttributeNoCustomerSupport = "nocusu"
AttributeBlockFundsIfFlagged = "blfuflsu"
AttributeUnclearRefundPolicy = "unrepo"
AttributeSellerWalletCustodial = "sewacu"
AttributeRefundRequiresKYC = "repedi"
AttributeToSNotScrapable = "tonosc"
AttributePartnersMayEnforceKYC = "pamakyc"
AttributeNonCustodialWallet = "nocuwa"
AttributeOpenSource = "ossli"
AttributeP2P = "p2p"
AttributeNoRegistrationNeeded = "norene"
AttributeNoPersonalInfoNeeded = "nopine"
AttributeStrictNoKYCPolicy = "stnokyc"
AttributeRefundNoKYC = "refnokyc"
AttributeStrictNoLogPolicy = "stnolog"
AttributeMobileAppAvailable = "moapp"
AttributeNoJavaScriptNeeded = "nojs"
AttributeTelegramBotAvailable = "tebot"
AttributeAPIAvailable = "api"
AttributeJavaScriptNeeded = "yesjs"
)
var ServiceAttributes map[string]Attribute = map[string]Attribute{
"auripr": {
Title: "KYC may be enforced by an automated risk prevention system",
Description: "If the trade is flagged suspicious by the service, the user will be required a KYC procedure in order to get a refund.",
Rating: AttributeRatingBad,
},
"kmausfe": {
Title: "KYC is mandatory to use some features",
Description: "Some features offered by this service need KYC for the user to use them.",
Rating: AttributeRatingBad,
},
"koblainpo": {
Title: "May require KYC/SOF if obliged by law or internal policies",
Description: "If obliged to do so by the law or in accordance with the service's internal policy, it may at any time introduce or amend mandatory identification / verification procedures and require the user to complete identification and/or verification and may also require to submit identification documents (KYC) or provide Source of Funds (SOF) information.",
Rating: AttributeRatingBad,
},
"cuwa": {
Title: "Custodial wallet",
Description: "A custodial wallet is a type of crypto wallet where a third party holds and manages the private keys to your wallet and your assets in custody. The custodian is responsible for safeguarding your funds, and you entrust them with your private keys. Custodial wallets are usually provided by centralized crypto exchanges like Coinbase or Binance. Using a custodial wallet requires a great deal of trust in the institution.",
Rating: AttributeRatingWarning,
},
"acneus": {
Title: "Account needed to use the service",
Description: "Users require creating and maintaining an account with the service in order to access and use its features. ",
Rating: AttributeRatingInfo,
},
"socopr": {
Title: "Source code is private",
Description: "The source code of the service is not publicly available. This means that the service cannot be audited by the community. This is not necessarily bad, but it is something to keep in mind.",
Rating: AttributeRatingInfo,
},
"nocusu": {
Title: "poor or no customer support",
Description: "The service does not offer customer support, or the quality of service provided by the support team is inadequate or unsatisfactory.",
Rating: AttributeRatingWarning,
},
"blfuflsu": {
Title: "may block funds if flagged suspicious",
Description: "User funds may pass through an analysis system that may flag the source of the funds as suspicious. If this happens, the user may be refunded or may be required a KYC procedure.",
Rating: AttributeRatingWarning,
},
"unrepo": {
Title: "unclear refund policy",
Description: "This service has an unclear or nonexistent statement about how they manage user refunds.",
Rating: AttributeRatingWarning,
},
"sewacu": {
Title: "The seller wallet is custodial",
Description: "The seller wallet is custodial, which means that the seller does not have control over the funds. This is a common practice in P2P exchanges, where the service needs to have control over the funds in order to be able to refund the buyer if the trade enters a dispute.",
Rating: AttributeRatingWarning,
},
"repedi": {
Title: "Refunds may require KYC or personal information disclosure",
Description: "In certain cases, the refund process of these services may require the completion of a Know Your Customer (KYC) procedure or the disclosure of personal information. Some services, such as aggregators, usually don't control the KYC procedures of their partners so they fall in this category.",
Rating: AttributeRatingWarning,
},
"tonosc": {
Title: "ToS page is not possible to scrape",
Description: "The ToS page has some mechanism that makes it impossible for KYCNOT.me to scrape its content. These methods include, for example, using a canvas and rendering the text on it.",
Rating: AttributeRatingWarning,
},
"pamakyc": {
Title: "Partners may enforce KYC policies",
Description: "The service has partners that may enforce or implement KYC policies. This is common in aggregators, where the service does not control the KYC policies of their partners.",
Rating: AttributeRatingWarning,
},
"nocuwa": {
Title: "Non-custodial wallet",
Description: "A non-custodial wallet is a type of crypto wallet where the user holds and manages the private keys to the wallet and the assets in custody. The user is responsible for safeguarding their funds, and they are the only ones with access to their private keys. Non-custodial wallets are usually provided by decentralized crypto exchanges like Uniswap or Sushiswap. Using a non-custodial wallet requires no trust in any institution.",
Rating: AttributeRatingGood,
},
"ossli": {
Title: "The service is open source",
Description: "The source code of the service is publicly available. This means that the service can be audited by the community. This is not necessarily good, but it is something to keep in mind.",
Rating: AttributeRatingGood,
},
"p2p": {
Title: "Peer to peer",
Description: "Peer-to-peer marketplaces are online platforms where people can trade goods, cryptocurrencies and services directly from each other, without the need for intermediaries like traditional retailers or service providers. In this case, the service is based on such type of market.",
Rating: AttributeRatingGood,
},
"norene": {
Title: "No registration needed",
Description: "Users can access and use the service without creating an account. This enables a faster and more convenient user experience while also offering enhanced privacy and anonymity.",
Rating: AttributeRatingGood,
},
"nopine": {
Title: "No personal information needed",
Description: "Users can create an account with these services without having to provide any personal information such as their name, address, or identification documents. This offers a high degree of privacy and anonymity to users who prefer to keep their personal information private.",
Rating: AttributeRatingGood,
},
"stnokyc": {
Title: "Strict no-KYC policy",
Description: "The service has a strict no-KYC policy, which means that it does not require users to complete any KYC procedure in order to access and use its features.",
Rating: AttributeRatingGood,
},
"refnokyc": {
Title: "Refunds do not require KYC",
Description: "The refund process of these services does not require the completion of a Know Your Customer (KYC) procedure or the disclosure of personal information.",
Rating: AttributeRatingGood,
},
"stnolog": {
Title: "Strict no-log policy",
Description: "The service has a strict no-log policy, which means that it does not collect or store any information about its users.",
Rating: AttributeRatingGood,
},
"moapp": {
Title: "Mobile app available",
Description: "The service has a mobile app available for download.",
Rating: AttributeRatingInfo,
},
"nojs": {
Title: "No JavaScript needed",
Description: "The service does not require the user to enable JavaScript in order to access and use its features.",
Rating: AttributeRatingInfo,
},
"tebot": {
Title: "Telegram bot available",
Description: "The service has a Telegram bot available.",
Rating: AttributeRatingInfo,
},
"api": {
Title: "API available",
Description: "The service has an API available.",
Rating: AttributeRatingInfo,
},
"yesjs": {
Title: "JavaScript needed",
Description: "The service requires the user to enable JavaScript in order to access and use its features.",
Rating: AttributeRatingInfo,
},
}

23
src/database/cache.go Normal file
View File

@ -0,0 +1,23 @@
package database
import (
"context"
"time"
"github.com/allegro/bigcache/v3"
)
var Cache *bigcache.BigCache
func InitCache() {
cache, _ := bigcache.New(context.Background(), bigcache.Config{
Shards: 1024,
LifeWindow: 10 * time.Minute,
CleanWindow: 2 * time.Minute,
MaxEntriesInWindow: 1000 * 10 * 60,
MaxEntrySize: 500,
Verbose: false,
HardMaxCacheSize: 0,
})
Cache = cache
}

112
src/database/database.go Normal file
View File

@ -0,0 +1,112 @@
package database
import (
"context"
"os"
_ "github.com/mattn/go-sqlite3"
"github.com/rs/zerolog/log"
"pluja.dev/kycnot.me/ent/schema"
)
var Client *ent.Client
func AddFakeData() {
sampleTosHighlight := schema.TosHighlight{
Title: "Transaction monitoring",
Text: "MEX relies on data analysis as a risk-assessment and suspicion detection tool. MEX performs a variety of compliance-related tasks, including capturing data, filtering, record-keeping, investigation management, and reporting.",
Reference: "https://example.com/tos",
}
_, err := Client.Service.Create().
SetName("bisq").
SetLogoURL("https://kycnot.me/static/img/bisq.webp").
SetDescription("Buy and sell bitcoin for fiat (or cryptocurrencies) privately and securely using Bisq's peer-to-peer network and open-source desktop software.").
SetUrls([]string{"https://bisq.network/"}).
SetTosUrls([]string{"https://bisq.network/privacy-policy/"}).
SetOnionUrls([]string{}).
SetKycLevel(0).
SetTags("market,p2p,buy,sell,anonymous").
SetPending(false).
SetListed(true).
SetVerified(true).
SetXmr(true).
SetBtc(true).
SetLightning(true).
SetFiat(true).
SetCash(true).
SetType(service.TypeExchange).
SetAttributes([]string{AttributeNonCustodialWallet, AttributeOpenSource, AttributeNoPersonalInfoNeeded, AttributeP2P}).
SetTosHighlights(&[]schema.TosHighlight{sampleTosHighlight}).
Save(context.Background())
if err != nil {
log.Fatal().Msgf("failed creating service: %v", err)
}
_, err = Client.Service.Create().
SetName("localmonero").
SetLogoURL("https://kycnot.me/static/img/localmonero.webp").
SetDescription("Peer-to-peer Monero trading platform. A marketplace where users can buy and sell Monero to and from each other. You'll be able to buy and sell online with more than 60 currencies.").
SetUrls([]string{"https://localmonero.co/"}).
SetTosUrls([]string{"https://localmonero.co/faq"}).
SetOnionUrls([]string{"http://nehdddktmhvqklsnkjqcbpmb63htee2iznpcbs5tgzctipxykpj6yrid.onion/"}).
SetKycLevel(1).
SetTags("market,p2p,buy,sell").
SetPending(false).
SetListed(true).
SetVerified(true).
SetXmr(true).
SetBtc(true).
SetLightning(false).
SetFiat(true).
SetCash(true).
SetType(service.TypeExchange).
SetAttributes([]string{AttributeNonCustodialWallet, AttributeOpenSource, AttributeNoPersonalInfoNeeded, AttributeP2P}).
SetTosHighlights(&[]schema.TosHighlight{sampleTosHighlight}).
Save(context.Background())
if err != nil {
log.Fatal().Msgf("failed creating service: %v", err)
}
}
func InitDb() {
var err error
if os.Getenv("DB_TYPE") == "sqlite" {
Client, err = ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatal().Msgf("failed opening connection to sqlite: %v", err)
}
} else if os.Getenv("DB_TYPE") == "mysql" {
Client, err = ent.Open("mysql", os.Getenv("DB_USER")+":"+os.Getenv("DB_PASS")+"@tcp("+os.Getenv("DB_HOST")+":"+os.Getenv("DB_PORT")+")/"+os.Getenv("DB_NAME"))
if err != nil {
log.Fatal().Msgf("failed opening connection to mysql: %v", err)
}
} else if os.Getenv("DB_TYPE") == "postgres" {
Client, err = ent.Open("postgres", "host="+os.Getenv("DB_HOST")+" port="+os.Getenv("DB_PORT")+" user="+os.Getenv("DB_USER")+" dbname="+os.Getenv("DB_NAME")+" password="+os.Getenv("DB_PASS")+" sslmode=disable")
if err != nil {
log.Fatal().Msgf("failed opening connection to postgres: %v", err)
}
} else {
log.Fatal().Msgf("failed opening connection to database: %v", err)
}
if err := Client.Schema.Create(context.Background()); err != nil {
panic(err)
}
// Run the auto migration tool.
ctx := context.Background()
if err := Client.Schema.Create(ctx); err != nil {
log.Fatal().Msgf("failed creating schema resources: %v", err)
}
if os.Getenv("DB_TYPE") == "sqlite" && os.Getenv("DEV_MODE") == "true" {
AddFakeData()
}
}
func Close() {
if err := Client.Close(); err != nil {
log.Fatal().Msgf("failed closing database connection: %v", err)
}
}

3
src/ent/generate.go Normal file
View File

@ -0,0 +1,3 @@
package ent
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema

48
src/ent/schema/service.go Normal file
View File

@ -0,0 +1,48 @@
package schema
import (
"time"
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
// Service holds the schema definition for the Service entity.
type Service struct {
ent.Schema
}
type TosHighlight struct {
Title string `json:"title"`
Text string `json:"text"`
Section string `json:"section"`
Reference string `json:"reference"`
}
// Fields of the Service.
func (Service) Fields() []ent.Field {
return []ent.Field{
field.String("name").NotEmpty().Unique().StructTag(`json:"name"`),
field.String("description").NotEmpty().StructTag(`json:"description"`),
field.String("logo_url").NotEmpty().StructTag(`json:"logo_url"`),
field.String("referral").Default("").StructTag(`json:"referral"`),
field.String("listing_comment").Default("").StructTag(`json:"listing_comment"`),
field.String("tags").StructTag(`json:"tags"`),
field.Strings("urls").StructTag(`json:"urls"`),
field.Strings("tos_urls").StructTag(`json:"tos_urls"`),
field.Strings("onion_urls").StructTag(`json:"onion_urls"`),
field.Strings("attributes").Default([]string{}).StructTag(`json:"attributes"`),
field.Int("kyc_level").Default(0).StructTag(`json:"kyc_level"`),
field.Bool("pending").Default(true).StructTag(`json:"pending"`),
field.Bool("listed").Default(false).StructTag(`json:"listed"`),
field.Bool("verified").Default(false).StructTag(`json:"verified"`),
field.Bool("xmr").Default(false).StructTag(`json:"xmr"`),
field.Bool("btc").Default(false).StructTag(`json:"btc"`),
field.Bool("lightning").Default(false).StructTag(`json:"lightning"`),
field.Bool("fiat").Default(false).StructTag(`json:"fiat"`),
field.Bool("cash").Default(false).StructTag(`json:"cash"`),
field.Enum("type").Values("exchange", "service", "wallet").StructTag(`json:"type"`).Default("service"),
field.Time("created_at").Default(time.Now).Immutable().StructTag(`json:"created_at"`),
field.JSON("tos_highlights", &[]TosHighlight{}).Optional().StructTag(`json:"tos_highlights"`),
}
}

40
src/go.mod Normal file
View File

@ -0,0 +1,40 @@
module pluja.dev/kycnot.me
go 1.21.3
require (
entgo.io/ent v0.12.4
github.com/allegro/bigcache/v3 v3.1.0
github.com/goccy/go-json v0.10.2
github.com/gofiber/fiber/v2 v2.50.0
github.com/gofiber/template/html/v2 v2.0.5
github.com/google/uuid v1.4.0
github.com/joho/godotenv v1.5.1
github.com/mattn/go-sqlite3 v1.14.16
github.com/rs/zerolog v1.31.0
)
require (
ariga.io/atlas v0.14.1-0.20230918065911-83ad451a4935 // indirect
github.com/agext/levenshtein v1.2.1 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/go-openapi/inflect v0.19.0 // indirect
github.com/gofiber/template v1.8.2 // indirect
github.com/gofiber/utils v1.1.0 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/hashicorp/hcl/v2 v2.13.0 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.50.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/zclconf/go-cty v1.8.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.8.0 // indirect
)

110
src/go.sum Normal file
View File

@ -0,0 +1,110 @@
ariga.io/atlas v0.14.1-0.20230918065911-83ad451a4935 h1:JnYs/y8RJ3+MiIUp+3RgyyeO48VHLAZimqiaZYnMKk8=
ariga.io/atlas v0.14.1-0.20230918065911-83ad451a4935/go.mod h1:isZrlzJ5cpoCoKFoY9knZug7Lq4pP1cm8g3XciLZ0Pw=
entgo.io/ent v0.12.4 h1:LddPnAyxls/O7DTXZvUGDj0NZIdGSu317+aoNLJWbD8=
entgo.io/ent v0.12.4/go.mod h1:Y3JVAjtlIk8xVZYSn3t3mf8xlZIn5SAOXZQxD6kKI+Q=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3bSzwk=
github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/fiber/v2 v2.50.0 h1:ia0JaB+uw3GpNSCR5nvC5dsaxXjRU5OEu36aytx+zGw=
github.com/gofiber/fiber/v2 v2.50.0/go.mod h1:21eytvay9Is7S6z+OgPi7c7n4++tnClWmhpimVHMimw=
github.com/gofiber/template v1.8.2 h1:PIv9s/7Uq6m+Fm2MDNd20pAFFKt5wWs7ZBd8iV9pWwk=
github.com/gofiber/template v1.8.2/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
github.com/gofiber/template/html/v2 v2.0.5 h1:BKLJ6Qr940NjntbGmpO3zVa4nFNGDCi/IfUiDB9OC20=
github.com/gofiber/template/html/v2 v2.0.5/go.mod h1:RCF14eLeQDCSUPp0IGc2wbSSDv6yt+V54XB/+Unz+LM=
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc=
github.com/hashicorp/hcl/v2 v2.13.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M=
github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA=
github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

65
src/main.go Normal file
View File

@ -0,0 +1,65 @@
package main
import (
"flag"
"os"
"github.com/joho/godotenv"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
// Flags
debug := flag.Bool("debug", false, "sets log level to debug")
dev := flag.Bool("dev", false, "sets dev mode")
nocache := flag.Bool("nocache", false, "disables cache")
flag.Parse()
// Flags
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if *debug {
log.Printf("Debug mode enabled")
log.Logger = log.Output(
zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: "02.01.2006 15:04:05",
},
).With().Caller().Logger()
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
if *dev {
log.Logger = log.Output(
zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: "02.01.2006 15:04:05",
},
).With().Caller().Logger()
zerolog.SetGlobalLevel(zerolog.DebugLevel)
os.Setenv("DEV_MODE", "true")
os.Setenv("SCRAPER", "false")
log.Printf("DEV mode enabled")
}
if *nocache {
log.Printf("Cache disabled")
os.Setenv("CACHE", "false")
}
// Load .env file
log.Info().Msg("Loading .env file.")
err := godotenv.Load()
if err != nil {
log.Info().Msg("No .env file found, using environment variables")
}
// Database init
log.Info().Msg("Initializing database.")
//database.InitDb()
//defer database.Close()
// Server init
log.Info().Msg("Initializing server.")
//server := server.NewServer(os.Getenv("LISTEN_ADDR"))
//server.Run()
}

112
src/pow/pow.go Normal file
View File

@ -0,0 +1,112 @@
package pow
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/allegro/bigcache/v3"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
)
type PowChallenger struct {
IncreaseEveryChallenges int
Cache *bigcache.BigCache
}
func (p *PowChallenger) Init() {
// Parse the environment variable once, at initialization
log.Printf("Initializing PoW challenger.")
if iec := os.Getenv("POW_INCREASE_EVERY_CHALLENGES"); iec != "" {
if parsed, err := strconv.Atoi(iec); err == nil {
p.IncreaseEveryChallenges = parsed
}
}
p.IncreaseEveryChallenges = 20
// Init cache
cache, _ := bigcache.New(context.Background(), bigcache.Config{
Shards: 1024,
LifeWindow: 10 * time.Minute,
CleanWindow: 2 * time.Minute,
MaxEntriesInWindow: 1000 * 10 * 60,
MaxEntrySize: 500,
Verbose: false,
HardMaxCacheSize: 0,
})
p.Cache = cache
}
// GenerateChallenge generates a new challenge for the client.
func (p *PowChallenger) PowGenerateChallenge(length int) (challenge, id string, difficulty int, err error) {
bytes := make([]byte, length/2) // since hex encoding doubles the length
_, err = rand.Read(bytes)
if err != nil {
return "", "", 0, err
}
id = uuid.New().String()
challenge = hex.EncodeToString(bytes)
difficulty, err = strconv.Atoi(os.Getenv("POW_DIFFICULTY"))
if err != nil {
log.Error().Err(err).Msg("Could not get difficulty from environment")
return "", "", 0, err
}
// Increase the difficulty by 1 every N challenges in a 10 minute period
count := p.Cache.Len()
if count%p.IncreaseEveryChallenges == 0 && count != 0 {
difficulty++
}
log.Printf("Generated challenge %v with difficulty %v", id, difficulty)
// Store the challenge in a temporary cache
err = p.Cache.Set(fmt.Sprintf("pow-%v-c", id), bytes)
if err != nil {
log.Error().Err(err).Msg("Could not set challenge in cache")
return "", "", 0, err
}
// Store the challenge in a temporary cache
difficultyBytes := make([]byte, 1)
difficultyBytes[0] = byte(difficulty)
err = p.Cache.Set(fmt.Sprintf("pow-%v-d", id), difficultyBytes)
if err != nil {
log.Error().Err(err).Msg("Could not set difficulty in cache")
return "", "", 0, err
}
return
}
// VerifyProof verifies the client's proof of work.
func (p *PowChallenger) PowVerifyProof(id, nonce string) bool {
// Get the challenge from the cache
challengeBytes, err := p.Cache.Get(fmt.Sprintf("pow-%v-c", id))
if err != nil {
log.Error().Err(err).Msg("Could not get challenge from cache")
return false
}
challenge := hex.EncodeToString(challengeBytes)
// Get the difficulty from the cache
difficultyBytes, err := p.Cache.Get(fmt.Sprintf("pow-%v-d", id))
if err != nil {
log.Error().Err(err).Msg("Could not get difficulty from cache")
return false
}
difficulty := int(difficultyBytes[0])
candidate := fmt.Sprintf("%s:%s", challenge, nonce)
hash := sha256.Sum256([]byte(candidate))
hashHex := hex.EncodeToString(hash[:])
return strings.HasPrefix(hashHex, strings.Repeat("0", difficulty))
}

View File

@ -0,0 +1,19 @@
package server
import (
"github.com/gofiber/fiber/v2"
)
func (s *Server) handleVerifyPow(c *fiber.Ctx) error {
// Get id, nonce and proof from the request
id := c.Params("id")
nonce := c.Params("nonce")
// Verify the proof of work
valid := s.PowChallenger.PowVerifyProof(id, nonce)
// Return the result
return c.JSON(fiber.Map{
"valid": valid,
})
}

193
src/server/handlers_web.go Normal file
View File

@ -0,0 +1,193 @@
package server
import (
"context"
"fmt"
"math/rand"
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog/log"
"pluja.dev/kycnot.me/database"
"pluja.dev/kycnot.me/ent/service"
"pluja.dev/kycnot.me/utils"
)
func (s *Server) handleIndex(c *fiber.Ctx) error {
nokycPf := []string{
"KYC-Free Crypto Freedom.",
"Goodbye KYC, hello privacy.",
"KYC is a scam. Don't fall for it.",
}
t := c.Query("t", "exchange")
q := c.Query("q", "")
// Currencies
xmr := c.Query("xmr", "")
btc := c.Query("btc", "")
ln := c.Query("ln", "")
cash := c.Query("cash", "")
fiat := c.Query("fiat", "")
// Query all services from the database and filter them by the query and the currencies
queryBuilder := database.Client.Service.Query().Where(service.Listed(true), service.Pending(false))
// Apply the type filter if present
if t != "" && t != "all" {
log.Printf("Type: %v", t)
queryBuilder = queryBuilder.Where(service.TypeEQ(service.Type(t)))
}
// Apply the query text filter if present
if q != "" {
log.Printf("Query: %v", q)
queryBuilder = queryBuilder.Where(
service.Or(
service.NameContains(q),
service.DescriptionContains(q),
service.TagsContains(q),
),
)
}
// Apply the currency filters if present
if xmr != "" {
queryBuilder = queryBuilder.Where(service.XmrEQ(xmr == "on"))
}
if btc != "" {
queryBuilder = queryBuilder.Where(service.BtcEQ(btc == "on"))
}
if ln != "" {
queryBuilder = queryBuilder.Where(service.LightningEQ(ln == "on"))
}
if cash != "" {
queryBuilder = queryBuilder.Where(service.CashEQ(cash == "on"))
}
if fiat != "" {
queryBuilder = queryBuilder.Where(service.FiatEQ(fiat == "on"))
}
// Execute the query
services, err := queryBuilder.All(context.Background())
if err != nil {
// Handle error (e.g., log it and return a generic error message to the client)
return c.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
return c.Render("index", fiber.Map{
"Title": "Home",
"Filters": map[string]string{
"Type": t,
"Query": q,
"Xmr": xmr,
"Btc": btc,
"Ln": ln,
"Cash": cash,
"Fiat": fiat,
},
"Current": "index",
"Services": services,
"RandomPitch": nokycPf[rand.Intn(len(nokycPf))],
}, "base")
}
func (s *Server) handleService(c *fiber.Ctx) error {
serviceName := strings.ToLower(c.Params("name"))
// Get service from database by name
service, err := database.Client.Service.Query().Where(service.NameEQ(serviceName)).First(context.Background())
if err != nil {
log.Error().Err(err).Msgf("Could not get service %v from database", serviceName)
return c.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
log.Printf("Service: %v", serviceName)
return c.Render("service", fiber.Map{
"Title": fmt.Sprintf("%v | Service", serviceName),
"Service": service,
}, "base")
}
func (s *Server) handleRequestServiceForm(c *fiber.Ctx) error {
challenge, id, difficulty, err := s.PowChallenger.PowGenerateChallenge(16)
if err != nil {
return err
}
return c.Render("request_service", fiber.Map{
"Title": "Request a service",
"Current": "request",
"Pow": fiber.Map{
"Challenge": challenge,
"Difficulty": difficulty,
"Id": id,
},
"Error": c.Query("error", ""),
}, "base")
}
func (s *Server) handleRequestServicePostForm(c *fiber.Ctx) error {
nonce, id := c.FormValue("pow-nonce"), c.FormValue("pow-id")
log.Printf("Nonce: %v, ID: %v", nonce, id)
if !s.PowChallenger.PowVerifyProof(id, nonce) {
return c.Redirect("/request/service?error=invalid-captcha")
}
// KYC Level
log.Printf("KYC Level: %v", c.FormValue("kyc_level"))
klInt, err := strconv.Atoi(c.FormValue("kyc_level"))
log.Printf("KYC Level Int: %v", klInt)
if err != nil || klInt < 0 || klInt >= 4 {
log.Error().Err(err).Msgf("Invalid KYC Level value: %v", c.FormValue("kyc_level"))
return c.Redirect("/request/service?error=invalid-kyc-level")
}
var serviceType service.Type
switch c.FormValue("type") {
case "exchange":
serviceType = service.TypeExchange
case "service":
serviceType = service.TypeService
case "wallet":
serviceType = service.TypeWallet
default:
return c.Redirect("/request/service?error=invalid-service-type")
}
// Parse tags
tags := strings.ReplaceAll(c.FormValue("tags"), " ", ",")
// Remove trailing commas
tags = strings.TrimSuffix(tags, ",")
// Remove duplicate commas
tags = strings.ReplaceAll(tags, ",,", ",")
// Remove leading commas
tags = strings.TrimPrefix(tags, ",")
// Convert to lowercase
tags = strings.ToLower(tags)
_, err = database.Client.Service.Create().
SetName(strings.ToLower(c.FormValue("name"))).
SetDescription(c.FormValue("description")).
SetType(serviceType).
SetLogoURL(utils.UrlParser(c.FormValue("logo_url"))).
SetUrls(utils.UrlListParser(c.FormValue("urls"))).
SetTosUrls(utils.UrlListParser(c.FormValue("tos_urls"))).
SetOnionUrls(utils.UrlListParser(c.FormValue("tor_urls"))).
SetTags(tags).
SetXmr(c.FormValue("xmr") == "on").
SetBtc(c.FormValue("btc") == "on").
SetLightning(c.FormValue("ln") == "on").
SetFiat(c.FormValue("fiat") == "on").
SetCash(c.FormValue("cash") == "on").
SetKycLevel(klInt).
SetPending(true).
Save(c.Context())
if err != nil {
log.Error().Err(err).Msg("Could not save service to database")
return c.Redirect("/request/service?error=internal-error")
}
return c.Redirect("/request/service?message=success")
}

91
src/server/server.go Normal file
View File

@ -0,0 +1,91 @@
package server
import (
"html/template"
"os"
"path"
"strings"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/template/html/v2"
"pluja.dev/kycnot.me/pow"
)
type Server struct {
ListenAddr string
Router *fiber.App
PowChallenger *pow.PowChallenger
}
func NewServer(listenAddr string) *Server {
// Create a new template engine
engine := html.New(path.Join(os.Getenv("ROOT_DIR"), "web/templates"), ".html")
if os.Getenv("DEV_MODE") == "true" {
engine.Reload(true)
}
engine.AddFuncMap(
map[string]interface{}{
"attr": func(s string) template.HTMLAttr {
return template.HTMLAttr(s)
},
"safe": func(s string) template.HTML {
return template.HTML(s)
},
"shortText": func(s string) string {
if len(s) > 50 {
return strings.TrimSpace(s[:50]) + "..."
}
return s
},
},
)
return &Server{
ListenAddr: listenAddr,
Router: fiber.New(fiber.Config{
JSONEncoder: json.Marshal,
JSONDecoder: json.Unmarshal,
BodyLimit: 2 * 1024 * 1024, // Increase body limit to 2MB
ServerHeader: "None", // Optional, for easier debugging
Views: engine,
}),
PowChallenger: &pow.PowChallenger{},
}
}
func (s *Server) Run() {
s.SetupMiddleware()
s.RegisterRoutes()
s.PowChallenger.Init()
s.Router.Listen(s.ListenAddr)
}
func (s *Server) SetupMiddleware() {
s.Router.Use(cors.New())
if os.Getenv("DEV_MODE") == "true" {
s.Router.Use(logger.New(logger.Config{
Format: "${time} | ${method} ${status} ${path} ${latency}\n",
}))
}
}
func (s *Server) RegisterRoutes() {
// Static routes
s.Router.Static("/static", path.Join(os.Getenv("ROOT_DIR"), "/web/static"), fiber.Static{
Compress: true,
ByteRange: true,
})
// Register HTTP route for getting initial state.
s.Router.Get("/", s.handleIndex)
s.Router.Get("/service/:name", s.handleService)
s.Router.Get("/request/service", s.handleRequestServiceForm)
s.Router.Post("/request/service", s.handleRequestServicePostForm)
// Register API routes
s.Router.Get("/api/pow/verify/:id/:nonce", s.handleVerifyPow)
}

26
src/utils/parsers.go Normal file
View File

@ -0,0 +1,26 @@
package utils
import "strings"
func UrlParser(url string) string {
url = strings.ReplaceAll(url, " ", "")
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
url = "https://" + url
}
return url
}
func UrlListParser(urls string) []string {
url_list := strings.Split(strings.ReplaceAll(urls, " ", ""), ",")
// Check all urls for http:// or https://, if not present, add it
for i, url := range url_list {
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
url_list[i] = "https://" + url
}
}
return url_list
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -0,0 +1 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_ZCsl{animation:spinner_qV4G 1.2s cubic-bezier(0.52,.6,.25,.99) infinite}.spinner_gaIW{animation-delay:.6s}@keyframes spinner_qV4G{0%{r:0;opacity:1}100%{r:11px;opacity:0}}</style><circle class="spinner_ZCsl" cx="12" cy="12" r="0"/><circle class="spinner_ZCsl spinner_gaIW" cx="12" cy="12" r="0"/></svg>

After

Width:  |  Height:  |  Size: 400 B

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

69
src/web/static/js/pow.js Normal file
View File

@ -0,0 +1,69 @@
async function computeProof(challenge, difficulty) {
let nonce = 0;
const leadingZeros = '0'.repeat(difficulty);
while (true) {
const candidate = `${challenge}:${nonce}`;
const hash = new TextEncoder().encode(candidate);
const hashArrayBuffer = await crypto.subtle.digest('SHA-256', hash);
const hashHex = Array.from(new Uint8Array(hashArrayBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
if (hashHex.startsWith(leadingZeros)) {
return nonce;
}
nonce++;
await new Promise(resolve => setTimeout(resolve, 0)); // Yield control to avoid freezing the UI
}
}
function startPow() {
const element = document.querySelector('#start-pow');
element.addEventListener('click', async () => {
const difficulty = parseInt(element.getAttribute('data-pow-d'), 10);
const challenge = element.getAttribute('data-pow-c');
const id = element.getAttribute('data-pow');
const spinner = document.querySelector('#pow-spinner');
spinner.classList.remove('hidden');
element.classList.remove('bg-blue-900');
element.classList.add('bg-gray-700');
const text = document.querySelector('#pow-text');
text.innerText = 'Computing...';
try {
const nonce = await computeProof(challenge, difficulty);
spinner.classList.add('hidden');
const url = `/api/pow/verify/${id}/${nonce}`;
const response = await fetch(url);
const data = await response.json();
if (data.valid) {
element.classList.remove('bg-gray-700');
element.classList.remove('cursor-pointer');
element.classList.add('bg-lime-700');
element.classList.add('disabled');
text.innerText = '✅ Verified!';
// Show submit button
const submit = document.querySelector('#submit-btn');
submit.classList.remove('hidden');
// Set pow-nonce form input value to nonce
const nonceInput = document.querySelector('#pow-nonce');
nonceInput.value = nonce;
} else {
element.classList.remove('bg-gray-700');
element.classList.add('bg-red-700');
text.innerText = '❌ Failed!';
}
} catch (error) {
console.error('Error:', error);
spinner.classList.add('hidden');
text.innerText = 'An error occurred!';
}
});
}

109
src/web/templates/base.html Normal file
View File

@ -0,0 +1,109 @@
<!doctype html>
<html lang='en' data-theme="dark">
<head>
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Find NON-KYC exchanges and services to use and spend your crypto anonymously.">
<meta name="keywords"
content="KYC, AML, Bitcoin, Crypto, Cryptocurrency, Exchange, Exchanges, Service, Services, Anonymous, Anonymity, Privacy, Pseudonymity, Pseudonymous, Non-KYC, No-KYC, No KYC, Monero, XMR, BTC">
<meta property="og:title" content="KYCNOT.ME" />
<meta property="og:description"
content="Find NON-KYC exchanges and services to use and spend your crypto anonymously." />
<meta property="og:type" content="website" />
<meta http-equiv="onion-location" content="http://kycnotmezdiftahfmc34pqbpicxlnx3jbf5p7jypge7gdvduu7i6qjqd.onion" />
<meta property="og:image" content="https://kycnot.me/static/img/logos/banner.webp" />
<meta property="thumbnail" content="https://kycnot.me/static/img/logos/banner.webp">
<meta name="twitter:image" content="https://kycnot.me/static/img/logos/banner.webp">
<meta property="og:url" content="https://kycnot.me" />
<link rel='stylesheet' href='/static/css/style.css'>
<link rel="shortcut icon" href="/static/assets/favicon.webp" type="image/x-icon">
<link rel="me" href="https://fosstodon.org/@kycnotme">
<!-- PROVIDE the pubkey so it can be tagged for responses/alerts -->
<meta property="nostr:pubkey" content="npub1tuta00sz4wvvzymqcfq42cqhxal6puqpylxs4yf0z28z3ryvfh9qkqmv92" />
<link rel="canonical" href="https://testing.com/testing-nostr-comments" />
<!-- CAN provide multiple relays -->
<meta property="nostr:relay" content="wss://relay.damus.io" />
<!-- MUST PROVIDE CSS -->
<link rel="stylesheet" href="/static/disgus/disgus.css">
<title>{{.Title}} - KYCnot.me</title>
</head>
<style>
.capitalize-first::first-letter {
text-transform: capitalize;
}
</style>
<body class="h-full font-mono text-gray-200 bg-fixed bg-black bg-center bg-cover" style="background-image: url('/static/assets/gradient.webp');">
{{if .Error}}
<div class="px-4 py-3 text-neutral-100 bg-error">
<p class="text-sm font-bold text-center">
{{ .Error }}
</p>
</div>
{{end}}
{{if .Message}}
<div class="px-4 py-3 text-neutral-100 bg-info">
<p class="text-sm font-bold text-center">
{{ .Message }}
</p>
</div>
{{end}}
<!-- Nav Bar -->
<nav class="bg-none">
<div class="max-w-6xl px-4 mx-auto">
<div class="flex justify-between">
<div class="flex flex-col items-center justify-center md:flex-row space-x-7">
<!-- Logo -->
<a href="/" class="block">
<img src="/static/assets/logo_wide.svg" alt="Logo" class="w-3/5 pt-2 mr-2 md:w-1/4" />
</a>
<!-- Primary Navbar items -->
<div class="flex items-center space-x-1 md:space-x-2">
<a
href="/"
class="px-1 py-1 text-xs font-semibold md:text-base {{if or (eq .Current "index") (eq .Current "")}} text-lime-500 border-lime-500 border-b-2 {{ else }} text-gray-500 hover:text-lime-500 {{ end }}"
>Home</a
>
<a
href="/request/service"
class="px-1 py-1 text-xs text-gray-500 transition duration-300 md:text-base {{if or (eq .Current "request") (eq .Current "")}} text-lime-500 border-lime-500 border-b-2 {{ else }} text-gray-500 hover:text-lime-500 {{ end }}"
>Request</a
>
<a
href=""
class="px-1 py-1 text-xs text-gray-500 transition duration-300 md:text-base {{if or (eq .Current "about") (eq .Current "")}} text-lime-500 border-lime-500 border-b-2 {{ else }} text-gray-500 hover:text-lime-500 {{ end }}"
>About</a
>
<a
href=""
class="px-1 py-1 text-xs text-gray-500 transition duration-300 md:text-base hover:text-lime-500"
>Support</a
>
</div>
</div>
</div>
</div>
</nav>
<!---->
{{embed}}
</body>
</html>
<style>
.gradient {
background: rgb(5, 58, 24);
background: linear-gradient(153deg,
rgb(3, 53, 21) 0%,
rgb(0, 17, 4) 20%,
rgba(0, 0, 0, 1) 100%);
}
</style>

View File

@ -0,0 +1,48 @@
<div class="flex flex-col justify-center max-w-lg p-4 text-sm border rounded-lg md:text-base
{{if eq . 0}} border-green-500/20 bg-green-500/10
{{else if eq . 1}} border-lime-300/40 bg-lime-300/10
{{else if eq . 2}} border-amber-700/40 bg-amber-600/20
{{else if eq . 3}} border-red-600/40 bg-red-600/20
{{end}}
">
<h2 class="flex items-center space-x-2 text-lg font-bold">
{{if eq . 0}}
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-green-600 circle-check-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z" stroke-width="0" fill="currentColor"></path>
</svg>
{{else if eq . 1}}
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-lime-400/70 circle-check" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"></path>
<path d="M9 12l2 2l4 -4"></path>
</svg>
{{else if eq . 2}}
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-amber-600 alert-triangle-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 1.67c.955 0 1.845 .467 2.39 1.247l.105 .16l8.114 13.548a2.914 2.914 0 0 1 -2.307 4.363l-.195 .008h-16.225a2.914 2.914 0 0 1 -2.582 -4.2l.099 -.185l8.11 -13.538a2.914 2.914 0 0 1 2.491 -1.403zm.01 13.33l-.127 .007a1 1 0 0 0 0 1.986l.117 .007l.127 -.007a1 1 0 0 0 0 -1.986l-.117 -.007zm-.01 -7a1 1 0 0 0 -.993 .883l-.007 .117v4l.007 .117a1 1 0 0 0 1.986 0l.007 -.117v-4l-.007 -.117a1 1 0 0 0 -.993 -.883z" stroke-width="0" fill="currentColor"></path>
</svg>
{{else if eq . 3}}
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-red-600 alert-octagon-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M14.897 1a4 4 0 0 1 2.664 1.016l.165 .156l4.1 4.1a4 4 0 0 1 1.168 2.605l.006 .227v5.794a4 4 0 0 1 -1.016 2.664l-.156 .165l-4.1 4.1a4 4 0 0 1 -2.603 1.168l-.227 .006h-5.795a3.999 3.999 0 0 1 -2.664 -1.017l-.165 -.156l-4.1 -4.1a4 4 0 0 1 -1.168 -2.604l-.006 -.227v-5.794a4 4 0 0 1 1.016 -2.664l.156 -.165l4.1 -4.1a4 4 0 0 1 2.605 -1.168l.227 -.006h5.793zm-2.887 14l-.127 .007a1 1 0 0 0 0 1.986l.117 .007l.127 -.007a1 1 0 0 0 0 -1.986l-.117 -.007zm-.01 -8a1 1 0 0 0 -.993 .883l-.007 .117v4l.007 .117a1 1 0 0 0 1.986 0l.007 -.117v-4l-.007 -.117a1 1 0 0 0 -.993 -.883z" stroke-width="0" fill="currentColor"></path>
</svg>
{{end}}
<span>
KYC LEVEL <b>{{.}}</b>
</span>
</h2>
{{if eq . 0}}
<p class="mt-2 text-sm">The exchange ToS do not mention that it will ever request the user for a KYC verification.</p>
{{else if eq . 1}}
<p class="mt-2 text-sm">KYC is not mentioned, but this service reserves the right to share data with representatives of the authorities, block funds or reject transactions.</p>
{{else if eq . 2}}
<p class="mt-2 text-sm">The exchange may request KYC from any user at any time, typically triggered by an automated flag system, leading to a temporary block of funds.</p>
{{else if eq . 3}}
<p class="mt-2 text-sm">KYC is mandatory to use some features. A non-KYCed user can be required to verify their identity at any time or any moment for any reason.</p>
{{end}}
</div>

View File

@ -0,0 +1,35 @@
<a href="/point/{{.ID}}" class="block py-1.5 px-3 text-center text-sm border rounded-lg transition duration-300
{{if eq "good" .Type }}border-green-900 bg-green-900/30 hover:bg-green-900/50 hover:border-green-700
{{else if eq "warning" .Type }}border-amber-900 bg-amber-900/30 hover:bg-amber-900/50 hover:border-amber-700
{{else if eq "bad" .Type }}border-red-900 bg-red-900/30 hover:bg-red-900/50 hover:border-red-700
{{else}}border-info-900 bg-info-900/30 hover:bg-info-900/50 hover:border-info-700
{{end}}">
<h3 class="flex items-center justify-center space-x-2">
<!-- Icon -->
{{if eq "good" .Type }}
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-green-600 circle-check-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z" stroke-width="0" fill="currentColor"></path>
</svg>
{{else if eq "warning" .Type }}
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-amber-600 alert-triangle-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 1.67c.955 0 1.845 .467 2.39 1.247l.105 .16l8.114 13.548a2.914 2.914 0 0 1 -2.307 4.363l-.195 .008h-16.225a2.914 2.914 0 0 1 -2.582 -4.2l.099 -.185l8.11 -13.538a2.914 2.914 0 0 1 2.491 -1.403zm.01 13.33l-.127 .007a1 1 0 0 0 0 1.986l.117 .007l.127 -.007a1 1 0 0 0 0 -1.986l-.117 -.007zm-.01 -7a1 1 0 0 0 -.993 .883l-.007 .117v4l.007 .117a1 1 0 0 0 1.986 0l.007 -.117v-4l-.007 -.117a1 1 0 0 0 -.993 -.883z" stroke-width="0" fill="currentColor"></path>
</svg>
{{else if eq "bad" .Type }}
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-red-600/75 alert-circle-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm.01 13l-.127 .007a1 1 0 0 0 0 1.986l.117 .007l.127 -.007a1 1 0 0 0 0 -1.986l-.117 -.007zm-.01 -8a1 1 0 0 0 -.993 .883l-.007 .117v4l.007 .117a1 1 0 0 0 1.986 0l.007 -.117v-4l-.007 -.117a1 1 0 0 0 -.993 -.883z" stroke-width="0" fill="currentColor"></path>
</svg>
{{else }}
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-blue-600 info-circle-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm0 9h-1l-.117 .007a1 1 0 0 0 0 1.986l.117 .007v3l.007 .117a1 1 0 0 0 .876 .876l.117 .007h1l.117 -.007a1 1 0 0 0 .876 -.876l.007 -.117l-.007 -.117a1 1 0 0 0 -.764 -.857l-.112 -.02l-.117 -.006v-3l-.007 -.117a1 1 0 0 0 -.876 -.876l-.117 -.007zm.01 -3l-.127 .007a1 1 0 0 0 0 1.986l.117 .007l.127 -.007a1 1 0 0 0 0 -1.986l-.117 -.007z" stroke-width="0" fill="currentColor"></path>
</svg>
{{end}}
<!-- End icon -->
<span class="capitalize-first">{{.Title}}</span>
</h3>
</a>

View File

@ -0,0 +1,35 @@
{{/* service_card.html */}}
<a href="/service/{{.Name}}">
<div class="p-4 m-2 transition duration-500 bg-gray-700 border border-transparent rounded-lg shadow-lg backdrop-blur-md bg-opacity-30 hover:border-lime-600 min-w-32">
<div class="flex items-center justify-between">
<div class="flex items-center">
<img src="{{.LogoURL}}" alt="{{.Name}} logo" class="w-12 h-12 mr-4 rounded-full">
<div class="flex items-center space-x-4">
<div class="flex flex-col text-xl font-bold text-white">
<div class="flex items-center justify-between space-x-3">
<span class="flex items-center justify-center capitalize">
{{if .Verified}}
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 mr-1 {{if .Verified}}text-blue-400/80{{else}} text-white/20 {{end}} discount-check-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12.01 2.011a3.2 3.2 0 0 1 2.113 .797l.154 .145l.698 .698a1.2 1.2 0 0 0 .71 .341l.135 .008h1a3.2 3.2 0 0 1 3.195 3.018l.005 .182v1c0 .27 .092 .533 .258 .743l.09 .1l.697 .698a3.2 3.2 0 0 1 .147 4.382l-.145 .154l-.698 .698a1.2 1.2 0 0 0 -.341 .71l-.008 .135v1a3.2 3.2 0 0 1 -3.018 3.195l-.182 .005h-1a1.2 1.2 0 0 0 -.743 .258l-.1 .09l-.698 .697a3.2 3.2 0 0 1 -4.382 .147l-.154 -.145l-.698 -.698a1.2 1.2 0 0 0 -.71 -.341l-.135 -.008h-1a3.2 3.2 0 0 1 -3.195 -3.018l-.005 -.182v-1a1.2 1.2 0 0 0 -.258 -.743l-.09 -.1l-.697 -.698a3.2 3.2 0 0 1 -.147 -4.382l.145 -.154l.698 -.698a1.2 1.2 0 0 0 .341 -.71l.008 -.135v-1l.005 -.182a3.2 3.2 0 0 1 3.013 -3.013l.182 -.005h1a1.2 1.2 0 0 0 .743 -.258l.1 -.09l.698 -.697a3.2 3.2 0 0 1 2.269 -.944zm3.697 7.282a1 1 0 0 0 -1.414 0l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.32 1.497l2 2l.094 .083a1 1 0 0 0 1.32 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z" stroke-width="0" fill="currentColor"></path>
</svg>
{{end}}
{{.Name}}
</span>
<span class="bg-green-500 backdrop-blur-md bg-opacity-70 font-bold rounded-lg py-0.5 px-2 text-sm">
10
</span>
</div>
<span class="pr-1 mt-2 text-xs font-normal text-white/60 text-opacity-60">{{shortText .Description}}</span>
</div>
</div>
</div>
</div>
<div class="flex items-center justify-center pt-3 text-xs">
<span class="px-2 py-1 mr-1 font-bold uppercase border text-[10px] rounded text-white/70 bg-zinc-900 border-zinc-700">
{{.Type}}
</span>
{{template "components/service_icons" .}}
</div>
</div>
</a>

View File

@ -0,0 +1,59 @@
<div class="grid grid-cols-5">
<!-- Monero -->
<span class="px-2 py-1 m-1 text-center text-white border rounded bg-zinc-900 border-zinc-700">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 {{if .Xmr}}text-orange-400/70{{else}} text-white/20 {{end}} tabler-coin-monero" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"></path>
<path d="M4 16h4v-7l4 4l4 -4v7h4"></path>
</svg>
</span>
<!-- Bitcoin -->
<span class="px-2 py-1 m-1 text-center text-white border rounded bg-zinc-900 border-zinc-700">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 {{if .Btc}} text-amber-400/70 {{else}} text-white/20 {{end}} coin-bitcoin" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"></path>
<path d="M9 8h4.09c1.055 0 1.91 .895 1.91 2s-.855 2 -1.91 2c1.055 0 1.91 .895 1.91 2s-.855 2 -1.91 2h-4.09"></path>
<path d="M10 12h4"></path>
<path d="M10 7v10v-9"></path>
<path d="M13 7v1"></path>
<path d="M13 16v1"></path>
</svg>
</span>
<!-- Lightning -->
<span class="px-2 py-1 m-1 text-center text-white border rounded bg-zinc-900 border-zinc-700">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 {{if .Lightning}} text-sky-400/70 {{else}} text-white/20 {{end}} bolt" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M13 3l0 7l6 0l-8 11l0 -7l-6 0l8 -11"></path>
</svg>
</span>
<!-- Cash -->
<span class="px-2 py-1 m-1 text-center text-white border rounded bg-zinc-900 border-zinc-700">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 {{if .Cash}} text-yellow-300/70 {{else}} text-white/20 {{end}}" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M9 14c0 1.657 2.686 3 6 3s6 -1.343 6 -3s-2.686 -3 -6 -3s-6 1.343 -6 3z"></path>
<path d="M9 14v4c0 1.656 2.686 3 6 3s6 -1.344 6 -3v-4"></path>
<path d="M3 6c0 1.072 1.144 2.062 3 2.598s4.144 .536 6 0c1.856 -.536 3 -1.526 3 -2.598c0 -1.072 -1.144 -2.062 -3 -2.598s-4.144 -.536 -6 0c-1.856 .536 -3 1.526 -3 2.598z"></path>
<path d="M3 6v10c0 .888 .772 1.45 2 2"></path>
<path d="M3 11c0 .888 .772 1.45 2 2"></path>
</svg>
</span>
<!-- Fiat -->
<span class="px-2 py-1 m-1 text-center text-white border rounded bg-zinc-900 border-zinc-700">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 {{if .Fiat}}text-green-400/70{{else}} text-white/20 {{end}} building-bank" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 21l18 0"></path>
<path d="M3 10l18 0"></path>
<path d="M5 6l7 -3l7 3"></path>
<path d="M4 10l0 11"></path>
<path d="M20 10l0 11"></path>
<path d="M8 14l0 3"></path>
<path d="M12 14l0 3"></path>
<path d="M16 14l0 3"></path>
</svg>
</span>
</div>

View File

@ -0,0 +1,7 @@
<div class="px-3 py-2 mt-2 border rounded-md border-amber-900 bg-amber-900/30">
<h3 class="font-bold uppercase">{{.Title}}</h3>
<p class="my-1.5 text-sm">{{.Text}}</p>
{{if .Section}}
<p class="text-xs text-gray-400 text-opacity-40">ToS Section: <span>{{.Section}}</span></p>
{{end}}
</div>

View File

@ -0,0 +1,91 @@
<main class="py-4 text-lime-500">
<div class="px-4 py-2">
<div class="mb-2 text-center bg-transparent md:grid">
<p class="font-mono text-sm font-bold uppercase md:text-base">{{.RandomPitch}}</p>
</div>
</div>
<!-- Filters -->
<div class="text-center text-gray-500">
<a href="/">
<span class="p-1.5 rounded-xl backdrop-blur-md bg-opacity-70 {{if eq "" .Filters.Type}}bg-lime-500 text-zinc-900 {{else}} border border-gray-500 {{end}} font-bold text-xs">Exchanges</span>
</a>
<a href="/?t=service">
<span class="p-1.5 rounded-xl backdrop-blur-md bg-opacity-70 {{if eq "service" .Filters.Type}}bg-lime-500 text-zinc-900 {{else}} border border-gray-500 {{end}} font-bold text-xs">Services</span>
</a>
<a href="/?t=all">
<span class="p-1.5 rounded-xl backdrop-blur-md bg-opacity-70 {{if eq "all" .Filters.Type}}bg-lime-500 text-zinc-900 {{else}} border border-gray-500 {{end}} font-bold text-xs">All</span>
</a>
</div>
<form action="/">
<input type="hidden" name="t" id="t" value="{{.Filters.Type}}"/>
<div class="flex items-center justify-center mt-6">
<input class="text-xs bg-transparent border rounded-lg border-lime-600 text-lime-600" placeholder="Search anything..." value="{{.Filters.Query}}" type="text" name="q" id="q">
</div>
<div class="flex items-center justify-center pt-4 space-x-2">
<span class="flex flex-col items-center justify-center">
<label class="text-xs uppercase" for="btc">btc</label>
<input
class="bg-transparent rounded-full outline-none appearance-none focus:ring-0 border-lime-500 text-lime-500"
type="checkbox"
{{if eq "on" .Filters.Btc}}checked{{end}}
name="btc"
id="btc"
>
</span>
<span class="flex flex-col items-center justify-center">
<label class="text-xs uppercase" for="xmr">xmr</label>
<input
class="bg-transparent rounded-full outline-none appearance-none focus:ring-0 border-lime-500 text-lime-500"
type="checkbox"
{{if eq "on" .Filters.Xmr}}checked{{end}}
name="xmr"
id="xmr"
>
</span>
<span class="flex flex-col items-center justify-center">
<label class="text-xs uppercase" for="ln">ln</label>
<input
class="bg-transparent rounded-full outline-none appearance-none focus:ring-0 border-lime-500 text-lime-500"
type="checkbox"
{{if eq "on" .Filters.Ln}}checked{{end}}
name="ln"
id="ln"
>
</span>
<span class="flex flex-col items-center justify-center">
<label class="text-xs uppercase" for="fiat">fiat</label>
<input
class="bg-transparent rounded-full outline-none appearance-none focus:ring-0 border-lime-500 text-lime-500"
type="checkbox"
{{if eq "on" .Filters.Fiat}}checked{{end}}
name="fiat"
id="fiat"
>
</span>
<span class="flex flex-col items-center justify-center">
<label class="text-xs uppercase" for="cash">cash</label>
<input
class="bg-transparent rounded-full outline-none appearance-none focus:ring-0 border-lime-500 text-lime-500"
type="checkbox"
{{if eq "on" .Filters.Cash}}checked{{end}}
name="cash"
id="cash"
>
</span>
<button class="p-1 text-xs font-bold uppercase border rounded-lg bg-none border-lime-500" type="submit">filter</button>
</div>
</form>
<!-- End Filters -->
</main>
<!-- Services list -->
<section class="flex items-center justify-center px-1 mt-4">
<div class="grid max-w-6xl grid-cols-1 md:grid-cols-3 lg:grid-cols-3">
{{range .Services}}
{{template "components/service_card" .}}
{{end}}
</div>
</section>

View File

@ -0,0 +1,149 @@
<section class="flex flex-col items-center w-full py-8">
<h2 class="py-3 text-2xl font-bold">Request a service</h2>
<form action="/request/service" method="POST" class="max-w-lg space-y-4 font-mono">
<div class="flex flex-col">
<select name="type" id="type" required class="p-2 text-white border rounded-lg bg-opacity-30 bg-zinc-900 border-white/30">
<option value="def" selected disabled>Select an option</option>
<option value="exchange">exchange</option>
<option value="service">service</option>
</select>
</div>
<div class="flex flex-col">
<label for="name">Service Name</label>
<input class="p-2 text-white border rounded-lg bg-opacity-30 bg-zinc-900 border-white/30" type="text" name="name" id="name" required>
</div>
<div class="flex flex-col">
<label for="description">Description</label>
<textarea class="p-2 font-mono text-white border rounded-lg bg-opacity-30 bg-zinc-900 border-white/30" name="description" id="description" cols="30" rows="5" required maxlength="500"></textarea>
</div>
<div class="flex flex-col">
<label for="urls">Service URL(s)</label>
<small class="text-white/30">*If more than one, separate with commas.</small>
<input class="p-2 text-white border rounded-lg bg-opacity-30 bg-zinc-900 border-white/30" type="text" name="urls" id="urls" required placeholder="Awesome Exchange">
</div>
<div class="flex flex-col">
<label for="logo_url">Logo URL</label>
<input class="p-2 text-white border rounded-lg bg-opacity-30 bg-zinc-900 border-white/30" type="text" name="logo_url" id="logo_url" placeholder="https://i.imgur.com/wwwHfCN.jpeg" required>
</div>
<div class="flex flex-col">
<label for="tos_urls">ToS URL(s)</label>
<small class="text-white/30">*If more than one, separate with commas.</small>
<input class="p-2 text-white border rounded-lg bg-opacity-30 bg-zinc-900 border-white/30" type="text" name="tos_urls" id="tos_urls" required placeholder="https://example.com/terms">
</div>
<div class="flex flex-col">
<label for="tor_urls">Tor Onion URL(s)</label>
<small class="text-white/30">*If more than one, separate with commas.</small>
<input class="p-2 text-white border rounded-lg bg-opacity-30 bg-zinc-900 border-white/30" type="text" name="tor_urls" id="tor_urls" placeholder="http://zbiowkw...onion">
</div>
<div class="flex flex-col">
<label for="tags">Keywords</label>
<small class="text-white/30">*If more than one, separate with commas.</small>
<input class="p-2 text-white border rounded-lg bg-opacity-30 bg-zinc-900 border-white/30" type="text" name="tags" id="tags" required placeholder="private,p2p, fast">
</div>
<div class="flex flex-col items-center justify-center pt-4 space-x-2">
<h3 class="mb-2 text-xl">Accepted currencies</h3>
<div class="flex space-x-4">
<span class="flex flex-col items-center justify-center">
<label class="text-xs uppercase" for="btc">Bitcoin</label>
<input
class="rounded-full outline-none appearance-none bg-opacity-30 bg-zinc-900 focus:ring-0 border-lime-500 text-lime-500"
type="checkbox"
{{if eq "on" .Filters.Btc}}checked{{end}}
name="btc"
id="btc"
>
</span>
<span class="flex flex-col items-center justify-center">
<label class="text-xs uppercase" for="xmr">MONERO</label>
<input
class="rounded-full outline-none appearance-none bg-opacity-30 bg-zinc-900 focus:ring-0 border-lime-500 text-lime-500"
type="checkbox"
{{if eq "on" .Filters.Xmr}}checked{{end}}
name="xmr"
id="xmr"
>
</span>
<span class="flex flex-col items-center justify-center">
<label class="text-xs uppercase" for="ln">LIGHTNING</label>
<input
class="rounded-full outline-none appearance-none bg-opacity-30 bg-zinc-900 focus:ring-0 border-lime-500 text-lime-500"
type="checkbox"
{{if eq "on" .Filters.Ln}}checked{{end}}
name="ln"
id="ln"
>
</span>
<span class="flex flex-col items-center justify-center">
<label class="text-xs uppercase" for="fiat">FIAT</label>
<input
class="rounded-full outline-none appearance-none bg-opacity-30 bg-zinc-900 focus:ring-0 border-lime-500 text-lime-500"
type="checkbox"
{{if eq "on" .Filters.Fiat}}checked{{end}}
name="fiat"
id="fiat"
>
</span>
<span class="flex flex-col items-center justify-center">
<label class="text-xs uppercase" for="cash">CASH</label>
<input
class="rounded-full outline-none appearance-none bg-opacity-30 bg-zinc-900 focus:ring-0 border-lime-500 text-lime-500"
type="checkbox"
{{if eq "on" .Filters.Cash}}checked{{end}}
name="cash"
id="cash"
>
</span>
</div>
</div>
<div class="space-y-4">
<h3>KYC Level</h3>
<label class="flex flex-row items-center p-2 border rounded-lg border-white/10" for="kyclevel0">
<input type="radio" name="kyc_level" id="kyclevel0" value="0">
<p class="ml-3"><b class="text-lime-400">Level 0</b>: No KYC ever. The ToS do not mention nor enforce any KYC/AML verification.</p>
</label>
<label class="flex flex-row items-center p-2 border rounded-lg border-white/10" for="kyclevel1">
<input type="radio" name="kyc_level" id="kyclevel1" value="1">
<p class="ml-3"><b class="text-lime-400">Level 1</b>: No KYC/AML procedures are mentioned as such, however the service reserves the right to share acquired data with authorities, block funds or reject transactions.</p>
</label>
<label class="flex flex-row items-center p-2 border rounded-lg border-white/10" for="kyclevel2">
<input type="radio" name="kyc_level" id="kyclevel2" value="2" checked>
<p class="ml-3"><b class="text-lime-400">Level 2</b>: KYC is not mandatory, but the service can require KYC from any user at any time, and it may block funds. This could occur, for example, if a transaccion is flagged suspicious.</p>
</label>
<label class="flex flex-row items-center p-2 border rounded-lg border-white/10" for="kyclevel3">
<input type="radio" name="kyc_level" id="kyclevel3" value="3">
<p class="ml-3"><b class="text-lime-400">Level 3</b>: KYC is mandatory to use certain features of the platform. Non-KYC users can be asked to verify their identity at any time, for any reason and may have their service restricted or revoked.</p>
</label>
</div>
<noscript>
<p class="my-2 font-bold text-yellow-500 uppercase">
You need to enable JavaScript to complete this form.
</p>
</noscript>
<div id="start-pow" class="flex items-center justify-center px-4 py-2 mt-2 mb-4 space-x-2 font-bold uppercase bg-blue-900 rounded-lg cursor-pointer" data-pow="{{.Pow.Id}}" data-pow-c="{{.Pow.Challenge}}" data-pow-d="{{.Pow.Difficulty}}">
<input type="text" name="pow-nonce" id="pow-nonce" hidden required value="">
<input type="text" name="pow-id" id="pow-id" hidden required value="{{.Pow.Id}}">
<svg id="pow-spinner" class="hidden text-white fill-white" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_ZCsl{animation:spinner_qV4G 1.2s cubic-bezier(0.52,.6,.25,.99) infinite}.spinner_gaIW{animation-delay:.6s}@keyframes spinner_qV4G{0%{r:0;opacity:1}100%{r:11px;opacity:0}}</style><circle class="spinner_ZCsl" cx="12" cy="12" r="0"/><circle class="spinner_ZCsl spinner_gaIW" cx="12" cy="12" r="0"/></svg>
<span id="pow-text">🧠 I'm Human</span>
</div>
<button id="submit-btn" class="hidden w-full px-4 py-2 my-4 space-x-2 font-bold uppercase rounded-lg cursor-pointer bg-lime-900">
Submit
</button>
</form>
</section>
<script src="/static/js/pow.js"></script>
<script>
startPow();
</script>

View File

@ -0,0 +1,159 @@
<section class="pt-14">
<div class="flex items-center justify-center space-x-4">
<img class="w-16 h-16 rounded-full" src="{{.Service.LogoURL}}" alt="">
<h1 class="text-2xl font-bold capitalize-first">{{.Service.Name}}</h1>
</div>
<!-- Links -->
<div class="flex items-center justify-center py-4 mt-2 text-sm md:text-md">
<div class="flex items-center justify-center">
<a href="{{index .Service.Urls 0}}" target="_blank" class="flex items-center justify-center px-2 py-1 m-1 space-x-2 text-center text-white transition duration-300 border rounded bg-zinc-900 border-zinc-700 hover:border-lime-600 hover:text-lime-600">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 tabler-world-www" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M19.5 7a9 9 0 0 0 -7.5 -4a8.991 8.991 0 0 0 -7.484 4"></path>
<path d="M11.5 3a16.989 16.989 0 0 0 -1.826 4"></path>
<path d="M12.5 3a16.989 16.989 0 0 1 1.828 4"></path>
<path d="M19.5 17a9 9 0 0 1 -7.5 4a8.991 8.991 0 0 1 -7.484 -4"></path>
<path d="M11.5 21a16.989 16.989 0 0 1 -1.826 -4"></path>
<path d="M12.5 21a16.989 16.989 0 0 0 1.828 -4"></path>
<path d="M2 10l1 4l1.5 -4l1.5 4l1 -4"></path>
<path d="M17 10l1 4l1.5 -4l1.5 4l1 -4"></path>
<path d="M9.5 10l1 4l1.5 -4l1.5 4l1 -4"></path>
</svg>
<span>Website</span>
</a>
{{if ne (len .Service.TosUrls) 0}}
<a href="{{index .Service.TosUrls 0}}" target="_blank" class="flex items-center justify-center px-2 py-1 m-1 space-x-2 text-center text-white transition duration-300 border rounded bg-zinc-900 border-zinc-700 hover:border-lime-600 hover:text-lime-600">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 tabler-report-search" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M8 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h5.697"></path>
<path d="M18 12v-5a2 2 0 0 0 -2 -2h-2"></path>
<path d="M8 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z"></path>
<path d="M8 11h4"></path>
<path d="M8 15h3"></path>
<path d="M16.5 17.5m-2.5 0a2.5 2.5 0 1 0 5 0a2.5 2.5 0 1 0 -5 0"></path>
<path d="M18.5 19.5l2.5 2.5"></path>
</svg>
<span>ToS</span>
</a>
{{end}}
{{if ne .Service.Referral ""}}
<a href="#" target="_blank" class="flex items-center justify-center px-2 py-1 m-1 space-x-2 text-center text-white transition duration-300 border rounded bg-zinc-900 border-zinc-700 hover:border-lime-600 hover:text-lime-600">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 tabler-users" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M9 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path>
<path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85"></path>
</svg>
<span>Referral</span>
</a>
{{end}}
{{if ne (len .Service.OnionUrls) 0}}
<a href="#" target="_blank" class="flex items-center justify-center px-2 py-1 m-1 space-x-2 text-center text-purple-600 transition duration-300 border border-purple-900 rounded bg-zinc-900 hover:border-lime-600 hover:text-lime-600">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 tabler-target" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
<path d="M12 12m-5 0a5 5 0 1 0 10 0a5 5 0 1 0 -10 0"></path>
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"></path>
</svg>
<span>Tor</span>
</a>
{{end}}
<a href="#discuss" class="flex items-center justify-center px-2 py-1 m-1 space-x-2 text-center transition duration-300 border rounded text-sky-400 border-sky-700 bg-zinc-900 hover:border-lime-600 hover:text-lime-600">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 messages" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M21 14l-3 -3h-7a1 1 0 0 1 -1 -1v-6a1 1 0 0 1 1 -1h9a1 1 0 0 1 1 1v10"></path>
<path d="M14 15v2a1 1 0 0 1 -1 1h-7l-3 3v-10a1 1 0 0 1 1 -1h2"></path>
</svg>
</a>
</div>
</div>
<!-- Icons -->
<div class="flex items-center justify-center text-xs">
{{template "components/service_icons" .Service}}
</div>
<!-- Description -->
<div class="flex items-center justify-center p-4">
<p class="max-w-lg p-4 text-sm border rounded-lg md:text-base border-white/20 bg-gray-400/10">
{{if ne .Service.Description ""}}
{{.Service.Description}}
{{else}}
This {{.Service.Type}} does not have a description.
{{end}}
</p>
</div>
</section>
<!-- KYC Level -->
<section class="flex items-center justify-center px-6">
{{template "components/kyc_level" .Service.KycLevel}}
</section>
<!-- Points -->
<section class="flex flex-col items-center justify-center p-8">
<div class="grid grid-cols-1 gap-2 md:grid-cols-2">
{{range .Points}}
{{template "components/point" .}}
{{end}}
</div>
</section>
<!-- Notes -->
<section class="flex flex-col items-center justify-center px-8 mt-4 mb-8">
<h2 class="mb-0.5 text-xl font-bold text-center capitalize">Additional info</h2>
<div class="max-w-lg space-y-1.5">
<p class="block font-bold py-1.5 px-3 text-center text-sm border rounded-lg transition duration-300 border-white/30 bg-white/10 hover:bg-white/20 hover:border-white">
* Lorem ipsum dolor sit amet.
</p>
</div>
</section>
<!-- ToS Checker -->
<section class="flex flex-col items-center justify-center p-8 pb-4 border-t-2 border-opacity-50 border-dashed border-t-lime-400/10" id="tos">
<a href="/about#tos-checker" class="text-xl font-bold">
Automated ToS Checker
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-4 h-4 question-mark" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M8 8a3.5 3 0 0 1 3.5 -3h1a3.5 3 0 0 1 3.5 3a3 3 0 0 1 -2 3a3 4 0 0 0 -2 4"></path>
<path d="M12 19l0 .01"></path>
</svg>
</a>
<p class="max-w-sm text-xs text-center text-gray-300 text-opacity-75">Automated, ai-driven weekly ToS checks.</p>
<p class="max-w-sm mt-1 text-xs text-center text-gray-400 text-opacity-75">Last Check: 2023-10-18</p>
{{range .Service.TosHighlights}}
<div class="max-w-lg">
{{template "components/tos_check" .}}
</div>
{{end}}
</section>
<!-- Comments -->
<section class="flex flex-col items-center justify-center px-8 pt-4 pb-8" id="discuss">
<a href="https://usenostr.org" class="text-xl font-bold">
Nostr Comments
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-4 h-4 question-mark" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M8 8a3.5 3 0 0 1 3.5 -3h1a3.5 3 0 0 1 3.5 3a3 3 0 0 1 -2 3a3 4 0 0 0 -2 4"></path>
<path d="M12 19l0 .01"></path>
</svg>
</a>
<h2 class="text-xl font-bold"></h2>
<p class="max-w-sm pb-4 mt-1 text-xs text-center text-gray-300 text-opacity-75">
Beware of fake reviews and accounts. Verify user profiles and conduct your own research.
</p>
<!-- div with the ID disgus where you would like to display the comments & form -->
<div id="disgus" class="max-w-xl">
<noscript>
<p class="font-bold text-center text-yellow-300">
You must have JavaScript enabled to see and use comments. It just will load a 200kb script to interact with Nostr. The source code is available <a target="_blank" class="underline" href="https://github.com/carlitoplatanito/disgus">here</a>.
</p>
</noscript>
</div>
</section>
<!-- this can go at the end of the body -->
<script type="module" src="/static/disgus/index.js" async></script>

12
tailwind.config.js Normal file
View File

@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/web/**/*.{html,js}"],
theme: {
extend: {},
},
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/forms'),
],
}