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:
parent
f81821785d
commit
d29946d3d7
206
docs/blueprints/referral-manager-v1.md
Normal file
206
docs/blueprints/referral-manager-v1.md
Normal 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
|
Loading…
Reference in New Issue
Block a user