207 lines
8.9 KiB
Markdown
207 lines
8.9 KiB
Markdown
|
# Referral Manager V1: Cross-Satellite Referral Link
|
||
|
|
||
|
## Abstract
|
||
|
|
||
|
This document describes how to handle user referrals across satellites.
|
||
|
|
||
|
## Background
|
||
|
|
||
|
The previous referral program only works per satellite. This means a user on us-east-1 satellite can only refer people to join us-east-1.
|
||
|
|
||
|
We want referrals to be valid on all Tardigrade satellites. This new design will allow the user to select any satellite, regardless of the satellite the referral was generated for.
|
||
|
|
||
|
### Non-Goals
|
||
|
A registration page where a user can select their preferred Satellite during registration.
|
||
|
|
||
|
## Design
|
||
|
|
||
|
### Definitions
|
||
|
|
||
|
Referral Manager
|
||
|
: A standalone process running separately from satellites. Only Tardigrade-branded satellites should be able to talk to it.
|
||
|
|
||
|
Referral Manager CLI
|
||
|
: A CLI that will allow operators to invoke referral link generation.
|
||
|
|
||
|
Invitation Token
|
||
|
: A random number generated by Referral Manager for each user in the referral program.
|
||
|
|
||
|
Referral Link
|
||
|
: A one-time-use URL that contains a unique invitation token.
|
||
|
|
||
|
Eligible Users
|
||
|
: Users who have fewer than the desired number of unredeemed referral links.
|
||
|
|
||
|
Owner's Satellite
|
||
|
: The satellite that the owner of a particular token belongs to.
|
||
|
|
||
|
Redeemed Satellite
|
||
|
: The satellite where the new user redeeming a token has registered on.
|
||
|
|
||
|
### How it will work
|
||
|
|
||
|
_User Interface_
|
||
|
|
||
|
1. Users will have a referral tab on the UI. When clicked, it will trigger the satellite to send a request to retrieve referral links from the Referral Manager:
|
||
|
- The Referral Manager will attempt to generate and/or fetch unredeemed tokens in the `tokens` table for this user ID and satellite.
|
||
|
- If the user ID doesn't exist in the Referral Manager's `users` table, the Referral Manager will add an entry for this user ID and satellite in the `users` table and return an empty response to the satellite.
|
||
|
- If the `users` table contains a value larger than `0` for `new_tokens` for this user ID, the Referral Manager will generate that number of new tokens, add the new unredeemed tokens to the `tokens` table, and set the value of `new_tokens` for that user to `0` in the `users` table.
|
||
|
- If there are unredeemed tokens in the `tokens` table, the Referral Manager will respond to the satellite's request with the tokens after generating new tokens. Otherwise, it will return an empty response.
|
||
|
- The satellite receives the response from Referral Manager.
|
||
|
- If the response payload contains tokens, the satellite will display them in the UI.
|
||
|
- If it's an empty response payload, the satellite will display a message `No available referral links. Try again later.`
|
||
|
|
||
|
|
||
|
_Referral Manager CLI_
|
||
|
|
||
|
1. `referral-manager start --tokens-per-user=3 --max-unredeemed-tokens-per-user=1 <satelliteURL1> <satelliteURL2> ...` initiates the invitation token generation process. The `--dry-run` flag can optionally be added to run the process without actually generating tokens. The dry run allows for estimating how many new tokens would be created by the command.
|
||
|
2. The Referral Manager queries both `users` and `tokens` tables for all users where `new_tokens + unredeemed_tokens < flags.tokens_per_user` and where the satellite matches one of the CLI-provided satellite URLs. These are the "eligible users".
|
||
|
3. The Referral Manager will count the number of eligible users and calculate the number of tokens needed to bring each user up to `tokens_per_user`. If `--dry-run` is set, these values will be returned to the CLI for output and the process will end. Otherwise, the `users` table will be updated to set `new_tokens` to `tokens_per_user - unredeemed_tokens` for each eligible user, and the values will be returned to the CLI for output.
|
||
|
|
||
|
_Referral Link Redemption_
|
||
|
|
||
|
1. User Alice tries to register a new account through a referral link, which triggers the redeemed satellite to verify invitation token using Referral manager. The satellite preemptively generates a new user ID and sends this ID along with the token to the Referral Manager.
|
||
|
2. Referral Manager checks the status of the token:
|
||
|
- If the token exists in `tokens` table:
|
||
|
- If it is not redeemed, Referral Manager sends back a success response to the redeemed satellite and marks the token as redeemed by the new user ID in the Referral Manager's `tokens` table. It also adds an entry for the new user ID in the `users` table.
|
||
|
- If the token is already redeemed, the Referral Manager sends back an `invalid token` response to the redeemed satellite.
|
||
|
- If the token doesn't exist in `tokens` table, Referral Manager sends back an `invalid token` response to the redeemed satellite.
|
||
|
3. Redeemed satellite receives the response, which:
|
||
|
- If it is a success, the satellite will proceed with the account creation
|
||
|
- If it is an invalid token, the satellite will display a proper message in the UI.
|
||
|
|
||
|
## Rationale
|
||
|
|
||
|
We will create an admin port that's only listening on `localhost` so CLI could be only used by Referral Manager operators.
|
||
|
|
||
|
We could also set the admin port to only accept requests coming from storj vpn.
|
||
|
|
||
|
## Implementation
|
||
|
|
||
|
- Create a private repository for Referral Manager.
|
||
|
- Create `tokens` and `users` table in Referral Manager database.
|
||
|
- Create an admin endpoint on Referral Manager.
|
||
|
- Implementing a method `GenerateTokens` for updating the `users` table on Referral Manager to contain new tokens for eligible users.
|
||
|
- Implementing an endpoint `GetTokens` for requesting unredeemed tokens from Referral Manager.
|
||
|
- Implementing an endpoint `Redeem` on Referral Manager for verifying invitation tokens and storing newly created user ID into `users` table.
|
||
|
- Replace existing registration token logic.
|
||
|
|
||
|
### Pseudocode
|
||
|
|
||
|
The Token struct represents both a Go struct (on satellite and Referral Manager) as well as the schema for the `tokens` table on the Referral Manager
|
||
|
|
||
|
```
|
||
|
type Token struct {
|
||
|
Secret [32]byte
|
||
|
OwnerID uuid.UUID
|
||
|
RedeemedID uuid.UUID
|
||
|
OwnerSatelliteURL string
|
||
|
RedeemedSatelliteURL string
|
||
|
// we should store it as an integer in the db and cast it into enum when retrieve it
|
||
|
Status enum
|
||
|
}
|
||
|
```
|
||
|
Statuses: `unsent` (owner's satellite doesn't know about it yet), `unredeemed` (owner's satellite knows about it but it has not been used), `redeemed` (someone has used this referral link to register alreadys)
|
||
|
|
||
|
The User struct represent both a Go struct as well as the schema for the `users` table on Referral Manager.
|
||
|
|
||
|
```
|
||
|
type User struct {
|
||
|
ID uuid.UUID
|
||
|
satelliteURL string
|
||
|
NewTokens int
|
||
|
UnredeemedTokens int
|
||
|
}
|
||
|
```
|
||
|
|
||
|
|
||
|
**Endpoints for satellites:**
|
||
|
|
||
|
```
|
||
|
// GetTokens retrieves a list of unredeemed tokens for a user.
|
||
|
GetTokens(userID uuid.UUID, satelliteURL string) []tokens {
|
||
|
tx := db.Tx
|
||
|
user, err := tx.GetUser(userID, satelliteURL)
|
||
|
if err == UserNotFound {
|
||
|
tx.CreateUser(userID, satelliteURL)
|
||
|
return nil
|
||
|
}
|
||
|
if user.NewTokens > 0 {
|
||
|
for i:=0; i<user.NewTokens; i++ {
|
||
|
newToken := Token{data: generateRandomToken(), user: user.ID, satellite: user.satelliteURL}
|
||
|
db.CreateToken(newToken)
|
||
|
}
|
||
|
tx.UpdateUserNewTokens(userID, satelliteURL, 0)
|
||
|
}
|
||
|
tokens := db.GetTokensByUserIDAndSatelliteURL(userID, satelliteURL)
|
||
|
|
||
|
return tokens
|
||
|
}
|
||
|
|
||
|
// Redeem marks a token as redeemed and stores user info into database
|
||
|
func Redeem(ctx, token, userID, satelliteURL) error {
|
||
|
tx := db.Tx
|
||
|
// only update the status if the status of a token is unredeemed
|
||
|
tokenStatus, err := tx.RedeemToken(ctx, token, userID)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// decrease unredeemed token count in users table by 1
|
||
|
err := tx.UpdateUserInfo(ctx, user)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// save user info into users table
|
||
|
err := tx.CreateUser(ctx, userID, satelliteURL)
|
||
|
if err != nil {
|
||
|
// log the error, but we shouldn't return an error to stop user registration process if we don't get their info here
|
||
|
log(err)
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
**Endpoints for CLI:**
|
||
|
|
||
|
```
|
||
|
|
||
|
// GenerateTokens generates tokens, saves those in the referral manager db
|
||
|
GenerateTokens(tokensPerUser int, satelliteURLs []string, dryRun bool) (tokenCount, eligibleUserCount int, error) {
|
||
|
eligibleUsers := db.GetEligibleUsers(satelliteURLs, tokensPerUser)
|
||
|
if eligibleUsers == nil {
|
||
|
return nil, Error.New("No users to generate tokens for.")
|
||
|
}
|
||
|
newTokenCount := 0
|
||
|
eligibleUserCount := len(eligibleUsers)
|
||
|
for _, user := range eligibleUsers {
|
||
|
for i:=(user.NewTokens + user.UnredeemedTokens); i<tokensPerUser; i++ {
|
||
|
newTokenCount++
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if !dryRun {
|
||
|
db.UpdateUserNewTokensBatch(eligibleUsers, tokensPerUser)
|
||
|
}
|
||
|
|
||
|
return newTokenCount, eligibleUserCount, nil
|
||
|
}
|
||
|
```
|
||
|
|
||
|
**CLI code:**
|
||
|
```
|
||
|
startCmd() {
|
||
|
tokenCount, userCount := referralManager.GenerateTokens(tokensPerUser, args.SatelliteURLs, flags.dryRun)
|
||
|
|
||
|
fmt.Printf("Successfully created %d tokens for %d users.\n", tokenCount, userCount)
|
||
|
if flags.dryRun {
|
||
|
fmt.Println("This was a dry run. Run again without the --dry-run flag to actually generate tokens.")
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
## Wrapup
|
||
|
|
||
|
## Open issues
|