343 Commits
1.1.0 ... 1.3.0

Author SHA1 Message Date
MartinBraquet
164eddecab Release v1.3.0 2025-10-13 12:39:38 +02:00
MartinBraquet
9eacb38eb9 Show bio in discord message of profile creation 2025-10-13 12:26:40 +02:00
MartinBraquet
20f5cfb9a7 Fix demo 2025-10-13 10:49:19 +02:00
Martin Braquet
6c6c1cc90a Upgrade geodb plan to increase radius and page limit (#14)
* Upgrade geodb plan to increase radius and page limit

* Speed location debounce
2025-10-12 15:58:52 +02:00
MartinBraquet
a32c099cc1 Rename social 2025-10-12 14:54:58 +02:00
MartinBraquet
fe2f832e83 Improve support page 2025-10-12 14:52:39 +02:00
MartinBraquet
868746cc23 Use Atkinson Hyperlegible font 2025-10-12 14:35:59 +02:00
MartinBraquet
3be7a54284 Fix compat modal (had to scroll to see Next on mobile) 2025-10-12 13:13:01 +02:00
MartinBraquet
635e1ec8e2 Add TODO readme info 2025-10-11 23:23:52 +02:00
MartinBraquet
a638a35a76 Upgrade ban logic 2025-10-11 22:58:50 +02:00
MartinBraquet
8cc33d3418 Add massive upgrade text 2025-10-11 21:42:46 +02:00
MartinBraquet
9947f7b967 Fix 2025-10-11 21:42:33 +02:00
MartinBraquet
daf5350f41 Add stem vector search in bio 2025-10-11 21:15:03 +02:00
MartinBraquet
020b9ddb8d Fix 2025-10-11 19:56:54 +02:00
MartinBraquet
23aff9497a Fix 2025-10-11 19:54:25 +02:00
MartinBraquet
3c119396f3 Add demo 2025-10-11 19:51:48 +02:00
MartinBraquet
f7c7c47ac0 Remove backup info from git 2025-10-11 19:44:38 +02:00
MartinBraquet
dbe2369bbe Fix avatar link 2025-10-11 19:44:12 +02:00
MartinBraquet
4e8033d221 Add info about contact 2025-10-11 19:44:02 +02:00
MartinBraquet
97a0f87cbd Use georgia font 2025-10-11 12:15:26 +02:00
MartinBraquet
bfa2713d43 Fix wording 2025-10-11 11:46:38 +02:00
MartinBraquet
fe5e109751 Improve reading 2025-10-10 22:53:30 +02:00
MartinBraquet
8cc96030b1 Speed up placeholder 2025-10-10 22:52:09 +02:00
MartinBraquet
a2b172ad58 Improve charts 2025-10-10 21:31:30 +02:00
MartinBraquet
e756225d8b Move pics above endorsements 2025-10-10 20:58:06 +02:00
MartinBraquet
dd803b604f Update reserved paths 2025-10-10 20:20:10 +02:00
MartinBraquet
b5c961c8ee Hide complete profile button 2025-10-10 20:05:01 +02:00
MartinBraquet
47cd9d227e Add shortBio filter to mobile filters 2025-10-10 19:13:32 +02:00
MartinBraquet
e2be3aafcd Add shortBio filter 2025-10-10 19:03:57 +02:00
MartinBraquet
015fe76c44 Hide profiles with small bio 2025-10-10 18:33:47 +02:00
MartinBraquet
44666aec03 Update post install 2025-10-10 18:33:11 +02:00
MartinBraquet
6a265e4f35 Do not render home before user loads 2025-10-10 18:32:37 +02:00
MartinBraquet
12c7316524 Refactor buttons 2025-10-10 17:04:26 +02:00
MartinBraquet
dcf9741d69 Format required form as step by step onboarding 2025-10-10 16:46:17 +02:00
MartinBraquet
63dd1fdd50 Replace user with voting member and member with volunteer for clarity and inclusion 2025-10-10 15:22:55 +02:00
MartinBraquet
5aa166bbfd Open links in same tab 2025-10-10 15:12:48 +02:00
MartinBraquet
34cbf7093e Skip welcome email if local 2025-10-10 14:51:22 +02:00
MartinBraquet
159d58949e Reformat 2025-10-09 21:51:21 +02:00
MartinBraquet
fcf802b7e3 Refactor bios and add character counter 2025-10-09 21:51:08 +02:00
MartinBraquet
92ff6dadb0 Add email 2025-10-09 20:00:01 +02:00
MartinBraquet
05fa2f9883 Add socials and organization pages 2025-10-09 19:47:32 +02:00
MartinBraquet
71bb8fd784 Commetn 2025-10-09 19:33:51 +02:00
MartinBraquet
16ffd6dfab Fix message view without sign in 2025-10-09 19:30:24 +02:00
MartinBraquet
2661d15910 Remove waitlist 2025-10-09 19:17:15 +02:00
MartinBraquet
394102bb93 Fix avatar icon 2025-10-09 18:37:11 +02:00
MartinBraquet
3585b12dfd Remove maintenance banner 2025-10-09 18:20:53 +02:00
MartinBraquet
423d87d5f1 Remove logs 2025-10-09 18:19:14 +02:00
MartinBraquet
13b13b1104 Fix 2025-10-09 18:02:15 +02:00
MartinBraquet
a77e7b96b7 Move logs to debug status 2025-10-09 17:59:10 +02:00
MartinBraquet
d7213c255c Add client side heartbeat 2025-10-09 17:50:43 +02:00
MartinBraquet
ddeb1dcdb7 Improve ping pong connection duration 2025-10-09 17:38:05 +02:00
MartinBraquet
221cfa3528 Fix websockets not reaching the container and remove v0/ prefix 2025-10-09 16:58:20 +02:00
MartinBraquet
d6f6348ff1 Add maintenance banner 2025-10-09 16:25:47 +02:00
MartinBraquet
0c6afdc98e Add star 2025-10-09 15:14:38 +02:00
MartinBraquet
02a2148b3f Improve messages width 2025-10-09 13:14:38 +02:00
MartinBraquet
36a02268d8 Fix 2025-10-09 11:28:11 +02:00
MartinBraquet
450f07f505 Add private backup 2025-10-09 11:27:24 +02:00
MartinBraquet
777eba9fed Move backup to private storage 2025-10-09 11:23:30 +02:00
MartinBraquet
eaa8fa57d1 Add private bucket 2025-10-09 11:18:37 +02:00
MartinBraquet
200bf479e1 Clean 2025-10-09 00:43:27 +02:00
MartinBraquet
331f409af9 Increase debounce 2025-10-09 00:28:27 +02:00
MartinBraquet
ce875a5e63 Fix Porto not showing 2025-10-09 00:16:58 +02:00
MartinBraquet
638013f835 Update email address 2025-10-08 23:44:37 +02:00
MartinBraquet
1de87cbfec Add welcome email 2025-10-08 23:40:53 +02:00
MartinBraquet
7f3428b36a Factor out unsubscribe url 2025-10-08 23:40:37 +02:00
MartinBraquet
35595ded47 Fix bullets 2025-10-08 23:40:18 +02:00
MartinBraquet
35e9264017 Show profiles number, not users number 2025-10-08 23:39:42 +02:00
MartinBraquet
02d33c8f83 Rename mock user 2025-10-08 20:38:31 +02:00
MartinBraquet
f229ebc3a8 Add email confirmation 2025-10-08 20:38:20 +02:00
MartinBraquet
0062351f6d Add welcome email 2025-10-08 20:38:09 +02:00
MartinBraquet
e86f6798ec Fix bullet 2025-10-08 20:37:35 +02:00
MartinBraquet
4f53f7136b Add members 2025-10-08 20:35:57 +02:00
MartinBraquet
d80b982dde Simplify tab title 2025-10-08 17:32:19 +02:00
MartinBraquet
24788aa9af Add optional Garamond font 2025-10-08 14:11:51 +02:00
MartinBraquet
9ffae658df Clean 2025-10-08 11:58:58 +02:00
MartinBraquet
82ad573cac Add stoat link 2025-10-08 11:58:52 +02:00
MartinBraquet
36bf7ad65b Fix 2025-10-07 22:53:37 +02:00
MartinBraquet
b30af128c7 Release 2025-10-07 22:11:34 +02:00
MartinBraquet
72c31ae097 Add compat score math video link 2025-10-07 21:25:08 +02:00
Martin Braquet
d2c608021d Improve home (#10) 2025-10-05 10:38:04 +02:00
Martin Braquet
1f36fb2413 Update faq.md 2025-10-05 10:14:09 +02:00
Martin Braquet
16a0cbcecf Update about.tsx 2025-10-05 10:00:30 +02:00
Martin Braquet
e068e246aa Update faq.md 2025-10-03 13:58:31 +02:00
MartinBraquet
ec7c77fcf9 Log 2025-10-02 15:14:13 +02:00
MartinBraquet
46a338b874 Clean 2025-10-02 14:54:47 +02:00
MartinBraquet
bfee7ff09d Fix age rendering 2025-10-02 14:14:24 +02:00
MartinBraquet
ce1305d8ae Host logos 2025-10-02 13:17:22 +02:00
MartinBraquet
aaebf88438 Log profile count 2025-10-01 09:02:50 +02:00
MartinBraquet
dde2c99e36 Fix 2025-10-01 08:58:11 +02:00
MartinBraquet
4dc2f3b9b9 Add Community Growth over Time 2025-10-01 08:56:03 +02:00
MartinBraquet
f30cfffb86 Fix bio parsing grid 2025-09-30 22:17:28 +02:00
MartinBraquet
ca3eb62ba7 Fix 2 2025-09-30 21:48:15 +02:00
MartinBraquet
c8e55ca4ce Fix 2025-09-30 21:46:31 +02:00
MartinBraquet
e4acb25a40 Better render headings and lists in profile grid 2025-09-30 21:45:09 +02:00
MartinBraquet
c741e10139 Stop spamming prod discord channels 2025-09-30 21:30:38 +02:00
MartinBraquet
28d0b35f8e Move discord move to create profile 2025-09-30 20:53:26 +02:00
MartinBraquet
f7f09cd9e5 Send message to Discord when reaching 50, 100, ..., users 2025-09-28 21:09:11 +02:00
MartinBraquet
501c92c350 Send discord message at every profile creation 2025-09-28 20:47:31 +02:00
MartinBraquet
f021101322 Remove unused React 2025-09-26 22:17:24 +02:00
MartinBraquet
369265bc2c Add firebase storage backup script 2025-09-26 22:12:08 +02:00
Martin Braquet
b1f1e5db1f Fully delete profile in database and Firebase auth + storage (#7) 2025-09-26 22:10:38 +02:00
MartinBraquet
51d32e5afb Link donations in FAQ 2025-09-26 13:52:40 +02:00
MartinBraquet
f396e8e482 Your filters 2025-09-26 13:51:08 +02:00
MartinBraquet
077321731e Add bio warning message 2025-09-25 23:02:04 +02:00
MartinBraquet
60eb0c6978 Add 'datingdoc', 'friendshipdoc', 'connectiondoc', 'workdoc' 2025-09-25 22:49:54 +02:00
MartinBraquet
475f0af78a Add supabase backup VM to Firebase storage and discord error hook 2025-09-24 15:59:46 +02:00
MartinBraquet
206fa07035 Ignore tf 2025-09-24 15:13:33 +02:00
MartinBraquet
aff949714c Add charts example 2025-09-24 13:28:46 +02:00
Martin Braquet
7e834b9ff6 Update FUNDING.yml 2025-09-24 12:46:20 +02:00
Martin Braquet
19bad26a98 Create FUNDING.yml 2025-09-24 12:44:37 +02:00
MartinBraquet
7cc7c8d27b Hide age, city and gender if null 2025-09-22 11:06:52 +02:00
MartinBraquet
ae5a8c7cfa Add page loading warning 2025-09-22 11:02:53 +02:00
MartinBraquet
5004b73210 Fix calendly link 2025-09-22 10:52:46 +02:00
MartinBraquet
02f613d269 Add Ko-fi donation link 2025-09-22 10:42:23 +02:00
MartinBraquet
439ac0310b Add option to delete an answered compatibility prompt 2025-09-22 10:09:13 +02:00
MartinBraquet
3e95467819 Add okcupid and calendly links 2025-09-22 00:06:05 +02:00
MartinBraquet
90522cb88b Comment log 2025-09-22 00:05:15 +02:00
MartinBraquet
af39b01d4a Reload env config after setting env vars 2025-09-21 16:56:42 +02:00
MartinBraquet
73a0a5ff0b Fix vercel env 2025-09-21 16:08:29 +02:00
MartinBraquet
e157f500bc Fix dev firebase admin key 2025-09-21 16:00:29 +02:00
MartinBraquet
274ee5ed5f Remove json 2025-09-21 15:44:33 +02:00
MartinBraquet
4cb11ba8c0 Remove log 2025-09-21 15:00:26 +02:00
MartinBraquet
7b8e775139 Fix google cloud env 2025-09-21 14:55:32 +02:00
MartinBraquet
86a7d26bfd Clean readme 2025-09-20 23:54:56 +02:00
MartinBraquet
84a437772d Make local DEV work out of the box 2025-09-20 23:51:28 +02:00
MartinBraquet
d7c95e2ae0 Clean ENV 2025-09-20 18:26:03 +02:00
MartinBraquet
b4f0ef8b43 Move supabase dev pwd inside code 2025-09-20 18:12:46 +02:00
MartinBraquet
6d30cd7ae4 Add open source to FAQ 2025-09-19 14:47:58 +02:00
MartinBraquet
f631236ee7 Add platform to FAQ 2025-09-19 14:42:36 +02:00
MartinBraquet
1a58ff5c4c Shuffle prompts 2025-09-18 15:33:23 +02:00
MartinBraquet
73aca913a1 Fix 2025-09-18 14:14:58 +02:00
MartinBraquet
24dee0cad6 Add bio char template 2025-09-18 13:34:29 +02:00
MartinBraquet
2d2de75372 Add bio tips 2025-09-18 13:12:48 +02:00
MartinBraquet
d98982e6fd Factor out links 2025-09-18 11:30:59 +02:00
MartinBraquet
14c12ffb08 Rename 2025-09-18 11:19:09 +02:00
MartinBraquet
f260afca11 Ignore 2025-09-18 11:18:22 +02:00
MartinBraquet
5bcbe25d97 Rm 2025-09-18 11:18:04 +02:00
MartinBraquet
2eee366fbd Update financials 2025-09-18 11:12:33 +02:00
MartinBraquet
85d57ec5e6 Fix resend email limit 2/sec 2025-09-17 18:35:29 +02:00
MartinBraquet
502c878f82 Fix 2025-09-17 18:15:02 +02:00
MartinBraquet
1136c3f767 Release 2025-09-17 17:58:59 +02:00
MartinBraquet
42b496cd77 Fix 2025-09-17 17:58:11 +02:00
MartinBraquet
4acb5ee020 Fix warnings 2025-09-17 15:56:17 +02:00
MartinBraquet
ea18781cc6 Rename lover -> profile 2025-09-17 15:51:19 +02:00
MartinBraquet
593617c0ff Rename Lover -> Profile 2025-09-17 15:43:19 +02:00
MartinBraquet
c6a139d88d Rename Lovers -> Profiles 2025-09-17 15:42:23 +02:00
MartinBraquet
b7357a4546 Set 3 words 2025-09-17 15:25:27 +02:00
MartinBraquet
5eac959d15 FIx paypal path 2025-09-17 15:20:49 +02:00
MartinBraquet
74c86ecfbe FIx path 2025-09-17 15:19:17 +02:00
MartinBraquet
f353e590e1 Rename lovers -> profiles 2025-09-17 15:11:53 +02:00
MartinBraquet
a4cc3e10c2 Upgrade keywords examples 2025-09-17 12:00:24 +02:00
MartinBraquet
7321f56ee2 Upgrade docs 2025-09-16 23:49:17 +02:00
MartinBraquet
8800d9adc6 Upgrade README.md 2025-09-16 23:37:45 +02:00
MartinBraquet
22cd535527 Upgrade README.md 2025-09-16 23:35:02 +02:00
MartinBraquet
1d0e9592df Replace github sponsors 2025-09-16 22:53:53 +02:00
MartinBraquet
2ef4af0ff2 Fix links submit your own 2025-09-16 22:49:53 +02:00
MartinBraquet
542a6b1592 Fix endpoint 2025-09-16 22:42:37 +02:00
MartinBraquet
613ef94dba Upgrade docs endpoint 2025-09-16 22:28:40 +02:00
MartinBraquet
1dc2a1fadf Fix 2025-09-16 22:12:19 +02:00
MartinBraquet
41a606f5c1 Fix ages in filters 2025-09-16 22:09:43 +02:00
MartinBraquet
7b2b9855f9 Fix notifs page 2025-09-16 21:54:29 +02:00
MartinBraquet
b2b519ba2e Clean 2025-09-16 21:38:52 +02:00
MartinBraquet
5cf89392ff Fix username not being updated when loading their profile after registration 2025-09-16 21:38:19 +02:00
MartinBraquet
0f05304ec3 Fix typo 2025-09-16 21:07:02 +02:00
MartinBraquet
87bc962c88 Fix Rename github org 2025-09-16 18:59:21 +02:00
MartinBraquet
546ce6e229 Revert "Rename github org"
This reverts commit 2163d5aaf6.
2025-09-16 18:58:29 +02:00
MartinBraquet
2163d5aaf6 Rename github org 2025-09-16 18:49:36 +02:00
MartinBraquet
905ea160f2 Fix link 2025-09-16 18:42:48 +02:00
MartinBraquet
675f4a372b Update paypal links 2025-09-16 18:41:32 +02:00
MartinBraquet
7ff42db0c6 Curl API 2025-09-16 18:41:25 +02:00
MartinBraquet
a01283a446 Fix links 2025-09-16 18:18:57 +02:00
MartinBraquet
fefa261e7d Restrict matched users to last 24h 2025-09-16 18:09:48 +02:00
MartinBraquet
0447b22dd2 Restrict internal/send-search-notifications with API key 2025-09-16 17:54:13 +02:00
MartinBraquet
cf125c1b48 Fix packages 2025-09-16 16:48:52 +02:00
MartinBraquet
81a9d8257c Trigger filter change only on slide commit to avoid API overload 2025-09-16 16:15:07 +02:00
MartinBraquet
ee3f471300 Skip 2025-09-16 16:14:39 +02:00
MartinBraquet
5c2e5f626d Upgrade subject 2025-09-16 16:14:34 +02:00
MartinBraquet
a0f4b62361 Allow for empty orderBy 2025-09-16 16:14:26 +02:00
MartinBraquet
786166b448 Fix connection clause 2025-09-16 16:14:16 +02:00
MartinBraquet
66e198b4ef Ignore 2025-09-16 15:13:40 +02:00
MartinBraquet
4919240242 Update readme info 2025-09-16 15:13:36 +02:00
MartinBraquet
d7e6a41e3f Clean installs 2025-09-16 14:31:39 +02:00
MartinBraquet
202ef737dd Add react email as dev 2025-09-16 14:08:36 +02:00
MartinBraquet
04993224dc Nice emails 2025-09-16 14:03:08 +02:00
MartinBraquet
bebe7c28f8 Fix 2025-09-16 12:48:44 +02:00
MartinBraquet
639991dde4 Add bookmarked search emails and factor out utils from web to common 2025-09-16 12:36:18 +02:00
MartinBraquet
31404cb89a Add allowSyntheticDefaultImports 2025-09-16 12:35:19 +02:00
MartinBraquet
f6205ca1dd Add source.sh 2025-09-16 12:35:12 +02:00
MartinBraquet
6e86fc0593 Add build_api.sh 2025-09-16 12:35:01 +02:00
MartinBraquet
f39a9845a3 Fix tsconfig include jsonapi 2025-09-15 21:08:27 +02:00
MartinBraquet
ba17582945 Factor out loadProfiles 2025-09-15 21:08:14 +02:00
MartinBraquet
02a1cbd467 Rename getLovers and add base send-search-notifications.ts 2025-09-15 18:48:34 +02:00
MartinBraquet
2cd102ef0b Add API docs 2025-09-15 18:45:03 +02:00
MartinBraquet
240361b55b Access API at / install of /v0/ 2025-09-15 18:07:25 +02:00
MartinBraquet
9beabc93cd Clean 2025-09-15 16:13:11 +02:00
MartinBraquet
8f4c6b911a Meke age optional in API requests 2025-09-15 16:04:30 +02:00
MartinBraquet
083ef3010d Add location column 2025-09-15 14:24:47 +02:00
MartinBraquet
e6c2253219 Fix location pretty print 2025-09-14 22:45:04 +02:00
MartinBraquet
d802eb3f28 Fix 2025-09-14 22:36:16 +02:00
MartinBraquet
a342d5d5ad Fix hidden close button 2025-09-14 22:35:00 +02:00
MartinBraquet
99adb77fcb Pretty print bookmarked searches 2025-09-14 22:18:21 +02:00
MartinBraquet
2ea4eae9d6 Add wantsKidsNames 2025-09-14 22:16:45 +02:00
MartinBraquet
9b079b2c3a Add hasKidsNames 2025-09-14 22:16:30 +02:00
MartinBraquet
8648e8569e Add list style 2025-09-14 22:16:03 +02:00
MartinBraquet
1be0ab8bcb Fix 2025-09-14 16:37:57 +02:00
MartinBraquet
718f76c1f2 Hide likes 2025-09-14 16:37:52 +02:00
MartinBraquet
155d1f4c06 Add bookmarked filters for notifications 2025-09-13 23:21:45 +02:00
MartinBraquet
cb79e27d5a Fix hover button gray 2025-09-13 23:19:51 +02:00
MartinBraquet
26991f8dd8 Remove blue message 2025-09-13 23:19:29 +02:00
MartinBraquet
2375330d76 Fix modal size on mobile 2025-09-13 23:18:56 +02:00
MartinBraquet
94e9b6d99b Fix UI 2025-09-13 23:18:33 +02:00
MartinBraquet
b516d24101 Fix font 2025-09-13 23:18:11 +02:00
MartinBraquet
1b131d9371 Fix typo 2025-09-13 23:18:01 +02:00
MartinBraquet
3f45ef192d Update db schema 2025-09-13 23:17:46 +02:00
MartinBraquet
c6684af521 Improve colors 2025-09-13 23:17:23 +02:00
MartinBraquet
52f12b81ff Move script 2025-09-13 23:17:10 +02:00
MartinBraquet
6630f787bf Debug log 2025-09-13 23:16:58 +02:00
MartinBraquet
2d7b2da3e2 Improve wording 2025-09-13 23:16:53 +02:00
MartinBraquet
d3b008fcd9 Add bookmarked_searches.sql 2025-09-13 18:12:20 +02:00
MartinBraquet
8a62fd0e6a Add migration 2025-09-13 18:11:57 +02:00
MartinBraquet
b044860f05 Add last_modification_time 2025-09-13 18:11:46 +02:00
MartinBraquet
1c5786dfb6 Add web readme 2025-09-13 16:55:09 +02:00
MartinBraquet
6bc9e3d695 Downgrade next 2025-09-13 16:44:14 +02:00
MartinBraquet
b74fe59f12 Upgrade next 2025-09-13 16:19:23 +02:00
MartinBraquet
6b57aa7f14 Add info 2025-09-13 16:12:05 +02:00
MartinBraquet
227125b35c Update packages 2025-09-13 16:11:59 +02:00
MartinBraquet
c4012d8dfc Fix 2025-09-13 15:38:08 +02:00
MartinBraquet
cf3fa9ffbc Fix 2025-09-13 15:36:04 +02:00
MartinBraquet
40640d029a Update packages 2025-09-13 15:33:23 +02:00
MartinBraquet
01eb7038dc Add /support page 2025-09-13 15:12:45 +02:00
MartinBraquet
58115bfd11 Dynamic filename finding 2025-09-13 15:12:32 +02:00
MartinBraquet
f1ea5031fb Remove supabase token 2025-09-13 14:54:10 +02:00
MartinBraquet
26d15a9fb3 Remove autogenerated line 2025-09-13 13:14:56 +02:00
MartinBraquet
54ba8e6047 Upgrade next 2025-09-13 12:39:24 +02:00
MartinBraquet
eca063ab75 Clean 2025-09-13 12:14:17 +02:00
MartinBraquet
8892f4144e Remove logs 2025-09-13 12:14:09 +02:00
MartinBraquet
d2c25f9d6c Remove firebase warning 2025-09-13 12:03:08 +02:00
MartinBraquet
b57457dc2f Add yarn clean-install 2025-09-13 11:57:41 +02:00
MartinBraquet
2861b0cfa2 Update caniuse 2025-09-13 11:47:58 +02:00
MartinBraquet
0c45dbb884 Fix warning font 2025-09-13 11:46:58 +02:00
MartinBraquet
a9f9261fb7 Update description 2025-09-12 21:42:18 +02:00
MartinBraquet
7e5f54a4f1 Add anim in search bar 2025-09-12 21:40:02 +02:00
MartinBraquet
1228e8759c Add multiple keywords per search 2025-09-12 21:39:44 +02:00
MartinBraquet
1daf771218 Clean file architecture 2025-09-12 20:47:37 +02:00
MartinBraquet
880cb08c3d Update FAQ 2025-09-12 20:17:24 +02:00
MartinBraquet
e2cbae3089 Add supabase dev key 2025-09-12 20:06:20 +02:00
MartinBraquet
42dcc3318c Comment 2025-09-12 18:33:46 +02:00
MartinBraquet
b32a85ae7e Fix cookie 2025-09-12 18:27:39 +02:00
MartinBraquet
af85edddca Ignore pics 2025-09-12 18:18:55 +02:00
MartinBraquet
eccd88e3c2 Debug cookie 2025-09-12 18:14:34 +02:00
MartinBraquet
e0e11629a1 Set up PostHog 2025-09-12 18:03:21 +02:00
MartinBraquet
968095c183 Fix LOCAL_DEV import in shared 2025-09-12 17:40:54 +02:00
MartinBraquet
d32b5115c5 Clean images and SEO 2025-09-12 17:21:03 +02:00
MartinBraquet
d3001ec887 Fix 2025-09-12 17:00:53 +02:00
MartinBraquet
fef6a52008 Fix og url for local 2025-09-12 17:00:49 +02:00
MartinBraquet
048e6affbc Update dev info 2025-09-12 16:42:53 +02:00
MartinBraquet
c653d49691 Update doc 2025-09-12 15:56:42 +02:00
MartinBraquet
6f5c9bd054 Update docs 2025-09-12 15:55:45 +02:00
MartinBraquet
9e5576244d Fix readme 2025-09-12 15:36:23 +02:00
MartinBraquet
ef91317232 Add local dev info 2025-09-12 15:29:17 +02:00
MartinBraquet
10c44d050f Fix 2025-09-12 15:03:44 +02:00
MartinBraquet
1845ea7170 Fix 2025-09-12 15:03:12 +02:00
MartinBraquet
d453294622 Fix 2025-09-12 15:02:47 +02:00
MartinBraquet
d11f9e4971 Fix font 2025-09-12 15:01:57 +02:00
MartinBraquet
08272dd04e Fix typo 2025-09-12 15:01:50 +02:00
MartinBraquet
42441b9b42 Add info 2025-09-12 14:54:06 +02:00
MartinBraquet
e4a293c046 Add todos 2025-09-12 14:50:47 +02:00
MartinBraquet
0cc5a39d63 Add todos 2025-09-12 14:48:32 +02:00
MartinBraquet
942ea3f125 Update readme todo 2025-09-12 14:36:03 +02:00
MartinBraquet
a8a70bb71c Make avatar pic if no pictures 2025-09-12 12:01:04 +02:00
MartinBraquet
0d7c3fb4b2 Allow everyone to message everyone for now 2025-09-12 12:00:41 +02:00
MartinBraquet
77c682454e Reduce like button size 2025-09-12 12:00:15 +02:00
MartinBraquet
dd3473f5d8 Improve prompts link 2025-09-12 03:39:50 +02:00
MartinBraquet
cceadc5e04 Center the icons, even in gmail 2025-09-12 03:15:17 +02:00
MartinBraquet
e48c3a3f9c Add links to emails 2025-09-12 02:42:00 +02:00
MartinBraquet
14981ef077 Fix 2025-09-12 02:15:06 +02:00
MartinBraquet
a7858d44bd Fix empty content 2025-09-12 02:10:19 +02:00
MartinBraquet
9ae5f27c04 Remove free responses for now as not implemented in the db 2025-09-12 02:03:30 +02:00
MartinBraquet
d691129842 Hide location if null 2025-09-12 01:57:22 +02:00
MartinBraquet
e26d551263 Fix overrides 2025-09-12 01:47:11 +02:00
MartinBraquet
277c6a444f Release 2025-09-12 01:40:58 +02:00
MartinBraquet
f344800fd6 Fix yarn install warnings 2025-09-12 01:38:48 +02:00
MartinBraquet
39a6fba33f Remove unused and confusing sub lock files 2025-09-12 01:30:50 +02:00
MartinBraquet
8e11657bd2 Update install 2025-09-12 01:26:49 +02:00
MartinBraquet
dfbeaa4edf Clean lock 2025-09-12 01:23:16 +02:00
MartinBraquet
e90dc3b7f4 Remove log 2025-09-12 01:23:06 +02:00
MartinBraquet
dba89e611a Fix package backend 2025-09-12 01:17:39 +02:00
MartinBraquet
1a3fecc89e Fix 2025-09-12 00:46:38 +02:00
MartinBraquet
407e6a3d06 Fix yarn.lock 2025-09-12 00:40:46 +02:00
MartinBraquet
6ee19d5359 Back to working on vercel 2025-09-12 00:31:11 +02:00
MartinBraquet
2df424dbac Remove log 2025-09-12 00:14:23 +02:00
MartinBraquet
9874be6bf1 Fix packages 2025-09-12 00:10:50 +02:00
MartinBraquet
a3d4199d1d Fix vercel build 2025-09-11 23:56:41 +02:00
MartinBraquet
247fa146a9 Fix 2025-09-11 23:26:38 +02:00
MartinBraquet
f2b2c02cd6 Add install.sh 2025-09-11 22:56:29 +02:00
MartinBraquet
a915f27f00 Roolback 2025-09-11 22:56:21 +02:00
MartinBraquet
e14a488934 Revert "Failed attempt to use react icons in emails"
This reverts commit e82a8d9bc3.
2025-09-11 22:35:54 +02:00
MartinBraquet
e82a8d9bc3 Failed attempt to use react icons in emails 2025-09-11 22:35:48 +02:00
MartinBraquet
4527a0d12b Rm add tsconfig 2025-09-11 22:34:00 +02:00
MartinBraquet
01be202484 Cd cur dir 2025-09-11 19:20:40 +02:00
MartinBraquet
d1fe99edc3 Remove log 2025-09-11 19:20:32 +02:00
MartinBraquet
fa629591e9 Fix unsubscribe URL 2025-09-11 18:45:42 +02:00
MartinBraquet
4ab3edc97b Add email footer 2025-09-11 18:37:31 +02:00
MartinBraquet
f1bfc6bf55 Fix connection type (2) 2025-09-11 16:51:13 +02:00
MartinBraquet
3283843ef3 Fix connection type 2025-09-11 16:21:01 +02:00
MartinBraquet
4cb14ec8cc Fix gender 2025-09-11 16:20:11 +02:00
MartinBraquet
41535a68be Improve email UI 2025-09-11 16:00:10 +02:00
MartinBraquet
d62447a12a Unstick like 2025-09-11 14:48:36 +02:00
MartinBraquet
802367c914 Search users 2025-09-11 14:48:24 +02:00
MartinBraquet
ff9b2c6ee8 Add compatibility score FAQ 2025-09-11 14:12:44 +02:00
MartinBraquet
a0e25c941a Smoot login 2025-09-11 13:59:23 +02:00
MartinBraquet
091c99e784 Reduce sidebar width 2025-09-11 13:27:44 +02:00
MartinBraquet
e264bb407b Improve sign in / up UI 2025-09-11 13:14:20 +02:00
MartinBraquet
16625210fc Fix 2025-09-11 12:51:37 +02:00
MartinBraquet
2550453ee4 Add keyword search 2025-09-10 22:50:20 +02:00
MartinBraquet
d1c480f23f Change genders 2025-09-10 21:54:03 +02:00
MartinBraquet
b4b0397589 Hide gender they are interested in 2025-09-10 21:53:55 +02:00
MartinBraquet
ab6b34e84c Hide supabase annon key (env) 2025-09-10 21:41:08 +02:00
MartinBraquet
87af9d5078 Hide supabase annon key 2025-09-10 21:40:31 +02:00
MartinBraquet
95fab7c395 Add reports table 2025-09-10 21:40:23 +02:00
MartinBraquet
90825925ff Improve share button layout 2025-09-10 21:39:57 +02:00
MartinBraquet
7036cf9e49 Profile view when signed up: no pic, see first lines of bio 2025-09-10 20:42:09 +02:00
MartinBraquet
53123eb0ee Improve UI 2025-09-10 18:24:44 +02:00
MartinBraquet
3c5407dd51 Fix colors 2025-09-10 17:09:35 +02:00
MartinBraquet
1ffe81f740 Fix link 2025-09-10 16:20:50 +02:00
MartinBraquet
6bb35d61e1 Clean 2025-09-10 16:16:11 +02:00
MartinBraquet
f36ccf7bdc Fix colors 2025-09-10 16:16:08 +02:00
MartinBraquet
4632e68a00 Add bookish font 2025-09-10 16:14:56 +02:00
MartinBraquet
09858d0783 Change md path 2025-09-10 16:14:33 +02:00
MartinBraquet
9d1423c41b Add todo 2025-09-10 14:29:11 +02:00
MartinBraquet
1a4b7786dd Fix blue links 2025-09-10 12:53:34 +02:00
MartinBraquet
77c0a21ad0 Add info about Martin 2025-09-10 12:23:33 +02:00
MartinBraquet
7cedf14121 Add members page 2025-09-10 12:23:25 +02:00
MartinBraquet
235346f3dd Add financials link 2025-09-10 11:26:14 +02:00
MartinBraquet
34c36b7c3a Fix 2025-09-09 19:24:08 +02:00
MartinBraquet
3e0f788ec3 Add FAQ and financials 2025-09-09 19:18:44 +02:00
MartinBraquet
867bb8a072 Fix 2025-09-09 18:55:47 +02:00
MartinBraquet
31a400158a Do not render your own profile in Profiles 2025-09-09 16:58:07 +02:00
MartinBraquet
8106ff6489 Show compatibility score of no preferred gender 2025-09-09 16:57:02 +02:00
MartinBraquet
de3508993c Factor out geodbFetch 2025-09-09 16:07:36 +02:00
MartinBraquet
fd3e7a6f8a Fix location filtering 2025-09-09 15:55:33 +02:00
MartinBraquet
4cf97a6054 Fix 2025-09-09 15:25:21 +02:00
MartinBraquet
75036e3ec7 Fix 2025-09-09 14:57:08 +02:00
381 changed files with 8339 additions and 18273 deletions

View File

@@ -1,17 +1,7 @@
# Rename this file to `.env` and fill in the values.
# You already have access to basic local functionality (UI, authentication, database read access).
# Required variables for basic local functionality
# For database connection. A 16-character password with digits and letters.
SUPABASE_DB_PASSWORD=
# For authentication.
# Ask the project admin. Should start with "AIza".
NEXT_PUBLIC_FIREBASE_API_KEY=
# The URL where your local backend server is running.
# You can change the port if needed.
NEXT_PUBLIC_API_URL=localhost:8088
# openssl enc -aes-256-cbc -salt -pbkdf2 -iter 100000 -in backend/shared/src/googleApplicationCredentials-dev.json -out secrets/googleApplicationCredentials-dev.json.enc
GOOGLE_CREDENTIALS_ENC_PWD=nP7s3274uzOG4c2t
# Optional variables for full local functionality
@@ -20,10 +10,6 @@ NEXT_PUBLIC_API_URL=localhost:8088
# Create a free account at https://rapidapi.com/wirefreethought/api/geodb-cities and get an API key.
GEODB_API_KEY=
# For analytics like page views, user actions, feature usage, etc.
# Create a free account at https://posthog.com and get a project API key. Should start with "phc_".
POSTHOG_KEY=
# For sending emails (e.g. for user sign up, password reset, notifications, etc.).
# Create a free account at https://resend.com and get an API key. Should start with "re_".
RESEND_API_KEY=
RESEND_KEY=

15
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: CompassMeet # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: compassconnections # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -46,10 +46,12 @@ jobs:
# npx playwright install
- name: Run E2E tests
env:
NEXT_PUBLIC_API_URL: localhost:8088
NEXT_PUBLIC_FIREBASE_ENV: PROD
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
NEXT_PUBLIC_SUPABASE_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_KEY }}
run: |
NEXT_PUBLIC_API_URL=localhost:8088 \
NEXT_PUBLIC_FIREBASE_ENV=PROD \
NEXT_PUBLIC_FIREBASE_API_KEY=${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }} \
yarn --cwd=web serve &
npx wait-on http://localhost:3000
npx playwright test tests/playwright

