From f4f8224078354aac4e1b0ab89860eed2c3edb411 Mon Sep 17 00:00:00 2001 From: Chase Manning Date: Fri, 12 Apr 2024 16:04:54 +0100 Subject: [PATCH] remove registration endpoints and replace with simple example endpoint --- README.md | 113 ++-------------------------------- functions/src/constants.ts | 36 +---------- functions/src/db.ts | 41 +++--------- functions/src/index.ts | 63 ++----------------- functions/src/registration.ts | 93 ---------------------------- functions/src/services.ts | 70 --------------------- functions/src/types.ts | 49 --------------- functions/src/utils.ts | 22 ------- 8 files changed, 18 insertions(+), 469 deletions(-) delete mode 100644 functions/src/registration.ts delete mode 100644 functions/src/services.ts diff --git a/README.md b/README.md index 7bd561c..8db1d13 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,7 @@ -# Scope +# TLX API -## Database +A collection of API endpoints in TS deployed on Firebase for use with the TLX Protocol. -The database will be built using Firebase Firestore Database. +## Endpoints -We will have two tables, one to store information about users, and one to store information about invite codes. - -### User: - -- `address` **primary key** Users's wallet address -- `twitter_access_token` The access token of the user's Twitter account -- `twitter_secret` The secret of the user's Twitter account -- `discord_access_token` The access token of the user's Discord account -- `discord_secret` The secret of the user's Discord account - -### Codes: - -- `invite_code` **primary key** The invite code -- `creator` Who created the invite code -- `user` **optional** The user who claimed in invite code - -## API - -The API will be a JavaScript Firebase Function with HTTP trigger. - -### Register - -The register endpoint is a POST endpoint. -It takes as an input the full user data including address, twitter info and discord info. And the invite code. -The endpoint first validates that the Twitter info is valid. -It then validates that the user is following us on Twitter. -It then validates that the Discord info is valid. -It then validates that the user is in our Discord. -It then validates that the signature is valid for the address. -It then validates that the address hasn't already been used in our database. -It then validates that the Twitter hasn't already been used in our database. -It then validates that the Discord hasn't already been used in our database. -It then validates that the invite code is valid, and not used. -It then updates that invite code as claimed and with the users address. -It then generates 5 invite codes for the user, and writes them to the database. -It then writes the user data to the database. -It then returns the invite codes in the response. -If any of the validations fail, it returns an appropriate error message to display on the UI. - -### Has Registered - -The Has Registered endpoint is a GET endpoint. -It takes an addres as an input. -It returns `true` or `false` based on if the user has already registered. - -### Invite Codes - -The Invite Codes endpoint is a GET endpoint. -It takes an addres as an input, and a signature to prove that the caller owns the address. -It returns a list of Invite codes for the user. - -### Is Invite Code Used - -The Is Invite Code Used endpoint is a GET endpoint. -It takes an invite code as an input. -It returns `true` or `false` based on if the invite code has been used yet. -If the invite code doesn't exist, it returns `false` (this is to prevent brute forcing to find invite codes). - -## Front end - -### Register button - -The top right Twitter button will be replaced with a Rebalance button. - -### Connect wallet popup - -A popup will be added for the users to connect their wallet when they click on the Register button - -### Check if user is registered - -A check will be done once the user's wallet is connected to see if they have registered yet. - -### Register popup - -If the user has not registered yet, a Register popup will show. -The popup will have the header "Register". -The popup will have 5 steps. -There will be an input field for inputting your invite code. -There will be a section for authenticating with Twitter. This will call to the Twitter API to register the user and get their access token and secret. -There will be a section for following us on Twitter which will open our Twitter in a new tab. -There will be a section for authenticating with Discord. This will call to the Discord API to register the user and get their access token and secret. -There will be a section for joining our Discord which will open our Discord in a new tab. -There will be a "Register" button at the bottom. This will be disabled until all the steps above are complete. -As the user complete each step above, a visual indicator will update to show this is done. -When the user clicks the register button, it will make a call to our register API to attemp to register them. -If there is an error, it will show in an error popup. -If it works, then the user will be redirected to the Invite Code view. - -### Sign for invite codes - -If the users wallet is registered then they click on the "Register" button, it will show a popup. -The popup will have the header "Sign transaction to view invite codes". -It will have a button "View Invite Codes". -When the click that button it will make them sign a transaction, and then make a request to our api to get their invite codes. -It will then redirect to the invite codes popup. - -### Invite Codes - -This view will show a list of the user's invite codes. -For each invite code it will call our API endpoint to check if that Invite code has been used. -If it has been used, there will be a visual indicator for this. -Next to each invite code will be a copy button allowing users to copy the invite code. - -### Privacy Policy - -Our Privacy Policy will need to be updated now that we are collecting additional information from our users. +- `example` Just a read endpoint that returns "Hello world" diff --git a/functions/src/constants.ts b/functions/src/constants.ts index 5262f73..cd61c7e 100644 --- a/functions/src/constants.ts +++ b/functions/src/constants.ts @@ -1,35 +1 @@ -import { OauthCredentials, ServiceType } from "./types"; - -export interface Secrets { - twitterClientID: string; - twitterClientSecret: string; - discordClientID: string; - discordClientSecret: string; -} - -export const redirectURI = "https://tlx.fi/register"; - -export function getCredentials(service: ServiceType, secrets: Secrets): OauthCredentials { - return { - clientID: service === ServiceType.Twitter ? secrets.twitterClientID : secrets.discordClientID, - clientSecret: service === ServiceType.Twitter ? secrets.twitterClientSecret : secrets.discordClientSecret, - }; -} - -export const oauthURLs = { - [ServiceType.Twitter]: "https://api.twitter.com/2/oauth2/token", - [ServiceType.Discord]: "https://discord.com/api/oauth2/token", -}; - -export const tlxGuidID = "1142096814573621308"; - -export const messageToSign = - "I agree to the TLX Terms of Service (https://tlx.fi/terms-of-service)" + - " and TLX Privacy Policy (https://tlx.fi/privacy-policiy)." + - " I acknowledge that TLX integrates with third-party applications, which may come with risks"; - -export const codeValidChars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; -export const codeLength = 4; -export const invitesPerUser = 5; - -export const partnerCodes = ["kirbycrypto", "raccooncrypto", "yungmatt", "cwesearch"]; +export const EXAMPLE_CONST = "FOO"; diff --git a/functions/src/db.ts b/functions/src/db.ts index c732aae..6af8d07 100644 --- a/functions/src/db.ts +++ b/functions/src/db.ts @@ -1,42 +1,19 @@ import admin from "firebase-admin"; -import { createRandomString } from "./utils"; -import { codeLength } from "./constants"; - -export async function usernameExists(key: string, username: string): Promise { - const db = admin.database(); - const snapshot = await db.ref("users").orderByChild(key).equalTo(username).get(); - return snapshot.exists(); -} - -export async function userExists(address: string): Promise { - const db = admin.database(); - const snapshot = await db.ref("users").child(address).get(); - return snapshot.exists(); -} - -export async function useCode(code: string, user: string): Promise { +export async function exampleRead(id: string): Promise { const db = admin.database(); - await db.ref("invites").child(code).update({ used: true, usedAt: Date.now(), user }); + const snapshot = await db.ref("databaseName").child(id).get(); + return snapshot.val().value; } -async function generateInviteCode(): Promise { +export async function exampleUpdate(id: string, value: string): Promise { const db = admin.database(); - let code: string; - do { - code = createRandomString(codeLength); - } while ((await db.ref("invites").child(code).get()).exists()); - return code; + await db.ref("databaseName").child(id).update({ value }); } -export async function generateAndSaveInviteCodes(user: string, count: number): Promise { +export async function exampleWrite(id: string, value: string): Promise { const db = admin.database(); - const codes = []; - for (let i = 0; i < count; i++) { - const code = await generateInviteCode(); - const codeMetadata = { creator: user, used: false, createdAt: Date.now(), code }; - codes.push(code); - await db.ref("invites").child(code).set(codeMetadata); - } - return codes; + const data = { id, value }; + await db.ref("databaseName").child(id).set(data); + return value; } diff --git a/functions/src/index.ts b/functions/src/index.ts index e3b7480..3007b2c 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -1,67 +1,12 @@ import { onRequest } from "firebase-functions/v2/https"; import admin from "firebase-admin"; -import registrationHandler from "./registration"; -import wrapHandler, { getUserAddress, isPartnerCode, validateParams } from "./utils"; -import { userExists } from "./db"; -import { APIError, SignedParams } from "./types"; -import { defineSecret } from "firebase-functions/params"; - -const twitterClientID = defineSecret("TWITTER_CLIENT_ID"); -const twitterClientSecret = defineSecret("TWITTER_CLIENT_SECRET"); -const discordClientID = defineSecret("DISCORD_CLIENT_ID"); -const discordClientSecret = defineSecret("DISCORD_CLIENT_SECRET"); +import wrapHandler from "./utils"; admin.initializeApp(); -export const register = onRequest( - { secrets: [twitterClientID, twitterClientSecret, discordClientID, discordClientSecret] }, - wrapHandler(async (request) => { - await registrationHandler(request, { - twitterClientID: twitterClientID.value(), - twitterClientSecret: twitterClientSecret.value(), - discordClientID: discordClientID.value(), - discordClientSecret: discordClientSecret.value(), - }); - }) -); - -export const inviteCodes = onRequest( - wrapHandler(async (request) => { - const { signature } = validateParams(request.query, "signature"); - - const user = getUserAddress(signature); - if (!(await userExists(user))) { - throw new APIError("Address not registered"); - } - - const db = admin.database(); - const snapshot = await db.ref("invites").orderByChild("creator").equalTo(user).get(); - return snapshot.val(); - }) -); - -export const hasRegistered = onRequest( - wrapHandler(async (request) => { - const { signature } = validateParams(request.query, "signature"); - - const user = getUserAddress(signature); - return { hasRegistered: await userExists(user) }; - }) -); - -export const inviteCodeUsed = onRequest( - wrapHandler(async (request) => { - const { inviteCode } = validateParams<{ inviteCode: string }>(request.query, "inviteCode"); - - const isPartner = isPartnerCode(inviteCode); - if (isPartner) return false; - - const db = admin.database(); - const codeSnapshot = await db.ref("invites").child(inviteCode).get(); - if (!codeSnapshot.exists()) { - throw new APIError("Invalid invite code"); - } - return codeSnapshot.val().used; +export const example = onRequest( + wrapHandler(async () => { + return { message: "Hello, world!" }; }) ); diff --git a/functions/src/registration.ts b/functions/src/registration.ts deleted file mode 100644 index cae6b00..0000000 --- a/functions/src/registration.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Request } from "firebase-functions"; -import * as logger from "firebase-functions/logger"; -import admin from "firebase-admin"; - -import { DiscordService, TwitterService } from "./services"; -import { APIError, RegistrationParams, Guild } from "./types"; -import { Secrets, invitesPerUser, tlxGuidID } from "./constants"; -import { generateAndSaveInviteCodes, useCode, userExists, usernameExists } from "./db"; -import { getUserAddress, isPartnerCode, validateParams } from "./utils"; - -const requiredKeys = ["twitterCode", "discordCode", "signature", "inviteCode"]; - -async function getTwitterUsername(code: string, secrets: Secrets): Promise { - try { - logger.info("Getting Twitter data"); - logger.info(`Code: ${code}`); - const twitterService = await TwitterService.fromCode(code, secrets); - return twitterService.getUsername(); - } catch (error) { - throw new APIError(`Twitter authentication failed: ${error}`); - } -} - -async function getDiscordData( - code: string, - secrets: Secrets -): Promise<{ username: string; guilds: Guild[]; error: boolean }> { - try { - logger.info("Getting Discord data"); - logger.info(`Code: ${code}`); - const discordService = await DiscordService.fromCode(code, secrets); - const username = await discordService.getUsername(); - const guilds = await discordService.getGuilds(); - return { username, guilds, error: false }; - } catch (error) { - logger.error(`Discord authentication failed: ${error}`); - return { username: "ERROR", guilds: [], error: true }; - } -} - -export default async function registrationHandler(request: Request, secrets: Secrets) { - throw new APIError("Registrations are closed"); - - const params = validateParams(request.body, ...requiredKeys); - const address = getUserAddress(params.signature); - const db = admin.database(); - - // Validating address - if (await userExists(address)) { - throw new APIError("Address already used"); - } - - // Validating invite code - const isPartner = isPartnerCode(params.inviteCode); - if (!isPartner) { - const codeSnapshot = await db.ref("invites").child(params.inviteCode).get(); - if (!codeSnapshot.exists()) { - throw new APIError("Invalid invite code"); - } - if (codeSnapshot.val().used) { - throw new APIError("Code already used"); - } - } - - // Validating Twitter - const twitterUsername = await getTwitterUsername(params.twitterCode, secrets); - if (await usernameExists("twitterUsername", twitterUsername)) { - throw new APIError("Twitter already used"); - } - - // Validating Discord - const { username: discordUsername, guilds, error } = await getDiscordData(params.discordCode, secrets); - if (!error) { - if (await usernameExists("discordUsername", discordUsername)) { - throw new APIError("Discord already used"); - } - if (!guilds.some((guild) => guild.id === tlxGuidID)) { - throw new APIError("Not in the TLX Discord server"); - } - } - - // Saving user - const user = { twitterUsername, discordUsername }; - await db.ref("users").child(address).set(user); - - // Generating invite codes - const codes = await generateAndSaveInviteCodes(address, invitesPerUser); - - // Using invite code - await useCode(params.inviteCode, address); - - return { address, ...user, codes }; -} diff --git a/functions/src/services.ts b/functions/src/services.ts deleted file mode 100644 index 1047ddf..0000000 --- a/functions/src/services.ts +++ /dev/null @@ -1,70 +0,0 @@ -import fetch, { Response } from "node-fetch"; -import { redirectURI, getCredentials, oauthURLs, Secrets } from "./constants"; -import { Guild, ServiceType, TokenResponse, TwitterUserResponse, User } from "./types"; - -async function processResponse(response: Response): Promise { - if (!response.ok) { - throw new Error(await response.text()); - } - return (await response.json()) as T; -} - -function getAuthorization(service: ServiceType, secrets: Secrets) { - const { clientID, clientSecret } = getCredentials(service, secrets); - const encodedCredentials = Buffer.from(`${clientID}:${clientSecret}`).toString("base64"); - const paddingLength = (4 - (encodedCredentials.length % 4)) % 4; - return encodedCredentials + "=".repeat(paddingLength); -} - -async function getAccessToken(service: ServiceType, code: string, secrets: Secrets): Promise { - const params = new URLSearchParams({ code, grant_type: "authorization_code", redirect_uri: redirectURI }); - if (service === ServiceType.Twitter) { - params.set("code_verifier", "challenge"); - } - - const headers = { - "Content-Type": "application/x-www-form-urlencoded", - "Authorization": `Basic ${getAuthorization(service, secrets)}`, - }; - const response = await fetch(oauthURLs[service], { method: "POST", body: params.toString(), headers }); - const data = await processResponse(response); - return data.access_token; -} - -class Service { - private readonly accessToken: string; - constructor(accessToken: string) { - this.accessToken = accessToken; - } - - protected async get(url: string): Promise { - const headers = { Authorization: `Bearer ${this.accessToken}` }; - return processResponse(await fetch(url, { headers })); - } -} - -export class TwitterService extends Service { - static async fromCode(code: string, secrets: Secrets): Promise { - return new TwitterService(await getAccessToken(ServiceType.Twitter, code, secrets)); - } - - async getUsername(): Promise { - const data = await this.get("https://api.twitter.com/2/users/me"); - return data.data.username; - } -} - -export class DiscordService extends Service { - static async fromCode(code: string, secrets: Secrets): Promise { - return new DiscordService(await getAccessToken(ServiceType.Discord, code, secrets)); - } - - async getUsername(): Promise { - const { username } = await this.get("https://discord.com/api/users/@me"); - return username; - } - - async getGuilds(): Promise { - return this.get("https://discord.com/api/users/@me/guilds"); - } -} diff --git a/functions/src/types.ts b/functions/src/types.ts index 5dba31b..ad1c9fc 100644 --- a/functions/src/types.ts +++ b/functions/src/types.ts @@ -1,56 +1,7 @@ import { Request } from "firebase-functions"; import { Response } from "express"; -export enum ServiceType { - Twitter, - Discord, -} - -export type TokenResponse = { - token_type: string; - expires_in: number; - access_token: string; - scope: string; - refresh_token: string; -}; - -export type User = { - id: string; - username: string; -}; - -export type Guild = { - id: string; - username: string; -}; - -export type Code = { - creator: string; - used: boolean; - createdAt: number; - usedAt?: number; - user?: string; -}; - -export type TwitterUserResponse = { - data: User; -}; - -export type OauthCredentials = { - clientID: string; - clientSecret: string; -}; export class APIError extends Error {} -export type SignedParams = { - signature: string; -}; - -export type RegistrationParams = SignedParams & { - twitterCode: string; - discordCode: string; - inviteCode: string; -}; - export type HTTPHandler = (request: Request, response: Response) => void | Promise; export type FunctionHandler = (request: Request) => Promise; diff --git a/functions/src/utils.ts b/functions/src/utils.ts index a56bd13..8cc95a2 100644 --- a/functions/src/utils.ts +++ b/functions/src/utils.ts @@ -1,26 +1,8 @@ -import { ethers } from "ethers"; -import { codeValidChars, messageToSign, partnerCodes } from "./constants"; import { APIError, FunctionHandler, HTTPHandler } from "./types"; import * as cors from "cors"; const corsFunc = cors({ origin: true }); -export function createRandomString(length: number): string { - let result = ""; - for (let i = 0; i < length; i++) { - result += codeValidChars.charAt(Math.floor(Math.random() * codeValidChars.length)); - } - return result; -} - -export function getUserAddress(signature: string): string { - try { - return ethers.verifyMessage(messageToSign, signature); - } catch (error) { - throw new APIError("Invalid signature"); - } -} - // eslint-disable-next-line @typescript-eslint/no-explicit-any export function validateParams(body: any, ...keys: string[]): T { for (const key of keys) { @@ -45,7 +27,3 @@ export default function wrapHandler(handler: FunctionHandler): HTTPHandler }); }; } - -export function isPartnerCode(code: string): boolean { - return partnerCodes.includes(code.toLowerCase()); -}