docs/blueprints: referral manager v1 (#3038)

* archive old referral offer doc

* init referral manager

* add Referral Manager CLI definition

* add draft implementation steps

* use existing registration_token table for storing invitation tokens

* fix numbering

* add open question about auth

* wip pseudocode

* add user interface design

* separate token generation step from getting users step

* change button to be a referral tab on the UI

* simplify some sentences. clarified CLI referral manager interaction

* add prevent two processes running at the same time

* add cli code

* clear users when CLI exites earlier or referral manager finishes sending tokens to satellites

* use transaction for saving tokens on satellite so we can roll back if error occurs

* add auth token and GetSatelliteURLs

* add schema for satellites table

* update to only have satelite to talk to referral manager

* add definition for eligible users and owner's satellite

* fix indentation

* add saving user info during redeem pseudocode

* wip

* address reviews

* add unredeemed token in users table

* address comments
This commit is contained in:
Yingrong Zhao 2019-10-07 09:38:05 -04:00 committed by GitHub
parent f81821785d
commit d29946d3d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 206 additions and 0 deletions

View File

@ -0,0 +1,206 @@
# 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