25
.gitignore vendored
View File

@@ -55,9 +55,34 @@ tsconfig.tsbuildinfo
*prisma/migrations
martin
email-preview
.obsidian
.idea
*.last-run.json
*lock.hcl
/web/pages/test.tsx
*.png
*.jpg
*.jpeg
*.gif
*.svg
*.mp4
*.mov
*.avi
*.wmv
*.mp3
*.wav
*.flac
*.aac
*.zip
*.tar.gz
*.rar
/favicon_color.ico
/backend/shared/src/googleApplicationCredentials-dev.json
*.tfstate
*.tfstate.backup
*.terraform
/backups/firebase/auth/data/
/backups/firebase/storage/data/

View File

@@ -19,7 +19,7 @@ We welcome pull requests, but only if they meet the project's quality and design
3. **Add the upstream remote**:
```bash
git remote add upstream https://github.com/CompassMeet/Compass.git
git remote add upstream https://github.com/CompassConnections/Compass.git
```
## Create a New Branch

152
README.md
View File

@@ -1,13 +1,13 @@
[![CI](https://github.com/CompassMeet/Compass/actions/workflows/ci.yml/badge.svg)](https://github.com/CompassMeet/Compass/actions/workflows/ci.yml)
[![CD](https://github.com/CompassMeet/Compass/actions/workflows/cd.yml/badge.svg)](https://github.com/CompassMeet/Compass/actions/workflows/cd.yml)
[![CI](https://github.com/CompassConnections/Compass/actions/workflows/ci.yml/badge.svg)](https://github.com/CompassConnections/Compass/actions/workflows/ci.yml)
[![CD](https://github.com/CompassConnections/Compass/actions/workflows/cd.yml/badge.svg)](https://github.com/CompassConnections/Compass/actions/workflows/cd.yml)
![Vercel](https://deploy-badge.vercel.app/vercel/bayesbond)
# Compass
This repository provides the source code for [Compass](https://compassmeet.com), a web application for people to form deep 1-on-1 relationships in a fully transparent and efficient way. And it just got released!
This repository contains the source code for [Compass](https://compassmeet.com) — an open platform for forming deep, authentic 1-on-1 connections with clarity and efficiency.
**We cant do this alone.** Whatever your skills—coding, design, writing, moderation, marketing, or even small donations—you can make a real difference. [Contribute](https://www.compassmeet.com/about) in any way you can and help our community thrive!
**We cant do this alone.** Whatever your skills—coding, design, writing, moderation, marketing, or even small donations—you can make a real difference. [Contribute](https://www.compassmeet.com/support) in any way you can and help our community thrive!
## Features
@@ -18,10 +18,32 @@ This repository provides the source code for [Compass](https://compassmeet.com),
- Open source
- Democratically governed
A detailed description of the vision is available [here](https://martinbraquet.com/meeting-rational).
You can find a lot of interesting info in the [About page](https://www.compassmeet.com/about) and the [FAQ](https://www.compassmeet.com/faq) as well.
A detailed description of the early vision is also available in this [blog post](https://martinbraquet.com/meeting-rational) (you can disregard the parts about rationality, as Compass shifted to a more general audience).
<p style="text-align: center;">
<img src="https://firebasestorage.googleapis.com/v0/b/compass-130ba.firebasestorage.app/o/misc%2Fdemo_compass.gif?alt=media&token=e3ae4334-4e3f-4026-b121-c08b4b724cd1" alt="Compass Demo" width="600">
</p>
## To Do
No contribution is too small—whether its changing a color, resizing a button, tweaking a font, or improving wording. Bigger contributions like adding new profile fields, building modules, or improving onboarding are equally welcome. The goal is to make the platform better step by step, and every improvement counts. If you see something that could be clearer, smoother, or more engaging, **please jump in**!
The complete, official list of tasks is available [here on ClickUp](https://sharing.clickup.com/90181043445/l/h/6-901810339879-1/bbfd32f4f4bf64b). If you are working on one task, just assign it to yourself and move its status to "in progress". If there is also a GitHub issue for that task, assign it to yourself as well.
To have edit access to the ClickUp workspace, you need an admin to manually give you permission (one time thing). To do so, just use your preferred option:
- Ask or DM an admin on Discord
- Email hello@compassmeet.com
- Raise an issue on GitHub
If you want to add tasks without creating an account, you can simply email
```
a.t.901810339879.u-276866260.b847aba1-2709-4f17-b4dc-565a6967c234@tasks.clickup.com
```
Put the task title in the email subject and the task description in the email content.
Here is a tailored selection of things that would be very useful. If you want to help but dont know where to start, just ask us on [Discord](https://discord.gg/8Vd7jzqjun).
- [x] Authentication (user/password and Google Sign In)
- [x] Set up PostgreSQL in Production with supabase
- [x] Set up web hosting (vercel)
@@ -29,20 +51,37 @@ A detailed description of the vision is available [here](https://martinbraquet.c
- [x] Ask for detailed info upon registration (location, desired type of connection, prompt answers, gender, etc.)
- [x] Set up page listing all the profiles
- [x] Search through most profile variables
- [x] (Set up chat / direct messaging)
- [x] Set up domain name (https://compassmeet.com)
- [x] Set up chat / direct messaging
- [x] Set up domain name (compassmeet.com)
- [ ] Add mobile app (React Native on Android and iOS)
- [ ] Add better onboarding (tooltips, modals, etc.)
- [ ] Add modules to learn more about each other (personality test, conflict style, love languages, etc.)
- [ ] Add modules to improve interpersonal skills (active listening, nonviolent communication, etc.)
- [ ] Add calendar integration and scheduling
- [ ] Add events (group calls, in-person meetups, etc.)
#### Secondary To Do
Any action item is open to anyone for collaboration, but the following ones are particularly easy to do for first-time contributors.
Everything is open to anyone for collaboration, but the following ones are particularly easy to do for first-time contributors.
- [x] Clean up learn more page
- [x] Add dark theme
- [ ] Add profile features (intellectual interests, cause areas, personality type, conflict style, etc.)
- [ ] Add filters to search through remaining profile features (politics, religion, education level, etc.)
- [ ] Cover with tests (very important, just the test template and framework are ready)
- [ ] Clean up terms and conditions
- [ ] Clean up privacy notice
- [x] Clean up learn more page
- [x] Add dark theme
- [ ] Make the app more user-friendly and appealing (UI/UX)
- [ ] Clean up terms and conditions (convert to Markdown)
- [ ] Clean up privacy notice (convert to Markdown)
- [ ] Add other authentication methods (GitHub, Facebook, Apple, phone, etc.)
- [ ] Add email verification
- [ ] Add password reset
- [x] Add automated welcome email
- [ ] Security audit and penetration testing
- [ ] Make `deploy-api.sh` run automatically on push to `main` branch
- [ ] Create settings page (change email, password, delete account, etc.)
- [ ] Improve [financials](web/public/md/financials.md) page (donor / acknowledgments, etc.)
- [ ] Improve loading sign (e.g., animation of a compass moving around)
- [ ] Show compatibility score in profile page
## Implementation
@@ -55,13 +94,13 @@ The web app is coded in Typescript using React as front-end. It includes:
## Development
Below are all the steps to contribute. If you have any trouble or questions, please don't hesitate to open an issue or contact us on [Discord](https://discord.gg/8Vd7jzqjun)! We're responsive and happy to help.
Below are the steps to contribute. If you have any trouble or questions, please don't hesitate to open an issue or contact us on [Discord](https://discord.gg/8Vd7jzqjun)! We're responsive and happy to help.
### Installation
Clone the repo and navigating into it:
Fork the [repo](https://github.com/CompassConnections/Compass) on GitHub (button in top right). Then, clone your repo and navigating into it:
```bash
git clone git@github.com:CompassMeet/Compass.git
git clone https://github.com/<your-username>/Compass.git
cd Compass
```
@@ -69,7 +108,7 @@ Install `opentofu`, `docker`, and `yarn`. Try running this on Linux or macOS for
```bash
./setup.sh
```
If it doesn't work, you can install them manually (Google how to install `opentofu`, `docker`, and `yarn` for your OS).
If it doesn't work, you can install them manually (google how to install `opentofu`, `docker`, and `yarn` for your OS).
Then, install the dependencies for this project:
```bash
@@ -78,59 +117,25 @@ yarn install
### Environment Variables
Almost all the features will work out of the box, so you can skip this step and come back later if you need to test the following services: email, geolocation.
We can't make the following information public, for security and privacy reasons:
- Database, otherwise anyone could access all the user data (including private messages)
- Firebase, otherwise anyone could remove users or modify the media files
- Email, analytics, and location services, otherwise anyone could use our paid plan
- Email, analytics, and location services, otherwise anyone could use the service plans Compass paid for and run up the bill.
So, for your development, we will give you user-specific access when possible (e.g., Firebase) and for the rest you will need to set up cloned services (email, locations, etc.) and store your secrets as environment variables.
That's why we separate all those services between production and development environments, so that you can code freely without impacting the functioning of the deployed platform.
Contributors should use the default keys for local development. Production uses a separate environment with stricter rules and private keys that are not shared.
To do so, simply create an `.env` file as a copy of `.env.example`, open it, and fill in the variables according to the instructions in the file:
```bash
cp .env.example .env
```
### Installing PostgreSQL
Run the following commands to set up your local development database. Run only the section that corresponds to your operating system.
On macOS:
```bash
brew install postgresql
brew services start postgresql
```
On Linux:
```bash
sudo apt update
sudo apt install postgresql postgresql-contrib
sudo systemctl start postgresql
````
On Windows, you can download PostgreSQL from the [official website](https://www.postgresql.org/download/windows/).
### Database Initialization
Create a database named `compass` and set the password for the `postgres` user:
```bash
sudo -u postgres psql
ALTER USER postgres WITH PASSWORD 'password';
\q
```
Create the database
```bash
...
```
Note that your local database will be made of synthetic data, not real users. This is fine for development and testing.
If you do need one of the few remaining services, you need to set them up and store your own secrets as environment variables. To do so, simply open `.env` and fill in the variables according to the instructions in the file.
### Tests
Make sure the tests pass:
```bash
yarn test
yarn test tests/jest/
```
TODO: fix tests
TODO: make `yarn test` run all the tests, not just the ones in `tests/jest/`.
### Running the Development Server
@@ -139,11 +144,40 @@ Start the development server:
yarn dev
```
Once the server is running, visit http://localhost:3000 to start using the app. You can sign up and visit the profiles; you should see 5 synthetic profiles.
Once the server is running, visit http://localhost:3000 to start using the app. You can sign up and visit the profiles; you should see a few synthetic profiles.
Note: it's normal if page loading locally is much slower than the deployed version. It can take up to 10 seconds, it would be great to improve that though!
### Contributing
Now you can start contributing by making changes and submitting pull requests!
See [development.md](docs/development.md) for additional instructions, such as adding new profile features.
### Submission
Add the original repo as upstream for syncing:
```bash
git remote add upstream https://github.com/CompassConnections/Compass.git
```
Create a new branch for your changes:
```bash
git checkout -b <branch-name>
```
Make changes, then stage and commit:
```bash
git add .
git commit -m "Describe your changes"
```
Push branch to your fork:
```bash
git push origin <branch-name>
```
Finally, open a Pull Request on GitHub from your `fork/<branch-name>``CompassConnections/Compass` main branch.
## Acknowledgements
This project is built on top of [manifold.love](https://github.com/sipec/polylove), an open-source dating platform licensed under the MIT License. We greatly appreciate their work and contributions to open-source, which have significantly aided in the development of some core features such as direct messaging, prompts, and email notifications. We invite the community to explore and contribute to other open-source projects like manifold.love as well, especially if you're interested in functionalities that deviate from Compass' ideals of deep, intentional connections.

View File

@@ -8,5 +8,5 @@
## Reporting a Vulnerability
Contact the development team to report a vulnerability. You should receive updates within a week.
Contact the development team at compass.meet.info@gmail.com to report a vulnerability. You should receive updates within a week.

View File

View File

View File

@@ -30,7 +30,7 @@ export default function RootLayout(
<footer className="p-6 text-center text-gray-500">
<div className="mb-2">
<a
href="https://github.com/CompassMeet/Compass"
href="https://github.com/CompassConnections/Compass"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center text-gray-500 hover:text-gray-700 transition"

View File

@@ -1,6 +1,5 @@
'use client';
import {aColor, supportEmail} from "@/lib/client/constants";
import Image from 'next/image';
export default function PrivacyPage() {

View File

@@ -49,7 +49,7 @@ export default function HomePage() {
{/* Header */}
{/*<header className="flex justify-between items-center p-2 max-w-6xl mx-auto w-full">*/}
{/* <a */}
{/* href="https://github.com/CompassMeet/Compass" */}
{/* href="https://github.com/CompassConnections/Compass" */}
{/* target="_blank" */}
{/* rel="noopener noreferrer"*/}
{/* className="text-gray-700 hover: transition"*/}

View File

@@ -3,7 +3,7 @@
import {useEffect, useState} from 'react';
import {Textarea} from '@/components/ui/textarea';
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/components/ui/select';
import {cons} from "effect/List";
// import {cons} from "effect/List";
type Prompt = {
id: string;

View File

@@ -0,0 +1,2 @@
'use client';

View File

View File

View File

View File

View File

View File

View File

View File

@@ -1,19 +1,20 @@
# prereq: first do `yarn build` to compile typescript & etc.
FROM node:19-alpine
FROM node:20-alpine
WORKDIR /usr/src/app
# Install PM2 globally
RUN yarn global add pm2
# Remove?
COPY tsconfig.json ./
# first get dependencies in for efficient docker layering
# Fet dependencies in for efficient docker layering
COPY dist/package.json dist/yarn.lock ./
RUN yarn install --frozen-lockfile --production
# then copy over typescript payload
# Clean yarn cache to reduce image size
RUN yarn install --frozen-lockfile --production && \
yarn cache clean --force && \
rm -rf /usr/local/share/.cache/yarn
# Copy over typescript payload
COPY dist ./
# Copy the PM2 ecosystem configuration

View File

@@ -1,33 +1,38 @@
# Backend API
This is the code for the API running at `api.compassmeet.com`.
This is the code for the API running at https://api.compassmeet.com.
It runs in a docker inside a Google Cloud virtual machine.
### Requirements
You must have the `gcloud` CLI.
On MacOS:
On macOS:
```bash
brew install --cask google-cloud-sdk
```
On Linux:
```bash
sudo apt-get update && sudo apt-get install google-cloud-sdk
```
Then:
```bash
gcloud init
gcloud auth login
gcloud config set project YOUR_PROJECT_ID
```
### Setup
This section is only for the people who are creating a server from scratch, for instance for a forked project.
One-time commands you may need to run:
```bash
gcloud artifacts repositories create builds \
--repository-format=docker \
@@ -49,8 +54,25 @@ gcloud projects add-iam-policy-binding compass-130ba \
--member="serviceAccount:253367029065-compute@developer.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
gcloud run services list
gcloud compute backend-services update api-backend \
--global \
--timeout=600s
```
Set up the saved search notifications job:
```bash
gcloud scheduler jobs create http daily-saved-search-notifications \
--schedule="0 16 * * *" \
--uri="https://api.compassmeet.com/internal/send-search-notifications" \
--http-method=POST \
--headers="x-api-key=<API_KEY>" \
--time-zone="UTC" \
--location=us-west1
```
View it [here](https://console.cloud.google.com/cloudscheduler).
##### DNS
* After deployment, Terraform assigns a static external IP to this resource.
@@ -60,6 +82,7 @@ gcloud run services list
gcloud compute addresses describe api-lb-ip-2 --global --format="get(address)"
34.117.20.215
```
Since Vercel manages your domain (`compassmeet.com`):
1. Log in to [Vercel dashboard](https://vercel.com/dashboard).
@@ -67,7 +90,7 @@ Since Vercel manages your domain (`compassmeet.com`):
3. Add an **A record** for your API subdomain:
| Type | Name | Value | TTL |
| ---- | ---- | ------------ | ----- |
|------|------|--------------|-------|
| A | api | 34.123.45.67 | 600 s |
* `Name` is just the subdomain: `api``api.compassmeet.com`.
@@ -85,7 +108,6 @@ curl -I https://api.compassmeet.com
* `nslookup` should return the LB IP (`34.123.45.67`).
* `curl -I` should return `200 OK` from your service.
If SSL isnt ready (may take 15 mins), check LB logs:
```bash
@@ -96,7 +118,9 @@ gcloud compute ssl-certificates describe api-lb-cert-2
Secrets are strings that shouldn't be checked into Git (eg API keys, passwords).
Add the secrets for your specific project in [Google Cloud Secrets manager](https://console.cloud.google.com/security/secret-manager), so that the virtual machine can access them.
Add the secrets for your specific project
in [Google Cloud Secrets manager](https://console.cloud.google.com/security/secret-manager), so that the virtual machine
can access them.
For Compass, the name of the secrets are in [secrets.ts](../../common/src/secrets.ts).
@@ -111,13 +135,16 @@ In root directory, run the local api with hot reload, along with all the other b
### Deploy
Run in this directory to deploy your code to the server.
```bash
./deploy-api.sh prod
```
### Connect to the server
Run in this directory to connect to the API server running as virtual machine in Google Cloud. You can access logs, files, debug, etc.
Run in this directory to connect to the API server running as virtual machine in Google Cloud. You can access logs,
files, debug, etc.
```bash
./ssh-api.sh prod
```
@@ -128,4 +155,11 @@ Useful commands once inside the server:
sudo journalctl -u konlet-startup --no-pager -efb
sudo docker logs -f $(sudo docker ps -alq)
docker exec -it $(sudo docker ps -alq) sh
docker run -it --rm $(docker images -q | head -n 1) sh
docker rmi -f $(docker images -aq)
```
### Documentation
The API docs are available at https://api.compassmeet.com. They are defined in [openapi.json](openapi.json).
Just a few endpoints are mentioned in that JSON doc. Feel free to help by adding the remaining ones!

View File

@@ -11,6 +11,8 @@
set -e
cd "$(dirname "$0")"
source ../../.env
ENV=${1:-prod}
@@ -28,7 +30,6 @@ IMAGE_TAG="${TIMESTAMP}-${GIT_REVISION}"
IMAGE_URL="${REGION}-docker.pkg.dev/${PROJECT}/builds/${SERVICE_NAME}:${IMAGE_TAG}"
echo "🚀 Deploying ${SERVICE_NAME} to ${ENV} ($(date "+%Y-%m-%d %I:%M:%S %p"))"
yarn add tsconfig-paths
yarn build
gcloud auth print-access-token | docker login -u oauth2accesstoken --password-stdin us-west1-docker.pkg.dev

View File

@@ -3,7 +3,7 @@ module.exports = {
{
name: "api",
script: "node",
args: "-r tsconfig-paths/register --dns-result-order=ipv4first backend/api/lib/serve.js",
args: "--dns-result-order=ipv4first backend/api/lib/serve.js",
env: {
NODE_ENV: "production",
NODE_PATH: "/usr/src/app/node_modules", // <- ensures Node finds tsconfig-paths

View File

@@ -175,7 +175,7 @@ resource "google_compute_backend_service" "api_backend" {
# URL map
resource "google_compute_url_map" "api_url_map" {
name = "${local.service_name}-url-map"
default_service = google_compute_backend_service.api_backend.id
default_service = google_compute_backend_service.api_backend.self_link
host_rule {
hosts = ["*"]
@@ -185,9 +185,33 @@ resource "google_compute_url_map" "api_url_map" {
path_matcher {
name = "allpaths"
default_service = google_compute_backend_service.api_backend.self_link
#
# # Priority 0: passthrough /v0/* requests
# route_rules {
# priority = 1
# match_rules {
# prefix_match = "/v0"
# }
# service = google_compute_backend_service.api_backend.self_link
# }
#
# # Priority 1: rewrite everything else to /v0
# route_rules {
# priority = 2
# match_rules {
# prefix_match = "/"
# }
# route_action {
# url_rewrite { # This may break websockets (the Upgrade and Connection headers must pass through untouched).
# path_prefix_rewrite = "/v0/"
# }
# }
# service = google_compute_backend_service.api_backend.self_link
# }
}
}
# HTTPS proxy
resource "google_compute_target_https_proxy" "api_https_proxy" {
name = "${local.service_name}-https-proxy"

29
backend/api/openapi.json Normal file
View File

@@ -0,0 +1,29 @@
{
"openapi": "3.0.0",
"info": {
"title": "Compass API",
"version": "1.0.0"
},
"paths": {
"/health": {
"get": {
"summary": "Health",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/get-profiles": {
"get": {
"summary": "List profiles",
"responses": {
"200": {
"description": "OK"
}
}
}
}
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "@compass/api",
"description": "Backend API endpoints",
"version": "0.1.0",
"version": "1.0.0",
"private": true,
"scripts": {
"watch:compile": "npx concurrently \"tsc -b --watch --preserveWatchOutput\" \"(cd ../../common && tsc-alias --watch)\" \"(cd ../shared && tsc-alias --watch)\" \"(cd ../email && tsc-alias --watch)\" \"tsc-alias --watch\"",
@@ -13,7 +13,7 @@
"debug": "nodemon -r tsconfig-paths/register --watch src -e ts --watch ../../common/src --watch ../shared/src --exec \"yarn build && node --inspect-brk src/serve.ts\"",
"dist": "yarn dist:clean && yarn dist:copy",
"dist:clean": "rm -rf dist && mkdir -p dist/common/lib dist/backend/shared/lib dist/backend/api/lib dist/backend/email/lib",
"dist:copy": "rsync -a --delete ../../common/lib/ dist/common/lib && rsync -a --delete ../shared/lib/ dist/backend/shared/lib && rsync -a --delete ../email/lib/ dist/backend/email/lib && rsync -a --delete ./lib/* dist/backend/api/lib && cp ../../yarn.lock dist && cp package.json dist",
"dist:copy": "rsync -a --delete ../../common/lib/ dist/common/lib && rsync -a --delete ../shared/lib/ dist/backend/shared/lib && rsync -a --delete ../email/lib/ dist/backend/email/lib && rsync -a --delete ./lib/* dist/backend/api/lib && cp ../../yarn.lock dist && cp package.json dist && cp openapi.json dist",
"watch": "tsc -w",
"verify": "yarn --cwd=../.. verify",
"verify:dir": "npx eslint . --max-warnings 0",
@@ -21,7 +21,7 @@
"regen-types-dev": "cd ../supabase && make ENV=dev regen-types"
},
"engines": {
"node": ">=16.0.0"
"node": ">=20.0.0"
},
"main": "src/serve.ts",
"dependencies": {
@@ -45,24 +45,28 @@
"cors": "2.8.5",
"dayjs": "1.11.4",
"express": "4.18.1",
"firebase-admin": "11.11.1",
"firebase-admin": "13.5.0",
"gcp-metadata": "6.1.0",
"jsonwebtoken": "9.0.0",
"lodash": "4.17.21",
"pg-promise": "11.4.1",
"posthog-node": "4.11.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-email": "3.0.7",
"resend": "4.1.2",
"string-similarity": "4.0.4",
"swagger-jsdoc": "6.2.8",
"swagger-ui-express": "5.0.1",
"tsconfig-paths": "4.2.0",
"twitter-api-v2": "1.15.0",
"ws": "8.17.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"zod": "3.21.4"
},
"devDependencies": {
"@types/cors": "2.8.17",
"@types/react": "18.3.5",
"@types/react-dom": "18.3.0",
"@types/swagger-ui-express": "4.1.8",
"@types/ws": "8.5.10"
}
}

View File

@@ -1,56 +1,58 @@
import { API, type APIPath } from 'common/api/schema'
import { APIError, pathWithPrefix } from 'common/api/utils'
import {API, type APIPath} from 'common/api/schema'
import {APIError, pathWithPrefix} from 'common/api/utils'
import cors from 'cors'
import * as crypto from 'crypto'
import express from 'express'
import { type ErrorRequestHandler, type RequestHandler } from 'express'
import { hrtime } from 'node:process'
import { withMonitoringContext } from 'shared/monitoring/context'
import { log } from 'shared/monitoring/log'
import { metrics } from 'shared/monitoring/metrics'
import { banUser } from './ban-user'
import { blockUser, unblockUser } from './block-user'
import { getCompatibleLoversHandler } from './compatible-lovers'
import { createComment } from './create-comment'
import { createCompatibilityQuestion } from './create-compatibility-question'
import { createLover } from './create-lover'
import { createUser } from './create-user'
import { getCompatibilityQuestions } from './get-compatibililty-questions'
import { getLikesAndShips } from './get-likes-and-ships'
import { getLoverAnswers } from './get-lover-answers'
import { getLovers } from './get-lovers'
import { getSupabaseToken } from './get-supabase-token'
import { getDisplayUser, getUser } from './get-user'
import { getMe } from './get-me'
import { hasFreeLike } from './has-free-like'
import { health } from './health'
import { typedEndpoint, type APIHandler } from './helpers/endpoint'
import { hideComment } from './hide-comment'
import { likeLover } from './like-lover'
import { markAllNotifsRead } from './mark-all-notifications-read'
import { removePinnedPhoto } from './remove-pinned-photo'
import { report } from './report'
import { searchLocation } from './search-location'
import { searchNearCity } from './search-near-city'
import { shipLovers } from './ship-lovers'
import { starLover } from './star-lover'
import { updateLover } from './update-lover'
import { updateMe } from './update-me'
import { deleteMe } from './delete-me'
import { getCurrentPrivateUser } from './get-current-private-user'
import { createPrivateUserMessage } from './create-private-user-message'
import express, {type ErrorRequestHandler, type RequestHandler} from 'express'
import {hrtime} from 'node:process'
import {withMonitoringContext} from 'shared/monitoring/context'
import {log} from 'shared/monitoring/log'
import {metrics} from 'shared/monitoring/metrics'
import {banUser} from './ban-user'
import {blockUser, unblockUser} from './block-user'
import {getCompatibleProfilesHandler} from './compatible-profiles'
import {createComment} from './create-comment'
import {createCompatibilityQuestion} from './create-compatibility-question'
import {createProfile} from './create-profile'
import {createUser} from './create-user'
import {getCompatibilityQuestions} from './get-compatibililty-questions'
import {getLikesAndShips} from './get-likes-and-ships'
import {getProfileAnswers} from './get-profile-answers'
import {getProfiles} from './get-profiles'
import {getSupabaseToken} from './get-supabase-token'
import {getDisplayUser, getUser} from './get-user'
import {getMe} from './get-me'
import {hasFreeLike} from './has-free-like'
import {health} from './health'
import {type APIHandler, typedEndpoint} from './helpers/endpoint'
import {hideComment} from './hide-comment'
import {likeProfile} from './like-profile'
import {markAllNotifsRead} from './mark-all-notifications-read'
import {removePinnedPhoto} from './remove-pinned-photo'
import {report} from './report'
import {searchLocation} from './search-location'
import {searchNearCity} from './search-near-city'
import {shipProfiles} from './ship-profiles'
import {starProfile} from './star-profile'
import {updateProfile} from './update-profile'
import {updateMe} from './update-me'
import {deleteMe} from './delete-me'
import {getCurrentPrivateUser} from './get-current-private-user'
import {createPrivateUserMessage} from './create-private-user-message'
import {
getChannelMemberships,
getChannelMessages,
getLastSeenChannelTime,
setChannelLastSeenTime,
} from 'api/get-private-messages'
import { searchUsers } from './search-users'
import { createPrivateUserMessageChannel } from './create-private-user-message-channel'
import { leavePrivateUserMessageChannel } from './leave-private-user-message-channel'
import { updatePrivateUserMessageChannel } from './update-private-user-message-channel'
import { getNotifications } from './get-notifications'
import { updateNotifSettings } from './update-notif-setting'
import {searchUsers} from './search-users'
import {createPrivateUserMessageChannel} from './create-private-user-message-channel'
import {leavePrivateUserMessageChannel} from './leave-private-user-message-channel'
import {updatePrivateUserMessageChannel} from './update-private-user-message-channel'
import {getNotifications} from './get-notifications'
import {updateNotifSettings} from './update-notif-setting'
import swaggerUi from "swagger-ui-express"
import * as fs from "fs"
import {sendSearchNotifications} from "api/send-search-notifications";
const allowCorsUnrestricted: RequestHandler = cors({})
@@ -66,15 +68,15 @@ const requestMonitoring: RequestHandler = (req, _res, next) => {
const traceId = traceContext
? traceContext.split('/')[0]
: crypto.randomUUID()
const context = { endpoint: req.path, traceId }
const context = {endpoint: req.path, traceId}
withMonitoringContext(context, () => {
const startTs = hrtime.bigint()
log(`${req.method} ${req.url}`)
metrics.inc('http/request_count', { endpoint: req.path })
metrics.inc('http/request_count', {endpoint: req.path})
next()
const endTs = hrtime.bigint()
const latencyMs = Number(endTs - startTs) / 1e6
metrics.push('http/request_latency', latencyMs, { endpoint: req.path })
metrics.push('http/request_latency', latencyMs, {endpoint: req.path})
})
}
@@ -82,7 +84,7 @@ const apiErrorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
if (error instanceof APIError) {
log.info(error)
if (!res.headersSent) {
const output: { [k: string]: unknown } = { message: error.message }
const output: { [k: string]: unknown } = {message: error.message}
if (error.details != null) {
output.details = error.details
}
@@ -91,7 +93,7 @@ const apiErrorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
} else {
log.error(error)
if (!res.headersSent) {
res.status(500).json({ message: error.stack, error })
res.status(500).json({message: error.stack, error})
}
}
}
@@ -99,6 +101,22 @@ const apiErrorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
export const app = express()
app.use(requestMonitoring)
const swaggerDocument = JSON.parse(fs.readFileSync("./openapi.json", "utf-8"))
swaggerDocument.info = {
...swaggerDocument.info,
description: "Compass is a free, open-source platform to help people form deep, meaningful, and lasting connections — whether platonic, romantic, or collaborative. Its made possible by contributions from the community, including code, ideas, feedback, and donations. Unlike typical apps, Compass prioritizes values, interests, and personality over swipes and ads, giving you full control over who you discover and how you connect.",
version: "1.0.0",
contact: {
name: "Compass",
email: "hello@compassmeet.com",
url: "https://compassmeet.com"
}
};
const rootPath = pathWithPrefix("/")
app.get(rootPath, swaggerUi.setup(swaggerDocument))
app.use(rootPath, swaggerUi.serve)
app.options('*', allowCorsUnrestricted)
const handlers: { [k in APIPath]: APIHandler<k> } = {
@@ -116,26 +134,26 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
'ban-user': banUser,
report: report,
'create-user': createUser,
'create-lover': createLover,
'create-profile': createProfile,
me: getMe,
'me/private': getCurrentPrivateUser,
'me/update': updateMe,
'update-notif-settings': updateNotifSettings,
'me/delete': deleteMe,
'update-lover': updateLover,
'like-lover': likeLover,
'ship-lovers': shipLovers,
'update-profile': updateProfile,
'like-profile': likeProfile,
'ship-profiles': shipProfiles,
'get-likes-and-ships': getLikesAndShips,
'has-free-like': hasFreeLike,
'star-lover': starLover,
'get-lovers': getLovers,
'get-lover-answers': getLoverAnswers,
'star-profile': starProfile,
'get-profiles': getProfiles,
'get-profile-answers': getProfileAnswers,
'get-compatibility-questions': getCompatibilityQuestions,
'remove-pinned-photo': removePinnedPhoto,
'create-comment': createComment,
'hide-comment': hideComment,
'create-compatibility-question': createCompatibilityQuestion,
'compatible-lovers': getCompatibleLoversHandler,
'compatible-profiles': getCompatibleProfilesHandler,
'search-location': searchLocation,
'search-near-city': searchNearCity,
'create-private-user-message': createPrivateUserMessage,
@@ -151,7 +169,7 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
Object.entries(handlers).forEach(([path, handler]) => {
const api = API[path as APIPath]
const cache = cacheController((api as any).cache)
const url = '/' + pathWithPrefix(path as APIPath)
const url = pathWithPrefix('/' + path as APIPath)
const apiRoute = [
url,
@@ -173,6 +191,27 @@ Object.entries(handlers).forEach(([path, handler]) => {
}
})
// console.debug('COMPASS_API_KEY:', process.env.COMPASS_API_KEY)
// Internal Endpoints
app.post(pathWithPrefix("/internal/send-search-notifications"),
async (req, res) => {
const apiKey = req.header("x-api-key");
if (apiKey !== process.env.COMPASS_API_KEY) {
return res.status(401).json({error: "Unauthorized"});
}
try {
const result = await sendSearchNotifications()
return res.status(200).json(result)
} catch (err) {
console.error("Failed to send notifications:", err);
return res.status(500).json({error: "Internal server error"});
}
}
);
app.use(allowCorsUnrestricted, (req, res) => {
if (req.method === 'OPTIONS') {
res.status(200).send()
@@ -181,7 +220,7 @@ app.use(allowCorsUnrestricted, (req, res) => {
.status(404)
.set('Content-Type', 'application/json')
.json({
message: `The requested route '${req.path}' does not exist. Please check your URL for any misspellings or refer to app.ts`,
message: `This is the Compass API, but the requested route '${req.path}' does not exist. Please check your URL for any misspellings, the docs at https://api.compassmeet.com, or simply refer to app.ts on GitHub`,
})
}
})

View File

@@ -1,61 +0,0 @@
import { groupBy, sortBy } from 'lodash'
import { APIError, type APIHandler } from 'api/helpers/endpoint'
import { getCompatibilityScore } from 'common/love/compatibility-score'
import {
getLover,
getCompatibilityAnswers,
getGenderCompatibleLovers,
} from 'shared/love/supabase'
import { log } from 'shared/utils'
export const getCompatibleLoversHandler: APIHandler<
'compatible-lovers'
> = async (props) => {
return getCompatibleLovers(props.userId)
}
export const getCompatibleLovers = async (userId: string) => {
const lover = await getLover(userId)
log('got lover', {
id: lover?.id,
userId: lover?.user_id,
username: lover?.user?.username,
})
if (!lover) throw new APIError(404, 'Lover not found')
const lovers = await getGenderCompatibleLovers(lover)
const loverAnswers = await getCompatibilityAnswers([
userId,
...lovers.map((l) => l.user_id),
])
log('got lover answers ' + loverAnswers.length)
const answersByUserId = groupBy(loverAnswers, 'creator_id')
const loverCompatibilityScores = Object.fromEntries(
lovers.map(
(l) =>
[
l.user_id,
getCompatibilityScore(
answersByUserId[lover.user_id] ?? [],
answersByUserId[l.user_id] ?? []
),
] as const
)
)
const sortedCompatibleLovers = sortBy(
lovers,
(l) => loverCompatibilityScores[l.user_id].score
).reverse()
return {
status: 'success',
lover,
compatibleLovers: sortedCompatibleLovers,
loverCompatibilityScores,
}
}

View File

@@ -0,0 +1,61 @@
import { groupBy, sortBy } from 'lodash'
import { APIError, type APIHandler } from 'api/helpers/endpoint'
import { getCompatibilityScore } from 'common/love/compatibility-score'
import {
getProfile,
getCompatibilityAnswers,
getGenderCompatibleProfiles,
} from 'shared/love/supabase'
import { log } from 'shared/utils'
export const getCompatibleProfilesHandler: APIHandler<
'compatible-profiles'
> = async (props) => {
return getCompatibleProfiles(props.userId)
}
export const getCompatibleProfiles = async (userId: string) => {
const profile = await getProfile(userId)
log('got profile', {
id: profile?.id,
userId: profile?.user_id,
username: profile?.user?.username,
})
if (!profile) throw new APIError(404, 'Profile not found')
const profiles = await getGenderCompatibleProfiles(profile)
const profileAnswers = await getCompatibilityAnswers([
userId,
...profiles.map((l) => l.user_id),
])
log('got profile answers ' + profileAnswers.length)
const answersByUserId = groupBy(profileAnswers, 'creator_id')
const profileCompatibilityScores = Object.fromEntries(
profiles.map(
(l) =>
[
l.user_id,
getCompatibilityScore(
answersByUserId[profile.user_id] ?? [],
answersByUserId[l.user_id] ?? []
),
] as const
)
)
const sortedCompatibleProfiles = sortBy(
profiles,
(l) => profileCompatibilityScores[l.user_id].score
).reverse()
return {
status: 'success',
profile,
compatibleProfiles: sortedCompatibleProfiles,
profileCompatibilityScores,
}
}

View File

@@ -32,8 +32,8 @@ export const createComment: APIHandler<'create-comment'> = async (
if (!onUser) throw new APIError(404, 'User not found')
const pg = createSupabaseDirectClient()
const comment = await pg.one<Row<'lover_comments'>>(
`insert into lover_comments (user_id, user_name, user_username, user_avatar_url, on_user_id, content, reply_to_comment_id)
const comment = await pg.one<Row<'profile_comments'>>(
`insert into profile_comments (user_id, user_name, user_username, user_avatar_url, on_user_id, content, reply_to_comment_id)
values ($1, $2, $3, $4, $5, $6, $7) returning *`,
[
creator.id,
@@ -46,7 +46,7 @@ export const createComment: APIHandler<'create-comment'> = async (
]
)
if (onUser.id !== creator.id)
await createNewCommentOnLoverNotification(
await createNewCommentOnProfileNotification(
onUser,
creator,
richTextToString(content),
@@ -84,7 +84,7 @@ const validateComment = async (
return { content, creator }
}
const createNewCommentOnLoverNotification = async (
const createNewCommentOnProfileNotification = async (
onUser: User,
creator: User,
sourceText: string,
@@ -104,7 +104,7 @@ const createNewCommentOnLoverNotification = async (
createdTime: Date.now(),
isSeen: false,
sourceId: commentId.toString(),
sourceType: 'comment_on_lover',
sourceType: 'comment_on_profile',
sourceUpdateType: 'created',
sourceUserName: creator.name,
sourceUserUsername: creator.username,

View File

@@ -1,46 +0,0 @@
import { APIError, APIHandler } from 'api/helpers/endpoint'
import { createSupabaseDirectClient } from 'shared/supabase/init'
import { log, getUser } from 'shared/utils'
import { HOUR_MS } from 'common/util/time'
import { removePinnedUrlFromPhotoUrls } from 'shared/love/parse-photos'
import { track } from 'shared/analytics'
import { updateUser } from 'shared/supabase/users'
import { tryCatch } from 'common/util/try-catch'
import { insert } from 'shared/supabase/utils'
export const createLover: APIHandler<'create-lover'> = async (body, auth) => {
const pg = createSupabaseDirectClient()
const { data: existingUser } = await tryCatch(
pg.oneOrNone<{ id: string }>('select id from lovers where user_id = $1', [
auth.uid,
])
)
if (existingUser) {
throw new APIError(400, 'User already exists')
}
await removePinnedUrlFromPhotoUrls(body)
const user = await getUser(auth.uid)
if (!user) throw new APIError(401, 'Your account was not found')
if (user.createdTime > Date.now() - HOUR_MS) {
// If they just signed up, set their avatar to be their pinned photo
updateUser(pg, auth.uid, { avatarUrl: body.pinned_url })
}
console.log('body', body)
const { data, error } = await tryCatch(
insert(pg, 'lovers', { user_id: auth.uid, ...body })
)
if (error) {
log.error('Error creating user: ' + error.message)
throw new APIError(500, 'Error creating user')
}
log('Created user', data)
await track(user.id, 'create lover', { username: user.username })
return data
}

View File

@@ -0,0 +1,115 @@
import { APIError, APIHandler } from 'api/helpers/endpoint'
import { createSupabaseDirectClient } from 'shared/supabase/init'
import { log, getUser } from 'shared/utils'
import { HOUR_MS } from 'common/util/time'
import { removePinnedUrlFromPhotoUrls } from 'shared/love/parse-photos'
import { track } from 'shared/analytics'
import { updateUser } from 'shared/supabase/users'
import { tryCatch } from 'common/util/try-catch'
import { insert } from 'shared/supabase/utils'
import {sendDiscordMessage} from "common/discord/core";
function extractTextFromBio(bio: any): string {
try {
const texts: string[] = []
const visit = (node: any) => {
if (!node) return
if (Array.isArray(node)) {
for (const item of node) visit(item)
return
}
if (typeof node === 'object') {
for (const [k, v] of Object.entries(node)) {
if (k === 'text' && typeof v === 'string') texts.push(v)
else visit(v as any)
}
}
}
visit(bio)
// Remove extra whitespace and join
return texts.map((t) => t.trim()).filter(Boolean).join(' ')
} catch {
return ''
}
}
export const createProfile: APIHandler<'create-profile'> = async (body, auth) => {
const pg = createSupabaseDirectClient()
const { data: existingUser } = await tryCatch(
pg.oneOrNone<{ id: string }>('select id from profiles where user_id = $1', [
auth.uid,
])
)
if (existingUser) {
throw new APIError(400, 'User already exists')
}
await removePinnedUrlFromPhotoUrls(body)
const user = await getUser(auth.uid)
if (!user) throw new APIError(401, 'Your account was not found')
if (user.createdTime > Date.now() - HOUR_MS) {
// If they just signed up, set their avatar to be their pinned photo
updateUser(pg, auth.uid, { avatarUrl: body.pinned_url })
}
console.debug('body', body)
const { data, error } = await tryCatch(
insert(pg, 'profiles', { user_id: auth.uid, ...body })
)
if (error) {
log.error('Error creating user: ' + error.message)
throw new APIError(500, 'Error creating user')
}
log('Created user', data)
const continuation = async () => {
try {
await track(auth.uid, 'create profile', {username: user.username})
} catch (e) {
console.error('Failed to track create profile', e)
}
try {
let message: string = `[**${user.name}**](https://www.compassmeet.com/${user.username}) just created a profile`
if (body.bio) {
const bioText = extractTextFromBio(body.bio)
if (bioText) message += `\n > ${bioText}`
}
await sendDiscordMessage(message, 'members')
} catch (e) {
console.error('Failed to send discord new profile', e)
}
try {
const nProfiles = await pg.one<number>(
`SELECT count(*) FROM profiles`,
[],
(r) => Number(r.count)
)
const isMilestone = (n: number) => {
return (
[15, 20, 30, 40].includes(n) || // early milestones
n % 50 === 0
)
}
console.debug(nProfiles, isMilestone(nProfiles))
if (isMilestone(nProfiles)) {
await sendDiscordMessage(
`We just reached **${nProfiles}** total profiles! 🎉`,
'general',
)
}
} catch (e) {
console.error('Failed to send discord user milestone', e)
}
}
return {
result: data,
continue: continuation,
}
}

View File

@@ -1,27 +1,26 @@
import * as admin from 'firebase-admin'
import { PrivateUser } from 'common/user'
import { randomString } from 'common/util/random'
import { cleanDisplayName, cleanUsername } from 'common/util/clean-username'
import { getIp, track } from 'shared/analytics'
import { APIError, APIHandler } from './helpers/endpoint'
import { getDefaultNotificationPreferences } from 'common/user-notification-preferences'
import { removeUndefinedProps } from 'common/util/object'
import { generateAvatarUrl } from 'shared/helpers/generate-and-update-avatar-urls'
import { getStorage } from 'firebase-admin/storage'
import { DEV_CONFIG } from 'common/envs/dev'
import { PROD_CONFIG } from 'common/envs/prod'
import { RESERVED_PATHS } from 'common/envs/constants'
import { log, isProd, getUser, getUserByUsername } from 'shared/utils'
import { createSupabaseDirectClient } from 'shared/supabase/init'
import { insert } from 'shared/supabase/utils'
import { convertPrivateUser, convertUser } from 'common/supabase/users'
import {PrivateUser} from 'common/user'
import {randomString} from 'common/util/random'
import {cleanDisplayName, cleanUsername} from 'common/util/clean-username'
import {getIp, track} from 'shared/analytics'
import {APIError, APIHandler} from './helpers/endpoint'
import {getDefaultNotificationPreferences} from 'common/user-notification-preferences'
import {removeUndefinedProps} from 'common/util/object'
import {generateAvatarUrl} from 'shared/helpers/generate-and-update-avatar-urls'
import {IS_LOCAL, RESERVED_PATHS} from 'common/envs/constants'
import {getUser, getUserByUsername, log} from 'shared/utils'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {insert} from 'shared/supabase/utils'
import {convertPrivateUser, convertUser} from 'common/supabase/users'
import {getBucket} from "shared/firebase-utils";
import {sendWelcomeEmail} from "email/functions/helpers";
export const createUser: APIHandler<'create-user'> = async (
props,
auth,
req
) => {
const { deviceToken: preDeviceToken, adminToken } = props
const {deviceToken: preDeviceToken} = props
const firebaseUser = await admin.auth().getUser(auth.uid)
const testUserAKAEmailPasswordUser =
@@ -52,7 +51,7 @@ export const createUser: APIHandler<'create-user'> = async (
const rawName = fbUser.displayName || emailName || 'User' + randomString(4)
const name = cleanDisplayName(rawName)
const bucket = getStorage().bucket(getStorageBucketId())
const bucket = getBucket()
const avatarUrl = fbUser.photoURL
? fbUser.photoURL
: await generateAvatarUrl(auth.uid, name, bucket)
@@ -63,7 +62,9 @@ export const createUser: APIHandler<'create-user'> = async (
// Check username case-insensitive
const dupes = await pg.one<number>(
`select count(*) from users where username ilike $1`,
`select count(*)
from users
where username ilike $1`,
[username],
(r) => r.count
)
@@ -71,7 +72,7 @@ export const createUser: APIHandler<'create-user'> = async (
const isReservedName = RESERVED_PATHS.includes(username)
if (usernameExists || isReservedName) username += randomString(4)
const { user, privateUser } = await pg.tx(async (tx) => {
const {user, privateUser} = await pg.tx(async (tx) => {
const preexistingUser = await getUser(auth.uid, tx)
if (preexistingUser)
throw new APIError(403, 'User already exists', {
@@ -81,13 +82,13 @@ export const createUser: APIHandler<'create-user'> = async (
// Check exact username to avoid problems with duplicate requests
const sameNameUser = await getUserByUsername(username, tx)
if (sameNameUser)
throw new APIError(403, 'Username already taken', { username })
throw new APIError(403, 'Username already taken', {username})
const user = removeUndefinedProps({
avatarUrl,
isBannedFromPosting: Boolean(
(deviceToken && bannedDeviceTokens.includes(deviceToken)) ||
(ip && bannedIpAddresses.includes(ip))
(ip && bannedIpAddresses.includes(ip))
),
link: {},
})
@@ -120,10 +121,19 @@ export const createUser: APIHandler<'create-user'> = async (
}
})
log('created user ', { username: user.username, firebaseId: auth.uid })
log('created user ', {username: user.username, firebaseId: auth.uid})
const continuation = async () => {
await track(auth.uid, 'create lover', { username: user.username })
try {
await track(auth.uid, 'create profile', {username: user.username})
} catch (e) {
console.error('Failed to track create profile', e)
}
try {
if (!IS_LOCAL) await sendWelcomeEmail(user, privateUser)
} catch (e) {
console.error('Failed to sendWelcomeEmail', e)
}
}
return {
@@ -135,12 +145,6 @@ export const createUser: APIHandler<'create-user'> = async (
}
}
function getStorageBucketId() {
return isProd()
? PROD_CONFIG.firebaseConfig.storageBucket
: DEV_CONFIG.firebaseConfig.storageBucket
}
// Automatically ban users with these device tokens or ip addresses.
const bannedDeviceTokens = [
'fa807d664415',

View File

@@ -1,11 +1,11 @@
import { getUser } from 'shared/utils'
import { APIError, APIHandler } from './helpers/endpoint'
import { updatePrivateUser, updateUser } from 'shared/supabase/users'
import { createSupabaseDirectClient } from 'shared/supabase/init'
import { FieldVal } from 'shared/supabase/utils'
import {getUser} from 'shared/utils'
import {APIError, APIHandler} from './helpers/endpoint'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import * as admin from "firebase-admin";
import {deleteUserFiles} from "shared/firebase-utils";
export const deleteMe: APIHandler<'me/delete'> = async (body, auth) => {
const { username } = body
const {username} = body
const user = await getUser(auth.uid)
if (!user) {
throw new APIError(401, 'Your account was not found')
@@ -16,13 +16,27 @@ export const deleteMe: APIHandler<'me/delete'> = async (body, auth) => {
`Incorrect username. You are logged in as ${user.username}. Are you sure you want to delete this account?`
)
}
const userId = user.id
if (!userId) {
throw new APIError(400, 'Invalid user ID')
}
// Remove user data from Supabase
const pg = createSupabaseDirectClient()
await updateUser(pg, auth.uid, {
userDeleted: true,
isBannedFromPosting: true,
})
await updatePrivateUser(pg, auth.uid, {
email: FieldVal.delete(),
})
await pg.none('DELETE FROM users WHERE id = $1', [userId])
await pg.none('DELETE FROM private_users WHERE id = $1', [userId])
await pg.none('DELETE FROM profiles WHERE user_id = $1', [userId])
// May need to also delete from other tables in the future (such as messages, compatibility responses, etc.)
// Delete user files from Firebase Storage
await deleteUserFiles(user.username)
// Remove user from Firebase Auth
try {
const auth = admin.auth()
await auth.deleteUser(userId)
console.debug(`Deleted user ${userId} from Firebase Auth and Supabase`)
} catch (e) {
console.error('Error deleting user from Firebase Auth:', e)
}
}

View File

@@ -2,12 +2,21 @@ import { type APIHandler } from 'api/helpers/endpoint'
import { createSupabaseDirectClient } from 'shared/supabase/init'
import { Row } from 'common/supabase/utils'
export function shuffle<T>(array: T[]): T[] {
const arr = [...array]; // copy to avoid mutating the original
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
export const getCompatibilityQuestions: APIHandler<
'get-compatibility-questions'
> = async (_props, _auth) => {
const pg = createSupabaseDirectClient()
const questions = await pg.manyOrNone<
const dbQuestions = await pg.manyOrNone<
Row<'love_questions'> & { answer_count: number; score: number }
>(
`SELECT
@@ -28,11 +37,12 @@ export const getCompatibilityQuestions: APIHandler<
[]
)
if (false)
console.log(
'got questions',
questions.map((q) => q.question + ' ' + q.score)
)
const questions = shuffle(dbQuestions)
// console.debug(
// 'got questions',
// questions.map((q) => q.question + ' ' + q.score)
// )
return {
status: 'success',

View File

@@ -22,11 +22,11 @@ export const getLikesAndShipsMain = async (userId: string) => {
`
select target_id, love_likes.created_time
from love_likes
join lovers on lovers.user_id = love_likes.target_id
join profiles on profiles.user_id = love_likes.target_id
join users on users.id = love_likes.target_id
where creator_id = $1
and looking_for_matches
and lovers.pinned_url is not null
and profiles.pinned_url is not null
and (data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)
order by created_time desc
`,
@@ -44,11 +44,11 @@ export const getLikesAndShipsMain = async (userId: string) => {
`
select creator_id, love_likes.created_time
from love_likes
join lovers on lovers.user_id = love_likes.creator_id
join profiles on profiles.user_id = love_likes.creator_id
join users on users.id = love_likes.creator_id
where target_id = $1
and looking_for_matches
and lovers.pinned_url is not null
and profiles.pinned_url is not null
and (data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)
order by created_time desc
`,
@@ -71,11 +71,11 @@ export const getLikesAndShipsMain = async (userId: string) => {
target1_id, target2_id, creator_id, love_ships.created_time,
target1_id as target_id
from love_ships
join lovers on lovers.user_id = love_ships.target1_id
join profiles on profiles.user_id = love_ships.target1_id
join users on users.id = love_ships.target1_id
where target2_id = $1
and lovers.looking_for_matches
and lovers.pinned_url is not null
and profiles.looking_for_matches
and profiles.pinned_url is not null
and (users.data->>'isBannedFromPosting' != 'true' or users.data->>'isBannedFromPosting' is null)
union all
@@ -84,11 +84,11 @@ export const getLikesAndShipsMain = async (userId: string) => {
target1_id, target2_id, creator_id, love_ships.created_time,
target2_id as target_id
from love_ships
join lovers on lovers.user_id = love_ships.target2_id
join profiles on profiles.user_id = love_ships.target2_id
join users on users.id = love_ships.target2_id
where target1_id = $1
and lovers.looking_for_matches
and lovers.pinned_url is not null
and profiles.looking_for_matches
and profiles.pinned_url is not null
and (users.data->>'isBannedFromPosting' != 'true' or users.data->>'isBannedFromPosting' is null)
`,
[userId],

View File

@@ -1,134 +0,0 @@
import { type APIHandler } from 'api/helpers/endpoint'
import { convertRow } from 'shared/love/supabase'
import { createSupabaseDirectClient } from 'shared/supabase/init'
import {
from,
join,
limit,
orderBy,
renderSql,
select,
where,
} from 'shared/supabase/sql-builder'
import { getCompatibleLovers } from 'api/compatible-lovers'
import { intersection } from 'lodash'
export const getLovers: APIHandler<'get-lovers'> = async (props, _auth) => {
const pg = createSupabaseDirectClient()
const {
limit: limitParam,
after,
name,
genders,
pref_gender,
pref_age_min,
pref_age_max,
pref_relation_styles,
wants_kids_strength,
has_kids,
is_smoker,
geodbCityIds,
compatibleWithUserId,
orderBy: orderByParam,
} = props
// compatibility. TODO: do this in sql
if (orderByParam === 'compatibility_score') {
if (!compatibleWithUserId) return { status: 'fail', lovers: [] }
const { compatibleLovers } = await getCompatibleLovers(compatibleWithUserId)
const lovers = compatibleLovers.filter(
(l) =>
(!name || l.user.name.toLowerCase().includes(name.toLowerCase())) &&
(!genders || genders.includes(l.gender)) &&
(!pref_gender || intersection(pref_gender, l.pref_gender).length) &&
(!pref_age_min || l.age >= pref_age_min) &&
(!pref_age_max || l.age <= pref_age_max) &&
(!pref_relation_styles ||
intersection(pref_relation_styles, l.pref_relation_styles).length) &&
(!wants_kids_strength ||
wants_kids_strength == -1 ||
(wants_kids_strength >= 2
? l.wants_kids_strength >= wants_kids_strength
: l.wants_kids_strength <= wants_kids_strength)) &&
(has_kids == undefined ||
has_kids == -1 ||
(has_kids == 0 && !l.has_kids) ||
(l.has_kids && l.has_kids > 0)) &&
(!is_smoker || l.is_smoker === is_smoker) &&
(!geodbCityIds ||
(l.geodb_city_id && geodbCityIds.includes(l.geodb_city_id)))
)
const cursor = after
? lovers.findIndex((l) => l.id.toString() === after) + 1
: 0
console.log(cursor)
return {
status: 'success',
lovers: lovers.slice(cursor, cursor + limitParam),
}
}
const query = renderSql(
select('lovers.*, name, username, users.data as user'),
from('lovers'),
join('users on users.id = lovers.user_id'),
where('looking_for_matches = true'),
// where(`pinned_url is not null and pinned_url != ''`),
where(
`(data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)`
),
where(`data->>'userDeleted' != 'true' or data->>'userDeleted' is null`),
name &&
where(`lower(users.name) ilike '%' || lower($(name)) || '%'`, { name }),
genders?.length && where(`gender = ANY($(gender))`, { gender: genders }),
pref_gender?.length &&
where(`pref_gender && $(pref_gender)`, { pref_gender }),
pref_age_min !== undefined &&
where(`age >= $(pref_age_min)`, { pref_age_min }),
pref_age_max !== undefined &&
where(`age <= $(pref_age_max)`, { pref_age_max }),
pref_relation_styles?.length &&
where(`pref_relation_styles && $(pref_relation_styles)`, {
pref_relation_styles,
}),
wants_kids_strength !== undefined &&
wants_kids_strength !== -1 &&
where(
wants_kids_strength >= 2
? `wants_kids_strength >= $(wants_kids_strength)`
: `wants_kids_strength <= $(wants_kids_strength)`,
{ wants_kids_strength }
),
has_kids === 0 && where(`has_kids IS NULL OR has_kids = 0`),
has_kids && has_kids > 0 && where(`has_kids > 0`),
is_smoker !== undefined && where(`is_smoker = $(is_smoker)`, { is_smoker }),
geodbCityIds?.length &&
where(`geodb_city_id = ANY($(geodbCityIds))`, { geodbCityIds }),
orderBy(`${orderByParam} desc`),
after &&
where(
`lovers.${orderByParam} < (select lovers.${orderByParam} from lovers where id = $(after))`,
{ after }
),
limit(limitParam)
)
const lovers = await pg.map(query, [], convertRow)
return { status: 'success', lovers: lovers }
}

View File

@@ -2,7 +2,7 @@ import { type APIHandler } from 'api/helpers/endpoint'
import { createSupabaseDirectClient } from 'shared/supabase/init'
import { Row } from 'common/supabase/utils'
export const getLoverAnswers: APIHandler<'get-lover-answers'> = async (
export const getProfileAnswers: APIHandler<'get-profile-answers'> = async (
props,
_auth
) => {

View File

@@ -0,0 +1,178 @@
import {type APIHandler} from 'api/helpers/endpoint'
import {convertRow} from 'shared/love/supabase'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {from, join, limit, orderBy, renderSql, select, where,} from 'shared/supabase/sql-builder'
import {getCompatibleProfiles} from 'api/compatible-profiles'
import {intersection} from 'lodash'
import {MAX_INT, MIN_BIO_LENGTH, MIN_INT} from "common/constants";
export type profileQueryType = {
limit?: number | undefined,
after?: string | undefined,
// Search and filter parameters
name?: string | undefined,
genders?: String[] | undefined,
pref_gender?: String[] | undefined,
pref_age_min?: number | undefined,
pref_age_max?: number | undefined,
pref_relation_styles?: String[] | undefined,
wants_kids_strength?: number | undefined,
has_kids?: number | undefined,
is_smoker?: boolean | undefined,
shortBio?: boolean | undefined,
geodbCityIds?: String[] | undefined,
compatibleWithUserId?: string | undefined,
skipId?: string | undefined,
orderBy?: string | undefined,
lastModificationWithin?: string | undefined,
}
export const loadProfiles = async (props: profileQueryType) => {
const pg = createSupabaseDirectClient()
console.debug(props)
const {
limit: limitParam,
after,
name,
genders,
pref_gender,
pref_age_min,
pref_age_max,
pref_relation_styles,
wants_kids_strength,
has_kids,
is_smoker,
shortBio,
geodbCityIds,
compatibleWithUserId,
orderBy: orderByParam = 'created_time',
lastModificationWithin,
skipId,
} = props
const keywords = name ? name.split(",").map(q => q.trim()).filter(Boolean) : []
// console.debug('keywords:', keywords)
// compatibility. TODO: do this in sql
if (orderByParam === 'compatibility_score') {
if (!compatibleWithUserId) {
console.error('Incompatible with user ID')
throw Error('Incompatible with user ID')
}
const {compatibleProfiles} = await getCompatibleProfiles(compatibleWithUserId)
const profiles = compatibleProfiles.filter(
(l) =>
(!name || l.user.name.toLowerCase().includes(name.toLowerCase())) &&
(!genders || genders.includes(l.gender)) &&
(!pref_gender || intersection(pref_gender, l.pref_gender).length) &&
(!pref_age_min || (l.age ?? MAX_INT) >= pref_age_min) &&
(!pref_age_max || (l.age ?? MIN_INT) <= pref_age_max) &&
(!pref_relation_styles ||
intersection(pref_relation_styles, l.pref_relation_styles).length) &&
(!wants_kids_strength ||
wants_kids_strength == -1 ||
(wants_kids_strength >= 2
? l.wants_kids_strength >= wants_kids_strength
: l.wants_kids_strength <= wants_kids_strength)) &&
(has_kids == undefined ||
has_kids == -1 ||
(has_kids == 0 && !l.has_kids) ||
(l.has_kids && l.has_kids > 0)) &&
(!is_smoker || l.is_smoker === is_smoker) &&
(l.id.toString() != skipId) &&
(!geodbCityIds ||
(l.geodb_city_id && geodbCityIds.includes(l.geodb_city_id))) &&
((l.bio_length ?? 0) >= MIN_BIO_LENGTH)
)
const cursor = after
? profiles.findIndex((l) => l.id.toString() === after) + 1
: 0
console.debug(cursor)
if (limitParam) return profiles.slice(cursor, cursor + limitParam)
return profiles
}
const query = renderSql(
select('profiles.*, name, username, users.data as user'),
from('profiles'),
join('users on users.id = profiles.user_id'),
where('looking_for_matches = true'),
// where(`pinned_url is not null and pinned_url != ''`),
where(
`(data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)`
),
where(`data->>'userDeleted' != 'true' or data->>'userDeleted' is null`),
...keywords.map(word => where(
`lower(users.name) ilike '%' || lower($(word)) || '%' or lower(bio::text) ilike '%' || lower($(word)) || '%' or bio_tsv @@ phraseto_tsquery('english', $(word))`,
{word}
)),
genders?.length && where(`gender = ANY($(gender))`, {gender: genders}),
pref_gender?.length &&
where(`pref_gender && $(pref_gender)`, {pref_gender}),
pref_age_min &&
where(`age >= $(pref_age_min) or age is null`, {pref_age_min}),
pref_age_max &&
where(`age <= $(pref_age_max) or age is null`, {pref_age_max}),
pref_relation_styles?.length &&
where(
`pref_relation_styles IS NULL OR pref_relation_styles = '{}' OR pref_relation_styles && $(pref_relation_styles)`,
{ pref_relation_styles }
),
!!wants_kids_strength &&
wants_kids_strength !== -1 &&
where(
wants_kids_strength >= 2
? `wants_kids_strength >= $(wants_kids_strength)`
: `wants_kids_strength <= $(wants_kids_strength)`,
{wants_kids_strength}
),
has_kids === 0 && where(`has_kids IS NULL OR has_kids = 0`),
has_kids && has_kids > 0 && where(`has_kids > 0`),
is_smoker !== undefined && where(`is_smoker = $(is_smoker)`, {is_smoker}),
geodbCityIds?.length &&
where(`geodb_city_id = ANY($(geodbCityIds))`, {geodbCityIds}),
skipId && where(`user_id != $(skipId)`, {skipId}),
orderBy(`${orderByParam} desc`),
after &&
where(
`profiles.${orderByParam} < (select profiles.${orderByParam} from profiles where id = $(after))`,
{after}
),
!shortBio && where(`bio_length >= ${MIN_BIO_LENGTH}`, {MIN_BIO_LENGTH}),
lastModificationWithin && where(`last_modification_time >= NOW() - INTERVAL $(lastModificationWithin)`, {lastModificationWithin}),
limitParam && limit(limitParam)
)
// console.debug('query:', query)
return await pg.map(query, [], convertRow)
}
export const getProfiles: APIHandler<'get-profiles'> = async (props, _auth) => {
try {
const profiles = await loadProfiles(props)
return {status: 'success', profiles: profiles}
} catch {
return {status: 'fail', profiles: []}
}
}

View File

@@ -1,8 +1,6 @@
import { sign } from 'jsonwebtoken'
import { APIError, APIHandler } from './helpers/endpoint'
import { DEV_CONFIG } from 'common/envs/dev'
import { PROD_CONFIG } from 'common/envs/prod'
import { isProd } from 'shared/utils'
import {sign} from 'jsonwebtoken'
import {APIError, APIHandler} from './helpers/endpoint'
import {ENV_CONFIG} from "common/envs/constants";
export const getSupabaseToken: APIHandler<'get-supabase-token'> = async (
_,
@@ -12,21 +10,17 @@ export const getSupabaseToken: APIHandler<'get-supabase-token'> = async (
if (jwtSecret == null) {
throw new APIError(500, "No SUPABASE_JWT_SECRET; couldn't sign token.")
}
const instanceId = isProd()
? PROD_CONFIG.supabaseInstanceId
: DEV_CONFIG.supabaseInstanceId
const instanceId = ENV_CONFIG.supabaseInstanceId
if (!instanceId) {
throw new APIError(500, 'No Supabase instance ID in config.')
}
const payload = { role: 'anon' } // postgres role
const payload = {role: 'anon'} // postgres role
return {
jwt: sign(payload, jwtSecret, {
algorithm: 'HS256', // same as what supabase uses for its auth tokens
expiresIn: '1d',
audience: instanceId,
issuer: isProd()
? PROD_CONFIG.firebaseConfig.projectId
: DEV_CONFIG.firebaseConfig.projectId,
issuer: ENV_CONFIG.firebaseConfig.projectId,
subject: auth.uid,
}),
}

View File

@@ -1,35 +1,29 @@
import * as admin from 'firebase-admin'
import { z } from 'zod'
import { Request, Response, NextFunction } from 'express'
import {z} from 'zod'
import {NextFunction, Request, Response} from 'express'
import { PrivateUser } from 'common/user'
import { APIError } from 'common/api/utils'
export { APIError } from 'common/api/utils'
import {
API,
APIPath,
APIResponseOptionalContinue,
APISchema,
ValidatedAPIParams,
} from 'common/api/schema'
import { log } from 'shared/utils'
import { getPrivateUserByKey } from 'shared/utils'
import {PrivateUser} from 'common/user'
import {APIError} from 'common/api/utils'
import {API, APIPath, APIResponseOptionalContinue, APISchema, ValidatedAPIParams,} from 'common/api/schema'
import {getPrivateUserByKey, log} from 'shared/utils'
export type Json = Record<string, unknown> | Json[]
export type JsonHandler<T extends Json> = (
req: Request,
res: Response
) => Promise<T>
export type AuthedHandler<T extends Json> = (
req: Request,
user: AuthedUser,
res: Response
) => Promise<T>
export type MaybeAuthedHandler<T extends Json> = (
req: Request,
user: AuthedUser | undefined,
res: Response
) => Promise<T>
export {APIError} from 'common/api/utils'
// export type Json = Record<string, unknown> | Json[]
// export type JsonHandler<T extends Json> = (
// req: Request,
// res: Response
// ) => Promise<T>
// export type AuthedHandler<T extends Json> = (
// req: Request,
// user: AuthedUser,
// res: Response
// ) => Promise<T>
// export type MaybeAuthedHandler<T extends Json> = (
// req: Request,
// user: AuthedUser | undefined,
// res: Response
// ) => Promise<T>
export type AuthedUser = {
uid: string
@@ -39,6 +33,29 @@ type JwtCredentials = { kind: 'jwt'; data: admin.auth.DecodedIdToken }
type KeyCredentials = { kind: 'key'; data: string }
type Credentials = JwtCredentials | KeyCredentials
// export async function verifyIdToken(payload: string): Promise<DecodedIdToken> {
// TODO: make local dev work without firebase admin SDK setup.
// if (IS_LOCAL) {
// // Skip real verification locally (to avoid needing to set up admin service account).
// return {
// aud: "",
// auth_time: 0,
// email_verified: false,
// exp: 0,
// firebase: {identities: {}, sign_in_provider: ""},
// iat: 0,
// iss: "",
// phone_number: "",
// picture: "",
// sub: "",
// uid: 'dev-user',
// user_id: 'dev-user',
// email: 'dev-user@example.com'
// };
// }
// return await admin.auth().verifyIdToken(payload);
// }
export const parseCredentials = async (req: Request): Promise<Credentials> => {
const auth = admin.auth()
const authHeader = req.get('Authorization')
@@ -57,14 +74,14 @@ export const parseCredentials = async (req: Request): Promise<Credentials> => {
throw new APIError(401, 'Firebase JWT payload undefined.')
}
try {
return { kind: 'jwt', data: await auth.verifyIdToken(payload) }
return {kind: 'jwt', data: await auth.verifyIdToken(payload)}
} catch (err) {
// This is somewhat suspicious, so get it into the firebase console
console.error('Error verifying Firebase JWT: ', err, scheme, payload)
throw new APIError(500, 'Error validating token.')
}
case 'Key':
return { kind: 'key', data: payload }
return {kind: 'key', data: payload}
default:
throw new APIError(401, 'Invalid auth scheme; must be "Key" or "Bearer".')
}
@@ -76,7 +93,7 @@ export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
if (typeof creds.data.user_id !== 'string') {
throw new APIError(401, 'JWT must contain user ID.')
}
return { uid: creds.data.user_id, creds }
return {uid: creds.data.user_id, creds}
}
case 'key': {
const key = creds.data
@@ -84,7 +101,7 @@ export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
if (!privateUser) {
throw new APIError(401, `No private user exists with API key ${key}.`)
}
return { uid: privateUser.id, creds: { privateUser, ...creds } }
return {uid: privateUser.id, creds: {privateUser, ...creds}}
}
default:
throw new APIError(401, 'Invalid credential type.')
@@ -109,45 +126,45 @@ export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => {
}
}
export const jsonEndpoint = <T extends Json>(fn: JsonHandler<T>) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
res.status(200).json(await fn(req, res))
} catch (e) {
next(e)
}
}
}
export const authEndpoint = <T extends Json>(fn: AuthedHandler<T>) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const authedUser = await lookupUser(await parseCredentials(req))
res.status(200).json(await fn(req, authedUser, res))
} catch (e) {
next(e)
}
}
}
export const MaybeAuthedEndpoint = <T extends Json>(
fn: MaybeAuthedHandler<T>
) => {
return async (req: Request, res: Response, next: NextFunction) => {
let authUser: AuthedUser | undefined = undefined
try {
authUser = await lookupUser(await parseCredentials(req))
} catch {
// it's treated as an anon request
}
try {
res.status(200).json(await fn(req, authUser, res))
} catch (e) {
next(e)
}
}
}
// export const jsonEndpoint = <T extends Json>(fn: JsonHandler<T>) => {
// return async (req: Request, res: Response, next: NextFunction) => {
// try {
// res.status(200).json(await fn(req, res))
// } catch (e) {
// next(e)
// }
// }
// }
//
// export const authEndpoint = <T extends Json>(fn: AuthedHandler<T>) => {
// return async (req: Request, res: Response, next: NextFunction) => {
// try {
// const authedUser = await lookupUser(await parseCredentials(req))
// res.status(200).json(await fn(req, authedUser, res))
// } catch (e) {
// next(e)
// }
// }
// }
//
// export const MaybeAuthedEndpoint = <T extends Json>(
// fn: MaybeAuthedHandler<T>
// ) => {
// return async (req: Request, res: Response, next: NextFunction) => {
// let authUser: AuthedUser | undefined = undefined
// try {
// authUser = await lookupUser(await parseCredentials(req))
// } catch {
// // it's treated as an anon request
// }
//
// try {
// res.status(200).json(await fn(req, authUser, res))
// } catch (e) {
// next(e)
// }
// }
// }
export type APIHandler<N extends APIPath> = (
props: ValidatedAPIParams<N>,
@@ -161,7 +178,7 @@ export const typedEndpoint = <N extends APIPath>(
name: N,
handler: APIHandler<N>
) => {
const { props: propSchema, authed: authRequired, method } = API[name]
const {props: propSchema, authed: authRequired, method} = API[name]
return async (req: Request, res: Response, next: NextFunction) => {
let authUser: AuthedUser | undefined = undefined
@@ -195,7 +212,7 @@ export const typedEndpoint = <N extends APIPath>(
// Convert bigint to number, b/c JSON doesn't support bigint.
const convertedResult = deepConvertBigIntToNumber(result)
res.status(200).json(convertedResult ?? { success: true })
res.status(200).json(convertedResult ?? {success: true})
}
if (hasContinue) {

View File

@@ -10,8 +10,8 @@ export const hideComment: APIHandler<'hide-comment'> = async (
auth
) => {
const pg = createSupabaseDirectClient()
const comment = await pg.oneOrNone<Row<'lover_comments'>>(
`select * from lover_comments where id = $1`,
const comment = await pg.oneOrNone<Row<'profile_comments'>>(
`select * from profile_comments where id = $1`,
[commentId]
)
if (!comment) {
@@ -26,7 +26,7 @@ export const hideComment: APIHandler<'hide-comment'> = async (
throw new APIError(403, 'You are not allowed to hide this comment')
}
await pg.none(`update lover_comments set hidden = $2 where id = $1`, [
await pg.none(`update profile_comments set hidden = $2 where id = $1`, [
commentId,
hide,
])

View File

@@ -163,7 +163,7 @@ const notifyOtherUserInChannelIfInactive = async (
// TODO: notification only for active user
const otherUser = await getUser(otherUserId.user_id)
console.log('otherUser:', otherUser)
console.debug('otherUser:', otherUser)
if (!otherUser) return
await createNewMessageNotification(creator, otherUser, channelId)
@@ -175,7 +175,7 @@ const createNewMessageNotification = async (
channelId: number
) => {
const privateUser = await getPrivateUser(toUser.id)
console.log('privateUser:', privateUser)
console.debug('privateUser:', privateUser)
if (!privateUser) return
await sendNewMessageEmail(privateUser, fromUser, toUser, channelId)
}

View File

@@ -6,7 +6,7 @@ import { log } from 'shared/utils'
import { tryCatch } from 'common/util/try-catch'
import { Row } from 'common/supabase/utils'
export const likeLover: APIHandler<'like-lover'> = async (props, auth) => {
export const likeProfile: APIHandler<'like-profile'> = async (props, auth) => {
const { targetUserId, remove } = props
const creatorId = auth.uid

View File

@@ -17,7 +17,7 @@ export const removePinnedPhoto: APIHandler<'remove-pinned-photo'> = async (
const pg = createSupabaseDirectClient()
const { error } = await tryCatch(
pg.none('update lovers set pinned_url = null where user_id = $1', [userId])
pg.none('update profiles set pinned_url = null where user_id = $1', [userId])
)
if (error) {

View File

@@ -1,36 +1,9 @@
import { APIHandler } from './helpers/endpoint'
import {APIHandler} from './helpers/endpoint'
import {geodbFetch} from "common/geodb";
export const searchLocation: APIHandler<'search-location'> = async (body) => {
const { term, limit } = body
const apiKey = process.env.GEODB_API_KEY
console.log('GEODB_API_KEY', apiKey)
if (!apiKey) {
return { status: 'failure', data: 'Missing GEODB API key' }
}
const host = 'wft-geo-db.p.rapidapi.com'
const baseUrl = `https://${host}/v1/geo`
const url = `${baseUrl}/cities?namePrefix=${term}&limit=${
limit ?? 10
}&offset=0&sort=-population`
try {
const res = await fetch(url, {
method: 'GET',
headers: {
'X-RapidAPI-Key': apiKey,
'X-RapidAPI-Host': host,
},
})
if (!res.ok) {
throw new Error(`HTTP error! Status: ${res.status} ${await res.text()}`)
}
const data = await res.json()
// console.log('GEO DB', data)
return { status: 'success', data: data }
} catch (error: any) {
console.log('failure', error)
return { status: 'failure', data: error.message }
}
const {term, limit} = body
const endpoint = `/cities?namePrefix=${term}&limit=${limit ?? 10}&offset=0&sort=-population`
// const endpoint = `/countries?namePrefix=${term}&limit=${limit ?? 10}&offset=0`
return await geodbFetch(endpoint)
}

Some files were not shown because too many files have changed in this diff Show More