mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-04 14:53:33 -04:00
Compare commits
150 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17f9e72a9f | ||
|
|
120aeed56f | ||
|
|
8128c3b2d7 | ||
|
|
4581a33cae | ||
|
|
d43e2af3ae | ||
|
|
0283eb4d85 | ||
|
|
f483ae42a8 | ||
|
|
f974eba465 | ||
|
|
7d7969fe0f | ||
|
|
2a3d7e8362 | ||
|
|
a38c03c4e0 | ||
|
|
342a0c612a | ||
|
|
f1f9970407 | ||
|
|
c83a3e6315 | ||
|
|
fbc65e7e2a | ||
|
|
d9e9407cab | ||
|
|
d0881b76e0 | ||
|
|
61c867b49c | ||
|
|
87de30d257 | ||
|
|
817605417c | ||
|
|
65b018db2a | ||
|
|
addb52e3fa | ||
|
|
c3124ec7c3 | ||
|
|
b1caa6dfdc | ||
|
|
26f28d55d9 | ||
|
|
cb66688529 | ||
|
|
40c61f11be | ||
|
|
9b45c75a5b | ||
|
|
09425c1910 | ||
|
|
591798e98c | ||
|
|
acdd82a680 | ||
|
|
5719ac3209 | ||
|
|
2ac687b0c2 | ||
|
|
a86a249f05 | ||
|
|
e49a7b0bb4 | ||
|
|
e904a7949c | ||
|
|
080d8110df | ||
|
|
d90826e851 | ||
|
|
e495da692b | ||
|
|
52970ef93e | ||
|
|
8f641d117a | ||
|
|
d164ebc7da | ||
|
|
632cc5810d | ||
|
|
e565a6c77f | ||
|
|
c1fe700d7a | ||
|
|
06ee267804 | ||
|
|
aad722c723 | ||
|
|
aefc58b636 | ||
|
|
fdd96507b8 | ||
|
|
2ad87a5ec5 | ||
|
|
b94cdba5af | ||
|
|
725261335c | ||
|
|
5fb0051fc6 | ||
|
|
1247847739 | ||
|
|
18cb4e74d6 | ||
|
|
e07cb7fca9 | ||
|
|
dc54ed46f8 | ||
|
|
0415d86d71 | ||
|
|
b8b95be5ce | ||
|
|
46820f0986 | ||
|
|
dcc022ac7f | ||
|
|
9142f0d633 | ||
|
|
181c72befe | ||
|
|
99f3459978 | ||
|
|
75fbc9679c | ||
|
|
700b7774b1 | ||
|
|
d9f0a9b1ca | ||
|
|
70644ff26d | ||
|
|
bbefcc3bc8 | ||
|
|
09767dbae3 | ||
|
|
57eafa95ba | ||
|
|
f4f28a411e | ||
|
|
f6059ef5c7 | ||
|
|
e3fa4efa95 | ||
|
|
6884a91eb8 | ||
|
|
71ba018a42 | ||
|
|
10f5232ac3 | ||
|
|
78d707484d | ||
|
|
69db66fbbb | ||
|
|
99691cd7ee | ||
|
|
47cef359ca | ||
|
|
046105498f | ||
|
|
4d3ef5dd2a | ||
|
|
8bcd5623bf | ||
|
|
a29b4a3a8e | ||
|
|
dee0fb396b | ||
|
|
b5c707e07f | ||
|
|
8fe35bd1d7 | ||
|
|
6c864c35cd | ||
|
|
f00acf6af1 | ||
|
|
49e1599bc4 | ||
|
|
7311d4b724 | ||
|
|
fa44e348a2 | ||
|
|
8cba02741c | ||
|
|
48d04d5e72 | ||
|
|
7cac25c0e2 | ||
|
|
88b0fa0163 | ||
|
|
3fcef24cc9 | ||
|
|
d9fba6ce6b | ||
|
|
8bc2f0c40e | ||
|
|
21254695d5 | ||
|
|
f063f0a6f4 | ||
|
|
2d847cbcdb | ||
|
|
547e99f526 | ||
|
|
a9794cd2ee | ||
|
|
c651abd8ae | ||
|
|
15781475b6 | ||
|
|
26a28175fd | ||
|
|
aa3680934b | ||
|
|
0b36586ddf | ||
|
|
7b58acac0d | ||
|
|
27bf4eadf9 | ||
|
|
c8d4353888 | ||
|
|
4876ca2643 | ||
|
|
e06a382c94 | ||
|
|
d1a421ca15 | ||
|
|
cd3c8d89d0 | ||
|
|
1f943ccead | ||
|
|
753776fa9a | ||
|
|
9787a2446e | ||
|
|
4cb29d274b | ||
|
|
df55d63f99 | ||
|
|
236e2d48c5 | ||
|
|
30d45d834f | ||
|
|
edf30897f2 | ||
|
|
3d31ebb576 | ||
|
|
d3bac8bcc0 | ||
|
|
a360f80cdf | ||
|
|
0cc7549546 | ||
|
|
283d2743e0 | ||
|
|
b431fa11fa | ||
|
|
648e00867f | ||
|
|
552af7bb6b | ||
|
|
92980f7c79 | ||
|
|
09a563bf73 | ||
|
|
141fa12a20 | ||
|
|
6e0035d4f3 | ||
|
|
97bac4132c | ||
|
|
b23b0280cd | ||
|
|
7ac093a8d0 | ||
|
|
dfc524b957 | ||
|
|
65ba0d348b | ||
|
|
ed07031539 | ||
|
|
93f3690344 | ||
|
|
1341d1356a | ||
|
|
38dcf16c03 | ||
|
|
8696a42959 | ||
|
|
c6fc7db1e9 | ||
|
|
58540aca57 | ||
|
|
b7b75279c2 |
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@@ -1,8 +1,8 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
github: [CompassConnections] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
patreon: CompassMeet # Replace with a single Patreon username
|
patreon: CompassMeet # Replace with a single Patreon username
|
||||||
open_collective: # Replace with a single Open Collective username
|
open_collective: compass-connection # Replace with a single Open Collective username
|
||||||
ko_fi: compassconnections # Replace with a single Ko-fi 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
|
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
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -68,6 +68,7 @@ email-preview
|
|||||||
*.jpeg
|
*.jpeg
|
||||||
*.gif
|
*.gif
|
||||||
*.svg
|
*.svg
|
||||||
|
*.ico
|
||||||
*.mp4
|
*.mp4
|
||||||
*.mov
|
*.mov
|
||||||
*.avi
|
*.avi
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
[](https://github.com/CompassConnections/Compass/actions/workflows/ci.yml)
|
[](https://github.com/CompassConnections/Compass/actions/workflows/ci.yml)
|
||||||
[](https://github.com/CompassConnections/Compass/actions/workflows/cd.yml)
|
[](https://github.com/CompassConnections/Compass/actions/workflows/cd.yml)
|
||||||

|

|
||||||
|
|
||||||
# Compass
|
# Compass
|
||||||
|
|
||||||
@@ -31,8 +31,8 @@ No contribution is too small—whether it’s changing a color, resizing a butto
|
|||||||
|
|
||||||
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.
|
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:
|
To have edit access to the ClickUp workspace, you need an admin to manually give you permission (one time thing). To do so, use your preferred option:
|
||||||
- Ask or DM an admin on Discord
|
- Ask or DM an admin on [Discord](https://discord.gg/8Vd7jzqjun)
|
||||||
- Email hello@compassmeet.com
|
- Email hello@compassmeet.com
|
||||||
- Raise an issue on GitHub
|
- Raise an issue on GitHub
|
||||||
|
|
||||||
@@ -66,9 +66,9 @@ Everything is open to anyone for collaboration, but the following ones are parti
|
|||||||
|
|
||||||
- [x] Clean up learn more page
|
- [x] Clean up learn more page
|
||||||
- [x] Add dark theme
|
- [x] Add dark theme
|
||||||
- [ ] Add profile features (intellectual interests, cause areas, personality type, conflict style, etc.)
|
- [ ] Add profile fields (intellectual interests, cause areas, personality type, conflict style, timezone, etc.)
|
||||||
- [ ] Add filters to search through remaining profile features (politics, religion, education level, etc.)
|
- [ ] Add filters to search through remaining profile fields (politics, religion, education level, etc.)
|
||||||
- [ ] Cover with tests (very important, just the test template and framework are ready)
|
- [ ] Cover with tests (crucial, just the test template and framework are ready)
|
||||||
- [ ] Make the app more user-friendly and appealing (UI/UX)
|
- [ ] Make the app more user-friendly and appealing (UI/UX)
|
||||||
- [ ] Clean up terms and conditions (convert to Markdown)
|
- [ ] Clean up terms and conditions (convert to Markdown)
|
||||||
- [ ] Clean up privacy notice (convert to Markdown)
|
- [ ] Clean up privacy notice (convert to Markdown)
|
||||||
@@ -162,7 +162,7 @@ You can also add `console.log()` statements in the code.
|
|||||||
|
|
||||||
If you are new to Typescript or the open-source space, you could start with small changes, such as tweaking some web components or improving wording in some pages. You can find those files in `web/public/md/`.
|
If you are new to Typescript or the open-source space, you could start with small changes, such as tweaking some web components or improving wording in some pages. You can find those files in `web/public/md/`.
|
||||||
|
|
||||||
See [development.md](docs/development.md) for additional instructions, such as adding new profile features.
|
See [development.md](docs/development.md) for additional instructions, such as adding new profile fields.
|
||||||
|
|
||||||
### Submission
|
### Submission
|
||||||
|
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
Contact the development team at compass.meet.info@gmail.com to report a vulnerability. You should receive updates within a week.
|
Contact the development team at hello@compassmeet.com to report a vulnerability. You should receive updates within a week.
|
||||||
|
|
||||||
|
|||||||
@@ -161,5 +161,4 @@ docker rmi -f $(docker images -aq)
|
|||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
The API docs are available at https://api.compassmeet.com. They are defined in [openapi.json](openapi.json).
|
The API doc is available at https://api.compassmeet.com. It's dynamically prepared in [app.ts](src/app.ts).
|
||||||
Just a few endpoints are mentioned in that JSON doc. Feel free to help by adding the remaining ones!
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,16 +4,18 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"watch:serve": "tsx watch src/serve.ts",
|
||||||
"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\"",
|
"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\"",
|
||||||
"watch:serve": "nodemon -r tsconfig-paths/register --watch lib --ignore 'lib/**/*.map' src/serve.ts",
|
"dev": "yarn watch:serve",
|
||||||
"dev": "npx concurrently -n COMPILE,SERVER -c cyan,green \"yarn watch:compile\" \"yarn watch:serve\"",
|
"prod": "npx concurrently -n COMPILE,SERVER -c cyan,green \"yarn watch:compile\" \"yarn watch:serve\"",
|
||||||
"build": "yarn compile && yarn dist:clean && yarn dist:copy",
|
"build": "yarn compile && yarn dist:clean && yarn dist:copy",
|
||||||
"build:fast": "yarn compile && yarn dist:copy",
|
"build:fast": "yarn compile && yarn dist:copy",
|
||||||
|
"clean": "rm -rf lib && (cd ../../common && rm -rf lib) && (cd ../shared && rm -rf lib) && (cd ../email && rm -rf lib)",
|
||||||
"compile": "tsc -b && tsc-alias && (cd ../../common && tsc-alias) && (cd ../shared && tsc-alias) && (cd ../email && tsc-alias)",
|
"compile": "tsc -b && tsc-alias && (cd ../../common && tsc-alias) && (cd ../shared && tsc-alias) && (cd ../email && tsc-alias)",
|
||||||
"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\"",
|
"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": "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: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 && cp openapi.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 package.json dist/backend/api",
|
||||||
"watch": "tsc -w",
|
"watch": "tsc -w",
|
||||||
"verify": "yarn --cwd=../.. verify",
|
"verify": "yarn --cwd=../.. verify",
|
||||||
"verify:dir": "npx eslint . --max-warnings 0",
|
"verify:dir": "npx eslint . --max-warnings 0",
|
||||||
@@ -44,29 +46,32 @@
|
|||||||
"colors": "1.4.0",
|
"colors": "1.4.0",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"dayjs": "1.11.4",
|
"dayjs": "1.11.4",
|
||||||
"express": "4.18.1",
|
"express": "5.0.0",
|
||||||
"firebase-admin": "13.5.0",
|
"firebase-admin": "13.5.0",
|
||||||
"gcp-metadata": "6.1.0",
|
"gcp-metadata": "6.1.0",
|
||||||
"jsonwebtoken": "9.0.0",
|
"jsonwebtoken": "9.0.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
"openapi-types": "12.1.3",
|
||||||
"pg-promise": "11.4.1",
|
"pg-promise": "11.4.1",
|
||||||
"posthog-node": "4.11.0",
|
"posthog-node": "4.11.0",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
"resend": "4.1.2",
|
"resend": "4.1.2",
|
||||||
"string-similarity": "4.0.4",
|
"string-similarity": "4.0.4",
|
||||||
"swagger-jsdoc": "6.2.8",
|
"swagger-jsdoc": "6.2.8",
|
||||||
"swagger-ui-express": "5.0.1",
|
"swagger-ui-express": "5.0.1",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"twitter-api-v2": "1.15.0",
|
"twitter-api-v2": "1.15.0",
|
||||||
"ws": "8.17.0",
|
"web-push": "3.6.7",
|
||||||
"react": "18.2.0",
|
"ws": "8.17.1",
|
||||||
"react-dom": "18.2.0",
|
"zod": "3.22.3"
|
||||||
"zod": "3.21.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "2.8.17",
|
"@types/cors": "2.8.17",
|
||||||
"@types/react": "18.3.5",
|
"@types/react": "18.3.5",
|
||||||
"@types/react-dom": "18.3.0",
|
"@types/react-dom": "18.3.0",
|
||||||
"@types/swagger-ui-express": "4.1.8",
|
"@types/swagger-ui-express": "4.1.8",
|
||||||
|
"@types/web-push": "3.6.4",
|
||||||
"@types/ws": "8.5.10"
|
"@types/ws": "8.5.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {blockUser, unblockUser} from './block-user'
|
|||||||
import {getCompatibleProfilesHandler} from './compatible-profiles'
|
import {getCompatibleProfilesHandler} from './compatible-profiles'
|
||||||
import {createComment} from './create-comment'
|
import {createComment} from './create-comment'
|
||||||
import {createCompatibilityQuestion} from './create-compatibility-question'
|
import {createCompatibilityQuestion} from './create-compatibility-question'
|
||||||
|
import {setCompatibilityAnswer} from './set-compatibility-answer'
|
||||||
import {createProfile} from './create-profile'
|
import {createProfile} from './create-profile'
|
||||||
import {createUser} from './create-user'
|
import {createUser} from './create-user'
|
||||||
import {getCompatibilityQuestions} from './get-compatibililty-questions'
|
import {getCompatibilityQuestions} from './get-compatibililty-questions'
|
||||||
@@ -19,7 +20,6 @@ import {getLikesAndShips} from './get-likes-and-ships'
|
|||||||
import {getProfileAnswers} from './get-profile-answers'
|
import {getProfileAnswers} from './get-profile-answers'
|
||||||
import {getProfiles} from './get-profiles'
|
import {getProfiles} from './get-profiles'
|
||||||
import {getSupabaseToken} from './get-supabase-token'
|
import {getSupabaseToken} from './get-supabase-token'
|
||||||
import {getDisplayUser, getUser} from './get-user'
|
|
||||||
import {getMe} from './get-me'
|
import {getMe} from './get-me'
|
||||||
import {hasFreeLike} from './has-free-like'
|
import {hasFreeLike} from './has-free-like'
|
||||||
import {health} from './health'
|
import {health} from './health'
|
||||||
@@ -40,7 +40,7 @@ import {getCurrentPrivateUser} from './get-current-private-user'
|
|||||||
import {createPrivateUserMessage} from './create-private-user-message'
|
import {createPrivateUserMessage} from './create-private-user-message'
|
||||||
import {
|
import {
|
||||||
getChannelMemberships,
|
getChannelMemberships,
|
||||||
getChannelMessages,
|
getChannelMessagesEndpoint,
|
||||||
getLastSeenChannelTime,
|
getLastSeenChannelTime,
|
||||||
setChannelLastSeenTime,
|
setChannelLastSeenTime,
|
||||||
} from 'api/get-private-messages'
|
} from 'api/get-private-messages'
|
||||||
@@ -52,14 +52,26 @@ import {getNotifications} from './get-notifications'
|
|||||||
import {updateNotifSettings} from './update-notif-setting'
|
import {updateNotifSettings} from './update-notif-setting'
|
||||||
import {setLastOnlineTime} from './set-last-online-time'
|
import {setLastOnlineTime} from './set-last-online-time'
|
||||||
import swaggerUi from "swagger-ui-express"
|
import swaggerUi from "swagger-ui-express"
|
||||||
import * as fs from "fs"
|
|
||||||
import {sendSearchNotifications} from "api/send-search-notifications";
|
import {sendSearchNotifications} from "api/send-search-notifications";
|
||||||
import {sendDiscordMessage} from "common/discord/core";
|
import {sendDiscordMessage} from "common/discord/core";
|
||||||
import {getMessagesCount} from "api/get-messages-count";
|
import {getMessagesCount} from "api/get-messages-count";
|
||||||
import {createVote} from "api/create-vote";
|
import {createVote} from "api/create-vote";
|
||||||
import {vote} from "api/vote";
|
import {vote} from "api/vote";
|
||||||
import {contact} from "api/contact";
|
import {contact} from "api/contact";
|
||||||
|
import {saveSubscription} from "api/save-subscription";
|
||||||
|
import {createBookmarkedSearch} from './create-bookmarked-search'
|
||||||
|
import {deleteBookmarkedSearch} from './delete-bookmarked-search'
|
||||||
|
import {OpenAPIV3} from 'openapi-types';
|
||||||
|
import {version as pkgVersion} from './../package.json'
|
||||||
|
import {z, ZodFirstPartyTypeKind, ZodTypeAny} from "zod";
|
||||||
|
import {getUser} from "api/get-user";
|
||||||
|
|
||||||
|
// const corsOptions: CorsOptions = {
|
||||||
|
// origin: ['*'], // Only allow requests from this domain
|
||||||
|
// methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||||
|
// allowedHeaders: ['Content-Type', 'Authorization'],
|
||||||
|
// credentials: true, // if you use cookies or auth headers
|
||||||
|
// };
|
||||||
const allowCorsUnrestricted: RequestHandler = cors({})
|
const allowCorsUnrestricted: RequestHandler = cors({})
|
||||||
|
|
||||||
function cacheController(policy?: string): RequestHandler {
|
function cacheController(policy?: string): RequestHandler {
|
||||||
@@ -107,33 +119,200 @@ const apiErrorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
|
|||||||
export const app = express()
|
export const app = express()
|
||||||
app.use(requestMonitoring)
|
app.use(requestMonitoring)
|
||||||
|
|
||||||
const swaggerDocument = JSON.parse(fs.readFileSync("./openapi.json", "utf-8"))
|
const schemaCache = new WeakMap<ZodTypeAny, any>();
|
||||||
swaggerDocument.info = {
|
|
||||||
...swaggerDocument.info,
|
export function zodToOpenApiSchema(
|
||||||
description: "Compass is a free, open-source platform to help people form deep, meaningful, and lasting connections — whether platonic, romantic, or collaborative. It’s 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.",
|
zodObj: ZodTypeAny,
|
||||||
version: "1.0.0",
|
nameHint?: string
|
||||||
contact: {
|
): any { // Prevent infinite recursion
|
||||||
name: "Compass",
|
if (schemaCache.has(zodObj)) {
|
||||||
email: "hello@compassmeet.com",
|
return schemaCache.get(zodObj);
|
||||||
url: "https://compassmeet.com"
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
const def: any = (zodObj as any)._def;
|
||||||
|
const typeName = def.typeName as ZodFirstPartyTypeKind;
|
||||||
|
|
||||||
|
// Placeholder so recursive references can point here
|
||||||
|
const placeholder: any = {};
|
||||||
|
schemaCache.set(zodObj, placeholder);
|
||||||
|
|
||||||
|
let schema: any;
|
||||||
|
|
||||||
|
switch (typeName) {
|
||||||
|
case 'ZodString':
|
||||||
|
schema = { type: 'string' };
|
||||||
|
break;
|
||||||
|
case 'ZodNumber':
|
||||||
|
schema = { type: 'number' };
|
||||||
|
break;
|
||||||
|
case 'ZodBoolean':
|
||||||
|
schema = { type: 'boolean' };
|
||||||
|
break;
|
||||||
|
case 'ZodEnum':
|
||||||
|
schema = { type: 'string', enum: def.values };
|
||||||
|
break;
|
||||||
|
case 'ZodArray':
|
||||||
|
schema = { type: 'array', items: zodToOpenApiSchema(def.type) };
|
||||||
|
break;
|
||||||
|
case 'ZodObject': {
|
||||||
|
const shape = def.shape();
|
||||||
|
const properties: Record<string, any> = {};
|
||||||
|
const required: string[] = [];
|
||||||
|
|
||||||
|
for (const key in shape) {
|
||||||
|
const child = shape[key];
|
||||||
|
properties[key] = zodToOpenApiSchema(child, key);
|
||||||
|
if (!child.isOptional()) required.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties,
|
||||||
|
...(required.length ? { required } : {}),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ZodRecord':
|
||||||
|
schema = {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: zodToOpenApiSchema(def.valueType),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'ZodIntersection': {
|
||||||
|
const left = zodToOpenApiSchema(def.left);
|
||||||
|
const right = zodToOpenApiSchema(def.right);
|
||||||
|
schema = { allOf: [left, right] };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ZodLazy':
|
||||||
|
// Recursive schema: use a $ref placeholder name
|
||||||
|
schema = {
|
||||||
|
$ref: `#/components/schemas/${nameHint ?? 'RecursiveType'}`,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'ZodUnion':
|
||||||
|
schema = {
|
||||||
|
oneOf: def.options.map((opt: ZodTypeAny) => zodToOpenApiSchema(opt)),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
schema = { type: 'string' }; // fallback for unhandled
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(placeholder, schema);
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSwaggerPaths(api: typeof API) {
|
||||||
|
const paths: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const [route, config] of Object.entries(api)) {
|
||||||
|
const pathKey = '/' + route.replace(/_/g, '-'); // optional: convert underscores to dashes
|
||||||
|
const method = config.method.toLowerCase();
|
||||||
|
const summary = (config as any).summary ?? route;
|
||||||
|
|
||||||
|
// Include props in request body for POST/PUT
|
||||||
|
const operation: any = {
|
||||||
|
summary,
|
||||||
|
tags: [(config as any).tag ?? 'API'],
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'OK',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {type: 'object'}, // could be improved by introspecting returns
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Include props in request body for POST/PUT
|
||||||
|
if (config.props && ['post', 'put', 'patch'].includes(method)) {
|
||||||
|
operation.requestBody = {
|
||||||
|
required: true,
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: zodToOpenApiSchema(config.props),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include props as query parameters for GET/DELETE
|
||||||
|
if (config.props && ['get', 'delete'].includes(method)) {
|
||||||
|
const shape = (config.props as z.ZodObject<any>)._def.shape();
|
||||||
|
operation.parameters = Object.entries(shape).map(([key, zodType]) => {
|
||||||
|
const typeMap: Record<string, string> = {
|
||||||
|
ZodString: 'string',
|
||||||
|
ZodNumber: 'number',
|
||||||
|
ZodBoolean: 'boolean',
|
||||||
|
};
|
||||||
|
const t = zodType as z.ZodTypeAny; // assert type to ZodTypeAny
|
||||||
|
return {
|
||||||
|
name: key,
|
||||||
|
in: 'query',
|
||||||
|
required: !(t.isOptional ?? false),
|
||||||
|
schema: {type: typeMap[t._def.typeName] ?? 'string'},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
paths[pathKey] = {
|
||||||
|
[method]: operation,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.authed) {
|
||||||
|
operation.security = [{BearerAuth: []}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const swaggerDocument: OpenAPIV3.Document = {
|
||||||
|
openapi: "3.0.0",
|
||||||
|
info: {
|
||||||
|
title: "Compass API",
|
||||||
|
description: "Compass is a free, open-source platform to help people form deep, meaningful, and lasting connections — whether platonic, romantic, or collaborative. It’s 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: pkgVersion,
|
||||||
|
contact: {
|
||||||
|
name: "Compass",
|
||||||
|
email: "hello@compassmeet.com",
|
||||||
|
url: "https://compassmeet.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
paths: generateSwaggerPaths(API),
|
||||||
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
BearerAuth: {
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'bearer',
|
||||||
|
bearerFormat: 'JWT',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} as OpenAPIV3.Document;
|
||||||
|
|
||||||
|
|
||||||
const rootPath = pathWithPrefix("/")
|
const rootPath = pathWithPrefix("/")
|
||||||
app.get(rootPath, swaggerUi.setup(swaggerDocument))
|
app.get(rootPath, swaggerUi.setup(swaggerDocument))
|
||||||
app.use(rootPath, swaggerUi.serve)
|
app.use(rootPath, swaggerUi.serve)
|
||||||
|
|
||||||
app.options('*', allowCorsUnrestricted)
|
// Triggers Missing parameter name at index 3: *; visit https://git.new/pathToRegexpError for info
|
||||||
|
// May not be necessary
|
||||||
|
// app.options('*', allowCorsUnrestricted)
|
||||||
|
|
||||||
const handlers: { [k in APIPath]: APIHandler<k> } = {
|
const handlers: { [k in APIPath]: APIHandler<k> } = {
|
||||||
health: health,
|
health: health,
|
||||||
'get-supabase-token': getSupabaseToken,
|
'get-supabase-token': getSupabaseToken,
|
||||||
'get-notifications': getNotifications,
|
'get-notifications': getNotifications,
|
||||||
'mark-all-notifs-read': markAllNotifsRead,
|
'mark-all-notifs-read': markAllNotifsRead,
|
||||||
'user/:username': getUser,
|
// 'user/:username': getUser,
|
||||||
'user/:username/lite': getDisplayUser,
|
// 'user/:username/lite': getDisplayUser,
|
||||||
'user/by-id/:id': getUser,
|
'user/by-id/:id': getUser,
|
||||||
'user/by-id/:id/lite': getDisplayUser,
|
// 'user/by-id/:id/lite': getDisplayUser,
|
||||||
'user/by-id/:id/block': blockUser,
|
'user/by-id/:id/block': blockUser,
|
||||||
'user/by-id/:id/unblock': unblockUser,
|
'user/by-id/:id/unblock': unblockUser,
|
||||||
'search-users': searchUsers,
|
'search-users': searchUsers,
|
||||||
@@ -159,6 +338,7 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
|
|||||||
'create-comment': createComment,
|
'create-comment': createComment,
|
||||||
'hide-comment': hideComment,
|
'hide-comment': hideComment,
|
||||||
'create-compatibility-question': createCompatibilityQuestion,
|
'create-compatibility-question': createCompatibilityQuestion,
|
||||||
|
'set-compatibility-answer': setCompatibilityAnswer,
|
||||||
'create-vote': createVote,
|
'create-vote': createVote,
|
||||||
'vote': vote,
|
'vote': vote,
|
||||||
'contact': contact,
|
'contact': contact,
|
||||||
@@ -170,11 +350,14 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
|
|||||||
'update-private-user-message-channel': updatePrivateUserMessageChannel,
|
'update-private-user-message-channel': updatePrivateUserMessageChannel,
|
||||||
'leave-private-user-message-channel': leavePrivateUserMessageChannel,
|
'leave-private-user-message-channel': leavePrivateUserMessageChannel,
|
||||||
'get-channel-memberships': getChannelMemberships,
|
'get-channel-memberships': getChannelMemberships,
|
||||||
'get-channel-messages': getChannelMessages,
|
'get-channel-messages': getChannelMessagesEndpoint,
|
||||||
'get-channel-seen-time': getLastSeenChannelTime,
|
'get-channel-seen-time': getLastSeenChannelTime,
|
||||||
'set-channel-seen-time': setChannelLastSeenTime,
|
'set-channel-seen-time': setChannelLastSeenTime,
|
||||||
'get-messages-count': getMessagesCount,
|
'get-messages-count': getMessagesCount,
|
||||||
'set-last-online-time': setLastOnlineTime,
|
'set-last-online-time': setLastOnlineTime,
|
||||||
|
'save-subscription': saveSubscription,
|
||||||
|
'create-bookmarked-search': createBookmarkedSearch,
|
||||||
|
'delete-bookmarked-search': deleteBookmarkedSearch,
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.entries(handlers).forEach(([path, handler]) => {
|
Object.entries(handlers).forEach(([path, handler]) => {
|
||||||
@@ -202,8 +385,6 @@ Object.entries(handlers).forEach(([path, handler]) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// console.debug('COMPASS_API_KEY:', process.env.COMPASS_API_KEY)
|
|
||||||
|
|
||||||
// Internal Endpoints
|
// Internal Endpoints
|
||||||
app.post(pathWithPrefix("/internal/send-search-notifications"),
|
app.post(pathWithPrefix("/internal/send-search-notifications"),
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { groupBy, sortBy } from 'lodash'
|
import { groupBy, sortBy } from 'lodash'
|
||||||
import { APIError, type APIHandler } from 'api/helpers/endpoint'
|
import { APIError, type APIHandler } from 'api/helpers/endpoint'
|
||||||
import { getCompatibilityScore } from 'common/love/compatibility-score'
|
import { getCompatibilityScore } from 'common/profiles/compatibility-score'
|
||||||
import {
|
import {
|
||||||
getProfile,
|
getProfile,
|
||||||
getCompatibilityAnswers,
|
getCompatibilityAnswers,
|
||||||
getGenderCompatibleProfiles,
|
getGenderCompatibleProfiles,
|
||||||
} from 'shared/love/supabase'
|
} from 'shared/profiles/supabase'
|
||||||
import { log } from 'shared/utils'
|
import { log } from 'shared/utils'
|
||||||
|
|
||||||
export const getCompatibleProfilesHandler: APIHandler<
|
export const getCompatibleProfilesHandler: APIHandler<
|
||||||
|
|||||||
23
backend/api/src/create-bookmarked-search.ts
Normal file
23
backend/api/src/create-bookmarked-search.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import {APIHandler} from './helpers/endpoint'
|
||||||
|
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||||
|
|
||||||
|
export const createBookmarkedSearch: APIHandler<'create-bookmarked-search'> = async (
|
||||||
|
props,
|
||||||
|
auth
|
||||||
|
) => {
|
||||||
|
const creator_id = auth.uid
|
||||||
|
const {search_filters, location = null, search_name = null} = props
|
||||||
|
|
||||||
|
const pg = createSupabaseDirectClient()
|
||||||
|
|
||||||
|
const inserted = await pg.one(
|
||||||
|
`
|
||||||
|
INSERT INTO bookmarked_searches (creator_id, search_filters, location, search_name)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
[creator_id, search_filters, location, search_name]
|
||||||
|
)
|
||||||
|
|
||||||
|
return inserted
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ export const createCompatibilityQuestion: APIHandler<
|
|||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
|
|
||||||
const { data, error } = await tryCatch(
|
const { data, error } = await tryCatch(
|
||||||
insert(pg, 'love_questions', {
|
insert(pg, 'compatibility_prompts', {
|
||||||
creator_id: creator.id,
|
creator_id: creator.id,
|
||||||
question,
|
question,
|
||||||
answer_type: 'compatibility_multiple_choice',
|
answer_type: 'compatibility_multiple_choice',
|
||||||
|
|||||||
76
backend/api/src/create-notification.ts
Normal file
76
backend/api/src/create-notification.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import {createSupabaseDirectClient, SupabaseDirectClient} from 'shared/supabase/init'
|
||||||
|
import {Notification} from 'common/notifications'
|
||||||
|
import {insertNotificationToSupabase} from 'shared/supabase/notifications'
|
||||||
|
import {tryCatch} from "common/util/try-catch";
|
||||||
|
import {Row} from "common/supabase/utils";
|
||||||
|
|
||||||
|
export const createShareNotifications = async () => {
|
||||||
|
const createdTime = Date.now();
|
||||||
|
const id = `share-${createdTime}`
|
||||||
|
const notification: Notification = {
|
||||||
|
id,
|
||||||
|
userId: 'todo',
|
||||||
|
createdTime: createdTime,
|
||||||
|
isSeen: false,
|
||||||
|
sourceType: 'info',
|
||||||
|
sourceUpdateType: 'created',
|
||||||
|
sourceSlug: '/contact',
|
||||||
|
sourceUserAvatarUrl: 'https://firebasestorage.googleapis.com/v0/b/compass-130ba.firebasestorage.app/o/misc%2Ficon-outreach-outstrip-outreach-272151502.jpg?alt=media&token=6d6fcecb-818c-4fca-a8e0-d2d0069b9445',
|
||||||
|
title: 'Give us tips to reach more people',
|
||||||
|
sourceText: '250 members already! Tell us where and how we can best share Compass.',
|
||||||
|
}
|
||||||
|
return await createNotifications(notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createVoteNotifications = async () => {
|
||||||
|
const createdTime = Date.now();
|
||||||
|
const id = `vote-${createdTime}`
|
||||||
|
const notification: Notification = {
|
||||||
|
id,
|
||||||
|
userId: 'todo',
|
||||||
|
createdTime: createdTime,
|
||||||
|
isSeen: false,
|
||||||
|
sourceType: 'info',
|
||||||
|
sourceUpdateType: 'created',
|
||||||
|
sourceSlug: '/vote',
|
||||||
|
sourceUserAvatarUrl: 'https://firebasestorage.googleapis.com/v0/b/compass-130ba.firebasestorage.app/o/misc%2Fvote-icon-design-free-vector.jpg?alt=media&token=f70b6d14-0511-49b2-830d-e7cabf7bb751',
|
||||||
|
title: 'New Proposals & Votes Page',
|
||||||
|
sourceText: 'Create proposals and vote on other people\'s suggestions!',
|
||||||
|
}
|
||||||
|
return await createNotifications(notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createNotifications = async (notification: Notification) => {
|
||||||
|
const pg = createSupabaseDirectClient()
|
||||||
|
const {data: users, error} = await tryCatch(
|
||||||
|
pg.many<Row<'users'>>('select * from users')
|
||||||
|
)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error fetching users', error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!users) {
|
||||||
|
console.error('No users found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
try {
|
||||||
|
await createNotification(user, notification, pg)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to create notification', e, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createNotification = async (user: Row<'users'>, notification: Notification, pg: SupabaseDirectClient) => {
|
||||||
|
notification.userId = user.id
|
||||||
|
console.log('notification', user.username)
|
||||||
|
return await insertNotificationToSupabase(notification, pg)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { APIError, APIHandler } from 'api/helpers/endpoint'
|
|||||||
import { filterDefined } from 'common/util/array'
|
import { filterDefined } from 'common/util/array'
|
||||||
import { uniq } from 'lodash'
|
import { uniq } from 'lodash'
|
||||||
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
||||||
import { addUsersToPrivateMessageChannel } from 'api/junk-drawer/private-messages'
|
import { addUsersToPrivateMessageChannel } from 'api/helpers/private-messages'
|
||||||
import { getPrivateUser, getUser } from 'shared/utils'
|
import { getPrivateUser, getUser } from 'shared/utils'
|
||||||
|
|
||||||
export const createPrivateUserMessageChannel: APIHandler<
|
export const createPrivateUserMessageChannel: APIHandler<
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
import { APIError, APIHandler } from 'api/helpers/endpoint'
|
import {APIError, APIHandler} from 'api/helpers/endpoint'
|
||||||
import { getUser } from 'shared/utils'
|
import {getUser} from 'shared/utils'
|
||||||
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||||
import { MAX_COMMENT_JSON_LENGTH } from 'api/create-comment'
|
import {MAX_COMMENT_JSON_LENGTH} from 'api/create-comment'
|
||||||
import { createPrivateUserMessageMain } from 'api/junk-drawer/private-messages'
|
import {createPrivateUserMessageMain} from 'api/helpers/private-messages'
|
||||||
|
|
||||||
export const createPrivateUserMessage: APIHandler<
|
export const createPrivateUserMessage: APIHandler<
|
||||||
'create-private-user-message'
|
'create-private-user-message'
|
||||||
> = async (body, auth) => {
|
> = async (body, auth) => {
|
||||||
const { content, channelId } = body
|
const {content, channelId} = body
|
||||||
if (JSON.stringify(content).length > MAX_COMMENT_JSON_LENGTH) {
|
if (JSON.stringify(content).length > MAX_COMMENT_JSON_LENGTH) {
|
||||||
throw new APIError(
|
throw new APIError(
|
||||||
400,
|
400,
|
||||||
`Message JSON should be less than ${MAX_COMMENT_JSON_LENGTH}`
|
`Message JSON should be less than ${MAX_COMMENT_JSON_LENGTH}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const pg = createSupabaseDirectClient()
|
|
||||||
const creator = await getUser(auth.uid)
|
const creator = await getUser(auth.uid)
|
||||||
if (!creator) throw new APIError(401, 'Your account was not found')
|
if (!creator) throw new APIError(401, 'Your account was not found')
|
||||||
if (creator.isBannedFromPosting) throw new APIError(403, 'You are banned')
|
if (creator.isBannedFromPosting) throw new APIError(403, 'You are banned')
|
||||||
|
|
||||||
|
const pg = createSupabaseDirectClient()
|
||||||
return await createPrivateUserMessageMain(
|
return await createPrivateUserMessageMain(
|
||||||
creator,
|
creator,
|
||||||
channelId,
|
channelId,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { APIError, APIHandler } from 'api/helpers/endpoint'
|
|||||||
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
||||||
import { log, getUser } from 'shared/utils'
|
import { log, getUser } from 'shared/utils'
|
||||||
import { HOUR_MS } from 'common/util/time'
|
import { HOUR_MS } from 'common/util/time'
|
||||||
import { removePinnedUrlFromPhotoUrls } from 'shared/love/parse-photos'
|
import { removePinnedUrlFromPhotoUrls } from 'shared/profiles/parse-photos'
|
||||||
import { track } from 'shared/analytics'
|
import { track } from 'shared/analytics'
|
||||||
import { updateUser } from 'shared/supabase/users'
|
import { updateUser } from 'shared/supabase/users'
|
||||||
import { tryCatch } from 'common/util/try-catch'
|
import { tryCatch } from 'common/util/try-catch'
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {insert} from 'shared/supabase/utils'
|
|||||||
import {convertPrivateUser, convertUser} from 'common/supabase/users'
|
import {convertPrivateUser, convertUser} from 'common/supabase/users'
|
||||||
import {getBucket} from "shared/firebase-utils";
|
import {getBucket} from "shared/firebase-utils";
|
||||||
import {sendWelcomeEmail} from "email/functions/helpers";
|
import {sendWelcomeEmail} from "email/functions/helpers";
|
||||||
|
import {setLastOnlineTimeUser} from "api/set-last-online-time";
|
||||||
|
|
||||||
export const createUser: APIHandler<'create-user'> = async (
|
export const createUser: APIHandler<'create-user'> = async (
|
||||||
props,
|
props,
|
||||||
@@ -134,6 +135,11 @@ export const createUser: APIHandler<'create-user'> = async (
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to sendWelcomeEmail', e)
|
console.error('Failed to sendWelcomeEmail', e)
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
await setLastOnlineTimeUser(auth.uid)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to set last online time', e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
23
backend/api/src/delete-bookmarked-search.ts
Normal file
23
backend/api/src/delete-bookmarked-search.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import {APIHandler} from './helpers/endpoint'
|
||||||
|
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||||
|
|
||||||
|
export const deleteBookmarkedSearch: APIHandler<'delete-bookmarked-search'> = async (
|
||||||
|
props,
|
||||||
|
auth
|
||||||
|
) => {
|
||||||
|
const creator_id = auth.uid
|
||||||
|
const {id} = props
|
||||||
|
|
||||||
|
const pg = createSupabaseDirectClient()
|
||||||
|
|
||||||
|
// Only allow deleting your own bookmarked searches
|
||||||
|
await pg.none(
|
||||||
|
`
|
||||||
|
DELETE FROM bookmarked_searches
|
||||||
|
WHERE id = $1 AND creator_id = $2
|
||||||
|
`,
|
||||||
|
[id, creator_id]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {}
|
||||||
|
}
|
||||||
@@ -24,10 +24,11 @@ export const deleteMe: APIHandler<'me/delete'> = async (body, auth) => {
|
|||||||
// Remove user data from Supabase
|
// Remove user data from Supabase
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
await pg.none('DELETE FROM users WHERE id = $1', [userId])
|
await pg.none('DELETE FROM users WHERE id = $1', [userId])
|
||||||
await pg.none('DELETE FROM private_users WHERE id = $1', [userId])
|
// Should cascade delete in other tables
|
||||||
await pg.none('DELETE FROM profiles WHERE user_id = $1', [userId])
|
// await pg.none('DELETE FROM private_users WHERE id = $1', [userId])
|
||||||
await pg.none('DELETE FROM bookmarked_searches WHERE creator_id = $1', [userId])
|
// await pg.none('DELETE FROM profiles WHERE user_id = $1', [userId])
|
||||||
await pg.none('DELETE FROM love_compatibility_answers WHERE creator_id = $1', [userId])
|
// await pg.none('DELETE FROM bookmarked_searches WHERE creator_id = $1', [userId])
|
||||||
|
// await pg.none('DELETE FROM compatibility_answers WHERE creator_id = $1', [userId])
|
||||||
// May need to also delete from other tables in the future (such as messages, compatibility responses, etc.)
|
// May need to also delete from other tables in the future (such as messages, compatibility responses, etc.)
|
||||||
|
|
||||||
// Delete user files from Firebase Storage
|
// Delete user files from Firebase Storage
|
||||||
|
|||||||
@@ -17,22 +17,22 @@ export const getCompatibilityQuestions: APIHandler<
|
|||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
|
|
||||||
const questions = await pg.manyOrNone<
|
const questions = await pg.manyOrNone<
|
||||||
Row<'love_questions'> & { answer_count: number; score: number }
|
Row<'compatibility_prompts'> & { answer_count: number; score: number }
|
||||||
>(
|
>(
|
||||||
`SELECT
|
`SELECT
|
||||||
love_questions.*,
|
compatibility_prompts.*,
|
||||||
COUNT(love_compatibility_answers.question_id) as answer_count,
|
COUNT(compatibility_answers.question_id) as answer_count,
|
||||||
AVG(POWER(love_compatibility_answers.importance + 1 + CASE WHEN love_compatibility_answers.explanation IS NULL THEN 1 ELSE 0 END, 2)) as score
|
AVG(POWER(compatibility_answers.importance + 1 + CASE WHEN compatibility_answers.explanation IS NULL THEN 1 ELSE 0 END, 2)) as score
|
||||||
FROM
|
FROM
|
||||||
love_questions
|
compatibility_prompts
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
love_compatibility_answers ON love_questions.id = love_compatibility_answers.question_id
|
compatibility_answers ON compatibility_prompts.id = compatibility_answers.question_id
|
||||||
WHERE
|
WHERE
|
||||||
love_questions.answer_type = 'compatibility_multiple_choice'
|
compatibility_prompts.answer_type = 'compatibility_multiple_choice'
|
||||||
GROUP BY
|
GROUP BY
|
||||||
love_questions.id
|
compatibility_prompts.id
|
||||||
ORDER BY
|
ORDER BY
|
||||||
love_questions.importance_score
|
compatibility_prompts.importance_score
|
||||||
`,
|
`,
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ export const getLikesAndShipsMain = async (userId: string) => {
|
|||||||
created_time: number
|
created_time: number
|
||||||
}>(
|
}>(
|
||||||
`
|
`
|
||||||
select target_id, love_likes.created_time
|
select target_id, profile_likes.created_time
|
||||||
from love_likes
|
from profile_likes
|
||||||
join profiles on profiles.user_id = love_likes.target_id
|
join profiles on profiles.user_id = profile_likes.target_id
|
||||||
join users on users.id = love_likes.target_id
|
join users on users.id = profile_likes.target_id
|
||||||
where creator_id = $1
|
where creator_id = $1
|
||||||
and looking_for_matches
|
and looking_for_matches
|
||||||
and profiles.pinned_url is not null
|
and profiles.pinned_url is not null
|
||||||
@@ -42,10 +42,10 @@ export const getLikesAndShipsMain = async (userId: string) => {
|
|||||||
created_time: number
|
created_time: number
|
||||||
}>(
|
}>(
|
||||||
`
|
`
|
||||||
select creator_id, love_likes.created_time
|
select creator_id, profile_likes.created_time
|
||||||
from love_likes
|
from profile_likes
|
||||||
join profiles on profiles.user_id = love_likes.creator_id
|
join profiles on profiles.user_id = profile_likes.creator_id
|
||||||
join users on users.id = love_likes.creator_id
|
join users on users.id = profile_likes.creator_id
|
||||||
where target_id = $1
|
where target_id = $1
|
||||||
and looking_for_matches
|
and looking_for_matches
|
||||||
and profiles.pinned_url is not null
|
and profiles.pinned_url is not null
|
||||||
@@ -68,11 +68,11 @@ export const getLikesAndShipsMain = async (userId: string) => {
|
|||||||
}>(
|
}>(
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
target1_id, target2_id, creator_id, love_ships.created_time,
|
target1_id, target2_id, creator_id, profile_ships.created_time,
|
||||||
target1_id as target_id
|
target1_id as target_id
|
||||||
from love_ships
|
from profile_ships
|
||||||
join profiles on profiles.user_id = love_ships.target1_id
|
join profiles on profiles.user_id = profile_ships.target1_id
|
||||||
join users on users.id = love_ships.target1_id
|
join users on users.id = profile_ships.target1_id
|
||||||
where target2_id = $1
|
where target2_id = $1
|
||||||
and profiles.looking_for_matches
|
and profiles.looking_for_matches
|
||||||
and profiles.pinned_url is not null
|
and profiles.pinned_url is not null
|
||||||
@@ -81,11 +81,11 @@ export const getLikesAndShipsMain = async (userId: string) => {
|
|||||||
union all
|
union all
|
||||||
|
|
||||||
select
|
select
|
||||||
target1_id, target2_id, creator_id, love_ships.created_time,
|
target1_id, target2_id, creator_id, profile_ships.created_time,
|
||||||
target2_id as target_id
|
target2_id as target_id
|
||||||
from love_ships
|
from profile_ships
|
||||||
join profiles on profiles.user_id = love_ships.target2_id
|
join profiles on profiles.user_id = profile_ships.target2_id
|
||||||
join users on users.id = love_ships.target2_id
|
join users on users.id = profile_ships.target2_id
|
||||||
where target1_id = $1
|
where target1_id = $1
|
||||||
and profiles.looking_for_matches
|
and profiles.looking_for_matches
|
||||||
and profiles.pinned_url is not null
|
and profiles.pinned_url is not null
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||||
import { APIHandler } from './helpers/endpoint'
|
import {APIError, APIHandler} from './helpers/endpoint'
|
||||||
import {
|
import {PrivateMessageChannel,} from 'common/supabase/private-messages'
|
||||||
convertPrivateChatMessage,
|
import {groupBy, mapValues} from 'lodash'
|
||||||
PrivateMessageChannel,
|
import {convertPrivateChatMessage} from "shared/supabase/messages";
|
||||||
} from 'common/supabase/private-messages'
|
import {tryCatch} from "common/util/try-catch";
|
||||||
import { groupBy, mapValues } from 'lodash'
|
|
||||||
|
|
||||||
export const getChannelMemberships: APIHandler<
|
export const getChannelMemberships: APIHandler<
|
||||||
'get-channel-memberships'
|
'get-channel-memberships'
|
||||||
> = async (props, auth) => {
|
> = async (props, auth) => {
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
const { channelId, lastUpdatedTime, createdTime, limit } = props
|
const {channelId, lastUpdatedTime, createdTime, limit} = props
|
||||||
|
|
||||||
let channels: PrivateMessageChannel[]
|
let channels: PrivateMessageChannel[]
|
||||||
const convertRow = (r: any) => ({
|
const convertRow = (r: any) => ({
|
||||||
@@ -24,55 +23,56 @@ export const getChannelMemberships: APIHandler<
|
|||||||
channels = await pg.map(
|
channels = await pg.map(
|
||||||
`select channel_id, notify_after_time, pumcm.created_time, last_updated_time
|
`select channel_id, notify_after_time, pumcm.created_time, last_updated_time
|
||||||
from private_user_message_channel_members pumcm
|
from private_user_message_channel_members pumcm
|
||||||
join private_user_message_channels pumc on pumc.id= pumcm.channel_id
|
join private_user_message_channels pumc on pumc.id = pumcm.channel_id
|
||||||
where user_id = $1
|
where user_id = $1
|
||||||
and channel_id = $2
|
and channel_id = $2
|
||||||
limit $3
|
limit $3
|
||||||
`,
|
`,
|
||||||
[auth.uid, channelId, limit],
|
[auth.uid, channelId, limit],
|
||||||
convertRow
|
convertRow
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
channels = await pg.map(
|
channels = await pg.map(
|
||||||
`with latest_channels as (
|
`with latest_channels as (select distinct on (pumc.id) pumc.id as channel_id,
|
||||||
select distinct on (pumc.id) pumc.id as channel_id, notify_after_time, pumc.created_time,
|
notify_after_time,
|
||||||
(select created_time
|
pumc.created_time,
|
||||||
from private_user_messages
|
(select created_time
|
||||||
where channel_id = pumc.id
|
from private_user_messages
|
||||||
and visibility != 'system_status'
|
where channel_id = pumc.id
|
||||||
and user_id != $1
|
and visibility != 'system_status'
|
||||||
order by created_time desc
|
and user_id != $1
|
||||||
limit 1) as last_updated_time, -- last_updated_time is the last possible unseen message time
|
order by created_time desc
|
||||||
pumc.last_updated_time as last_updated_channel_time -- last_updated_channel_time is the last time the channel was updated
|
limit 1) as last_updated_time, -- last_updated_time is the last possible unseen message time
|
||||||
from private_user_message_channels pumc
|
pumc.last_updated_time as last_updated_channel_time -- last_updated_channel_time is the last time the channel was updated
|
||||||
join private_user_message_channel_members pumcm on pumcm.channel_id = pumc.id
|
from private_user_message_channels pumc
|
||||||
inner join private_user_messages pum on pumc.id = pum.channel_id
|
join private_user_message_channel_members pumcm on pumcm.channel_id = pumc.id
|
||||||
and (pum.visibility != 'introduction' or pum.user_id != $1)
|
inner join private_user_messages pum on pumc.id = pum.channel_id
|
||||||
where pumcm.user_id = $1
|
and (pum.visibility != 'introduction' or pum.user_id != $1)
|
||||||
and not status = 'left'
|
where pumcm.user_id = $1
|
||||||
and ($2 is null or pumcm.created_time > $2)
|
and not status = 'left'
|
||||||
and ($4 is null or pumc.last_updated_time > $4)
|
and ($2 is null or pumcm.created_time > $2)
|
||||||
order by pumc.id, pumc.last_updated_time desc
|
and ($4 is null or pumc.last_updated_time > $4)
|
||||||
)
|
order by pumc.id, pumc.last_updated_time desc)
|
||||||
select * from latest_channels
|
select *
|
||||||
|
from latest_channels
|
||||||
order by last_updated_channel_time desc
|
order by last_updated_channel_time desc
|
||||||
limit $3
|
limit $3
|
||||||
`,
|
`,
|
||||||
[auth.uid, createdTime ?? null, limit, lastUpdatedTime ?? null],
|
[auth.uid, createdTime ?? null, limit, lastUpdatedTime ?? null],
|
||||||
convertRow
|
convertRow
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!channels || channels.length === 0)
|
if (!channels || channels.length === 0)
|
||||||
return { channels: [], memberIdsByChannelId: {} }
|
return {channels: [], memberIdsByChannelId: {}}
|
||||||
const channelIds = channels.map((c) => c.channel_id)
|
const channelIds = channels.map((c) => c.channel_id)
|
||||||
|
|
||||||
const members = await pg.map(
|
const members = await pg.map(
|
||||||
`select channel_id, user_id
|
`select channel_id, user_id
|
||||||
from private_user_message_channel_members
|
from private_user_message_channel_members
|
||||||
where not user_id = $1
|
where not user_id = $1
|
||||||
and channel_id in ($2:list)
|
and channel_id in ($2:list)
|
||||||
and not status = 'left'
|
and not status = 'left'
|
||||||
`,
|
`,
|
||||||
[auth.uid, channelIds],
|
[auth.uid, channelIds],
|
||||||
(r) => ({
|
(r) => ({
|
||||||
channel_id: r.channel_id as number,
|
channel_id: r.channel_id as number,
|
||||||
@@ -91,39 +91,56 @@ export const getChannelMemberships: APIHandler<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getChannelMessages: APIHandler<'get-channel-messages'> = async (
|
export const getChannelMessagesEndpoint: APIHandler<'get-channel-messages'> = async (
|
||||||
props,
|
props,
|
||||||
auth
|
auth
|
||||||
) => {
|
) => {
|
||||||
|
const userId = auth.uid
|
||||||
|
return await getChannelMessages({...props, userId})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getChannelMessages(props: {
|
||||||
|
channelId: number;
|
||||||
|
limit: number;
|
||||||
|
id?: number | undefined;
|
||||||
|
userId: string;
|
||||||
|
}) {
|
||||||
|
// console.log('initial message request', props)
|
||||||
|
const {channelId, limit, id, userId} = props
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
const { channelId, limit, id } = props
|
const {data, error} = await tryCatch(pg.map(
|
||||||
return await pg.map(
|
|
||||||
`select *, created_time as created_time_ts
|
`select *, created_time as created_time_ts
|
||||||
from private_user_messages
|
from private_user_messages
|
||||||
where channel_id = $1
|
where channel_id = $1
|
||||||
and exists (select 1 from private_user_message_channel_members pumcm
|
and exists (select 1
|
||||||
where pumcm.user_id = $2
|
from private_user_message_channel_members pumcm
|
||||||
and pumcm.channel_id = $1
|
where pumcm.user_id = $2
|
||||||
)
|
and pumcm.channel_id = $1)
|
||||||
and ($4 is null or id > $4)
|
and ($4 is null or id > $4)
|
||||||
and not visibility = 'system_status'
|
and not visibility = 'system_status'
|
||||||
order by created_time desc
|
order by created_time desc
|
||||||
limit $3
|
limit $3
|
||||||
`,
|
`,
|
||||||
[channelId, auth.uid, limit, id],
|
[channelId, userId, limit, id],
|
||||||
convertPrivateChatMessage
|
convertPrivateChatMessage
|
||||||
)
|
))
|
||||||
|
if (error) {
|
||||||
|
console.error(error)
|
||||||
|
throw new APIError(401, 'Error getting messages')
|
||||||
|
}
|
||||||
|
// console.log('final messages', data)
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getLastSeenChannelTime: APIHandler<
|
export const getLastSeenChannelTime: APIHandler<
|
||||||
'get-channel-seen-time'
|
'get-channel-seen-time'
|
||||||
> = async (props, auth) => {
|
> = async (props, auth) => {
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
const { channelIds } = props
|
const {channelIds} = props
|
||||||
const unseens = await pg.map(
|
const unseens = await pg.map(
|
||||||
`select distinct on (channel_id) channel_id, created_time
|
`select distinct on (channel_id) channel_id, created_time
|
||||||
from private_user_seen_message_channels
|
from private_user_seen_message_channels
|
||||||
where channel_id = any($1)
|
where channel_id = any ($1)
|
||||||
and user_id = $2
|
and user_id = $2
|
||||||
order by channel_id, created_time desc
|
order by channel_id, created_time desc
|
||||||
`,
|
`,
|
||||||
@@ -137,11 +154,11 @@ export const setChannelLastSeenTime: APIHandler<
|
|||||||
'set-channel-seen-time'
|
'set-channel-seen-time'
|
||||||
> = async (props, auth) => {
|
> = async (props, auth) => {
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
const { channelId } = props
|
const {channelId} = props
|
||||||
await pg.none(
|
await pg.none(
|
||||||
`insert into private_user_seen_message_channels (user_id, channel_id)
|
`insert into private_user_seen_message_channels (user_id, channel_id)
|
||||||
values ($1, $2)
|
values ($1, $2)
|
||||||
`,
|
`,
|
||||||
[auth.uid, channelId]
|
[auth.uid, channelId]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export const getProfileAnswers: APIHandler<'get-profile-answers'> = async (
|
|||||||
const { userId } = props
|
const { userId } = props
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
|
|
||||||
const answers = await pg.manyOrNone<Row<'love_compatibility_answers'>>(
|
const answers = await pg.manyOrNone<Row<'compatibility_answers'>>(
|
||||||
`select * from love_compatibility_answers
|
`select * from compatibility_answers
|
||||||
where
|
where
|
||||||
creator_id = $1
|
creator_id = $1
|
||||||
order by created_time desc
|
order by created_time desc
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {type APIHandler} from 'api/helpers/endpoint'
|
import {type APIHandler} from 'api/helpers/endpoint'
|
||||||
import {convertRow} from 'shared/love/supabase'
|
import {convertRow} from 'shared/profiles/supabase'
|
||||||
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||||
import {from, join, leftJoin, limit, orderBy, renderSql, select, where,} from 'shared/supabase/sql-builder'
|
import {from, join, leftJoin, limit, orderBy, renderSql, select, where,} from 'shared/supabase/sql-builder'
|
||||||
import {getCompatibleProfiles} from 'api/compatible-profiles'
|
import {getCompatibleProfiles} from 'api/compatible-profiles'
|
||||||
@@ -12,11 +12,16 @@ export type profileQueryType = {
|
|||||||
// Search and filter parameters
|
// Search and filter parameters
|
||||||
name?: string | undefined,
|
name?: string | undefined,
|
||||||
genders?: String[] | undefined,
|
genders?: String[] | undefined,
|
||||||
|
education_levels?: String[] | undefined,
|
||||||
pref_gender?: String[] | undefined,
|
pref_gender?: String[] | undefined,
|
||||||
pref_age_min?: number | undefined,
|
pref_age_min?: number | undefined,
|
||||||
pref_age_max?: number | undefined,
|
pref_age_max?: number | undefined,
|
||||||
|
drinks_min?: number | undefined,
|
||||||
|
drinks_max?: number | undefined,
|
||||||
pref_relation_styles?: String[] | undefined,
|
pref_relation_styles?: String[] | undefined,
|
||||||
pref_romantic_styles?: String[] | undefined,
|
pref_romantic_styles?: String[] | undefined,
|
||||||
|
diet?: String[] | undefined,
|
||||||
|
political_beliefs?: String[] | undefined,
|
||||||
wants_kids_strength?: number | undefined,
|
wants_kids_strength?: number | undefined,
|
||||||
has_kids?: number | undefined,
|
has_kids?: number | undefined,
|
||||||
is_smoker?: boolean | undefined,
|
is_smoker?: boolean | undefined,
|
||||||
@@ -42,11 +47,16 @@ export const loadProfiles = async (props: profileQueryType) => {
|
|||||||
after,
|
after,
|
||||||
name,
|
name,
|
||||||
genders,
|
genders,
|
||||||
|
education_levels,
|
||||||
pref_gender,
|
pref_gender,
|
||||||
pref_age_min,
|
pref_age_min,
|
||||||
pref_age_max,
|
pref_age_max,
|
||||||
|
drinks_min,
|
||||||
|
drinks_max,
|
||||||
pref_relation_styles,
|
pref_relation_styles,
|
||||||
pref_romantic_styles,
|
pref_romantic_styles,
|
||||||
|
diet,
|
||||||
|
political_beliefs,
|
||||||
wants_kids_strength,
|
wants_kids_strength,
|
||||||
has_kids,
|
has_kids,
|
||||||
is_smoker,
|
is_smoker,
|
||||||
@@ -78,15 +88,24 @@ export const loadProfiles = async (props: profileQueryType) => {
|
|||||||
(l) =>
|
(l) =>
|
||||||
(!name || l.user.name.toLowerCase().includes(name.toLowerCase())) &&
|
(!name || l.user.name.toLowerCase().includes(name.toLowerCase())) &&
|
||||||
(!genders || genders.includes(l.gender)) &&
|
(!genders || genders.includes(l.gender)) &&
|
||||||
|
(!education_levels || education_levels.includes(l.education_level ?? '')) &&
|
||||||
(!pref_gender || intersection(pref_gender, l.pref_gender).length) &&
|
(!pref_gender || intersection(pref_gender, l.pref_gender).length) &&
|
||||||
(!pref_age_min || (l.age ?? MAX_INT) >= pref_age_min) &&
|
(!pref_age_min || (l.age ?? MAX_INT) >= pref_age_min) &&
|
||||||
(!pref_age_max || (l.age ?? MIN_INT) <= pref_age_max) &&
|
(!pref_age_max || (l.age ?? MIN_INT) <= pref_age_max) &&
|
||||||
|
(!drinks_min || (l.drinks_per_month ?? MAX_INT) >= drinks_min) &&
|
||||||
|
(!drinks_max || (l.drinks_per_month ?? MIN_INT) <= drinks_max) &&
|
||||||
(!pref_relation_styles ||
|
(!pref_relation_styles ||
|
||||||
intersection(pref_relation_styles, l.pref_relation_styles).length) &&
|
intersection(pref_relation_styles, l.pref_relation_styles).length) &&
|
||||||
(!pref_romantic_styles ||
|
(!pref_romantic_styles ||
|
||||||
intersection(pref_romantic_styles, l.pref_romantic_styles).length) &&
|
intersection(pref_romantic_styles, l.pref_romantic_styles).length) &&
|
||||||
|
(!diet ||
|
||||||
|
intersection(diet, l.diet).length) &&
|
||||||
|
(!political_beliefs ||
|
||||||
|
intersection(political_beliefs, l.political_beliefs).length) &&
|
||||||
(!wants_kids_strength ||
|
(!wants_kids_strength ||
|
||||||
wants_kids_strength == -1 ||
|
wants_kids_strength == -1 ||
|
||||||
|
!l.wants_kids_strength ||
|
||||||
|
l.wants_kids_strength == -1 ||
|
||||||
(wants_kids_strength >= 2
|
(wants_kids_strength >= 2
|
||||||
? l.wants_kids_strength >= wants_kids_strength
|
? l.wants_kids_strength >= wants_kids_strength
|
||||||
: l.wants_kids_strength <= wants_kids_strength)) &&
|
: l.wants_kids_strength <= wants_kids_strength)) &&
|
||||||
@@ -94,7 +113,7 @@ export const loadProfiles = async (props: profileQueryType) => {
|
|||||||
has_kids == -1 ||
|
has_kids == -1 ||
|
||||||
(has_kids == 0 && !l.has_kids) ||
|
(has_kids == 0 && !l.has_kids) ||
|
||||||
(l.has_kids && l.has_kids > 0)) &&
|
(l.has_kids && l.has_kids > 0)) &&
|
||||||
(!is_smoker || l.is_smoker === is_smoker) &&
|
(is_smoker === undefined || l.is_smoker === is_smoker) &&
|
||||||
(l.id.toString() != skipId) &&
|
(l.id.toString() != skipId) &&
|
||||||
(!geodbCityIds ||
|
(!geodbCityIds ||
|
||||||
(l.geodb_city_id && geodbCityIds.includes(l.geodb_city_id))) &&
|
(l.geodb_city_id && geodbCityIds.includes(l.geodb_city_id))) &&
|
||||||
@@ -137,10 +156,12 @@ export const loadProfiles = async (props: profileQueryType) => {
|
|||||||
{word}
|
{word}
|
||||||
)),
|
)),
|
||||||
|
|
||||||
genders?.length && where(`gender = ANY($(gender))`, {gender: genders}),
|
genders?.length && where(`gender = ANY($(genders))`, {genders}),
|
||||||
|
|
||||||
|
education_levels?.length && where(`education_level = ANY($(education_levels))`, {education_levels}),
|
||||||
|
|
||||||
pref_gender?.length &&
|
pref_gender?.length &&
|
||||||
where(`pref_gender && $(pref_gender)`, {pref_gender}),
|
where(`pref_gender is NULL or pref_gender = '{}' OR pref_gender && $(pref_gender)`, {pref_gender}),
|
||||||
|
|
||||||
pref_age_min &&
|
pref_age_min &&
|
||||||
where(`age >= $(pref_age_min) or age is null`, {pref_age_min}),
|
where(`age >= $(pref_age_min) or age is null`, {pref_age_min}),
|
||||||
@@ -148,6 +169,12 @@ export const loadProfiles = async (props: profileQueryType) => {
|
|||||||
pref_age_max &&
|
pref_age_max &&
|
||||||
where(`age <= $(pref_age_max) or age is null`, {pref_age_max}),
|
where(`age <= $(pref_age_max) or age is null`, {pref_age_max}),
|
||||||
|
|
||||||
|
drinks_min &&
|
||||||
|
where(`drinks_per_month >= $(drinks_min) or drinks_per_month is null`, {drinks_min}),
|
||||||
|
|
||||||
|
drinks_max &&
|
||||||
|
where(`drinks_per_month <= $(drinks_max) or drinks_per_month is null`, {drinks_max}),
|
||||||
|
|
||||||
pref_relation_styles?.length &&
|
pref_relation_styles?.length &&
|
||||||
where(
|
where(
|
||||||
`pref_relation_styles IS NULL OR pref_relation_styles = '{}' OR pref_relation_styles && $(pref_relation_styles)`,
|
`pref_relation_styles IS NULL OR pref_relation_styles = '{}' OR pref_relation_styles && $(pref_relation_styles)`,
|
||||||
@@ -160,19 +187,34 @@ export const loadProfiles = async (props: profileQueryType) => {
|
|||||||
{pref_romantic_styles}
|
{pref_romantic_styles}
|
||||||
),
|
),
|
||||||
|
|
||||||
|
diet?.length &&
|
||||||
|
where(
|
||||||
|
`diet IS NULL OR diet = '{}' OR diet && $(diet)`,
|
||||||
|
{diet}
|
||||||
|
),
|
||||||
|
|
||||||
|
political_beliefs?.length &&
|
||||||
|
where(
|
||||||
|
`political_beliefs IS NULL OR political_beliefs = '{}' OR political_beliefs && $(political_beliefs)`,
|
||||||
|
{political_beliefs}
|
||||||
|
),
|
||||||
|
|
||||||
!!wants_kids_strength &&
|
!!wants_kids_strength &&
|
||||||
wants_kids_strength !== -1 &&
|
wants_kids_strength !== -1 &&
|
||||||
where(
|
where(
|
||||||
wants_kids_strength >= 2
|
'wants_kids_strength = -1 OR wants_kids_strength IS NULL OR ' + (wants_kids_strength >= 2 ? `wants_kids_strength >= $(wants_kids_strength)` : `wants_kids_strength <= $(wants_kids_strength)`),
|
||||||
? `wants_kids_strength >= $(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 === 0 && where(`has_kids IS NULL OR has_kids = 0`),
|
||||||
has_kids && has_kids > 0 && where(`has_kids > 0`),
|
has_kids && has_kids > 0 && where(`has_kids > 0`),
|
||||||
|
|
||||||
is_smoker !== undefined && where(`is_smoker = $(is_smoker)`, {is_smoker}),
|
is_smoker !== undefined && (
|
||||||
|
where(
|
||||||
|
(is_smoker ? '' : 'is_smoker IS NULL OR ') + // smokers are rare, so we don't include the people who didn't answer if we're looking for smokers
|
||||||
|
`is_smoker = $(is_smoker)`, {is_smoker}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
geodbCityIds?.length &&
|
geodbCityIds?.length &&
|
||||||
where(`geodb_city_id = ANY($(geodbCityIds))`, {geodbCityIds}),
|
where(`geodb_city_id = ANY($(geodbCityIds))`, {geodbCityIds}),
|
||||||
|
|||||||
@@ -17,17 +17,18 @@ export const getUser = async (props: { id: string } | { username: string }) => {
|
|||||||
return toUserAPIResponse(user)
|
return toUserAPIResponse(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getDisplayUser = async (
|
// export const getDisplayUser = async (
|
||||||
props: { id: string } | { username: string }
|
// props: { id: string } | { username: string }
|
||||||
) => {
|
// ) => {
|
||||||
const pg = createSupabaseDirectClient()
|
// console.log('getDisplayUser', props)
|
||||||
const liteUser = await pg.oneOrNone(
|
// const pg = createSupabaseDirectClient()
|
||||||
`select ${displayUserColumns}
|
// const liteUser = await pg.oneOrNone(
|
||||||
from users
|
// `select ${displayUserColumns}
|
||||||
where ${'id' in props ? 'id' : 'username'} = $1`,
|
// from users
|
||||||
['id' in props ? props.id : props.username]
|
// where ${'id' in props ? 'id' : 'username'} = $1`,
|
||||||
)
|
// ['id' in props ? props.id : props.username]
|
||||||
if (!liteUser) throw new APIError(404, 'User not found')
|
// )
|
||||||
|
// if (!liteUser) throw new APIError(404, 'User not found')
|
||||||
return removeNullOrUndefinedProps(liteUser)
|
//
|
||||||
}
|
// return removeNullOrUndefinedProps(liteUser)
|
||||||
|
// }
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const getHasFreeLike = async (userId: string) => {
|
|||||||
const likeGivenToday = await pg.oneOrNone<object>(
|
const likeGivenToday = await pg.oneOrNone<object>(
|
||||||
`
|
`
|
||||||
select 1
|
select 1
|
||||||
from love_likes
|
from profile_likes
|
||||||
where creator_id = $1
|
where creator_id = $1
|
||||||
and created_time at time zone 'UTC' at time zone 'America/Los_Angeles' >= (now() at time zone 'UTC' at time zone 'America/Los_Angeles')::date
|
and created_time at time zone 'UTC' at time zone 'America/Los_Angeles' >= (now() at time zone 'UTC' at time zone 'America/Los_Angeles')::date
|
||||||
and created_time at time zone 'UTC' at time zone 'America/Los_Angeles' < ((now() at time zone 'UTC' at time zone 'America/Los_Angeles')::date + interval '1 day')
|
and created_time at time zone 'UTC' at time zone 'America/Los_Angeles' < ((now() at time zone 'UTC' at time zone 'America/Los_Angeles')::date + interval '1 day')
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ export const typedEndpoint = <N extends APIPath>(
|
|||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
// Convert bigint to number, b/c JSON doesn't support bigint.
|
// Convert bigint to number, b/c JSON doesn't support bigint.
|
||||||
const convertedResult = deepConvertBigIntToNumber(result)
|
const convertedResult = deepConvertBigIntToNumber(result)
|
||||||
|
// console.debug('API result', convertedResult)
|
||||||
res.status(200).json(convertedResult ?? {success: true})
|
res.status(200).json(convertedResult ?? {success: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
260
backend/api/src/helpers/private-messages.ts
Normal file
260
backend/api/src/helpers/private-messages.ts
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
import {Json} from 'common/supabase/schema'
|
||||||
|
import {SupabaseDirectClient} from 'shared/supabase/init'
|
||||||
|
import {ChatVisibility} from 'common/chat-message'
|
||||||
|
import {User} from 'common/user'
|
||||||
|
import {first} from 'lodash'
|
||||||
|
import {log} from 'shared/monitoring/log'
|
||||||
|
import {getPrivateUser, getUser} from 'shared/utils'
|
||||||
|
import {type JSONContent} from '@tiptap/core'
|
||||||
|
import {APIError} from 'common/api/utils'
|
||||||
|
import {broadcast} from 'shared/websockets/server'
|
||||||
|
import {track} from 'shared/analytics'
|
||||||
|
import {sendNewMessageEmail} from 'email/functions/helpers'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import utc from 'dayjs/plugin/utc'
|
||||||
|
import timezone from 'dayjs/plugin/timezone'
|
||||||
|
import webPush from 'web-push';
|
||||||
|
import {parseJsonContentToText} from "common/util/parse";
|
||||||
|
import {encryptMessage} from "shared/encryption";
|
||||||
|
|
||||||
|
dayjs.extend(utc)
|
||||||
|
dayjs.extend(timezone)
|
||||||
|
|
||||||
|
export const leaveChatContent = (userName: string) => ({
|
||||||
|
type: 'doc',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{text: `${userName} left the chat`, type: 'text'}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export const joinChatContent = (userName: string) => {
|
||||||
|
return {
|
||||||
|
type: 'doc',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{text: `${userName} joined the chat!`, type: 'text'}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const insertPrivateMessage = async (
|
||||||
|
content: Json,
|
||||||
|
channelId: number,
|
||||||
|
userId: string,
|
||||||
|
visibility: ChatVisibility,
|
||||||
|
pg: SupabaseDirectClient
|
||||||
|
) => {
|
||||||
|
const plaintext = JSON.stringify(content);
|
||||||
|
const {ciphertext, iv, tag} = encryptMessage(plaintext);
|
||||||
|
const lastMessage = await pg.one(
|
||||||
|
`insert into private_user_messages (ciphertext, iv, tag, channel_id, user_id, visibility)
|
||||||
|
values ($1, $2, $3, $4, $5, $6)
|
||||||
|
returning created_time`,
|
||||||
|
[ciphertext, iv, tag, channelId, userId, visibility]
|
||||||
|
)
|
||||||
|
await pg.none(
|
||||||
|
`update private_user_message_channels
|
||||||
|
set last_updated_time = $1
|
||||||
|
where id = $2`,
|
||||||
|
[lastMessage.created_time, channelId]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addUsersToPrivateMessageChannel = async (
|
||||||
|
userIds: string[],
|
||||||
|
channelId: number,
|
||||||
|
pg: SupabaseDirectClient
|
||||||
|
) => {
|
||||||
|
await Promise.all(
|
||||||
|
userIds.map((id) =>
|
||||||
|
pg.none(
|
||||||
|
`insert into private_user_message_channel_members (channel_id, user_id, role, status)
|
||||||
|
values ($1, $2, 'member', 'proposed')
|
||||||
|
on conflict do nothing
|
||||||
|
`,
|
||||||
|
[channelId, id]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await pg.none(
|
||||||
|
`update private_user_message_channels
|
||||||
|
set last_updated_time = now()
|
||||||
|
where id = $1`,
|
||||||
|
[channelId]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createPrivateUserMessageMain = async (
|
||||||
|
creator: User,
|
||||||
|
channelId: number,
|
||||||
|
content: JSONContent,
|
||||||
|
pg: SupabaseDirectClient,
|
||||||
|
visibility: ChatVisibility
|
||||||
|
) => {
|
||||||
|
log('createPrivateUserMessageMain', creator, channelId, content)
|
||||||
|
|
||||||
|
// Normally, users can only submit messages to channels that they are members of
|
||||||
|
const authorized = await pg.oneOrNone(
|
||||||
|
`select 1
|
||||||
|
from private_user_message_channel_members
|
||||||
|
where channel_id = $1
|
||||||
|
and user_id = $2`,
|
||||||
|
[channelId, creator.id]
|
||||||
|
)
|
||||||
|
if (!authorized)
|
||||||
|
throw new APIError(403, 'You are not authorized to post to this channel')
|
||||||
|
|
||||||
|
await insertPrivateMessage(content, channelId, creator.id, visibility, pg)
|
||||||
|
|
||||||
|
const privateMessage = {
|
||||||
|
content: content as Json,
|
||||||
|
channel_id: channelId,
|
||||||
|
user_id: creator.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
const otherUserIds = await pg.map<string>(
|
||||||
|
`select user_id
|
||||||
|
from private_user_message_channel_members
|
||||||
|
where channel_id = $1
|
||||||
|
and user_id != $2
|
||||||
|
and status != 'left'
|
||||||
|
`,
|
||||||
|
[channelId, creator.id],
|
||||||
|
(r) => r.user_id
|
||||||
|
)
|
||||||
|
otherUserIds.concat(creator.id).forEach((otherUserId) => {
|
||||||
|
broadcast(`private-user-messages/${otherUserId}`, {})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fire and forget safely
|
||||||
|
void notifyOtherUserInChannelIfInactive(channelId, creator, content, pg)
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('notifyOtherUserInChannelIfInactive failed', err)
|
||||||
|
});
|
||||||
|
|
||||||
|
track(creator.id, 'send private message', {
|
||||||
|
channelId,
|
||||||
|
otherUserIds,
|
||||||
|
})
|
||||||
|
|
||||||
|
return privateMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifyOtherUserInChannelIfInactive = async (
|
||||||
|
channelId: number,
|
||||||
|
creator: User,
|
||||||
|
content: JSONContent,
|
||||||
|
pg: SupabaseDirectClient
|
||||||
|
) => {
|
||||||
|
const otherUserIds = await pg.manyOrNone<{ user_id: string }>(
|
||||||
|
`select user_id
|
||||||
|
from private_user_message_channel_members
|
||||||
|
where channel_id = $1
|
||||||
|
and user_id != $2
|
||||||
|
and status != 'left'
|
||||||
|
`,
|
||||||
|
[channelId, creator.id]
|
||||||
|
)
|
||||||
|
// We're only sending notifs for 1:1 channels
|
||||||
|
if (!otherUserIds || otherUserIds.length > 1) return
|
||||||
|
|
||||||
|
const otherUserId = first(otherUserIds)
|
||||||
|
if (!otherUserId) return
|
||||||
|
|
||||||
|
// TODO: notification only for active user
|
||||||
|
|
||||||
|
const otherUser = await getUser(otherUserId.user_id)
|
||||||
|
console.debug('otherUser:', otherUser)
|
||||||
|
if (!otherUser) return
|
||||||
|
|
||||||
|
// Push notif
|
||||||
|
webPush.setVapidDetails(
|
||||||
|
'mailto:hello@compassmeet.com',
|
||||||
|
process.env.VAPID_PUBLIC_KEY!,
|
||||||
|
process.env.VAPID_PRIVATE_KEY!
|
||||||
|
);
|
||||||
|
const textContent = parseJsonContentToText(content)
|
||||||
|
// Retrieve subscription from the database
|
||||||
|
const subscriptions = await getSubscriptionsFromDB(otherUser.id, pg);
|
||||||
|
for (const subscription of subscriptions) {
|
||||||
|
try {
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
title: `${creator.name}`,
|
||||||
|
body: textContent,
|
||||||
|
url: `/messages/${channelId}`,
|
||||||
|
})
|
||||||
|
console.log('Sending notification to:', subscription.endpoint, payload);
|
||||||
|
await webPush.sendNotification(subscription, payload);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.log('Failed to send notification', err);
|
||||||
|
if (err.statusCode === 410 || err.statusCode === 404) {
|
||||||
|
console.warn('Removing expired subscription', subscription.endpoint);
|
||||||
|
await pg.none(
|
||||||
|
`DELETE
|
||||||
|
FROM push_subscriptions
|
||||||
|
WHERE endpoint = $1
|
||||||
|
AND user_id = $2`,
|
||||||
|
[subscription.endpoint, otherUser.id]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error('Push failed', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startOfDay = dayjs()
|
||||||
|
.tz('America/Los_Angeles')
|
||||||
|
.startOf('day')
|
||||||
|
.toISOString()
|
||||||
|
const previousMessagesThisDayBetweenTheseUsers = await pg.one(
|
||||||
|
`select count(*)
|
||||||
|
from private_user_messages
|
||||||
|
where channel_id = $1
|
||||||
|
and user_id = $2
|
||||||
|
and created_time > $3
|
||||||
|
`,
|
||||||
|
[channelId, creator.id, startOfDay]
|
||||||
|
)
|
||||||
|
log('previous messages this day', previousMessagesThisDayBetweenTheseUsers)
|
||||||
|
if (previousMessagesThisDayBetweenTheseUsers.count > 0) return
|
||||||
|
|
||||||
|
await createNewMessageNotification(creator, otherUser, channelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createNewMessageNotification = async (
|
||||||
|
fromUser: User,
|
||||||
|
toUser: User,
|
||||||
|
channelId: number,
|
||||||
|
) => {
|
||||||
|
const privateUser = await getPrivateUser(toUser.id)
|
||||||
|
console.debug('privateUser:', privateUser)
|
||||||
|
if (!privateUser) return
|
||||||
|
await sendNewMessageEmail(privateUser, fromUser, toUser, channelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function getSubscriptionsFromDB(
|
||||||
|
userId: string,
|
||||||
|
pg: SupabaseDirectClient
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const subscriptions = await pg.manyOrNone(`
|
||||||
|
select endpoint, keys
|
||||||
|
from push_subscriptions
|
||||||
|
where user_id = $1
|
||||||
|
`, [userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
return subscriptions.map(sub => ({
|
||||||
|
endpoint: sub.endpoint,
|
||||||
|
keys: sub.keys,
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching subscriptions', err);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
import { Json } from 'common/supabase/schema'
|
|
||||||
import { SupabaseDirectClient } from 'shared/supabase/init'
|
|
||||||
import { ChatVisibility } from 'common/chat-message'
|
|
||||||
import { User } from 'common/user'
|
|
||||||
import { first } from 'lodash'
|
|
||||||
import { log } from 'shared/monitoring/log'
|
|
||||||
import { getPrivateUser, getUser } from 'shared/utils'
|
|
||||||
import { type JSONContent } from '@tiptap/core'
|
|
||||||
import { APIError } from 'common/api/utils'
|
|
||||||
import { broadcast } from 'shared/websockets/server'
|
|
||||||
import { track } from 'shared/analytics'
|
|
||||||
import { sendNewMessageEmail } from 'email/functions/helpers'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import utc from 'dayjs/plugin/utc'
|
|
||||||
import timezone from 'dayjs/plugin/timezone'
|
|
||||||
|
|
||||||
dayjs.extend(utc)
|
|
||||||
dayjs.extend(timezone)
|
|
||||||
|
|
||||||
export const leaveChatContent = (userName: string) => ({
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'paragraph',
|
|
||||||
content: [{ text: `${userName} left the chat`, type: 'text' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export const joinChatContent = (userName: string) => {
|
|
||||||
return {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'paragraph',
|
|
||||||
content: [{ text: `${userName} joined the chat!`, type: 'text' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const insertPrivateMessage = async (
|
|
||||||
content: Json,
|
|
||||||
channelId: number,
|
|
||||||
userId: string,
|
|
||||||
visibility: ChatVisibility,
|
|
||||||
pg: SupabaseDirectClient
|
|
||||||
) => {
|
|
||||||
const lastMessage = await pg.one(
|
|
||||||
`insert into private_user_messages (content, channel_id, user_id, visibility)
|
|
||||||
values ($1, $2, $3, $4) returning created_time`,
|
|
||||||
[content, channelId, userId, visibility]
|
|
||||||
)
|
|
||||||
await pg.none(
|
|
||||||
`update private_user_message_channels set last_updated_time = $1 where id = $2`,
|
|
||||||
[lastMessage.created_time, channelId]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const addUsersToPrivateMessageChannel = async (
|
|
||||||
userIds: string[],
|
|
||||||
channelId: number,
|
|
||||||
pg: SupabaseDirectClient
|
|
||||||
) => {
|
|
||||||
await Promise.all(
|
|
||||||
userIds.map((id) =>
|
|
||||||
pg.none(
|
|
||||||
`insert into private_user_message_channel_members (channel_id, user_id, role, status)
|
|
||||||
values
|
|
||||||
($1, $2, 'member', 'proposed')
|
|
||||||
on conflict do nothing
|
|
||||||
`,
|
|
||||||
[channelId, id]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await pg.none(
|
|
||||||
`update private_user_message_channels set last_updated_time = now() where id = $1`,
|
|
||||||
[channelId]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createPrivateUserMessageMain = async (
|
|
||||||
creator: User,
|
|
||||||
channelId: number,
|
|
||||||
content: JSONContent,
|
|
||||||
pg: SupabaseDirectClient,
|
|
||||||
visibility: ChatVisibility
|
|
||||||
) => {
|
|
||||||
log('createPrivateUserMessageMain', creator, channelId, content)
|
|
||||||
// Normally, users can only submit messages to channels that they are members of
|
|
||||||
const authorized = await pg.oneOrNone(
|
|
||||||
`select 1
|
|
||||||
from private_user_message_channel_members
|
|
||||||
where channel_id = $1
|
|
||||||
and user_id = $2`,
|
|
||||||
[channelId, creator.id]
|
|
||||||
)
|
|
||||||
if (!authorized)
|
|
||||||
throw new APIError(403, 'You are not authorized to post to this channel')
|
|
||||||
|
|
||||||
await notifyOtherUserInChannelIfInactive(channelId, creator, pg)
|
|
||||||
await insertPrivateMessage(content, channelId, creator.id, visibility, pg)
|
|
||||||
|
|
||||||
const privateMessage = {
|
|
||||||
content: content as Json,
|
|
||||||
channel_id: channelId,
|
|
||||||
user_id: creator.id,
|
|
||||||
}
|
|
||||||
|
|
||||||
const otherUserIds = await pg.map<string>(
|
|
||||||
`select user_id from private_user_message_channel_members
|
|
||||||
where channel_id = $1 and user_id != $2
|
|
||||||
and status != 'left'
|
|
||||||
`,
|
|
||||||
[channelId, creator.id],
|
|
||||||
(r) => r.user_id
|
|
||||||
)
|
|
||||||
otherUserIds.concat(creator.id).forEach((otherUserId) => {
|
|
||||||
broadcast(`private-user-messages/${otherUserId}`, {})
|
|
||||||
})
|
|
||||||
|
|
||||||
track(creator.id, 'send private message', {
|
|
||||||
channelId,
|
|
||||||
otherUserIds,
|
|
||||||
})
|
|
||||||
|
|
||||||
return privateMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
const notifyOtherUserInChannelIfInactive = async (
|
|
||||||
channelId: number,
|
|
||||||
creator: User,
|
|
||||||
pg: SupabaseDirectClient
|
|
||||||
) => {
|
|
||||||
const otherUserIds = await pg.manyOrNone<{ user_id: string }>(
|
|
||||||
`select user_id from private_user_message_channel_members
|
|
||||||
where channel_id = $1 and user_id != $2
|
|
||||||
and status != 'left'
|
|
||||||
`,
|
|
||||||
[channelId, creator.id]
|
|
||||||
)
|
|
||||||
// We're only sending notifs for 1:1 channels
|
|
||||||
if (!otherUserIds || otherUserIds.length > 1) return
|
|
||||||
|
|
||||||
const otherUserId = first(otherUserIds)
|
|
||||||
if (!otherUserId) return
|
|
||||||
|
|
||||||
const startOfDay = dayjs()
|
|
||||||
.tz('America/Los_Angeles')
|
|
||||||
.startOf('day')
|
|
||||||
.toISOString()
|
|
||||||
const previousMessagesThisDayBetweenTheseUsers = await pg.one(
|
|
||||||
`select count(*) from private_user_messages
|
|
||||||
where channel_id = $1
|
|
||||||
and user_id = $2
|
|
||||||
and created_time > $3
|
|
||||||
`,
|
|
||||||
[channelId, creator.id, startOfDay]
|
|
||||||
)
|
|
||||||
log('previous messages this day', previousMessagesThisDayBetweenTheseUsers)
|
|
||||||
if (previousMessagesThisDayBetweenTheseUsers.count > 0) return
|
|
||||||
|
|
||||||
// TODO: notification only for active user
|
|
||||||
|
|
||||||
const otherUser = await getUser(otherUserId.user_id)
|
|
||||||
console.debug('otherUser:', otherUser)
|
|
||||||
if (!otherUser) return
|
|
||||||
|
|
||||||
await createNewMessageNotification(creator, otherUser, channelId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const createNewMessageNotification = async (
|
|
||||||
fromUser: User,
|
|
||||||
toUser: User,
|
|
||||||
channelId: number
|
|
||||||
) => {
|
|
||||||
const privateUser = await getPrivateUser(toUser.id)
|
|
||||||
console.debug('privateUser:', privateUser)
|
|
||||||
if (!privateUser) return
|
|
||||||
await sendNewMessageEmail(privateUser, fromUser, toUser, channelId)
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ import { createSupabaseDirectClient } from 'shared/supabase/init'
|
|||||||
import {
|
import {
|
||||||
insertPrivateMessage,
|
insertPrivateMessage,
|
||||||
leaveChatContent,
|
leaveChatContent,
|
||||||
} from 'api/junk-drawer/private-messages'
|
} from 'api/helpers/private-messages'
|
||||||
|
|
||||||
export const leavePrivateUserMessageChannel: APIHandler<
|
export const leavePrivateUserMessageChannel: APIHandler<
|
||||||
'leave-private-user-message-channel'
|
'leave-private-user-message-channel'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
||||||
import { APIError, APIHandler } from './helpers/endpoint'
|
import { APIError, APIHandler } from './helpers/endpoint'
|
||||||
import { createLoveLikeNotification } from 'shared/create-love-notification'
|
import { createProfileLikeNotification } from 'shared/create-profile-notification'
|
||||||
import { getHasFreeLike } from './has-free-like'
|
import { getHasFreeLike } from './has-free-like'
|
||||||
import { log } from 'shared/utils'
|
import { log } from 'shared/utils'
|
||||||
import { tryCatch } from 'common/util/try-catch'
|
import { tryCatch } from 'common/util/try-catch'
|
||||||
@@ -15,7 +15,7 @@ export const likeProfile: APIHandler<'like-profile'> = async (props, auth) => {
|
|||||||
if (remove) {
|
if (remove) {
|
||||||
const { error } = await tryCatch(
|
const { error } = await tryCatch(
|
||||||
pg.none(
|
pg.none(
|
||||||
'delete from love_likes where creator_id = $1 and target_id = $2',
|
'delete from profile_likes where creator_id = $1 and target_id = $2',
|
||||||
[creatorId, targetUserId]
|
[creatorId, targetUserId]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -28,8 +28,8 @@ export const likeProfile: APIHandler<'like-profile'> = async (props, auth) => {
|
|||||||
|
|
||||||
// Check if like already exists
|
// Check if like already exists
|
||||||
const { data: existing } = await tryCatch(
|
const { data: existing } = await tryCatch(
|
||||||
pg.oneOrNone<Row<'love_likes'>>(
|
pg.oneOrNone<Row<'profile_likes'>>(
|
||||||
'select * from love_likes where creator_id = $1 and target_id = $2',
|
'select * from profile_likes where creator_id = $1 and target_id = $2',
|
||||||
[creatorId, targetUserId]
|
[creatorId, targetUserId]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -48,8 +48,8 @@ export const likeProfile: APIHandler<'like-profile'> = async (props, auth) => {
|
|||||||
|
|
||||||
// Insert the new like
|
// Insert the new like
|
||||||
const { data, error } = await tryCatch(
|
const { data, error } = await tryCatch(
|
||||||
pg.one<Row<'love_likes'>>(
|
pg.one<Row<'profile_likes'>>(
|
||||||
'insert into love_likes (creator_id, target_id) values ($1, $2) returning *',
|
'insert into profile_likes (creator_id, target_id) values ($1, $2) returning *',
|
||||||
[creatorId, targetUserId]
|
[creatorId, targetUserId]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -59,7 +59,7 @@ export const likeProfile: APIHandler<'like-profile'> = async (props, auth) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const continuation = async () => {
|
const continuation = async () => {
|
||||||
await createLoveLikeNotification(data)
|
await createProfileLikeNotification(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
41
backend/api/src/save-subscription.ts
Normal file
41
backend/api/src/save-subscription.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import {APIError, APIHandler} from './helpers/endpoint'
|
||||||
|
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||||
|
|
||||||
|
export const saveSubscription: APIHandler<'save-subscription'> = async (body, auth) => {
|
||||||
|
const {subscription} = body
|
||||||
|
|
||||||
|
if (!subscription?.endpoint || !subscription?.keys) {
|
||||||
|
throw new APIError(400, `Invalid subscription object`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = auth?.uid
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pg = createSupabaseDirectClient()
|
||||||
|
// Check if a subscription already exists
|
||||||
|
const exists = await pg.oneOrNone(
|
||||||
|
'select id from push_subscriptions where endpoint = $1',
|
||||||
|
[subscription.endpoint]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
// Already exists, optionally update keys and userId
|
||||||
|
await pg.none(
|
||||||
|
'update push_subscriptions set keys = $1, user_id = $2 where id = $3',
|
||||||
|
[subscription.keys, userId, exists.id]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await pg.none(
|
||||||
|
`insert into push_subscriptions(endpoint, keys, user_id) values($1, $2, $3)
|
||||||
|
on conflict(endpoint) do update set keys = excluded.keys
|
||||||
|
`,
|
||||||
|
[subscription.endpoint, subscription.keys, userId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {success: true};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error saving subscription', err);
|
||||||
|
throw new APIError(500, `Failed to save subscription`)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,15 +20,15 @@ export const searchUsers: APIHandler<'search-users'> = async (props, auth) => {
|
|||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
|
|
||||||
const offset = page * limit
|
const offset = page * limit
|
||||||
const userId = auth?.uid
|
// const userId = auth?.uid
|
||||||
const searchFollowersSQL = getSearchUserSQL({ term, offset, limit, userId })
|
// const searchFollowersSQL = getSearchUserSQL({ term, offset, limit, userId })
|
||||||
const searchAllSQL = getSearchUserSQL({ term, offset, limit })
|
const searchAllSQL = getSearchUserSQL({ term, offset, limit })
|
||||||
const [followers, all] = await Promise.all([
|
const [all] = await Promise.all([
|
||||||
pg.map(searchFollowersSQL, null, convertUser),
|
// pg.map(searchFollowersSQL, null, convertUser),
|
||||||
pg.map(searchAllSQL, null, convertUser),
|
pg.map(searchAllSQL, null, convertUser),
|
||||||
])
|
])
|
||||||
|
|
||||||
return uniqBy([...followers, ...all], 'id')
|
return uniqBy([...all], 'id')
|
||||||
.map(toUserAPIResponse)
|
.map(toUserAPIResponse)
|
||||||
.slice(0, limit)
|
.slice(0, limit)
|
||||||
}
|
}
|
||||||
@@ -39,17 +39,18 @@ function getSearchUserSQL(props: {
|
|||||||
limit: number
|
limit: number
|
||||||
userId?: string // search only this user's followers
|
userId?: string // search only this user's followers
|
||||||
}) {
|
}) {
|
||||||
const { term, userId } = props
|
const { term } = props
|
||||||
|
|
||||||
return renderSql(
|
return renderSql(
|
||||||
userId
|
// userId
|
||||||
? [
|
// ? [
|
||||||
select('users.*'),
|
// select('users.*'),
|
||||||
from('users'),
|
// from('users'),
|
||||||
join('user_follows on user_follows.follow_id = users.id'),
|
// join('user_follows on user_follows.follow_id = users.id'),
|
||||||
where('user_follows.user_id = $1', [userId]),
|
// where('user_follows.user_id = $1', [userId]),
|
||||||
]
|
// ]
|
||||||
: [select('*'), from('users')],
|
// :
|
||||||
|
[select('*'), from('users')],
|
||||||
term
|
term
|
||||||
? [
|
? [
|
||||||
where(
|
where(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {from, renderSql, select} from "shared/supabase/sql-builder";
|
|||||||
import {loadProfiles, profileQueryType} from "api/get-profiles";
|
import {loadProfiles, profileQueryType} from "api/get-profiles";
|
||||||
import {Row} from "common/supabase/utils";
|
import {Row} from "common/supabase/utils";
|
||||||
import {sendSearchAlertsEmail} from "email/functions/helpers";
|
import {sendSearchAlertsEmail} from "email/functions/helpers";
|
||||||
import {MatchesByUserType} from "common/love/bookmarked_searches";
|
import {MatchesByUserType} from "common/profiles/bookmarked_searches";
|
||||||
import {keyBy} from "lodash";
|
import {keyBy} from "lodash";
|
||||||
|
|
||||||
export function convertSearchRow(row: any): any {
|
export function convertSearchRow(row: any): any {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import "tsconfig-paths/register";
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import {initAdmin} from 'shared/init-admin'
|
import {initAdmin} from 'shared/init-admin'
|
||||||
import {loadSecretsToEnv} from 'common/secrets'
|
import {loadSecretsToEnv} from 'common/secrets'
|
||||||
|
|||||||
34
backend/api/src/set-compatibility-answer.ts
Normal file
34
backend/api/src/set-compatibility-answer.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {APIHandler} from './helpers/endpoint'
|
||||||
|
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||||
|
import {Row} from 'common/supabase/utils'
|
||||||
|
|
||||||
|
export const setCompatibilityAnswer: APIHandler<'set-compatibility-answer'> = async (
|
||||||
|
{questionId, multipleChoice, prefChoices, importance, explanation},
|
||||||
|
auth
|
||||||
|
) => {
|
||||||
|
const pg = createSupabaseDirectClient()
|
||||||
|
|
||||||
|
const result = await pg.one<Row<'compatibility_answers'>>({
|
||||||
|
text: `
|
||||||
|
INSERT INTO compatibility_answers
|
||||||
|
(creator_id, question_id, multiple_choice, pref_choices, importance, explanation)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
ON CONFLICT (question_id, creator_id)
|
||||||
|
DO UPDATE SET multiple_choice = EXCLUDED.multiple_choice,
|
||||||
|
pref_choices = EXCLUDED.pref_choices,
|
||||||
|
importance = EXCLUDED.importance,
|
||||||
|
explanation = EXCLUDED.explanation
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
values: [
|
||||||
|
auth.uid,
|
||||||
|
questionId,
|
||||||
|
multipleChoice,
|
||||||
|
prefChoices,
|
||||||
|
importance,
|
||||||
|
explanation ?? null,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -6,7 +6,12 @@ export const setLastOnlineTime: APIHandler<'set-last-online-time'> = async (
|
|||||||
auth
|
auth
|
||||||
) => {
|
) => {
|
||||||
if (!auth || !auth.uid) return
|
if (!auth || !auth.uid) return
|
||||||
|
await setLastOnlineTimeUser(auth.uid)
|
||||||
|
// console.log('setLastOnline')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const setLastOnlineTimeUser = async (userId: string) => {
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
await pg.none(`
|
await pg.none(`
|
||||||
INSERT INTO user_activity (user_id, last_online_time)
|
INSERT INTO user_activity (user_id, last_online_time)
|
||||||
@@ -16,7 +21,6 @@ export const setLastOnlineTime: APIHandler<'set-last-online-time'> = async (
|
|||||||
SET last_online_time = EXCLUDED.last_online_time
|
SET last_online_time = EXCLUDED.last_online_time
|
||||||
WHERE user_activity.last_online_time < now() - interval '1 minute';
|
WHERE user_activity.last_online_time < now() - interval '1 minute';
|
||||||
`,
|
`,
|
||||||
[auth.uid]
|
[userId]
|
||||||
)
|
)
|
||||||
// console.log('setLastOnline')
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
||||||
import { APIError, APIHandler } from './helpers/endpoint'
|
import { APIError, APIHandler } from './helpers/endpoint'
|
||||||
import { createLoveShipNotification } from 'shared/create-love-notification'
|
import { createProfileShipNotification } from 'shared/create-profile-notification'
|
||||||
import { log } from 'shared/utils'
|
import { log } from 'shared/utils'
|
||||||
import { tryCatch } from 'common/util/try-catch'
|
import { tryCatch } from 'common/util/try-catch'
|
||||||
import { insert } from 'shared/supabase/utils'
|
import { insert } from 'shared/supabase/utils'
|
||||||
@@ -14,7 +14,7 @@ export const shipProfiles: APIHandler<'ship-profiles'> = async (props, auth) =>
|
|||||||
// Check if ship already exists or with swapped target IDs
|
// Check if ship already exists or with swapped target IDs
|
||||||
const existing = await tryCatch(
|
const existing = await tryCatch(
|
||||||
pg.oneOrNone<{ ship_id: string }>(
|
pg.oneOrNone<{ ship_id: string }>(
|
||||||
`select ship_id from love_ships
|
`select ship_id from profile_ships
|
||||||
where creator_id = $1
|
where creator_id = $1
|
||||||
and (
|
and (
|
||||||
target1_id = $2 and target2_id = $3
|
target1_id = $2 and target2_id = $3
|
||||||
@@ -33,7 +33,7 @@ export const shipProfiles: APIHandler<'ship-profiles'> = async (props, auth) =>
|
|||||||
if (existing.data) {
|
if (existing.data) {
|
||||||
if (remove) {
|
if (remove) {
|
||||||
const { error } = await tryCatch(
|
const { error } = await tryCatch(
|
||||||
pg.none('delete from love_ships where ship_id = $1', [
|
pg.none('delete from profile_ships where ship_id = $1', [
|
||||||
existing.data.ship_id,
|
existing.data.ship_id,
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
@@ -48,7 +48,7 @@ export const shipProfiles: APIHandler<'ship-profiles'> = async (props, auth) =>
|
|||||||
|
|
||||||
// Insert the new ship
|
// Insert the new ship
|
||||||
const { data, error } = await tryCatch(
|
const { data, error } = await tryCatch(
|
||||||
insert(pg, 'love_ships', {
|
insert(pg, 'profile_ships', {
|
||||||
creator_id: creatorId,
|
creator_id: creatorId,
|
||||||
target1_id: targetUserId1,
|
target1_id: targetUserId1,
|
||||||
target2_id: targetUserId2,
|
target2_id: targetUserId2,
|
||||||
@@ -61,8 +61,8 @@ export const shipProfiles: APIHandler<'ship-profiles'> = async (props, auth) =>
|
|||||||
|
|
||||||
const continuation = async () => {
|
const continuation = async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
createLoveShipNotification(data, data.target1_id),
|
createProfileShipNotification(data, data.target1_id),
|
||||||
createLoveShipNotification(data, data.target2_id),
|
createProfileShipNotification(data, data.target2_id),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const starProfile: APIHandler<'star-profile'> = async (props, auth) => {
|
|||||||
if (remove) {
|
if (remove) {
|
||||||
const { error } = await tryCatch(
|
const { error } = await tryCatch(
|
||||||
pg.none(
|
pg.none(
|
||||||
'delete from love_stars where creator_id = $1 and target_id = $2',
|
'delete from profile_stars where creator_id = $1 and target_id = $2',
|
||||||
[creatorId, targetUserId]
|
[creatorId, targetUserId]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -27,8 +27,8 @@ export const starProfile: APIHandler<'star-profile'> = async (props, auth) => {
|
|||||||
|
|
||||||
// Check if star already exists
|
// Check if star already exists
|
||||||
const { data: existing } = await tryCatch(
|
const { data: existing } = await tryCatch(
|
||||||
pg.oneOrNone<Row<'love_stars'>>(
|
pg.oneOrNone<Row<'profile_stars'>>(
|
||||||
'select * from love_stars where creator_id = $1 and target_id = $2',
|
'select * from profile_stars where creator_id = $1 and target_id = $2',
|
||||||
[creatorId, targetUserId]
|
[creatorId, targetUserId]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -40,7 +40,7 @@ export const starProfile: APIHandler<'star-profile'> = async (props, auth) => {
|
|||||||
|
|
||||||
// Insert the new star
|
// Insert the new star
|
||||||
const { error } = await tryCatch(
|
const { error } = await tryCatch(
|
||||||
insert(pg, 'love_stars', { creator_id: creatorId, target_id: targetUserId })
|
insert(pg, 'profile_stars', { creator_id: creatorId, target_id: targetUserId })
|
||||||
)
|
)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { APIError, APIHandler } from 'api/helpers/endpoint'
|
import { APIError, APIHandler } from 'api/helpers/endpoint'
|
||||||
import { removePinnedUrlFromPhotoUrls } from 'shared/love/parse-photos'
|
import { removePinnedUrlFromPhotoUrls } from 'shared/profiles/parse-photos'
|
||||||
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
||||||
import { updateUser } from 'shared/supabase/users'
|
import { updateUser } from 'shared/supabase/users'
|
||||||
import { log } from 'shared/utils'
|
import { log } from 'shared/utils'
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ set -e
|
|||||||
SERVICE_NAME="api"
|
SERVICE_NAME="api"
|
||||||
SERVICE_GROUP="${SERVICE_NAME}-group"
|
SERVICE_GROUP="${SERVICE_NAME}-group"
|
||||||
ZONE="us-west1-c"
|
ZONE="us-west1-c"
|
||||||
ENV=${1:-dev}
|
#ENV=${1:-dev}
|
||||||
|
ENV=prod
|
||||||
|
|
||||||
case $ENV in
|
case $ENV in
|
||||||
dev)
|
dev)
|
||||||
@@ -28,10 +29,19 @@ INSTANCE_ID=$(gcloud compute instances list \
|
|||||||
--format="value(name)" \
|
--format="value(name)" \
|
||||||
--limit=1)
|
--limit=1)
|
||||||
|
|
||||||
echo "Forwarding debugging port 9229 to ${INSTANCE_ID}. Open chrome://inspect in Chrome to connect."
|
#echo "Forwarding debugging port 9229 to ${INSTANCE_ID}. Open chrome://inspect in Chrome to connect."
|
||||||
echo gcloud compute ssh ${INSTANCE_ID} --project=${GCLOUD_PROJECT} --zone=${ZONE}
|
|
||||||
gcloud compute ssh ${INSTANCE_ID} \
|
if [ "$1" = "logs" ]; then
|
||||||
--project=${GCLOUD_PROJECT} \
|
CMD=(--command="sudo docker logs -f \$(sudo docker ps -alq)")
|
||||||
--zone=${ZONE} \
|
else
|
||||||
|
CMD=()
|
||||||
|
fi
|
||||||
|
|
||||||
|
gcloud compute ssh "${INSTANCE_ID}" \
|
||||||
|
--project="${GCLOUD_PROJECT}" \
|
||||||
|
--zone="${ZONE}" \
|
||||||
|
"${CMD[@]}"
|
||||||
|
|
||||||
# -- \
|
# -- \
|
||||||
# -NL 9229:localhost:9229
|
# -NL 9229:localhost:9229
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"tsBuildInfoFile": "lib/tsconfig.tsbuildinfo",
|
"tsBuildInfoFile": "lib/tsconfig.tsbuildinfo",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
@@ -50,6 +51,7 @@
|
|||||||
"compileOnSave": true,
|
"compileOnSave": true,
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
"openapi.json"
|
"package.json",
|
||||||
|
"backend/api/package.json"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import {sendEmail} from './send-email'
|
|||||||
import {NewMessageEmail} from '../new-message'
|
import {NewMessageEmail} from '../new-message'
|
||||||
import {NewEndorsementEmail} from '../new-endorsement'
|
import {NewEndorsementEmail} from '../new-endorsement'
|
||||||
import {Test} from '../test'
|
import {Test} from '../test'
|
||||||
import {getProfile} from 'shared/love/supabase'
|
import {getProfile} from 'shared/profiles/supabase'
|
||||||
import { render } from "@react-email/render"
|
import { render } from "@react-email/render"
|
||||||
import {MatchesType} from "common/love/bookmarked_searches";
|
import {MatchesType} from "common/profiles/bookmarked_searches";
|
||||||
import NewSearchAlertsEmail from "email/new-search_alerts";
|
import NewSearchAlertsEmail from "email/new-search_alerts";
|
||||||
import WelcomeEmail from "email/welcome";
|
import WelcomeEmail from "email/welcome";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ProfileRow} from 'common/love/profile'
|
import {ProfileRow} from 'common/profiles/profile'
|
||||||
import type {User} from 'common/user'
|
import type {User} from 'common/user'
|
||||||
|
|
||||||
// for email template testing
|
// for email template testing
|
||||||
@@ -17,7 +17,6 @@ export const mockUser: User = {
|
|||||||
id: '0k1suGSJKVUnHbCPEhHNpgZPkUP2',
|
id: '0k1suGSJKVUnHbCPEhHNpgZPkUP2',
|
||||||
username: 'Sinclair',
|
username: 'Sinclair',
|
||||||
name: 'Sinclair',
|
name: 'Sinclair',
|
||||||
// url: 'https://manifold.love/Sinclair',
|
|
||||||
// isAdmin: true,
|
// isAdmin: true,
|
||||||
// isTrustworthy: false,
|
// isTrustworthy: false,
|
||||||
link: {
|
link: {
|
||||||
@@ -47,7 +46,7 @@ export const sinclairProfile: ProfileRow = {
|
|||||||
has_kids: 0,
|
has_kids: 0,
|
||||||
is_smoker: false,
|
is_smoker: false,
|
||||||
drinks_per_month: 0,
|
drinks_per_month: 0,
|
||||||
is_vegetarian_or_vegan: null,
|
diet: null,
|
||||||
political_beliefs: ['e/acc', 'libertarian'],
|
political_beliefs: ['e/acc', 'libertarian'],
|
||||||
religious_belief_strength: null,
|
religious_belief_strength: null,
|
||||||
religious_beliefs: null,
|
religious_beliefs: null,
|
||||||
@@ -148,7 +147,7 @@ export const jamesProfile: ProfileRow = {
|
|||||||
has_kids: 0,
|
has_kids: 0,
|
||||||
is_smoker: false,
|
is_smoker: false,
|
||||||
drinks_per_month: 5,
|
drinks_per_month: 5,
|
||||||
is_vegetarian_or_vegan: null,
|
diet: null,
|
||||||
political_beliefs: ['libertarian'],
|
political_beliefs: ['libertarian'],
|
||||||
religious_belief_strength: null,
|
religious_belief_strength: null,
|
||||||
religious_beliefs: '',
|
religious_beliefs: '',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {Body, Button, Container, Head, Html, Preview, Section, Text,} from '@react-email/components'
|
import {Body, Button, Container, Head, Html, Preview, Section, Text,} from '@react-email/components'
|
||||||
import {DOMAIN} from 'common/envs/constants'
|
import {DOMAIN} from 'common/envs/constants'
|
||||||
import {type ProfileRow} from 'common/love/profile'
|
import {type ProfileRow} from 'common/profiles/profile'
|
||||||
import {type User} from 'common/user'
|
import {type User} from 'common/user'
|
||||||
import {jamesProfile, jamesUser, mockUser} from './functions/mock'
|
import {jamesProfile, jamesUser, mockUser} from './functions/mock'
|
||||||
import {Footer} from "email/utils";
|
import {Footer} from "email/utils";
|
||||||
@@ -21,7 +21,7 @@ export const NewMatchEmail = ({
|
|||||||
email
|
email
|
||||||
}: NewMatchEmailProps) => {
|
}: NewMatchEmailProps) => {
|
||||||
const name = onUser.name.split(' ')[0]
|
const name = onUser.name.split(' ')[0]
|
||||||
// const userImgSrc = getLoveOgImageUrl(matchedWithUser, matchedProfile)
|
// const userImgSrc = getOgImageUrl(matchedWithUser, matchedProfile)
|
||||||
const userUrl = `https://${DOMAIN}/${matchedWithUser.username}`
|
const userUrl = `https://${DOMAIN}/${matchedWithUser.username}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {Body, Button, Container, Head, Html, Preview, Section, Text,} from '@react-email/components'
|
import {Body, Button, Container, Head, Html, Preview, Section, Text,} from '@react-email/components'
|
||||||
import {type User} from 'common/user'
|
import {type User} from 'common/user'
|
||||||
import {type ProfileRow} from 'common/love/profile'
|
import {type ProfileRow} from 'common/profiles/profile'
|
||||||
import {jamesProfile, jamesUser, mockUser,} from './functions/mock'
|
import {jamesProfile, jamesUser, mockUser,} from './functions/mock'
|
||||||
import {DOMAIN} from 'common/envs/constants'
|
import {DOMAIN} from 'common/envs/constants'
|
||||||
import {button, container, content, Footer, imageContainer, main, paragraph} from "email/utils";
|
import {button, container, content, Footer, imageContainer, main, paragraph} from "email/utils";
|
||||||
@@ -25,7 +25,7 @@ export const NewMessageEmail = ({
|
|||||||
const name = toUser.name.split(' ')[0]
|
const name = toUser.name.split(' ')[0]
|
||||||
const creatorName = fromUser.name
|
const creatorName = fromUser.name
|
||||||
const messagesUrl = `https://${DOMAIN}/messages/${channelId}`
|
const messagesUrl = `https://${DOMAIN}/messages/${channelId}`
|
||||||
// const userImgSrc = getLoveOgImageUrl(fromUser, fromUserProfile)
|
// const userImgSrc = getOgImageUrl(fromUser, fromUserProfile)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {type User} from 'common/user'
|
|||||||
import {mockUser,} from './functions/mock'
|
import {mockUser,} from './functions/mock'
|
||||||
import {DOMAIN} from 'common/envs/constants'
|
import {DOMAIN} from 'common/envs/constants'
|
||||||
import {container, content, Footer, main, paragraph} from "email/utils";
|
import {container, content, Footer, main, paragraph} from "email/utils";
|
||||||
import {MatchesType} from "common/love/bookmarked_searches";
|
import {MatchesType} from "common/profiles/bookmarked_searches";
|
||||||
import {formatFilters, locationType} from "common/searches"
|
import {formatFilters, locationType} from "common/searches"
|
||||||
import {FilterFields} from "common/filters";
|
import {FilterFields} from "common/filters";
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,6 @@ select
|
|||||||
from
|
from
|
||||||
temp_users;
|
temp_users;
|
||||||
|
|
||||||
-- Rename temp_love_messages
|
|
||||||
-- alter table temp_love_messages
|
|
||||||
-- rename to private_user_messages;
|
|
||||||
|
|
||||||
-- alter table private_user_messages
|
-- alter table private_user_messages
|
||||||
-- alter column channel_id set not null,
|
-- alter column channel_id set not null,
|
||||||
-- alter column content set not null,
|
-- alter column content set not null,
|
||||||
@@ -12,7 +12,7 @@ DB_USER="postgres"
|
|||||||
PORT="5432"
|
PORT="5432"
|
||||||
|
|
||||||
psql -U $DB_USER -d postgres -h $DB_NAME -p $PORT -w \
|
psql -U $DB_USER -d postgres -h $DB_NAME -p $PORT -w \
|
||||||
-f ./love-stars-dump.sql \
|
-f ./....sql \
|
||||||
|
|
||||||
|
|
||||||
# psql -U $DB_USER -d postgres -h $DB_NAME -p $PORT -w \
|
# psql -U $DB_USER -d postgres -h $DB_NAME -p $PORT -w \
|
||||||
@@ -25,8 +25,5 @@ psql -U $DB_USER -d postgres -h $DB_NAME -p $PORT -w \
|
|||||||
# -f ../supabase/private_users.sql \
|
# -f ../supabase/private_users.sql \
|
||||||
# -f ../supabase/users.sql
|
# -f ../supabase/users.sql
|
||||||
|
|
||||||
# psql -U $DB_USER -d postgres -h $DB_NAME -p $PORT -w \
|
|
||||||
# -f './import-love-finalize.sql'
|
|
||||||
|
|
||||||
echo "Done"
|
echo "Done"
|
||||||
)
|
)
|
||||||
@@ -1,19 +1,24 @@
|
|||||||
import {initAdmin} from 'shared/init-admin'
|
import {initAdmin} from 'shared/init-admin'
|
||||||
import {loadSecretsToEnv} from 'common/secrets'
|
|
||||||
import {createSupabaseDirectClient, type SupabaseDirectClient,} from 'shared/supabase/init'
|
import {createSupabaseDirectClient, type SupabaseDirectClient,} from 'shared/supabase/init'
|
||||||
import {getServiceAccountCredentials} from "shared/firebase-utils";
|
import {refreshConfig} from "common/envs/prod";
|
||||||
|
|
||||||
initAdmin()
|
|
||||||
|
|
||||||
export const runScript = async (
|
export const runScript = async (
|
||||||
main: (services: { pg: SupabaseDirectClient }) => Promise<any> | any
|
main: (services: { pg: SupabaseDirectClient }) => Promise<any> | any
|
||||||
) => {
|
) => {
|
||||||
const credentials = getServiceAccountCredentials()
|
initAdmin()
|
||||||
|
await initEnvVariables()
|
||||||
await loadSecretsToEnv(credentials)
|
console.debug('Environment variables in runScript:')
|
||||||
|
for (const k of Object.keys(process.env)) console.debug(`${k}=${process.env[k]}`)
|
||||||
|
|
||||||
|
console.debug('runScript: creating pg client...')
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
|
console.debug('runScript: running main...')
|
||||||
await main({pg})
|
await main({pg})
|
||||||
|
}
|
||||||
process.exit()
|
|
||||||
|
|
||||||
|
export async function initEnvVariables() {
|
||||||
|
const {config} = await import('dotenv')
|
||||||
|
config({ path: __dirname + '/../../.env' })
|
||||||
|
refreshConfig()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { createSupabaseDirectClient } from './supabase/init'
|
|||||||
import { getNotificationDestinationsForUser } from 'common/user-notification-preferences'
|
import { getNotificationDestinationsForUser } from 'common/user-notification-preferences'
|
||||||
import { Notification } from 'common/notifications'
|
import { Notification } from 'common/notifications'
|
||||||
import { insertNotificationToSupabase } from './supabase/notifications'
|
import { insertNotificationToSupabase } from './supabase/notifications'
|
||||||
import { getProfile } from './love/supabase'
|
import { getProfile } from 'shared/profiles/supabase'
|
||||||
|
|
||||||
export const createLoveLikeNotification = async (like: Row<'love_likes'>) => {
|
export const createProfileLikeNotification = async (like: Row<'profile_likes'>) => {
|
||||||
const { creator_id, target_id, like_id } = like
|
const { creator_id, target_id, like_id } = like
|
||||||
|
|
||||||
const targetPrivateUser = await getPrivateUser(target_id)
|
const targetPrivateUser = await getPrivateUser(target_id)
|
||||||
@@ -16,7 +16,7 @@ export const createLoveLikeNotification = async (like: Row<'love_likes'>) => {
|
|||||||
|
|
||||||
const { sendToBrowser } = getNotificationDestinationsForUser(
|
const { sendToBrowser } = getNotificationDestinationsForUser(
|
||||||
targetPrivateUser,
|
targetPrivateUser,
|
||||||
'new_love_like'
|
'new_profile_like'
|
||||||
)
|
)
|
||||||
if (!sendToBrowser) return
|
if (!sendToBrowser) return
|
||||||
|
|
||||||
@@ -24,11 +24,11 @@ export const createLoveLikeNotification = async (like: Row<'love_likes'>) => {
|
|||||||
const notification: Notification = {
|
const notification: Notification = {
|
||||||
id,
|
id,
|
||||||
userId: target_id,
|
userId: target_id,
|
||||||
reason: 'new_love_like',
|
reason: 'new_profile_like',
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
isSeen: false,
|
isSeen: false,
|
||||||
sourceId: like_id,
|
sourceId: like_id,
|
||||||
sourceType: 'love_like',
|
sourceType: 'profile_like',
|
||||||
sourceUpdateType: 'created',
|
sourceUpdateType: 'created',
|
||||||
sourceUserName: profile.user.name,
|
sourceUserName: profile.user.name,
|
||||||
sourceUserUsername: profile.user.username,
|
sourceUserUsername: profile.user.username,
|
||||||
@@ -39,8 +39,8 @@ export const createLoveLikeNotification = async (like: Row<'love_likes'>) => {
|
|||||||
return await insertNotificationToSupabase(notification, pg)
|
return await insertNotificationToSupabase(notification, pg)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createLoveShipNotification = async (
|
export const createProfileShipNotification = async (
|
||||||
ship: Row<'love_ships'>,
|
ship: Row<'profile_ships'>,
|
||||||
recipientId: string
|
recipientId: string
|
||||||
) => {
|
) => {
|
||||||
const { creator_id, target1_id, target2_id, ship_id } = ship
|
const { creator_id, target1_id, target2_id, ship_id } = ship
|
||||||
@@ -61,7 +61,7 @@ export const createLoveShipNotification = async (
|
|||||||
|
|
||||||
const { sendToBrowser } = getNotificationDestinationsForUser(
|
const { sendToBrowser } = getNotificationDestinationsForUser(
|
||||||
targetPrivateUser,
|
targetPrivateUser,
|
||||||
'new_love_ship'
|
'new_profile_ship'
|
||||||
)
|
)
|
||||||
if (!sendToBrowser) return
|
if (!sendToBrowser) return
|
||||||
|
|
||||||
@@ -69,11 +69,11 @@ export const createLoveShipNotification = async (
|
|||||||
const notification: Notification = {
|
const notification: Notification = {
|
||||||
id,
|
id,
|
||||||
userId: recipientId,
|
userId: recipientId,
|
||||||
reason: 'new_love_ship',
|
reason: 'new_profile_ship',
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
isSeen: false,
|
isSeen: false,
|
||||||
sourceId: ship_id,
|
sourceId: ship_id,
|
||||||
sourceType: 'love_ship',
|
sourceType: 'profile_ship',
|
||||||
sourceUpdateType: 'created',
|
sourceUpdateType: 'created',
|
||||||
sourceUserName: profile.user.name,
|
sourceUserName: profile.user.name,
|
||||||
sourceUserUsername: profile.user.username,
|
sourceUserUsername: profile.user.username,
|
||||||
58
backend/shared/src/encryption.ts
Normal file
58
backend/shared/src/encryption.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
import {ENV_CONFIG} from "common/envs/constants";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MASTER_KEY must be a 32-byte Buffer (AES-256).
|
||||||
|
* Load it from a secrets manager at runtime; do NOT hardcode.
|
||||||
|
*/
|
||||||
|
let _MASTER_KEY: Buffer | null = null
|
||||||
|
const getMasterKey = () => {
|
||||||
|
if (_MASTER_KEY) return _MASTER_KEY
|
||||||
|
|
||||||
|
if (ENV_CONFIG.dbEncryptionKey) {
|
||||||
|
const MASTER_KEY_BASE64 = ENV_CONFIG.dbEncryptionKey
|
||||||
|
_MASTER_KEY = Buffer.from(MASTER_KEY_BASE64, "base64")
|
||||||
|
if (_MASTER_KEY.length !== 32) throw new Error("MASTER_KEY must be 32 bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_MASTER_KEY) throw new Error("MASTER_KEY not set")
|
||||||
|
|
||||||
|
return _MASTER_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt a UTF-8 message string into base64 ciphertext + iv + tag.
|
||||||
|
* The IV makes the encryption probabilistic to ensure uniqueness in ciphertexts even when encrypting the same plaintext
|
||||||
|
* multiple times and has therefore no intent of being secret. The authentication tag works similar to a MAC.
|
||||||
|
* It's used to prove the authenticity and integrity of a message
|
||||||
|
*/
|
||||||
|
export function encryptMessage(plaintext: string) {
|
||||||
|
const iv = crypto.randomBytes(12); // 96-bit IV, recommended for AES-GCM
|
||||||
|
const cipher = crypto.createCipheriv("aes-256-gcm", getMasterKey(), iv);
|
||||||
|
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
||||||
|
const tag = cipher.getAuthTag();
|
||||||
|
|
||||||
|
// console.debug(plaintext, iv, ciphertext, tag)
|
||||||
|
|
||||||
|
return {
|
||||||
|
ciphertext: ciphertext.toString("base64"),
|
||||||
|
iv: iv.toString("base64"),
|
||||||
|
tag: tag.toString("base64"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt base64 ciphertext + iv + tag -> plaintext string.
|
||||||
|
* Throws on auth failure.
|
||||||
|
*/
|
||||||
|
export function decryptMessage({ciphertext, iv, tag}: { ciphertext: string; iv: string; tag: string; }) {
|
||||||
|
const ivBuf = Buffer.from(iv, "base64");
|
||||||
|
const ctBuf = Buffer.from(ciphertext, "base64");
|
||||||
|
const tagBuf = Buffer.from(tag, "base64");
|
||||||
|
|
||||||
|
const decipher = crypto.createDecipheriv("aes-256-gcm", getMasterKey(), ivBuf);
|
||||||
|
decipher.setAuthTag(tagBuf);
|
||||||
|
const plaintext = Buffer.concat([decipher.update(ctBuf), decipher.final()]).toString("utf8");
|
||||||
|
// console.debug("Decrypted message:", plaintext);
|
||||||
|
return plaintext;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { areGenderCompatible } from 'common/love/compatibility-util'
|
import { areGenderCompatible } from 'common/profiles/compatibility-util'
|
||||||
import { type Profile, type ProfileRow } from 'common/love/profile'
|
import { type Profile, type ProfileRow } from 'common/profiles/profile'
|
||||||
import { type User } from 'common/user'
|
import { type User } from 'common/user'
|
||||||
import { Row } from 'common/supabase/utils'
|
import { Row } from 'common/supabase/utils'
|
||||||
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
||||||
@@ -24,14 +24,14 @@ export function convertRow(row: ProfileAndUserRow | undefined): Profile | null {
|
|||||||
return profile as Profile
|
return profile as Profile
|
||||||
}
|
}
|
||||||
|
|
||||||
const LOVER_COLS = 'profiles.*, name, username, users.data as user'
|
const PROFILE_COLS = 'profiles.*, name, username, users.data as user'
|
||||||
|
|
||||||
export const getProfile = async (userId: string) => {
|
export const getProfile = async (userId: string) => {
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
return await pg.oneOrNone(
|
return await pg.oneOrNone(
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
${LOVER_COLS}
|
${PROFILE_COLS}
|
||||||
from
|
from
|
||||||
profiles
|
profiles
|
||||||
join
|
join
|
||||||
@@ -49,7 +49,7 @@ export const getProfiles = async (userIds: string[]) => {
|
|||||||
return await pg.map(
|
return await pg.map(
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
${LOVER_COLS}
|
${PROFILE_COLS}
|
||||||
from
|
from
|
||||||
profiles
|
profiles
|
||||||
join
|
join
|
||||||
@@ -67,7 +67,7 @@ export const getGenderCompatibleProfiles = async (profile: ProfileRow) => {
|
|||||||
const profiles = await pg.map(
|
const profiles = await pg.map(
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
${LOVER_COLS}
|
${PROFILE_COLS}
|
||||||
from profiles
|
from profiles
|
||||||
join
|
join
|
||||||
users on users.id = profiles.user_id
|
users on users.id = profiles.user_id
|
||||||
@@ -92,7 +92,7 @@ export const getCompatibleProfiles = async (
|
|||||||
return await pg.map(
|
return await pg.map(
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
${LOVER_COLS}
|
${PROFILE_COLS}
|
||||||
from profiles
|
from profiles
|
||||||
join
|
join
|
||||||
users on users.id = profiles.user_id
|
users on users.id = profiles.user_id
|
||||||
@@ -122,9 +122,9 @@ export const getCompatibleProfiles = async (
|
|||||||
|
|
||||||
export const getCompatibilityAnswers = async (userIds: string[]) => {
|
export const getCompatibilityAnswers = async (userIds: string[]) => {
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
return await pg.manyOrNone<Row<'love_compatibility_answers'>>(
|
return await pg.manyOrNone<Row<'compatibility_answers'>>(
|
||||||
`
|
`
|
||||||
select * from love_compatibility_answers
|
select * from compatibility_answers
|
||||||
where creator_id = any($1)
|
where creator_id = any($1)
|
||||||
`,
|
`,
|
||||||
[userIds]
|
[userIds]
|
||||||
@@ -11,7 +11,7 @@ export {SupabaseClient} from 'common/supabase/utils'
|
|||||||
export const pgp = pgPromise({
|
export const pgp = pgPromise({
|
||||||
error(err: any, e: pgPromise.IEventContext) {
|
error(err: any, e: pgPromise.IEventContext) {
|
||||||
// Read more: https://node-postgres.com/apis/pool#error
|
// Read more: https://node-postgres.com/apis/pool#error
|
||||||
log.error('pgPromise background error', {
|
log.error(`pgPromise background error: ${err?.detail}`, {
|
||||||
error: err,
|
error: err,
|
||||||
event: e,
|
event: e,
|
||||||
})
|
})
|
||||||
|
|||||||
48
backend/shared/src/supabase/messages.ts
Normal file
48
backend/shared/src/supabase/messages.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import {convertSQLtoTS, Row, tsToMillis} from "common/supabase/utils";
|
||||||
|
import {ChatMessage, PrivateChatMessage} from "common/chat-message";
|
||||||
|
import {decryptMessage} from "shared/encryption";
|
||||||
|
|
||||||
|
export type DbPrivateChatMessage = PrivateChatMessage & {
|
||||||
|
ciphertext: string
|
||||||
|
iv: string
|
||||||
|
tag: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const convertChatMessage = (row: Row<'private_user_messages'>) =>
|
||||||
|
convertSQLtoTS<'private_user_messages', ChatMessage>(row, {
|
||||||
|
created_time: tsToMillis as any,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const convertPrivateChatMessage = (row: Row<'private_user_messages'>) => {
|
||||||
|
const message = convertSQLtoTS<'private_user_messages', DbPrivateChatMessage>(
|
||||||
|
row,
|
||||||
|
{created_time: tsToMillis as any,}
|
||||||
|
);
|
||||||
|
parseMessageObject(message);
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageObject = Omit<ChatMessage, "id"> & { id: number; createdTimeTs: string } & {
|
||||||
|
ciphertext: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseMessageObject(message: MessageObject) {
|
||||||
|
if (message.ciphertext && message.iv && message.tag) {
|
||||||
|
const plaintText = decryptMessage({
|
||||||
|
ciphertext: message.ciphertext,
|
||||||
|
iv: message.iv,
|
||||||
|
tag: message.tag,
|
||||||
|
});
|
||||||
|
message.content = JSON.parse(plaintText)
|
||||||
|
delete (message as any).ciphertext
|
||||||
|
delete (message as any).iv
|
||||||
|
delete (message as any).tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDecryptedMessage(message: MessageObject) {
|
||||||
|
parseMessageObject(message)
|
||||||
|
return message.content
|
||||||
|
}
|
||||||
@@ -8,6 +8,13 @@ CREATE TABLE IF NOT EXISTS bookmarked_searches (
|
|||||||
search_name TEXT DEFAULT NULL
|
search_name TEXT DEFAULT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ALTER TABLE bookmarked_searches
|
||||||
|
ADD CONSTRAINT bookmarked_searches_creator_id_fkey
|
||||||
|
FOREIGN KEY (creator_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
|
||||||
|
|
||||||
-- Row Level Security
|
-- Row Level Security
|
||||||
ALTER TABLE bookmarked_searches ENABLE ROW LEVEL SECURITY;
|
ALTER TABLE bookmarked_searches ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
@@ -17,17 +24,17 @@ DROP POLICY IF EXISTS "public read" ON bookmarked_searches;
|
|||||||
CREATE POLICY "public read" ON bookmarked_searches
|
CREATE POLICY "public read" ON bookmarked_searches
|
||||||
FOR SELECT USING (true);
|
FOR SELECT USING (true);
|
||||||
|
|
||||||
DROP POLICY IF EXISTS "self delete" ON bookmarked_searches;
|
-- DROP POLICY IF EXISTS "self delete" ON bookmarked_searches;
|
||||||
CREATE POLICY "self delete" ON bookmarked_searches
|
-- CREATE POLICY "self delete" ON bookmarked_searches
|
||||||
FOR DELETE USING (creator_id = firebase_uid());
|
-- FOR DELETE USING (creator_id = firebase_uid());
|
||||||
|
--
|
||||||
DROP POLICY IF EXISTS "self insert" ON bookmarked_searches;
|
-- DROP POLICY IF EXISTS "self insert" ON bookmarked_searches;
|
||||||
CREATE POLICY "self insert" ON bookmarked_searches
|
-- CREATE POLICY "self insert" ON bookmarked_searches
|
||||||
FOR INSERT WITH CHECK (creator_id = firebase_uid());
|
-- FOR INSERT WITH CHECK (creator_id = firebase_uid());
|
||||||
|
--
|
||||||
DROP POLICY IF EXISTS "self update" ON bookmarked_searches;
|
-- DROP POLICY IF EXISTS "self update" ON bookmarked_searches;
|
||||||
CREATE POLICY "self update" ON bookmarked_searches
|
-- CREATE POLICY "self update" ON bookmarked_searches
|
||||||
FOR UPDATE USING (creator_id = firebase_uid());
|
-- FOR UPDATE USING (creator_id = firebase_uid());
|
||||||
|
|
||||||
-- Indexes
|
-- Indexes
|
||||||
CREATE INDEX IF NOT EXISTS bookmarked_searches_creator_id_created_time_idx
|
CREATE INDEX IF NOT EXISTS bookmarked_searches_creator_id_created_time_idx
|
||||||
|
|||||||
52
backend/supabase/compatibility_answers.sql
Normal file
52
backend/supabase/compatibility_answers.sql
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS compatibility_answers (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
|
creator_id TEXT NOT NULL,
|
||||||
|
explanation TEXT,
|
||||||
|
importance INTEGER NOT NULL,
|
||||||
|
multiple_choice INTEGER NOT NULL,
|
||||||
|
pref_choices INTEGER[] NOT NULL,
|
||||||
|
question_id BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE compatibility_answers
|
||||||
|
ADD CONSTRAINT compatibility_answers_creator_id_fkey
|
||||||
|
FOREIGN KEY (creator_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
|
||||||
|
|
||||||
|
-- Row Level Security
|
||||||
|
ALTER TABLE compatibility_answers ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
ALTER TABLE compatibility_answers
|
||||||
|
ADD CONSTRAINT unique_question_creator
|
||||||
|
UNIQUE (question_id, creator_id);
|
||||||
|
|
||||||
|
|
||||||
|
-- Policies
|
||||||
|
DROP POLICY IF EXISTS "public read" ON compatibility_answers;
|
||||||
|
CREATE POLICY "public read" ON compatibility_answers
|
||||||
|
FOR SELECT USING (true);
|
||||||
|
|
||||||
|
-- DROP POLICY IF EXISTS "self delete" ON compatibility_answers;
|
||||||
|
-- CREATE POLICY "self delete" ON compatibility_answers
|
||||||
|
-- FOR DELETE USING (creator_id = firebase_uid());
|
||||||
|
--
|
||||||
|
-- DROP POLICY IF EXISTS "self insert" ON compatibility_answers;
|
||||||
|
-- CREATE POLICY "self insert" ON compatibility_answers
|
||||||
|
-- FOR INSERT WITH CHECK (creator_id = firebase_uid());
|
||||||
|
--
|
||||||
|
-- DROP POLICY IF EXISTS "self update" ON compatibility_answers;
|
||||||
|
-- CREATE POLICY "self update" ON compatibility_answers
|
||||||
|
-- FOR UPDATE USING (creator_id = firebase_uid());
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS compatibility_answers_creator_id_created_time_idx
|
||||||
|
ON public.compatibility_answers (creator_id, created_time DESC);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS compatibility_answers_question_creator_unique
|
||||||
|
ON public.compatibility_answers (question_id, creator_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS compatibility_answers_question_id_idx
|
||||||
|
ON public.compatibility_answers (question_id);
|
||||||
45
backend/supabase/compatibility_answers_free.sql
Normal file
45
backend/supabase/compatibility_answers_free.sql
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS compatibility_answers_free (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
|
creator_id TEXT NOT NULL,
|
||||||
|
free_response TEXT,
|
||||||
|
integer INTEGER,
|
||||||
|
multiple_choice INTEGER,
|
||||||
|
question_id BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE compatibility_answers_free
|
||||||
|
ADD CONSTRAINT compatibility_answers_free_creator_id_fkey
|
||||||
|
FOREIGN KEY (creator_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
|
||||||
|
|
||||||
|
-- Row Level Security
|
||||||
|
ALTER TABLE compatibility_answers_free ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Policies
|
||||||
|
DROP POLICY IF EXISTS "public read" ON compatibility_answers_free;
|
||||||
|
CREATE POLICY "public read" ON compatibility_answers_free FOR SELECT USING (true);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "self delete" ON compatibility_answers_free;
|
||||||
|
CREATE POLICY "self delete" ON compatibility_answers_free FOR DELETE USING (creator_id = firebase_uid());
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "self insert" ON compatibility_answers_free;
|
||||||
|
CREATE POLICY "self insert" ON compatibility_answers_free FOR INSERT WITH CHECK (creator_id = firebase_uid());
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "self update" ON compatibility_answers_free;
|
||||||
|
CREATE POLICY "self update" ON compatibility_answers_free FOR UPDATE USING (creator_id = firebase_uid());
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
DROP INDEX IF EXISTS compatibility_answers_free_creator_id_created_time_idx;
|
||||||
|
CREATE INDEX IF NOT EXISTS compatibility_answers_free_creator_id_created_time_idx
|
||||||
|
ON public.compatibility_answers_free USING btree (creator_id, created_time DESC);
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS compatibility_answers_free_question_creator_unique;
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS compatibility_answers_free_question_creator_unique
|
||||||
|
ON public.compatibility_answers_free USING btree (question_id, creator_id);
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS compatibility_answers_free_question_id_idx;
|
||||||
|
CREATE INDEX IF NOT EXISTS compatibility_answers_free_question_id_idx
|
||||||
|
ON public.compatibility_answers_free USING btree (question_id);
|
||||||
28
backend/supabase/compatibility_prompts.sql
Normal file
28
backend/supabase/compatibility_prompts.sql
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS compatibility_prompts (
|
||||||
|
answer_type TEXT DEFAULT 'free_response' NOT NULL,
|
||||||
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
|
creator_id TEXT,
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
importance_score NUMERIC DEFAULT 0 NOT NULL,
|
||||||
|
multiple_choice_options JSONB,
|
||||||
|
question TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE compatibility_prompts
|
||||||
|
ADD CONSTRAINT compatibility_prompts_creator_id_fkey
|
||||||
|
FOREIGN KEY (creator_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
|
||||||
|
|
||||||
|
-- Row Level Security
|
||||||
|
ALTER TABLE compatibility_prompts ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Policies
|
||||||
|
DROP POLICY IF EXISTS "public read" ON compatibility_prompts;
|
||||||
|
CREATE POLICY "public read" ON compatibility_prompts
|
||||||
|
FOR SELECT USING (true);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
-- The primary key automatically creates a unique index on (id),
|
||||||
|
-- so the explicit index on id is redundant and removed.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
create table if not exists
|
create table if not exists
|
||||||
contact (
|
contact (
|
||||||
id text default uuid_generate_v4 () not null,
|
id text default uuid_generate_v4 () not null primary key,
|
||||||
created_time timestamp with time zone default now(),
|
created_time timestamp with time zone default now(),
|
||||||
user_id text,
|
user_id text,
|
||||||
content jsonb
|
content jsonb
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
create
|
create
|
||||||
or replace function public.to_jsonb (jsonb) returns jsonb language sql immutable parallel SAFE strict as $function$ select $1 $function$;
|
or replace function public.to_jsonb (jsonb) returns jsonb language sql immutable parallel SAFE strict as $function$ select $1 $function$;
|
||||||
|
|
||||||
create
|
-- create
|
||||||
or replace function public.ts_to_millis (ts timestamp without time zone) returns bigint language sql immutable parallel SAFE as $function$
|
-- or replace function public.ts_to_millis (ts timestamp without time zone) returns bigint language sql immutable parallel SAFE as $function$
|
||||||
select extract(epoch from ts)::bigint * 1000
|
-- select extract(epoch from ts)::bigint * 1000
|
||||||
$function$;
|
-- $function$;
|
||||||
|
--
|
||||||
create
|
-- create
|
||||||
or replace function public.ts_to_millis (ts timestamp with time zone) returns bigint language sql immutable parallel SAFE as $function$
|
-- or replace function public.ts_to_millis (ts timestamp with time zone) returns bigint language sql immutable parallel SAFE as $function$
|
||||||
select (extract(epoch from ts) * 1000)::bigint
|
-- select (extract(epoch from ts) * 1000)::bigint
|
||||||
$function$;
|
-- $function$;
|
||||||
|
|
||||||
create
|
create
|
||||||
or replace function public.millis_to_ts (millis bigint) returns timestamp with time zone language sql immutable parallel SAFE as $function$
|
or replace function public.millis_to_ts (millis bigint) returns timestamp with time zone language sql immutable parallel SAFE as $function$
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
|
|
||||||
|
|
||||||
create
|
create
|
||||||
or replace function public.get_compatibility_questions_with_answer_count () returns setof record language plpgsql as $function$
|
or replace function public.get_compatibility_prompts_with_answer_count () returns setof record language plpgsql as $function$
|
||||||
BEGIN
|
BEGIN
|
||||||
RETURN QUERY
|
RETURN QUERY
|
||||||
SELECT
|
SELECT
|
||||||
love_questions.*,
|
compatibility_prompts.*,
|
||||||
COUNT(love_compatibility_answers.question_id) as answer_count
|
COUNT(compatibility_answers.question_id) as answer_count
|
||||||
FROM
|
FROM
|
||||||
love_questions
|
compatibility_prompts
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
love_compatibility_answers ON love_questions.id = love_compatibility_answers.question_id
|
compatibility_answers ON compatibility_prompts.id = compatibility_answers.question_id
|
||||||
WHERE love_questions.answer_type='compatibility_multiple_choice'
|
WHERE compatibility_prompts.answer_type='compatibility_multiple_choice'
|
||||||
GROUP BY
|
GROUP BY
|
||||||
love_questions.id
|
compatibility_prompts.id
|
||||||
ORDER BY
|
ORDER BY
|
||||||
answer_count DESC;
|
answer_count DESC;
|
||||||
END;
|
END;
|
||||||
$function$;
|
$function$;
|
||||||
|
|
||||||
create
|
create
|
||||||
or replace function public.get_love_question_answers_and_profiles (p_question_id bigint) returns setof record language plpgsql as $function$
|
or replace function public.get_compatibility_answers_and_profiles (p_question_id bigint) returns setof record language plpgsql as $function$
|
||||||
BEGIN
|
BEGIN
|
||||||
RETURN QUERY
|
RETURN QUERY
|
||||||
SELECT
|
SELECT
|
||||||
love_answers.question_id,
|
compatibility_answers_free.question_id,
|
||||||
love_answers.created_time,
|
compatibility_answers_free.created_time,
|
||||||
love_answers.free_response,
|
compatibility_answers_free.free_response,
|
||||||
love_answers.multiple_choice,
|
compatibility_answers_free.multiple_choice,
|
||||||
love_answers.integer,
|
compatibility_answers_free.integer,
|
||||||
profiles.age,
|
profiles.age,
|
||||||
profiles.gender,
|
profiles.gender,
|
||||||
profiles.city,
|
profiles.city,
|
||||||
@@ -36,11 +36,11 @@ SELECT
|
|||||||
FROM
|
FROM
|
||||||
profiles
|
profiles
|
||||||
JOIN
|
JOIN
|
||||||
love_answers ON profiles.user_id = love_answers.creator_id
|
compatibility_answers_free ON profiles.user_id = compatibility_answers_free.creator_id
|
||||||
join
|
join
|
||||||
users on profiles.user_id = users.id
|
users on profiles.user_id = users.id
|
||||||
WHERE
|
WHERE
|
||||||
love_answers.question_id = p_question_id
|
compatibility_answers_free.question_id = p_question_id
|
||||||
order by love_answers.created_time desc;
|
order by compatibility_answers_free.created_time desc;
|
||||||
END;
|
END;
|
||||||
$function$;
|
$function$;
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS love_answers (
|
|
||||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
creator_id TEXT NOT NULL,
|
|
||||||
free_response TEXT,
|
|
||||||
integer INTEGER,
|
|
||||||
multiple_choice INTEGER,
|
|
||||||
question_id BIGINT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Row Level Security
|
|
||||||
ALTER TABLE love_answers ENABLE ROW LEVEL SECURITY;
|
|
||||||
|
|
||||||
-- Policies
|
|
||||||
DROP POLICY IF EXISTS "public read" ON love_answers;
|
|
||||||
CREATE POLICY "public read" ON love_answers FOR SELECT USING (true);
|
|
||||||
|
|
||||||
DROP POLICY IF EXISTS "self delete" ON love_answers;
|
|
||||||
CREATE POLICY "self delete" ON love_answers FOR DELETE USING (creator_id = firebase_uid());
|
|
||||||
|
|
||||||
DROP POLICY IF EXISTS "self insert" ON love_answers;
|
|
||||||
CREATE POLICY "self insert" ON love_answers FOR INSERT WITH CHECK (creator_id = firebase_uid());
|
|
||||||
|
|
||||||
DROP POLICY IF EXISTS "self update" ON love_answers;
|
|
||||||
CREATE POLICY "self update" ON love_answers FOR UPDATE USING (creator_id = firebase_uid());
|
|
||||||
|
|
||||||
-- Indexes
|
|
||||||
DROP INDEX IF EXISTS love_answers_creator_id_created_time_idx;
|
|
||||||
CREATE INDEX IF NOT EXISTS love_answers_creator_id_created_time_idx
|
|
||||||
ON public.love_answers USING btree (creator_id, created_time DESC);
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS love_answers_question_creator_unique;
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS love_answers_question_creator_unique
|
|
||||||
ON public.love_answers USING btree (question_id, creator_id);
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS love_answers_question_id_idx;
|
|
||||||
CREATE INDEX IF NOT EXISTS love_answers_question_id_idx
|
|
||||||
ON public.love_answers USING btree (question_id);
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS love_compatibility_answers (
|
|
||||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
creator_id TEXT NOT NULL,
|
|
||||||
explanation TEXT,
|
|
||||||
importance INTEGER NOT NULL,
|
|
||||||
multiple_choice INTEGER NOT NULL,
|
|
||||||
pref_choices INTEGER[] NOT NULL,
|
|
||||||
question_id BIGINT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Row Level Security
|
|
||||||
ALTER TABLE love_compatibility_answers ENABLE ROW LEVEL SECURITY;
|
|
||||||
|
|
||||||
ALTER TABLE love_compatibility_answers
|
|
||||||
ADD CONSTRAINT unique_question_creator
|
|
||||||
UNIQUE (question_id, creator_id);
|
|
||||||
|
|
||||||
|
|
||||||
-- Policies
|
|
||||||
DROP POLICY IF EXISTS "public read" ON love_compatibility_answers;
|
|
||||||
CREATE POLICY "public read" ON love_compatibility_answers
|
|
||||||
FOR SELECT USING (true);
|
|
||||||
|
|
||||||
DROP POLICY IF EXISTS "self delete" ON love_compatibility_answers;
|
|
||||||
CREATE POLICY "self delete" ON love_compatibility_answers
|
|
||||||
FOR DELETE USING (creator_id = firebase_uid());
|
|
||||||
|
|
||||||
DROP POLICY IF EXISTS "self insert" ON love_compatibility_answers;
|
|
||||||
CREATE POLICY "self insert" ON love_compatibility_answers
|
|
||||||
FOR INSERT WITH CHECK (creator_id = firebase_uid());
|
|
||||||
|
|
||||||
DROP POLICY IF EXISTS "self update" ON love_compatibility_answers;
|
|
||||||
CREATE POLICY "self update" ON love_compatibility_answers
|
|
||||||
FOR UPDATE USING (creator_id = firebase_uid());
|
|
||||||
|
|
||||||
-- Indexes
|
|
||||||
CREATE INDEX IF NOT EXISTS love_compatibility_answers_creator_id_created_time_idx
|
|
||||||
ON public.love_compatibility_answers (creator_id, created_time DESC);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS love_compatibility_answers_question_creator_unique
|
|
||||||
ON public.love_compatibility_answers (question_id, creator_id);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS love_compatibility_answers_question_id_idx
|
|
||||||
ON public.love_compatibility_answers (question_id);
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS love_likes (
|
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
creator_id TEXT NOT NULL,
|
|
||||||
like_id TEXT DEFAULT random_alphanumeric(12) NOT NULL,
|
|
||||||
target_id TEXT NOT NULL,
|
|
||||||
CONSTRAINT love_likes_pkey PRIMARY KEY (creator_id, like_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Row Level Security
|
|
||||||
ALTER TABLE love_likes ENABLE ROW LEVEL SECURITY;
|
|
||||||
|
|
||||||
-- Policies
|
|
||||||
DROP POLICY IF EXISTS "public read" ON love_likes;
|
|
||||||
CREATE POLICY "public read" ON love_likes
|
|
||||||
FOR SELECT USING (true);
|
|
||||||
|
|
||||||
-- Indexes
|
|
||||||
-- The primary key already creates a unique index on (creator_id, like_id)
|
|
||||||
-- so we do not recreate that. Additional indexes:
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS user_likes_target_id_raw
|
|
||||||
ON public.love_likes (target_id);
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS love_questions (
|
|
||||||
answer_type TEXT DEFAULT 'free_response' NOT NULL,
|
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
creator_id TEXT NOT NULL,
|
|
||||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
||||||
importance_score NUMERIC DEFAULT 0 NOT NULL,
|
|
||||||
multiple_choice_options JSONB,
|
|
||||||
question TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Row Level Security
|
|
||||||
ALTER TABLE love_questions ENABLE ROW LEVEL SECURITY;
|
|
||||||
|
|
||||||
-- Policies
|
|
||||||
DROP POLICY IF EXISTS "public read" ON love_questions;
|
|
||||||
CREATE POLICY "public read" ON love_questions
|
|
||||||
FOR ALL USING (true);
|
|
||||||
|
|
||||||
-- Indexes
|
|
||||||
-- The primary key automatically creates a unique index on (id),
|
|
||||||
-- so the explicit index on id is redundant and removed.
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS love_ships (
|
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
creator_id TEXT NOT NULL,
|
|
||||||
ship_id TEXT DEFAULT random_alphanumeric(12) NOT NULL,
|
|
||||||
target1_id TEXT NOT NULL,
|
|
||||||
target2_id TEXT NOT NULL,
|
|
||||||
CONSTRAINT love_ships_pkey PRIMARY KEY (creator_id, ship_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Row Level Security
|
|
||||||
ALTER TABLE love_ships ENABLE ROW LEVEL SECURITY;
|
|
||||||
|
|
||||||
-- Policies
|
|
||||||
DROP POLICY IF EXISTS "public read" ON love_ships;
|
|
||||||
CREATE POLICY "public read" ON love_ships
|
|
||||||
FOR SELECT USING (true);
|
|
||||||
|
|
||||||
-- Indexes
|
|
||||||
-- Primary key automatically creates a unique index on (creator_id, ship_id), so no need to recreate it.
|
|
||||||
-- Keep additional indexes for query optimization:
|
|
||||||
DROP INDEX IF EXISTS love_ships_target1_id;
|
|
||||||
CREATE INDEX love_ships_target1_id ON public.love_ships USING btree (target1_id);
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS love_ships_target2_id;
|
|
||||||
CREATE INDEX love_ships_target2_id ON public.love_ships USING btree (target2_id);
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS love_stars (
|
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
creator_id TEXT NOT NULL,
|
|
||||||
star_id TEXT DEFAULT random_alphanumeric(12) NOT NULL,
|
|
||||||
target_id TEXT NOT NULL,
|
|
||||||
CONSTRAINT love_stars_pkey PRIMARY KEY (creator_id, star_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Row Level Security
|
|
||||||
ALTER TABLE love_stars ENABLE ROW LEVEL SECURITY;
|
|
||||||
|
|
||||||
-- Policies
|
|
||||||
DROP POLICY IF EXISTS "public read" ON love_stars;
|
|
||||||
CREATE POLICY "public read" ON love_stars
|
|
||||||
FOR SELECT USING (true);
|
|
||||||
|
|
||||||
-- Indexes
|
|
||||||
-- The primary key already creates a unique index on (creator_id, star_id), so no need to recreate it.
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS love_stars_target_id_idx;
|
|
||||||
CREATE INDEX love_stars_target_id_idx ON public.love_stars USING btree (target_id);
|
|
||||||
@@ -8,14 +8,14 @@ BEGIN;
|
|||||||
\i backend/supabase/private_users.sql
|
\i backend/supabase/private_users.sql
|
||||||
\i backend/supabase/private_user_messages.sql
|
\i backend/supabase/private_user_messages.sql
|
||||||
\i backend/supabase/private_user_seen_message_channels.sql
|
\i backend/supabase/private_user_seen_message_channels.sql
|
||||||
\i backend/supabase/love_answers.sql
|
\i backend/supabase/compatibility_answers_free.sql
|
||||||
\i backend/supabase/profile_comments.sql
|
\i backend/supabase/profile_comments.sql
|
||||||
\i backend/supabase/love_compatibility_answers.sql
|
\i backend/supabase/compatibility_answers.sql
|
||||||
\i backend/supabase/love_likes.sql
|
\i backend/supabase/profile_likes.sql
|
||||||
\i backend/supabase/love_questions.sql
|
\i backend/supabase/compatibility_prompts.sql
|
||||||
\i backend/supabase/love_ships.sql
|
\i backend/supabase/profile_ships.sql
|
||||||
\i backend/supabase/love_stars.sql
|
\i backend/supabase/profile_stars.sql
|
||||||
\i backend/supabase/love_waitlist.sql
|
\i backend/supabase/user_waitlist.sql
|
||||||
\i backend/supabase/user_events.sql
|
\i backend/supabase/user_events.sql
|
||||||
\i backend/supabase/user_notifications.sql
|
\i backend/supabase/user_notifications.sql
|
||||||
\i backend/supabase/functions_others.sql
|
\i backend/supabase/functions_others.sql
|
||||||
|
|||||||
0
backend/supabase/migrations/.keep
Normal file
0
backend/supabase/migrations/.keep
Normal file
@@ -10,6 +10,12 @@ CREATE TABLE IF NOT EXISTS private_user_message_channel_members (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- Foreign Keys
|
-- Foreign Keys
|
||||||
|
ALTER TABLE private_user_message_channel_members
|
||||||
|
ADD CONSTRAINT private_user_message_channel_members_user_id_fkey
|
||||||
|
FOREIGN KEY (user_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF NOT EXISTS (
|
IF NOT EXISTS (
|
||||||
@@ -30,12 +36,10 @@ END$$;
|
|||||||
ALTER TABLE private_user_message_channel_members ENABLE ROW LEVEL SECURITY;
|
ALTER TABLE private_user_message_channel_members ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
-- Indexes
|
-- Indexes
|
||||||
DROP INDEX IF EXISTS pumcm_members_idx;
|
CREATE INDEX IF NOT EXISTS pumcm_members_idx
|
||||||
CREATE INDEX pumcm_members_idx
|
|
||||||
ON public.private_user_message_channel_members
|
ON public.private_user_message_channel_members
|
||||||
USING btree (channel_id, user_id);
|
USING btree (channel_id, user_id);
|
||||||
|
|
||||||
DROP INDEX IF EXISTS unique_user_channel;
|
CREATE UNIQUE INDEX IF NOT EXISTS unique_user_channel
|
||||||
CREATE UNIQUE INDEX unique_user_channel
|
|
||||||
ON public.private_user_message_channel_members
|
ON public.private_user_message_channel_members
|
||||||
USING btree (channel_id, user_id);
|
USING btree (channel_id, user_id);
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ CREATE TABLE IF NOT EXISTS private_user_message_channels (
|
|||||||
-- Row Level Security
|
-- Row Level Security
|
||||||
ALTER TABLE private_user_message_channels ENABLE ROW LEVEL SECURITY;
|
ALTER TABLE private_user_message_channels ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Policies
|
-- Policies
|
||||||
DROP POLICY IF EXISTS "public read" ON private_user_message_channels;
|
DROP POLICY IF EXISTS "public read" ON private_user_message_channels;
|
||||||
|
|
||||||
CREATE POLICY "public read" ON private_user_message_channels
|
CREATE POLICY "public read" ON private_user_message_channels
|
||||||
FOR ALL
|
FOR SELECT USING (true);
|
||||||
USING (true);
|
|
||||||
|
|
||||||
-- Indexes
|
-- Indexes
|
||||||
-- Removed redundant primary key index creation because PRIMARY KEY already creates a unique index on id
|
-- Removed redundant primary key index creation because PRIMARY KEY already creates a unique index on id
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
CREATE TABLE IF NOT EXISTS private_user_messages (
|
CREATE TABLE IF NOT EXISTS private_user_messages (
|
||||||
channel_id BIGINT NOT NULL,
|
channel_id BIGINT NOT NULL,
|
||||||
content JSONB NOT NULL,
|
content JSONB,
|
||||||
|
ciphertext text, -- base64
|
||||||
|
iv text, -- base64
|
||||||
|
tag text, -- base64 (GCM auth tag)
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
old_id BIGINT,
|
user_id TEXT,
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
visibility TEXT DEFAULT 'private'::TEXT NOT NULL,
|
visibility TEXT DEFAULT 'private'::TEXT NOT NULL,
|
||||||
CONSTRAINT private_user_messages_channel_id_fkey
|
CONSTRAINT private_user_messages_channel_id_fkey
|
||||||
FOREIGN KEY (channel_id)
|
FOREIGN KEY (channel_id)
|
||||||
@@ -12,11 +14,16 @@ CREATE TABLE IF NOT EXISTS private_user_messages (
|
|||||||
ON UPDATE CASCADE ON DELETE CASCADE
|
ON UPDATE CASCADE ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ALTER TABLE private_user_messages
|
||||||
|
ADD CONSTRAINT private_user_messages_user_id_fkey
|
||||||
|
FOREIGN KEY (user_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
|
||||||
|
|
||||||
-- Row Level Security
|
-- Row Level Security
|
||||||
ALTER TABLE private_user_messages ENABLE ROW LEVEL SECURITY;
|
ALTER TABLE private_user_messages ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
-- Indexes
|
-- Indexes
|
||||||
DROP INDEX IF EXISTS private_user_messages_channel_id_idx;
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS private_user_messages_channel_id_idx
|
CREATE INDEX IF NOT EXISTS private_user_messages_channel_id_idx
|
||||||
ON public.private_user_messages USING btree (channel_id, created_time DESC);
|
ON public.private_user_messages USING btree (channel_id, created_time DESC);
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ CREATE TABLE IF NOT EXISTS private_user_seen_message_channels (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- Foreign Keys
|
-- Foreign Keys
|
||||||
|
ALTER TABLE private_user_seen_message_channels
|
||||||
|
ADD CONSTRAINT private_user_seen_message_channels_user_id_fkey
|
||||||
|
FOREIGN KEY (user_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF NOT EXISTS (
|
IF NOT EXISTS (
|
||||||
@@ -47,8 +53,6 @@ FOR SELECT
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- Indexes
|
-- Indexes
|
||||||
DROP INDEX IF EXISTS user_seen_private_messages_created_time_desc_idx;
|
CREATE INDEX IF NOT EXISTS user_seen_private_messages_created_time_desc_idx
|
||||||
|
|
||||||
CREATE INDEX user_seen_private_messages_created_time_desc_idx
|
|
||||||
ON public.private_user_seen_message_channels
|
ON public.private_user_seen_message_channels
|
||||||
USING btree (user_id, channel_id, created_time DESC);
|
USING btree (user_id, channel_id, created_time DESC);
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ CREATE TABLE IF NOT EXISTS private_users (
|
|||||||
CONSTRAINT private_users_pkey PRIMARY KEY (id)
|
CONSTRAINT private_users_pkey PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ALTER TABLE private_users
|
||||||
|
ADD CONSTRAINT private_users_id_fkey
|
||||||
|
FOREIGN KEY (id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
|
||||||
-- Row Level Security
|
-- Row Level Security
|
||||||
ALTER TABLE private_users ENABLE ROW LEVEL SECURITY;
|
ALTER TABLE private_users ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,28 @@ CREATE TABLE IF NOT EXISTS profile_comments (
|
|||||||
user_username TEXT NOT NULL
|
user_username TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ALTER TABLE profile_comments
|
||||||
|
ADD CONSTRAINT profile_comments_user_id_fkey
|
||||||
|
FOREIGN KEY (user_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE profile_comments
|
||||||
|
ADD CONSTRAINT profile_comments_on_user_id_fkey
|
||||||
|
FOREIGN KEY (on_user_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
|
||||||
-- Row Level Security
|
-- Row Level Security
|
||||||
ALTER TABLE profile_comments ENABLE ROW LEVEL SECURITY;
|
ALTER TABLE profile_comments ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "public read" ON profile_comments;
|
||||||
|
CREATE POLICY "public read" ON profile_comments
|
||||||
|
FOR SELECT USING (true);
|
||||||
|
|
||||||
-- Policies
|
-- Policies
|
||||||
DROP POLICY IF EXISTS "public read" ON profile_comments;
|
DROP POLICY IF EXISTS "public read" ON profile_comments;
|
||||||
CREATE POLICY "public read" ON profile_comments FOR ALL USING (true);
|
CREATE POLICY "public read" ON profile_comments FOR SELECT USING (true);
|
||||||
|
|
||||||
-- Indexes
|
-- Indexes
|
||||||
CREATE INDEX IF NOT EXISTS profile_comments_user_id_idx
|
CREATE INDEX IF NOT EXISTS profile_comments_user_id_idx
|
||||||
|
|||||||
28
backend/supabase/profile_likes.sql
Normal file
28
backend/supabase/profile_likes.sql
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS profile_likes (
|
||||||
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
|
creator_id TEXT NOT NULL,
|
||||||
|
like_id TEXT DEFAULT random_alphanumeric(12) NOT NULL,
|
||||||
|
target_id TEXT NOT NULL,
|
||||||
|
CONSTRAINT profile_likes_pkey PRIMARY KEY (creator_id, like_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE profile_likes
|
||||||
|
ADD CONSTRAINT profile_likes_creator_id_fkey
|
||||||
|
FOREIGN KEY (creator_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- Row Level Security
|
||||||
|
ALTER TABLE profile_likes ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Policies
|
||||||
|
DROP POLICY IF EXISTS "public read" ON profile_likes;
|
||||||
|
CREATE POLICY "public read" ON profile_likes
|
||||||
|
FOR SELECT USING (true);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
-- The primary key already creates a unique index on (creator_id, like_id)
|
||||||
|
-- so we do not recreate that. Additional indexes:
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS user_likes_target_id_raw
|
||||||
|
ON public.profile_likes (target_id);
|
||||||
28
backend/supabase/profile_ships.sql
Normal file
28
backend/supabase/profile_ships.sql
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS profile_ships (
|
||||||
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
|
creator_id TEXT NOT NULL,
|
||||||
|
ship_id TEXT DEFAULT random_alphanumeric(12) NOT NULL,
|
||||||
|
target1_id TEXT NOT NULL,
|
||||||
|
target2_id TEXT NOT NULL,
|
||||||
|
CONSTRAINT profile_ships_pkey PRIMARY KEY (creator_id, ship_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE profile_ships
|
||||||
|
ADD CONSTRAINT profile_ships_creator_id_fkey
|
||||||
|
FOREIGN KEY (creator_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- Row Level Security
|
||||||
|
ALTER TABLE profile_ships ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Policies
|
||||||
|
DROP POLICY IF EXISTS "public read" ON profile_ships;
|
||||||
|
CREATE POLICY "public read" ON profile_ships
|
||||||
|
FOR SELECT USING (true);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
-- Primary key automatically creates a unique index on (creator_id, ship_id), so no need to recreate it.
|
||||||
|
CREATE INDEX IF NOT EXISTS profile_ships_target1_id ON public.profile_ships USING btree (target1_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS profile_ships_target2_id ON public.profile_ships USING btree (target2_id);
|
||||||
31
backend/supabase/profile_stars.sql
Normal file
31
backend/supabase/profile_stars.sql
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS profile_stars (
|
||||||
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
|
creator_id TEXT NOT NULL,
|
||||||
|
star_id TEXT DEFAULT random_alphanumeric(12) NOT NULL,
|
||||||
|
target_id TEXT NOT NULL,
|
||||||
|
CONSTRAINT profile_stars_pkey PRIMARY KEY (creator_id, star_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE profile_stars
|
||||||
|
ADD CONSTRAINT profile_stars_creator_id_fkey
|
||||||
|
FOREIGN KEY (creator_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE profile_stars
|
||||||
|
ADD CONSTRAINT profile_stars_target_id_fkey
|
||||||
|
FOREIGN KEY (target_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- Row Level Security
|
||||||
|
ALTER TABLE profile_stars ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Policies
|
||||||
|
DROP POLICY IF EXISTS "public read" ON profile_stars;
|
||||||
|
CREATE POLICY "public read" ON profile_stars
|
||||||
|
FOR SELECT USING (true);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
-- The primary key already creates a unique index on (creator_id, star_id), so no need to recreate it.
|
||||||
|
CREATE INDEX IF NOT EXISTS profile_stars_target_id_idx ON public.profile_stars USING btree (target_id);
|
||||||
@@ -27,7 +27,7 @@ CREATE TABLE IF NOT EXISTS profiles (
|
|||||||
height_in_inches INTEGER,
|
height_in_inches INTEGER,
|
||||||
id BIGINT GENERATED ALWAYS AS IDENTITY NOT NULL,
|
id BIGINT GENERATED ALWAYS AS IDENTITY NOT NULL,
|
||||||
is_smoker BOOLEAN,
|
is_smoker BOOLEAN,
|
||||||
is_vegetarian_or_vegan BOOLEAN,
|
diet TEXT[],
|
||||||
last_modification_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
last_modification_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
looking_for_matches BOOLEAN DEFAULT TRUE NOT NULL,
|
looking_for_matches BOOLEAN DEFAULT TRUE NOT NULL,
|
||||||
messaging_status TEXT DEFAULT 'open'::TEXT NOT NULL,
|
messaging_status TEXT DEFAULT 'open'::TEXT NOT NULL,
|
||||||
@@ -54,6 +54,13 @@ CREATE TABLE IF NOT EXISTS profiles (
|
|||||||
CONSTRAINT profiles_pkey PRIMARY KEY (id)
|
CONSTRAINT profiles_pkey PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE profiles
|
||||||
|
ADD CONSTRAINT profiles_user_id_fkey
|
||||||
|
FOREIGN KEY (user_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
|
||||||
-- Row Level Security
|
-- Row Level Security
|
||||||
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
|
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
@@ -69,11 +76,9 @@ FOR UPDATE
|
|||||||
WITH CHECK ((user_id = firebase_uid()));
|
WITH CHECK ((user_id = firebase_uid()));
|
||||||
|
|
||||||
-- Indexes
|
-- Indexes
|
||||||
DROP INDEX IF EXISTS profiles_user_id_idx;
|
CREATE INDEX IF NOT EXISTS profiles_user_id_idx ON public.profiles USING btree (user_id);
|
||||||
CREATE INDEX profiles_user_id_idx ON public.profiles USING btree (user_id);
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS unique_user_id;
|
CREATE UNIQUE INDEX IF NOT EXISTS unique_user_id ON public.profiles USING btree (user_id);
|
||||||
CREATE UNIQUE INDEX unique_user_id ON public.profiles USING btree (user_id);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_profiles_last_mod_24h
|
CREATE INDEX IF NOT EXISTS idx_profiles_last_mod_24h
|
||||||
ON public.profiles USING btree (last_modification_time);
|
ON public.profiles USING btree (last_modification_time);
|
||||||
@@ -82,13 +87,10 @@ CREATE INDEX IF NOT EXISTS idx_profiles_bio_length
|
|||||||
ON profiles (bio_length);
|
ON profiles (bio_length);
|
||||||
|
|
||||||
-- Fastest general-purpose index
|
-- Fastest general-purpose index
|
||||||
DROP INDEX IF EXISTS profiles_lat_lon_idx;
|
CREATE INDEX IF NOT EXISTS profiles_lat_lon_idx ON profiles (city_latitude, city_longitude);
|
||||||
CREATE INDEX profiles_lat_lon_idx ON profiles (city_latitude, city_longitude);
|
|
||||||
|
|
||||||
-- Optional additional index for large tables / clustered inserts
|
-- Optional additional index for large tables / clustered inserts
|
||||||
DROP INDEX IF EXISTS profiles_lat_lon_brin_idx;
|
CREATE INDEX IF NOT EXISTS profiles_lat_lon_brin_idx ON profiles USING BRIN (city_latitude, city_longitude) WITH (pages_per_range = 32);
|
||||||
CREATE INDEX profiles_lat_lon_brin_idx ON profiles USING BRIN (city_latitude, city_longitude) WITH (pages_per_range = 32);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Functions and Triggers
|
-- Functions and Triggers
|
||||||
|
|||||||
17
backend/supabase/push_subscriptions.sql
Normal file
17
backend/supabase/push_subscriptions.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
create table push_subscriptions (
|
||||||
|
id serial primary key,
|
||||||
|
user_id TEXT references users(id) on delete cascade,
|
||||||
|
endpoint text not null unique,
|
||||||
|
keys jsonb not null,
|
||||||
|
created_at timestamptz default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Row Level Security
|
||||||
|
ALTER TABLE push_subscriptions ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX IF not exists user_id_idx ON push_subscriptions (user_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF not exists endpoint_idx ON push_subscriptions (endpoint);
|
||||||
|
|
||||||
|
CREATE INDEX IF not exists endpoint_user_id_idx ON push_subscriptions (endpoint, user_id);
|
||||||
@@ -5,6 +5,13 @@ CREATE TABLE IF NOT EXISTS user_notifications (
|
|||||||
CONSTRAINT user_notifications_pkey PRIMARY KEY (notification_id, user_id)
|
CONSTRAINT user_notifications_pkey PRIMARY KEY (notification_id, user_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE user_notifications
|
||||||
|
ADD CONSTRAINT user_notifications_user_id_fkey
|
||||||
|
FOREIGN KEY (user_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
|
||||||
-- Row Level Security
|
-- Row Level Security
|
||||||
ALTER TABLE user_notifications ENABLE ROW LEVEL SECURITY;
|
ALTER TABLE user_notifications ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
CREATE TABLE IF NOT EXISTS love_waitlist (
|
CREATE TABLE IF NOT EXISTS user_waitlist (
|
||||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
email TEXT NOT NULL
|
email TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Row Level Security
|
-- Row Level Security
|
||||||
ALTER TABLE love_waitlist ENABLE ROW LEVEL SECURITY;
|
ALTER TABLE user_waitlist ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
-- Policies
|
-- Policies
|
||||||
DROP POLICY IF EXISTS "anon insert" ON love_waitlist;
|
DROP POLICY IF EXISTS "anon insert" ON user_waitlist;
|
||||||
CREATE POLICY "anon insert" ON love_waitlist
|
CREATE POLICY "anon insert" ON user_waitlist
|
||||||
FOR INSERT WITH CHECK (true);
|
FOR INSERT WITH CHECK (true);
|
||||||
|
|
||||||
-- Indexes
|
-- Indexes
|
||||||
@@ -18,20 +18,16 @@ ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
|||||||
|
|
||||||
-- Policies
|
-- Policies
|
||||||
DROP POLICY IF EXISTS "public read" ON users;
|
DROP POLICY IF EXISTS "public read" ON users;
|
||||||
|
|
||||||
CREATE POLICY "public read" ON users
|
CREATE POLICY "public read" ON users
|
||||||
FOR SELECT
|
FOR SELECT
|
||||||
USING (true);
|
USING (true);
|
||||||
|
|
||||||
-- Indexes
|
-- Indexes
|
||||||
DROP INDEX IF EXISTS user_username_idx;
|
CREATE INDEX IF NOT EXISTS user_username_idx ON public.users USING btree (username);
|
||||||
CREATE INDEX user_username_idx ON public.users USING btree (username);
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS users_created_time;
|
CREATE INDEX IF NOT EXISTS users_created_time ON public.users USING btree (created_time DESC);
|
||||||
CREATE INDEX users_created_time ON public.users USING btree (created_time DESC);
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS users_name_idx;
|
CREATE INDEX IF NOT EXISTS users_name_idx ON public.users USING btree (name);
|
||||||
CREATE INDEX users_name_idx ON public.users USING btree (name);
|
|
||||||
|
|
||||||
-- Remove these if you trust PRIMARY KEY auto-index:
|
-- Remove these if you trust PRIMARY KEY auto-index:
|
||||||
-- DROP INDEX IF EXISTS users_pkey;
|
-- DROP INDEX IF EXISTS users_pkey;
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -21,23 +21,18 @@ ALTER TABLE vote_results ENABLE ROW LEVEL SECURITY;
|
|||||||
-- Policies
|
-- Policies
|
||||||
DROP POLICY IF EXISTS "public read" ON vote_results;
|
DROP POLICY IF EXISTS "public read" ON vote_results;
|
||||||
CREATE POLICY "public read" ON vote_results
|
CREATE POLICY "public read" ON vote_results
|
||||||
FOR ALL USING (true);
|
FOR SELECT USING (true);
|
||||||
|
|
||||||
-- Indexes
|
-- Indexes
|
||||||
DROP INDEX IF EXISTS user_id_idx;
|
CREATE INDEX IF NOT EXISTS user_id_idx ON vote_results (user_id);
|
||||||
CREATE INDEX user_id_idx ON vote_results (user_id);
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS vote_id_idx;
|
CREATE INDEX IF NOT EXISTS vote_id_idx ON vote_results (vote_id);
|
||||||
CREATE INDEX vote_id_idx ON vote_results (vote_id);
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS idx_vote_results_vote_choice;
|
CREATE INDEX IF NOT EXISTS idx_vote_results_vote_choice ON vote_results (vote_id, choice);
|
||||||
CREATE INDEX idx_vote_results_vote_choice ON vote_results (vote_id, choice);
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS idx_vote_results_vote_choice_priority;
|
CREATE INDEX IF NOT EXISTS idx_vote_results_vote_choice_priority ON vote_results (vote_id, choice, priority);
|
||||||
CREATE INDEX idx_vote_results_vote_choice_priority ON vote_results (vote_id, choice, priority);
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS idx_votes_created_time;
|
CREATE INDEX IF NOT EXISTS idx_votes_created_time ON votes (created_time DESC);
|
||||||
CREATE INDEX idx_votes_created_time ON votes (created_time DESC);
|
|
||||||
|
|
||||||
|
|
||||||
drop function if exists get_votes_with_results;
|
drop function if exists get_votes_with_results;
|
||||||
|
|||||||
@@ -17,11 +17,9 @@ ALTER TABLE votes ENABLE ROW LEVEL SECURITY;
|
|||||||
-- Policies
|
-- Policies
|
||||||
DROP POLICY IF EXISTS "public read" ON votes;
|
DROP POLICY IF EXISTS "public read" ON votes;
|
||||||
CREATE POLICY "public read" ON votes
|
CREATE POLICY "public read" ON votes
|
||||||
FOR ALL USING (true);
|
FOR SELECT USING (true);
|
||||||
|
|
||||||
-- Indexes
|
-- Indexes
|
||||||
DROP INDEX IF EXISTS creator_id_idx;
|
CREATE INDEX IF NOT EXISTS creator_id_idx ON votes (creator_id);
|
||||||
CREATE INDEX creator_id_idx ON votes (creator_id);
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS idx_votes_created_time;
|
CREATE INDEX IF NOT EXISTS idx_votes_created_time ON votes (created_time DESC);
|
||||||
CREATE INDEX idx_votes_created_time ON votes (created_time DESC);
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
"dayjs": "1.11.4",
|
"dayjs": "1.11.4",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"string-similarity": "4.0.4",
|
"string-similarity": "4.0.4",
|
||||||
"zod": "3.21.4"
|
"zod": "3.22.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "29.2.4",
|
"@types/jest": "29.2.4",
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import {
|
import {arraybeSchema, baseProfilesSchema, combinedProfileSchema, contentSchema, zBoolean,} from 'common/api/zod-types'
|
||||||
contentSchema,
|
|
||||||
combinedLoveUsersSchema,
|
|
||||||
baseProfilesSchema,
|
|
||||||
arraybeSchema,
|
|
||||||
} from 'common/api/zod-types'
|
|
||||||
import {PrivateChatMessage} from 'common/chat-message'
|
import {PrivateChatMessage} from 'common/chat-message'
|
||||||
import {CompatibilityScore} from 'common/love/compatibility-score'
|
import {CompatibilityScore} from 'common/profiles/compatibility-score'
|
||||||
import {MAX_COMPATIBILITY_QUESTION_LENGTH} from 'common/love/constants'
|
import {MAX_COMPATIBILITY_QUESTION_LENGTH} from 'common/profiles/constants'
|
||||||
import {Profile, ProfileRow} from 'common/love/profile'
|
import {Profile, ProfileRow} from 'common/profiles/profile'
|
||||||
import {Row} from 'common/supabase/utils'
|
import {Row} from 'common/supabase/utils'
|
||||||
import {PrivateUser, User} from 'common/user'
|
import {PrivateUser, User} from 'common/user'
|
||||||
import {z} from 'zod'
|
import {z} from 'zod'
|
||||||
import {LikeData, ShipData} from './love-types'
|
import {LikeData, ShipData} from './profile-types'
|
||||||
import {DisplayUser, FullUser} from './user-types'
|
import {FullUser} from './user-types'
|
||||||
import {PrivateMessageChannel} from 'common/supabase/private-messages'
|
import {PrivateMessageChannel} from 'common/supabase/private-messages'
|
||||||
import {Notification} from 'common/notifications'
|
import {Notification} from 'common/notifications'
|
||||||
import {arrify} from 'common/util/array'
|
import {arrify} from 'common/util/array'
|
||||||
@@ -36,6 +31,10 @@ type APIGenericSchema = {
|
|||||||
returns?: Record<string, any>
|
returns?: Record<string, any>
|
||||||
// Cache-Control header. like, 'max-age=60'
|
// Cache-Control header. like, 'max-age=60'
|
||||||
cache?: string
|
cache?: string
|
||||||
|
// Description of the endpoint
|
||||||
|
summary?: string
|
||||||
|
// Tag for grouping endpoints in documentation
|
||||||
|
tag?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
let _apiTypeCheck: { [x: string]: APIGenericSchema }
|
let _apiTypeCheck: { [x: string]: APIGenericSchema }
|
||||||
@@ -47,6 +46,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
rateLimited: false,
|
rateLimited: false,
|
||||||
props: z.object({}),
|
props: z.object({}),
|
||||||
returns: {} as { message: 'Server is working.'; uid?: string },
|
returns: {} as { message: 'Server is working.'; uid?: string },
|
||||||
|
summary: 'Check whether the API server is running',
|
||||||
|
tag: 'General',
|
||||||
},
|
},
|
||||||
'get-supabase-token': {
|
'get-supabase-token': {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -54,24 +55,69 @@ export const API = (_apiTypeCheck = {
|
|||||||
rateLimited: false,
|
rateLimited: false,
|
||||||
props: z.object({}),
|
props: z.object({}),
|
||||||
returns: {} as { jwt: string },
|
returns: {} as { jwt: string },
|
||||||
|
summary: 'Return a Supabase JWT for authenticated clients',
|
||||||
|
tag: 'Tokens',
|
||||||
},
|
},
|
||||||
'mark-all-notifs-read': {
|
'mark-all-notifs-read': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
authed: true,
|
authed: true,
|
||||||
rateLimited: false,
|
rateLimited: false,
|
||||||
props: z.object({}),
|
props: z.object({}),
|
||||||
|
summary: 'Mark all user notifications as read',
|
||||||
|
tag: 'Notifications',
|
||||||
},
|
},
|
||||||
|
// 'user/:username': {
|
||||||
|
// method: 'GET',
|
||||||
|
// authed: false,
|
||||||
|
// rateLimited: false,
|
||||||
|
// cache: DEFAULT_CACHE_STRATEGY,
|
||||||
|
// returns: {} as FullUser,
|
||||||
|
// props: z.object({username: z.string()}).strict(),
|
||||||
|
// summary: 'Get full public profile by username',
|
||||||
|
// },
|
||||||
|
// 'user/:username/lite': {
|
||||||
|
// method: 'GET',
|
||||||
|
// authed: false,
|
||||||
|
// rateLimited: false,
|
||||||
|
// cache: DEFAULT_CACHE_STRATEGY,
|
||||||
|
// returns: {} as DisplayUser,
|
||||||
|
// props: z.object({username: z.string()}).strict(),
|
||||||
|
// summary: 'Get lightweight public profile by username',
|
||||||
|
// },
|
||||||
|
'user/by-id/:id': {
|
||||||
|
method: 'GET',
|
||||||
|
authed: true,
|
||||||
|
rateLimited: true,
|
||||||
|
cache: DEFAULT_CACHE_STRATEGY,
|
||||||
|
returns: {} as FullUser,
|
||||||
|
props: z.object({id: z.string()}).strict(),
|
||||||
|
summary: 'Get full profile by user ID',
|
||||||
|
tag: 'Users',
|
||||||
|
},
|
||||||
|
// 'user/by-id/:id/lite': {
|
||||||
|
// method: 'GET',
|
||||||
|
// authed: false,
|
||||||
|
// rateLimited: false,
|
||||||
|
// cache: DEFAULT_CACHE_STRATEGY,
|
||||||
|
// returns: {} as DisplayUser,
|
||||||
|
// props: z.object({id: z.string()}).strict(),
|
||||||
|
// summary: 'Get lightweight profile by user ID',
|
||||||
|
// },
|
||||||
'user/by-id/:id/block': {
|
'user/by-id/:id/block': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
authed: true,
|
authed: true,
|
||||||
rateLimited: false,
|
rateLimited: false,
|
||||||
props: z.object({id: z.string()}).strict(),
|
props: z.object({id: z.string()}).strict(),
|
||||||
|
summary: 'Block a user by their ID',
|
||||||
|
tag: 'Users',
|
||||||
},
|
},
|
||||||
'user/by-id/:id/unblock': {
|
'user/by-id/:id/unblock': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
authed: true,
|
authed: true,
|
||||||
rateLimited: false,
|
rateLimited: false,
|
||||||
props: z.object({id: z.string()}).strict(),
|
props: z.object({id: z.string()}).strict(),
|
||||||
|
summary: 'Unblock a user by their ID',
|
||||||
|
tag: 'Users',
|
||||||
},
|
},
|
||||||
'ban-user': {
|
'ban-user': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -83,9 +129,10 @@ export const API = (_apiTypeCheck = {
|
|||||||
unban: z.boolean().optional(),
|
unban: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.strict(),
|
.strict(),
|
||||||
|
summary: 'Ban or unban a user',
|
||||||
|
tag: 'Admin',
|
||||||
},
|
},
|
||||||
'create-user': {
|
'create-user': {
|
||||||
// TODO rest
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
authed: true,
|
authed: true,
|
||||||
rateLimited: true,
|
rateLimited: true,
|
||||||
@@ -96,6 +143,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
adminToken: z.string().optional(),
|
adminToken: z.string().optional(),
|
||||||
})
|
})
|
||||||
.strict(),
|
.strict(),
|
||||||
|
summary: 'Create a new user (admin or onboarding flow)',
|
||||||
|
tag: 'Users',
|
||||||
},
|
},
|
||||||
'create-profile': {
|
'create-profile': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -103,6 +152,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
rateLimited: true,
|
rateLimited: true,
|
||||||
returns: {} as Row<'profiles'>,
|
returns: {} as Row<'profiles'>,
|
||||||
props: baseProfilesSchema,
|
props: baseProfilesSchema,
|
||||||
|
summary: 'Create a new profile for the authenticated user',
|
||||||
|
tag: 'Profiles',
|
||||||
},
|
},
|
||||||
report: {
|
report: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -119,6 +170,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
})
|
})
|
||||||
.strict(),
|
.strict(),
|
||||||
returns: {} as any,
|
returns: {} as any,
|
||||||
|
summary: 'Submit a report for content or a user',
|
||||||
|
tag: 'Moderation',
|
||||||
},
|
},
|
||||||
me: {
|
me: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -127,6 +180,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
cache: DEFAULT_CACHE_STRATEGY,
|
cache: DEFAULT_CACHE_STRATEGY,
|
||||||
props: z.object({}),
|
props: z.object({}),
|
||||||
returns: {} as FullUser,
|
returns: {} as FullUser,
|
||||||
|
summary: 'Get the authenticated user full data',
|
||||||
|
tag: 'Users',
|
||||||
},
|
},
|
||||||
'me/update': {
|
'me/update': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -155,13 +210,17 @@ export const API = (_apiTypeCheck = {
|
|||||||
discordHandle: z.string().optional(),
|
discordHandle: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
returns: {} as FullUser,
|
returns: {} as FullUser,
|
||||||
|
summary: 'Update authenticated user profile and settings',
|
||||||
|
tag: 'Users',
|
||||||
},
|
},
|
||||||
'update-profile': {
|
'update-profile': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
authed: true,
|
authed: true,
|
||||||
rateLimited: true,
|
rateLimited: true,
|
||||||
props: combinedLoveUsersSchema.partial(),
|
props: combinedProfileSchema.partial(),
|
||||||
returns: {} as ProfileRow,
|
returns: {} as ProfileRow,
|
||||||
|
summary: 'Update profile fields for the authenticated user',
|
||||||
|
tag: 'Profiles',
|
||||||
},
|
},
|
||||||
'update-notif-settings': {
|
'update-notif-settings': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -172,6 +231,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
medium: z.enum(['email', 'browser', 'mobile']),
|
medium: z.enum(['email', 'browser', 'mobile']),
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
}),
|
}),
|
||||||
|
summary: 'Update a notification preference for the user',
|
||||||
|
tag: 'Notifications',
|
||||||
},
|
},
|
||||||
'me/delete': {
|
'me/delete': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -180,6 +241,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
props: z.object({
|
props: z.object({
|
||||||
username: z.string(), // just so you're sure
|
username: z.string(), // just so you're sure
|
||||||
}),
|
}),
|
||||||
|
summary: 'Delete the authenticated user account',
|
||||||
|
tag: 'Users',
|
||||||
},
|
},
|
||||||
'me/private': {
|
'me/private': {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -187,38 +250,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
rateLimited: false,
|
rateLimited: false,
|
||||||
props: z.object({}),
|
props: z.object({}),
|
||||||
returns: {} as PrivateUser,
|
returns: {} as PrivateUser,
|
||||||
},
|
summary: 'Get private user data for the authenticated user',
|
||||||
'user/:username': {
|
tag: 'Users',
|
||||||
method: 'GET',
|
|
||||||
authed: false,
|
|
||||||
rateLimited: false,
|
|
||||||
cache: DEFAULT_CACHE_STRATEGY,
|
|
||||||
returns: {} as FullUser,
|
|
||||||
props: z.object({username: z.string()}).strict(),
|
|
||||||
},
|
|
||||||
'user/:username/lite': {
|
|
||||||
method: 'GET',
|
|
||||||
authed: false,
|
|
||||||
rateLimited: false,
|
|
||||||
cache: DEFAULT_CACHE_STRATEGY,
|
|
||||||
returns: {} as DisplayUser,
|
|
||||||
props: z.object({username: z.string()}).strict(),
|
|
||||||
},
|
|
||||||
'user/by-id/:id': {
|
|
||||||
method: 'GET',
|
|
||||||
authed: false,
|
|
||||||
rateLimited: false,
|
|
||||||
cache: DEFAULT_CACHE_STRATEGY,
|
|
||||||
returns: {} as FullUser,
|
|
||||||
props: z.object({id: z.string()}).strict(),
|
|
||||||
},
|
|
||||||
'user/by-id/:id/lite': {
|
|
||||||
method: 'GET',
|
|
||||||
authed: false,
|
|
||||||
rateLimited: false,
|
|
||||||
cache: DEFAULT_CACHE_STRATEGY,
|
|
||||||
returns: {} as DisplayUser,
|
|
||||||
props: z.object({id: z.string()}).strict(),
|
|
||||||
},
|
},
|
||||||
'search-users': {
|
'search-users': {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -233,6 +266,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
page: z.coerce.number().gte(0).default(0),
|
page: z.coerce.number().gte(0).default(0),
|
||||||
})
|
})
|
||||||
.strict(),
|
.strict(),
|
||||||
|
summary: 'Search users by term with pagination',
|
||||||
|
tag: 'Users',
|
||||||
},
|
},
|
||||||
'compatible-profiles': {
|
'compatible-profiles': {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -246,6 +281,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
[userId: string]: CompatibilityScore
|
[userId: string]: CompatibilityScore
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
summary: 'Find profiles compatible with a given user',
|
||||||
|
tag: 'Profiles',
|
||||||
},
|
},
|
||||||
'remove-pinned-photo': {
|
'remove-pinned-photo': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -257,6 +294,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
})
|
})
|
||||||
.strict(),
|
.strict(),
|
||||||
|
summary: 'Remove the pinned photo from a profile',
|
||||||
|
tag: 'Profiles',
|
||||||
},
|
},
|
||||||
'get-compatibility-questions': {
|
'get-compatibility-questions': {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -265,11 +304,13 @@ export const API = (_apiTypeCheck = {
|
|||||||
props: z.object({}),
|
props: z.object({}),
|
||||||
returns: {} as {
|
returns: {} as {
|
||||||
status: 'success'
|
status: 'success'
|
||||||
questions: (Row<'love_questions'> & {
|
questions: (Row<'compatibility_prompts'> & {
|
||||||
answer_count: number
|
answer_count: number
|
||||||
score: number
|
score: number
|
||||||
})[]
|
})[]
|
||||||
},
|
},
|
||||||
|
summary: 'Retrieve compatibility questions and stats',
|
||||||
|
tag: 'Compatibility',
|
||||||
},
|
},
|
||||||
'like-profile': {
|
'like-profile': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -282,6 +323,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
returns: {} as {
|
returns: {} as {
|
||||||
status: 'success'
|
status: 'success'
|
||||||
},
|
},
|
||||||
|
summary: 'Like or unlike a profile',
|
||||||
|
tag: 'Profiles',
|
||||||
},
|
},
|
||||||
'ship-profiles': {
|
'ship-profiles': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -295,6 +338,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
returns: {} as {
|
returns: {} as {
|
||||||
status: 'success'
|
status: 'success'
|
||||||
},
|
},
|
||||||
|
summary: 'Create or remove a ship between two profiles',
|
||||||
|
tag: 'Profiles',
|
||||||
},
|
},
|
||||||
'get-likes-and-ships': {
|
'get-likes-and-ships': {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -311,6 +356,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
likesGiven: LikeData[]
|
likesGiven: LikeData[]
|
||||||
ships: ShipData[]
|
ships: ShipData[]
|
||||||
},
|
},
|
||||||
|
summary: 'Fetch likes and ships for a user',
|
||||||
|
tag: 'Profiles',
|
||||||
},
|
},
|
||||||
'has-free-like': {
|
'has-free-like': {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -321,6 +368,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
status: 'success'
|
status: 'success'
|
||||||
hasFreeLike: boolean
|
hasFreeLike: boolean
|
||||||
},
|
},
|
||||||
|
summary: 'Check whether the user has a free like available',
|
||||||
|
tag: 'Profiles',
|
||||||
},
|
},
|
||||||
'star-profile': {
|
'star-profile': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -333,6 +382,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
returns: {} as {
|
returns: {} as {
|
||||||
status: 'success'
|
status: 'success'
|
||||||
},
|
},
|
||||||
|
summary: 'Star or unstar a profile',
|
||||||
|
tag: 'Profiles',
|
||||||
},
|
},
|
||||||
'get-profiles': {
|
'get-profiles': {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -345,15 +396,20 @@ export const API = (_apiTypeCheck = {
|
|||||||
// Search and filter parameters
|
// Search and filter parameters
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
genders: arraybeSchema.optional(),
|
genders: arraybeSchema.optional(),
|
||||||
|
education_levels: arraybeSchema.optional(),
|
||||||
pref_gender: arraybeSchema.optional(),
|
pref_gender: arraybeSchema.optional(),
|
||||||
pref_age_min: z.coerce.number().optional(),
|
pref_age_min: z.coerce.number().optional(),
|
||||||
pref_age_max: z.coerce.number().optional(),
|
pref_age_max: z.coerce.number().optional(),
|
||||||
|
drinks_min: z.coerce.number().optional(),
|
||||||
|
drinks_max: z.coerce.number().optional(),
|
||||||
pref_relation_styles: arraybeSchema.optional(),
|
pref_relation_styles: arraybeSchema.optional(),
|
||||||
pref_romantic_styles: arraybeSchema.optional(),
|
pref_romantic_styles: arraybeSchema.optional(),
|
||||||
|
diet: arraybeSchema.optional(),
|
||||||
|
political_beliefs: arraybeSchema.optional(),
|
||||||
wants_kids_strength: z.coerce.number().optional(),
|
wants_kids_strength: z.coerce.number().optional(),
|
||||||
has_kids: z.coerce.number().optional(),
|
has_kids: z.coerce.number().optional(),
|
||||||
is_smoker: z.coerce.boolean().optional(),
|
is_smoker: zBoolean.optional().optional(),
|
||||||
shortBio: z.coerce.boolean().optional(),
|
shortBio: zBoolean.optional().optional(),
|
||||||
geodbCityIds: arraybeSchema.optional(),
|
geodbCityIds: arraybeSchema.optional(),
|
||||||
lat: z.coerce.number().optional(),
|
lat: z.coerce.number().optional(),
|
||||||
lon: z.coerce.number().optional(),
|
lon: z.coerce.number().optional(),
|
||||||
@@ -369,16 +425,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
status: 'success' | 'fail'
|
status: 'success' | 'fail'
|
||||||
profiles: Profile[]
|
profiles: Profile[]
|
||||||
},
|
},
|
||||||
},
|
summary: 'List profiles with filters, pagination and ordering',
|
||||||
'get-profile-answers': {
|
tag: 'Profiles',
|
||||||
method: 'GET',
|
|
||||||
authed: true,
|
|
||||||
rateLimited: true,
|
|
||||||
props: z.object({userId: z.string()}).strict(),
|
|
||||||
returns: {} as {
|
|
||||||
status: 'success'
|
|
||||||
answers: Row<'love_compatibility_answers'>[]
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
'create-comment': {
|
'create-comment': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -390,6 +438,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
replyToCommentId: z.string().optional(),
|
replyToCommentId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
returns: {} as any,
|
returns: {} as any,
|
||||||
|
summary: 'Create a comment or reply',
|
||||||
|
tag: 'Profiles',
|
||||||
},
|
},
|
||||||
'hide-comment': {
|
'hide-comment': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -400,6 +450,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
hide: z.boolean(),
|
hide: z.boolean(),
|
||||||
}),
|
}),
|
||||||
returns: {} as any,
|
returns: {} as any,
|
||||||
|
summary: 'Hide or unhide a comment',
|
||||||
|
tag: 'Profiles',
|
||||||
},
|
},
|
||||||
'get-channel-memberships': {
|
'get-channel-memberships': {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -415,6 +467,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
channels: [] as PrivateMessageChannel[],
|
channels: [] as PrivateMessageChannel[],
|
||||||
memberIdsByChannelId: {} as { [channelId: string]: string[] },
|
memberIdsByChannelId: {} as { [channelId: string]: string[] },
|
||||||
},
|
},
|
||||||
|
summary: 'List private message channel memberships',
|
||||||
|
tag: 'Messages',
|
||||||
},
|
},
|
||||||
'get-channel-messages': {
|
'get-channel-messages': {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -426,6 +480,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
id: z.coerce.number().optional(),
|
id: z.coerce.number().optional(),
|
||||||
}),
|
}),
|
||||||
returns: [] as PrivateChatMessage[],
|
returns: [] as PrivateChatMessage[],
|
||||||
|
summary: 'Retrieve messages for a private channel',
|
||||||
|
tag: 'Messages',
|
||||||
},
|
},
|
||||||
'get-channel-seen-time': {
|
'get-channel-seen-time': {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -438,6 +494,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
.transform(arrify),
|
.transform(arrify),
|
||||||
}),
|
}),
|
||||||
returns: [] as [number, string][],
|
returns: [] as [number, string][],
|
||||||
|
summary: 'Get last seen times for one or more channels',
|
||||||
|
tag: 'Messages',
|
||||||
},
|
},
|
||||||
'set-channel-seen-time': {
|
'set-channel-seen-time': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -446,12 +504,16 @@ export const API = (_apiTypeCheck = {
|
|||||||
props: z.object({
|
props: z.object({
|
||||||
channelId: z.coerce.number(),
|
channelId: z.coerce.number(),
|
||||||
}),
|
}),
|
||||||
|
summary: 'Set last seen time for a channel',
|
||||||
|
tag: 'Messages',
|
||||||
},
|
},
|
||||||
'set-last-online-time': {
|
'set-last-online-time': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
authed: true,
|
authed: true,
|
||||||
rateLimited: false,
|
rateLimited: false,
|
||||||
props: z.object({}),
|
props: z.object({}),
|
||||||
|
summary: 'Update the user last online timestamp',
|
||||||
|
tag: 'Users',
|
||||||
},
|
},
|
||||||
'get-notifications': {
|
'get-notifications': {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -464,6 +526,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
limit: z.coerce.number().gte(0).lte(1000).default(100),
|
limit: z.coerce.number().gte(0).lte(1000).default(100),
|
||||||
})
|
})
|
||||||
.strict(),
|
.strict(),
|
||||||
|
summary: 'Fetch notifications for the authenticated user',
|
||||||
|
tag: 'Notifications',
|
||||||
},
|
},
|
||||||
'create-private-user-message': {
|
'create-private-user-message': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -474,6 +538,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
content: contentSchema,
|
content: contentSchema,
|
||||||
channelId: z.number(),
|
channelId: z.number(),
|
||||||
}),
|
}),
|
||||||
|
summary: 'Send a message in a private channel',
|
||||||
|
tag: 'Messages',
|
||||||
},
|
},
|
||||||
'create-private-user-message-channel': {
|
'create-private-user-message-channel': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -483,6 +549,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
props: z.object({
|
props: z.object({
|
||||||
userIds: z.array(z.string()),
|
userIds: z.array(z.string()),
|
||||||
}),
|
}),
|
||||||
|
summary: 'Create a new private message channel between users',
|
||||||
|
tag: 'Messages',
|
||||||
},
|
},
|
||||||
'update-private-user-message-channel': {
|
'update-private-user-message-channel': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -493,6 +561,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
channelId: z.number(),
|
channelId: z.number(),
|
||||||
notifyAfterTime: z.number(),
|
notifyAfterTime: z.number(),
|
||||||
}),
|
}),
|
||||||
|
summary: 'Update settings for a private message channel',
|
||||||
|
tag: 'Messages',
|
||||||
},
|
},
|
||||||
'leave-private-user-message-channel': {
|
'leave-private-user-message-channel': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -502,6 +572,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
props: z.object({
|
props: z.object({
|
||||||
channelId: z.number(),
|
channelId: z.number(),
|
||||||
}),
|
}),
|
||||||
|
summary: 'Leave a private message channel',
|
||||||
|
tag: 'Messages',
|
||||||
},
|
},
|
||||||
'create-compatibility-question': {
|
'create-compatibility-question': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -512,6 +584,37 @@ export const API = (_apiTypeCheck = {
|
|||||||
question: z.string().min(1).max(MAX_COMPATIBILITY_QUESTION_LENGTH),
|
question: z.string().min(1).max(MAX_COMPATIBILITY_QUESTION_LENGTH),
|
||||||
options: z.record(z.string(), z.number()),
|
options: z.record(z.string(), z.number()),
|
||||||
}),
|
}),
|
||||||
|
summary: 'Create a new compatibility question with options',
|
||||||
|
tag: 'Compatibility',
|
||||||
|
},
|
||||||
|
'set-compatibility-answer': {
|
||||||
|
method: 'POST',
|
||||||
|
authed: true,
|
||||||
|
rateLimited: true,
|
||||||
|
returns: {} as Row<'compatibility_answers'>,
|
||||||
|
props: z
|
||||||
|
.object({
|
||||||
|
questionId: z.number(),
|
||||||
|
multipleChoice: z.number(),
|
||||||
|
prefChoices: z.array(z.number()),
|
||||||
|
importance: z.number(),
|
||||||
|
explanation: z.string().nullable().optional(),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
summary: 'Submit or update a compatibility answer',
|
||||||
|
tag: 'Compatibility',
|
||||||
|
},
|
||||||
|
'get-profile-answers': {
|
||||||
|
method: 'GET',
|
||||||
|
authed: true,
|
||||||
|
rateLimited: true,
|
||||||
|
props: z.object({userId: z.string()}).strict(),
|
||||||
|
returns: {} as {
|
||||||
|
status: 'success'
|
||||||
|
answers: Row<'compatibility_answers'>[]
|
||||||
|
},
|
||||||
|
summary: 'Get compatibility answers for a profile',
|
||||||
|
tag: 'Compatibility',
|
||||||
},
|
},
|
||||||
'create-vote': {
|
'create-vote': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -523,6 +626,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
isAnonymous: z.boolean(),
|
isAnonymous: z.boolean(),
|
||||||
description: contentSchema,
|
description: contentSchema,
|
||||||
}),
|
}),
|
||||||
|
summary: 'Create a new vote/poll',
|
||||||
|
tag: 'Votes',
|
||||||
},
|
},
|
||||||
'vote': {
|
'vote': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -534,6 +639,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
priority: z.number(),
|
priority: z.number(),
|
||||||
choice: z.enum(['for', 'abstain', 'against']),
|
choice: z.enum(['for', 'abstain', 'against']),
|
||||||
}),
|
}),
|
||||||
|
summary: 'Cast a vote on an existing poll',
|
||||||
|
tag: 'Votes',
|
||||||
},
|
},
|
||||||
'search-location': {
|
'search-location': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -544,6 +651,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
term: z.string(),
|
term: z.string(),
|
||||||
limit: z.number().optional(),
|
limit: z.number().optional(),
|
||||||
}),
|
}),
|
||||||
|
summary: 'Search for a location by text',
|
||||||
|
tag: 'Locations',
|
||||||
},
|
},
|
||||||
'search-near-city': {
|
'search-near-city': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -554,6 +663,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
cityId: z.string(),
|
cityId: z.string(),
|
||||||
radius: z.number().min(1).max(500),
|
radius: z.number().min(1).max(500),
|
||||||
}),
|
}),
|
||||||
|
summary: 'Find places near a GeoDB city ID within a radius',
|
||||||
|
tag: 'Locations',
|
||||||
},
|
},
|
||||||
'contact': {
|
'contact': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -564,6 +675,8 @@ export const API = (_apiTypeCheck = {
|
|||||||
content: contentSchema,
|
content: contentSchema,
|
||||||
userId: z.string().optional(),
|
userId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
|
summary: 'Send a contact/support message',
|
||||||
|
tag: 'Contact',
|
||||||
},
|
},
|
||||||
'get-messages-count': {
|
'get-messages-count': {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -571,6 +684,44 @@ export const API = (_apiTypeCheck = {
|
|||||||
rateLimited: false,
|
rateLimited: false,
|
||||||
props: z.object({}),
|
props: z.object({}),
|
||||||
returns: {} as { count: number },
|
returns: {} as { count: number },
|
||||||
|
summary: 'Get the total number of messages (public endpoint)',
|
||||||
|
tag: 'Messages',
|
||||||
|
},
|
||||||
|
'save-subscription': {
|
||||||
|
method: 'POST',
|
||||||
|
authed: true,
|
||||||
|
rateLimited: true,
|
||||||
|
returns: {} as any,
|
||||||
|
props: z.object({
|
||||||
|
subscription: z.record(z.any())
|
||||||
|
}),
|
||||||
|
summary: 'Save a push/browser subscription for the user',
|
||||||
|
tag: 'Notifications',
|
||||||
|
},
|
||||||
|
'create-bookmarked-search': {
|
||||||
|
method: 'POST',
|
||||||
|
authed: true,
|
||||||
|
rateLimited: true,
|
||||||
|
returns: {} as Row<'bookmarked_searches'>,
|
||||||
|
props: z
|
||||||
|
.object({
|
||||||
|
search_filters: z.any().optional(),
|
||||||
|
location: z.any().optional(),
|
||||||
|
search_name: z.string().nullable().optional(),
|
||||||
|
}),
|
||||||
|
summary: 'Create a bookmarked search for quick reuse',
|
||||||
|
tag: 'Searches',
|
||||||
|
},
|
||||||
|
'delete-bookmarked-search': {
|
||||||
|
method: 'POST',
|
||||||
|
authed: true,
|
||||||
|
rateLimited: true,
|
||||||
|
returns: {} as any,
|
||||||
|
props: z.object({
|
||||||
|
id: z.number(),
|
||||||
|
}),
|
||||||
|
summary: 'Delete a bookmarked search by ID',
|
||||||
|
tag: 'Searches',
|
||||||
},
|
},
|
||||||
} as const)
|
} as const)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ENV_CONFIG, MOD_IDS } from 'common/envs/constants'
|
import { ENV_CONFIG, MOD_USERNAMES } from 'common/envs/constants'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { removeUndefinedProps } from 'common/util/object'
|
import { removeUndefinedProps } from 'common/util/object'
|
||||||
|
|
||||||
@@ -22,6 +22,6 @@ export function toUserAPIResponse(user: User): FullUser {
|
|||||||
...user,
|
...user,
|
||||||
url: `https://${ENV_CONFIG.domain}/${user.username}`,
|
url: `https://${ENV_CONFIG.domain}/${user.username}`,
|
||||||
isAdmin: ENV_CONFIG.adminIds.includes(user.id),
|
isAdmin: ENV_CONFIG.adminIds.includes(user.id),
|
||||||
isTrustworthy: MOD_IDS.includes(user.id),
|
isTrustworthy: MOD_USERNAMES.includes(user.username),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ const genderType = z.string()
|
|||||||
// ])
|
// ])
|
||||||
const genderTypes = z.array(genderType)
|
const genderTypes = z.array(genderType)
|
||||||
|
|
||||||
|
export const zBoolean = z
|
||||||
|
.union([z.boolean(), z.string()])
|
||||||
|
.transform((val) => val === true || val === "true");
|
||||||
|
|
||||||
export const baseProfilesSchema = z.object({
|
export const baseProfilesSchema = z.object({
|
||||||
// Required fields
|
// Required fields
|
||||||
age: z.number().min(18).max(100).optional(),
|
age: z.number().min(18).max(100).optional(),
|
||||||
@@ -51,7 +55,7 @@ export const baseProfilesSchema = z.object({
|
|||||||
pref_age_max: z.number().min(18).max(100).optional(),
|
pref_age_max: z.number().min(18).max(100).optional(),
|
||||||
pref_relation_styles: z.array(z.string()),
|
pref_relation_styles: z.array(z.string()),
|
||||||
wants_kids_strength: z.number(),
|
wants_kids_strength: z.number(),
|
||||||
looking_for_matches: z.boolean(),
|
looking_for_matches: zBoolean,
|
||||||
photo_urls: z.array(z.string()),
|
photo_urls: z.array(z.string()),
|
||||||
visibility: z.union([z.literal('public'), z.literal('member')]),
|
visibility: z.union([z.literal('public'), z.literal('member')]),
|
||||||
|
|
||||||
@@ -76,23 +80,25 @@ const optionalProfilesSchema = z.object({
|
|||||||
ethnicity: z.array(z.string()).optional(),
|
ethnicity: z.array(z.string()).optional(),
|
||||||
born_in_location: z.string().optional(),
|
born_in_location: z.string().optional(),
|
||||||
height_in_inches: z.number().optional(),
|
height_in_inches: z.number().optional(),
|
||||||
has_pets: z.boolean().optional(),
|
has_pets: zBoolean.optional().optional(),
|
||||||
education_level: z.string().optional(),
|
education_level: z.string().optional(),
|
||||||
is_smoker: z.boolean().optional(),
|
is_smoker: zBoolean.optional().optional(),
|
||||||
drinks_per_month: z.number().min(0).optional(),
|
drinks_per_month: z.number().min(0).optional(),
|
||||||
is_vegetarian_or_vegan: z.boolean().optional(),
|
diet: z.array(z.string()).optional(),
|
||||||
has_kids: z.number().min(0).optional(),
|
has_kids: z.number().min(0).optional(),
|
||||||
university: z.string().optional(),
|
university: z.string().optional(),
|
||||||
occupation_title: z.string().optional(),
|
occupation_title: z.string().optional(),
|
||||||
occupation: z.string().optional(),
|
occupation: z.string().optional(),
|
||||||
company: z.string().optional(),
|
company: z.string().optional(),
|
||||||
comments_enabled: z.boolean().optional(),
|
comments_enabled: zBoolean.optional().optional(),
|
||||||
website: z.string().optional(),
|
website: z.string().optional(),
|
||||||
bio: contentSchema.optional().nullable(),
|
bio: contentSchema.optional().nullable(),
|
||||||
twitter: z.string().optional(),
|
twitter: z.string().optional(),
|
||||||
avatar_url: z.string().optional(),
|
avatar_url: z.string().optional(),
|
||||||
pref_romantic_styles: z.array(z.string()),
|
pref_romantic_styles: z.array(z.string()),
|
||||||
|
drinks_min: z.number().min(0).optional(),
|
||||||
|
drinks_max: z.number().min(0).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const combinedLoveUsersSchema =
|
export const combinedProfileSchema =
|
||||||
baseProfilesSchema.merge(optionalProfilesSchema)
|
baseProfilesSchema.merge(optionalProfilesSchema)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const MAX_INT = 99999
|
export const MIN_INT = Number.MIN_SAFE_INTEGER
|
||||||
export const MIN_INT = -MAX_INT
|
export const MAX_INT = Number.MAX_SAFE_INTEGER
|
||||||
|
|
||||||
export const supportEmail = 'hello@compassmeet.com';
|
export const supportEmail = 'hello@compassmeet.com';
|
||||||
// export const marketingEmail = 'hello@compassmeet.com';
|
// export const marketingEmail = 'hello@compassmeet.com';
|
||||||
@@ -8,6 +8,7 @@ export const githubRepo = "https://github.com/CompassConnections/Compass";
|
|||||||
export const githubIssues = `${githubRepo}/issues`
|
export const githubIssues = `${githubRepo}/issues`
|
||||||
|
|
||||||
export const paypalLink = "https://www.paypal.com/paypalme/CompassConnections"
|
export const paypalLink = "https://www.paypal.com/paypalme/CompassConnections"
|
||||||
|
export const openCollectiveLink = "https://opencollective.com/compass-connection"
|
||||||
export const patreonLink = "https://patreon.com/CompassMeet"
|
export const patreonLink = "https://patreon.com/CompassMeet"
|
||||||
export const discordLink = "https://discord.gg/8Vd7jzqjun"
|
export const discordLink = "https://discord.gg/8Vd7jzqjun"
|
||||||
export const stoatLink = "https://stt.gg/YKQp81yA"
|
export const stoatLink = "https://stt.gg/YKQp81yA"
|
||||||
|
|||||||
@@ -5,6 +5,20 @@ import {isProd} from "common/envs/is-prod";
|
|||||||
export const MAX_DESCRIPTION_LENGTH = 100000
|
export const MAX_DESCRIPTION_LENGTH = 100000
|
||||||
export const MAX_ANSWER_LENGTH = 240
|
export const MAX_ANSWER_LENGTH = 240
|
||||||
|
|
||||||
|
export const LOCAL_WEB_DOMAIN = 'localhost:3000';
|
||||||
|
export const LOCAL_BACKEND_DOMAIN = 'localhost:8088';
|
||||||
|
|
||||||
|
export const IS_GOOGLE_CLOUD = !!process.env.GOOGLE_CLOUD_PROJECT
|
||||||
|
export const IS_VERCEL = !!process.env.NEXT_PUBLIC_VERCEL
|
||||||
|
export const IS_DEPLOYED = IS_GOOGLE_CLOUD || IS_VERCEL
|
||||||
|
export const IS_LOCAL = !IS_DEPLOYED
|
||||||
|
export const HOSTING_ENV = IS_GOOGLE_CLOUD ? 'Google Cloud' : IS_VERCEL ? 'Vercel' : IS_LOCAL ? 'local' : 'unknown'
|
||||||
|
|
||||||
|
if (IS_LOCAL && !process.env.ENVIRONMENT && !process.env.NEXT_PUBLIC_FIREBASE_ENV) {
|
||||||
|
console.warn("No ENVIRONMENT set, defaulting to DEV")
|
||||||
|
process.env.ENVIRONMENT = 'DEV'
|
||||||
|
}
|
||||||
|
|
||||||
export const ENV_CONFIG = isProd() ? PROD_CONFIG : DEV_CONFIG
|
export const ENV_CONFIG = isProd() ? PROD_CONFIG : DEV_CONFIG
|
||||||
|
|
||||||
export function isAdminId(id: string) {
|
export function isAdminId(id: string) {
|
||||||
@@ -12,20 +26,13 @@ export function isAdminId(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isModId(id: string) {
|
export function isModId(id: string) {
|
||||||
return MOD_IDS.includes(id)
|
return MOD_USERNAMES.includes(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ENV = isProd() ? 'prod' : 'dev'
|
export const ENV = isProd() ? 'prod' : 'dev'
|
||||||
export const IS_PROD = ENV === 'prod'
|
export const IS_PROD = ENV === 'prod'
|
||||||
export const IS_DEV = ENV === 'dev'
|
export const IS_DEV = ENV === 'dev'
|
||||||
|
|
||||||
export const LOCAL_WEB_DOMAIN = 'localhost:3000';
|
|
||||||
export const LOCAL_BACKEND_DOMAIN = 'localhost:8088';
|
|
||||||
|
|
||||||
export const IS_GOOGLE_CLOUD = !!process.env.GOOGLE_CLOUD_PROJECT
|
|
||||||
export const IS_VERCEL = !!process.env.NEXT_PUBLIC_VERCEL
|
|
||||||
export const IS_LOCAL = !IS_GOOGLE_CLOUD && !IS_VERCEL
|
|
||||||
export const HOSTING_ENV = IS_GOOGLE_CLOUD ? 'Google Cloud' : IS_VERCEL ? 'Vercel' : IS_LOCAL ? 'local' : 'unknown'
|
|
||||||
console.debug(`Running in ${HOSTING_ENV} (${ENV})`,);
|
console.debug(`Running in ${HOSTING_ENV} (${ENV})`,);
|
||||||
|
|
||||||
// class MissingKeyError implements Error {
|
// class MissingKeyError implements Error {
|
||||||
@@ -57,8 +64,8 @@ export const AUTH_COOKIE_NAME = `FBUSER_${PROJECT_ID.toUpperCase().replace(
|
|||||||
'_'
|
'_'
|
||||||
)}`
|
)}`
|
||||||
|
|
||||||
export const MOD_IDS = [
|
export const MOD_USERNAMES = [
|
||||||
'...',
|
'Martin',
|
||||||
]
|
]
|
||||||
|
|
||||||
export const VERIFIED_USERNAMES = [
|
export const VERIFIED_USERNAMES = [
|
||||||
@@ -86,6 +93,7 @@ export const RESERVED_PATHS = [
|
|||||||
'chat',
|
'chat',
|
||||||
'chats',
|
'chats',
|
||||||
'common',
|
'common',
|
||||||
|
'compatibility',
|
||||||
'confirm-email',
|
'confirm-email',
|
||||||
'contact',
|
'contact',
|
||||||
'contacts',
|
'contacts',
|
||||||
@@ -109,7 +117,7 @@ export const RESERVED_PATHS = [
|
|||||||
'links',
|
'links',
|
||||||
'live',
|
'live',
|
||||||
'login',
|
'login',
|
||||||
'love-questions',
|
'questions',
|
||||||
'manifest',
|
'manifest',
|
||||||
'market',
|
'market',
|
||||||
'markets',
|
'markets',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const DEV_CONFIG: EnvConfig = {
|
|||||||
domain: 'dev.compassmeet.com',
|
domain: 'dev.compassmeet.com',
|
||||||
backendDomain: 'api.dev.compassmeet.com',
|
backendDomain: 'api.dev.compassmeet.com',
|
||||||
supabaseInstanceId: 'zbspxezubpzxmuxciurg',
|
supabaseInstanceId: 'zbspxezubpzxmuxciurg',
|
||||||
|
dbEncryptionKey: 'MIWD5LuZ6KoRNChcmOn762MKJRHhH4rLUGYse7+GeS4=',
|
||||||
supabasePwd: 'ZTNlifGKofSKhu8c', // For database write access (dev). A 16-character password with digits and letters.
|
supabasePwd: 'ZTNlifGKofSKhu8c', // For database write access (dev). A 16-character password with digits and letters.
|
||||||
supabaseAnonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inpic3B4ZXp1YnB6eG11eGNpdXJnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTc2ODM0MTMsImV4cCI6MjA3MzI1OTQxM30.ZkM7zlawP8Nke0T3KJrqpOQ4DzqPaXTaJXLC2WU8Y7c',
|
supabaseAnonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inpic3B4ZXp1YnB6eG11eGNpdXJnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTc2ODM0MTMsImV4cCI6MjA3MzI1OTQxM30.ZkM7zlawP8Nke0T3KJrqpOQ4DzqPaXTaJXLC2WU8Y7c',
|
||||||
googleApplicationCredentials: 'googleApplicationCredentials-dev.json',
|
googleApplicationCredentials: 'googleApplicationCredentials-dev.json',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export type EnvConfig = {
|
export type EnvConfig = {
|
||||||
domain: string
|
domain: string
|
||||||
firebaseConfig: FirebaseConfig
|
firebaseConfig: FirebaseConfig
|
||||||
|
dbEncryptionKey: string
|
||||||
supabaseInstanceId: string
|
supabaseInstanceId: string
|
||||||
supabaseAnonKey: string
|
supabaseAnonKey: string
|
||||||
supabasePwd?: string
|
supabasePwd?: string
|
||||||
@@ -38,6 +39,7 @@ export const PROD_CONFIG: EnvConfig = {
|
|||||||
supabaseInstanceId: 'ltzepxnhhnrnvovqblfr',
|
supabaseInstanceId: 'ltzepxnhhnrnvovqblfr',
|
||||||
supabaseAnonKey: '',
|
supabaseAnonKey: '',
|
||||||
supabasePwd: '',
|
supabasePwd: '',
|
||||||
|
dbEncryptionKey: '',
|
||||||
googleApplicationCredentials: undefined,
|
googleApplicationCredentials: undefined,
|
||||||
firebaseConfig: {
|
firebaseConfig: {
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
@@ -63,6 +65,7 @@ export const refreshConfig = () => {
|
|||||||
PROD_CONFIG.supabasePwd = process.env.SUPABASE_DB_PASSWORD || ''
|
PROD_CONFIG.supabasePwd = process.env.SUPABASE_DB_PASSWORD || ''
|
||||||
PROD_CONFIG.googleApplicationCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS
|
PROD_CONFIG.googleApplicationCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS
|
||||||
PROD_CONFIG.firebaseConfig.apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY || ''
|
PROD_CONFIG.firebaseConfig.apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY || ''
|
||||||
|
PROD_CONFIG.dbEncryptionKey = process.env.DB_ENC_MASTER_KEY_BASE64 || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshConfig()
|
refreshConfig()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Profile, ProfileRow} from "common/love/profile";
|
import {Profile, ProfileRow} from "common/profiles/profile";
|
||||||
import {cloneDeep} from "lodash";
|
import {cloneDeep} from "lodash";
|
||||||
import {filterDefined} from "common/util/array";
|
import {filterDefined} from "common/util/array";
|
||||||
|
|
||||||
@@ -15,12 +15,18 @@ export type FilterFields = {
|
|||||||
lon: number | null
|
lon: number | null
|
||||||
radius: number | null
|
radius: number | null
|
||||||
genders: string[]
|
genders: string[]
|
||||||
|
education_levels: string[]
|
||||||
name: string | undefined
|
name: string | undefined
|
||||||
shortBio: boolean | undefined
|
shortBio: boolean | undefined
|
||||||
|
drinks_min: number | undefined
|
||||||
|
drinks_max: number | undefined
|
||||||
} & Pick<
|
} & Pick<
|
||||||
ProfileRow,
|
ProfileRow,
|
||||||
| 'wants_kids_strength'
|
| 'wants_kids_strength'
|
||||||
| 'pref_relation_styles'
|
| 'pref_relation_styles'
|
||||||
|
| 'pref_romantic_styles'
|
||||||
|
| 'diet'
|
||||||
|
| 'political_beliefs'
|
||||||
| 'is_smoker'
|
| 'is_smoker'
|
||||||
| 'has_kids'
|
| 'has_kids'
|
||||||
| 'pref_gender'
|
| 'pref_gender'
|
||||||
@@ -54,14 +60,20 @@ export const initialFilters: Partial<FilterFields> = {
|
|||||||
radius: undefined,
|
radius: undefined,
|
||||||
name: undefined,
|
name: undefined,
|
||||||
genders: undefined,
|
genders: undefined,
|
||||||
|
education_levels: undefined,
|
||||||
pref_age_max: undefined,
|
pref_age_max: undefined,
|
||||||
pref_age_min: undefined,
|
pref_age_min: undefined,
|
||||||
has_kids: undefined,
|
has_kids: undefined,
|
||||||
wants_kids_strength: undefined,
|
wants_kids_strength: undefined,
|
||||||
is_smoker: undefined,
|
is_smoker: undefined,
|
||||||
pref_relation_styles: undefined,
|
pref_relation_styles: undefined,
|
||||||
|
pref_romantic_styles: undefined,
|
||||||
|
diet: undefined,
|
||||||
|
political_beliefs: undefined,
|
||||||
pref_gender: undefined,
|
pref_gender: undefined,
|
||||||
shortBio: undefined,
|
shortBio: undefined,
|
||||||
|
drinks_min: undefined,
|
||||||
|
drinks_max: undefined,
|
||||||
orderBy: 'created_time',
|
orderBy: 'created_time',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export interface HasKidsLabelsMap {
|
|||||||
export const hasKidsLabels: HasKidsLabelsMap = {
|
export const hasKidsLabels: HasKidsLabelsMap = {
|
||||||
no_preference: {
|
no_preference: {
|
||||||
name: 'Any kids',
|
name: 'Any kids',
|
||||||
shortName: 'Any kids',
|
shortName: 'Either',
|
||||||
value: -1,
|
value: -1,
|
||||||
},
|
},
|
||||||
has_kids: {
|
has_kids: {
|
||||||
|
|||||||
@@ -3,19 +3,20 @@ import { Row, SupabaseClient } from 'common/supabase/utils'
|
|||||||
export type Notification = {
|
export type Notification = {
|
||||||
id: string
|
id: string
|
||||||
userId: string
|
userId: string
|
||||||
|
title?: string
|
||||||
reasonText?: string
|
reasonText?: string
|
||||||
reason: string
|
reason?: string
|
||||||
createdTime: number
|
createdTime: number
|
||||||
viewTime?: number
|
viewTime?: number
|
||||||
isSeen: boolean
|
isSeen: boolean
|
||||||
|
|
||||||
sourceId: string
|
sourceId?: string
|
||||||
sourceType: string
|
sourceType: string
|
||||||
sourceUpdateType?: 'created' | 'updated' | 'deleted'
|
sourceUpdateType?: 'created' | 'updated' | 'deleted'
|
||||||
|
|
||||||
sourceUserName: string
|
sourceUserName?: string
|
||||||
sourceUserUsername: string
|
sourceUserUsername?: string
|
||||||
sourceUserAvatarUrl: string
|
sourceUserAvatarUrl?: string
|
||||||
sourceText: string
|
sourceText: string
|
||||||
data?: { [key: string]: any }
|
data?: { [key: string]: any }
|
||||||
|
|
||||||
@@ -29,12 +30,12 @@ export type Notification = {
|
|||||||
isSeenOnHref?: string
|
isSeenOnHref?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NOTIFICATION_TYPES_TO_SELECT = [
|
// export const NOTIFICATION_TYPES_TO_SELECT = [
|
||||||
'new_match', // new match markets
|
// 'new_match', // new match markets
|
||||||
'comment_on_profile', // endorsements
|
// 'comment_on_profile', // endorsements
|
||||||
'love_like',
|
// 'profile_like',
|
||||||
'love_ship',
|
// 'profile_ship',
|
||||||
]
|
// ]
|
||||||
|
|
||||||
export const NOTIFICATIONS_PER_PAGE = 30
|
export const NOTIFICATIONS_PER_PAGE = 30
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user