@casual-simulation/aux-records
Advanced tools
Comparing version 3.2.3 to 3.2.4-alpha.5857788915
@@ -403,6 +403,2 @@ import { AddressType, AuthStore, AuthUser } from './AuthStore'; | ||
subscriptionTier: string; | ||
/** | ||
* The OpenAI API Key that the user has configured in their account. | ||
*/ | ||
openAiKey: string | null; | ||
} | ||
@@ -429,3 +425,3 @@ export interface GetUserInfoFailure { | ||
*/ | ||
update: Partial<Pick<AuthUser, 'name' | 'email' | 'phoneNumber' | 'avatarUrl' | 'avatarPortraitUrl' | 'openAiKey'>>; | ||
update: Partial<Pick<AuthUser, 'name' | 'email' | 'phoneNumber' | 'avatarUrl' | 'avatarPortraitUrl'>>; | ||
} | ||
@@ -432,0 +428,0 @@ export type UpdateUserInfoResult = UpdateUserInfoSuccess | UpdateUserInfoFailure; |
@@ -15,3 +15,3 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
import { cleanupObject, isActiveSubscription, isStringValid, } from './Utils'; | ||
import { formatV1OpenAiKey, formatV1SessionKey, parseSessionKey, randomCode, } from './AuthUtils'; | ||
import { formatV1SessionKey, parseSessionKey, randomCode, } from './AuthUtils'; | ||
/** | ||
@@ -780,3 +780,2 @@ * The number of miliseconds that a login request should be valid for before expiration. | ||
subscriptionTier: hasActiveSubscription ? tier : null, | ||
openAiKey: hasActiveSubscription ? result.openAiKey : null, | ||
}; | ||
@@ -874,4 +873,2 @@ } | ||
} | ||
const hasActiveSubscription = this._forceAllowSubscriptionFeatures || | ||
isActiveSubscription(user.subscriptionStatus); | ||
const cleaned = cleanupObject({ | ||
@@ -883,9 +880,3 @@ name: request.update.name, | ||
phoneNumber: request.update.phoneNumber, | ||
openAiKey: hasActiveSubscription | ||
? request.update.openAiKey | ||
: undefined, | ||
}); | ||
if (cleaned.openAiKey) { | ||
cleaned.openAiKey = formatV1OpenAiKey(cleaned.openAiKey); | ||
} | ||
yield this._store.saveUser(Object.assign(Object.assign({}, user), cleaned)); | ||
@@ -892,0 +883,0 @@ return { |
@@ -113,6 +113,2 @@ import { RegexRule } from './Utils'; | ||
/** | ||
* The OpenAI API Key that the user has configured in their account. | ||
*/ | ||
openAiKey?: string | null; | ||
/** | ||
* The ID of the stripe customer that is associated with this user. | ||
@@ -119,0 +115,0 @@ */ |
@@ -1,5 +0,10 @@ | ||
import { ListedRecord, Record, RecordKey, RecordsStore } from './RecordsStore'; | ||
import { ListStudioAssignmentFilters, ListedRecord, ListedStudio, ListedStudioAssignment, ListedUserAssignment, Record, RecordKey, RecordsStore, Studio, StudioAssignment } from './RecordsStore'; | ||
import { AuthStore } from './AuthStore'; | ||
export declare class MemoryRecordsStore implements RecordsStore { | ||
private _records; | ||
private _recordKeys; | ||
private _studios; | ||
private _studioAssignments; | ||
private _auth; | ||
constructor(auth: AuthStore); | ||
get recordKeys(): RecordKey[]; | ||
@@ -12,3 +17,19 @@ getRecordByName(name: string): Promise<Record>; | ||
listRecordsByOwnerId(ownerId: string): Promise<ListedRecord[]>; | ||
listRecordsByStudioId(studioId: string): Promise<ListedRecord[]>; | ||
listRecordsByStudioIdAndUserId(studioId: string, userId: string): Promise<ListedRecord[]>; | ||
addStudio(studio: Studio): Promise<void>; | ||
createStudioForUser(studio: Studio, adminId: string): Promise<{ | ||
studio: Studio; | ||
assignment: StudioAssignment; | ||
}>; | ||
updateStudio(studio: Studio): Promise<void>; | ||
getStudioById(id: string): Promise<Studio>; | ||
getStudioByStripeCustomerId(customerId: string): Promise<Studio>; | ||
listStudiosForUser(userId: string): Promise<ListedStudio[]>; | ||
addStudioAssignment(assignment: StudioAssignment): Promise<void>; | ||
removeStudioAssignment(studioId: string, userId: string): Promise<void>; | ||
updateStudioAssignment(assignment: StudioAssignment): Promise<void>; | ||
listStudioAssignments(studioId: string, filters?: ListStudioAssignmentFilters): Promise<ListedStudioAssignment[]>; | ||
listUserAssignments(userId: string): Promise<ListedUserAssignment[]>; | ||
} | ||
//# sourceMappingURL=MemoryRecordsStore.d.ts.map |
@@ -12,5 +12,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
export class MemoryRecordsStore { | ||
constructor() { | ||
constructor(auth) { | ||
this._records = []; | ||
this._recordKeys = []; | ||
this._studios = []; | ||
this._studioAssignments = []; | ||
this._auth = auth; | ||
} | ||
@@ -64,6 +67,164 @@ get recordKeys() { | ||
ownerId: r.ownerId, | ||
studioId: r.studioId, | ||
})), (r) => r.name); | ||
}); | ||
} | ||
listRecordsByStudioId(studioId) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
return sortBy(this._records | ||
.filter((r) => r.studioId === studioId) | ||
.map((r) => ({ | ||
name: r.name, | ||
ownerId: r.ownerId, | ||
studioId: r.studioId, | ||
})), (r) => r.name); | ||
}); | ||
} | ||
listRecordsByStudioIdAndUserId(studioId, userId) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
return sortBy(this._records | ||
.filter((s) => { | ||
if (s.studioId !== studioId) { | ||
return false; | ||
} | ||
const isAssigned = this._studioAssignments.some((a) => a.studioId === studioId && a.userId === userId); | ||
return isAssigned; | ||
}) | ||
.map((r) => ({ | ||
name: r.name, | ||
ownerId: r.ownerId, | ||
studioId: r.studioId, | ||
})), (r) => r.name); | ||
}); | ||
} | ||
addStudio(studio) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const existingStudioIndex = this._studios.findIndex((r) => r.id === studio.id); | ||
if (existingStudioIndex < 0) { | ||
this._studios.push(studio); | ||
} | ||
}); | ||
} | ||
createStudioForUser(studio, adminId) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
yield this.addStudio(studio); | ||
const assignment = { | ||
studioId: studio.id, | ||
userId: adminId, | ||
isPrimaryContact: true, | ||
role: 'admin', | ||
}; | ||
yield this.addStudioAssignment(assignment); | ||
return { | ||
studio, | ||
assignment, | ||
}; | ||
}); | ||
} | ||
updateStudio(studio) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const existingStudioIndex = this._studios.findIndex((r) => r.id === studio.id); | ||
if (existingStudioIndex >= 0) { | ||
this._studios[existingStudioIndex] = studio; | ||
} | ||
}); | ||
} | ||
getStudioById(id) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
return this._studios.find((s) => s.id === id); | ||
}); | ||
} | ||
getStudioByStripeCustomerId(customerId) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
return this._studios.find((s) => s.stripeCustomerId === customerId); | ||
}); | ||
} | ||
listStudiosForUser(userId) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const assignments = yield this.listUserAssignments(userId); | ||
const studios = yield Promise.all(assignments.map((a) => __awaiter(this, void 0, void 0, function* () { | ||
const s = yield this.getStudioById(a.studioId); | ||
return Object.assign(Object.assign({}, s), a); | ||
}))); | ||
return studios.map((s) => ({ | ||
studioId: s.id, | ||
displayName: s.displayName, | ||
role: s.role, | ||
isPrimaryContact: s.isPrimaryContact, | ||
})); | ||
}); | ||
} | ||
addStudioAssignment(assignment) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const existingAssignmentIndex = this._studioAssignments.findIndex((r) => r.studioId === assignment.studioId && | ||
r.userId === assignment.userId); | ||
if (existingAssignmentIndex < 0) { | ||
this._studioAssignments.push(assignment); | ||
} | ||
}); | ||
} | ||
removeStudioAssignment(studioId, userId) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
this._studioAssignments = this._studioAssignments.filter((s) => s.studioId !== studioId || s.userId !== userId); | ||
}); | ||
} | ||
updateStudioAssignment(assignment) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const existingAssignmentIndex = this._studioAssignments.findIndex((r) => r.studioId === assignment.studioId && | ||
r.userId === assignment.userId); | ||
if (existingAssignmentIndex >= 0) { | ||
this._studioAssignments[existingAssignmentIndex] = assignment; | ||
} | ||
}); | ||
} | ||
listStudioAssignments(studioId, filters) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const assignments = this._studioAssignments.filter((s) => { | ||
const matchesRole = !(filters === null || filters === void 0 ? void 0 : filters.role) || s.role === filters.role; | ||
const matchesPrimaryContact = !(filters === null || filters === void 0 ? void 0 : filters.isPrimaryContact) || | ||
s.isPrimaryContact === filters.isPrimaryContact; | ||
const matchesUserId = !(filters === null || filters === void 0 ? void 0 : filters.userId) || s.userId === filters.userId; | ||
return (s.studioId === studioId && | ||
matchesRole && | ||
matchesPrimaryContact && | ||
matchesUserId); | ||
}); | ||
let results = []; | ||
for (let s of assignments) { | ||
const user = yield this._auth.findUser(s.userId); | ||
if (!user) { | ||
continue; | ||
} | ||
results.push({ | ||
studioId: s.studioId, | ||
userId: s.userId, | ||
isPrimaryContact: s.isPrimaryContact, | ||
role: s.role, | ||
user: { | ||
id: user.id, | ||
name: user.name, | ||
email: user.email, | ||
phoneNumber: user.phoneNumber, | ||
}, | ||
}); | ||
} | ||
return results; | ||
}); | ||
} | ||
listUserAssignments(userId) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const assignments = this._studioAssignments.filter((s) => s.userId === userId); | ||
return assignments.map((s) => { | ||
const studio = this._studios.find((studio) => studio.id === s.studioId); | ||
return { | ||
displayName: studio.displayName, | ||
studioId: s.studioId, | ||
userId: s.userId, | ||
isPrimaryContact: s.isPrimaryContact, | ||
role: s.role, | ||
}; | ||
}); | ||
}); | ||
} | ||
} | ||
//# sourceMappingURL=MemoryRecordsStore.js.map |
{ | ||
"name": "@casual-simulation/aux-records", | ||
"version": "3.2.3", | ||
"version": "3.2.4-alpha.5857788915", | ||
"description": "Helpers and managers used by the CasualOS records system.", | ||
@@ -47,3 +47,3 @@ "keywords": [], | ||
}, | ||
"gitHead": "49eebdd054045b4d77655b3936fdbef7cefed5d6" | ||
"gitHead": "b5f195ca2463185090d5537158ae1e8bb4984bc4" | ||
} |
@@ -5,3 +5,3 @@ import { AuthController } from './AuthController'; | ||
import { AvailablePermissions, PolicyDocument } from './PolicyPermissions'; | ||
import { PublicRecordKeyPolicy } from './RecordsStore'; | ||
import { ListedStudioAssignment, PublicRecordKeyPolicy } from './RecordsStore'; | ||
import { AssignedRole, GetUserPolicyFailure, ListedUserPolicy, PolicyStore, RoleAssignment, UpdateUserPolicyFailure, UpdateUserRolesFailure } from './PolicyStore'; | ||
@@ -209,2 +209,3 @@ /** | ||
private _byRecordOwner; | ||
private _byStudioRole; | ||
private _byEveryoneRole; | ||
@@ -256,2 +257,4 @@ private _byAdminRole; | ||
recordOwnerId: string; | ||
recordStudioId: string; | ||
recordStudioMembers?: ListedStudioAssignment[]; | ||
subjectPolicy: PublicRecordKeyPolicy; | ||
@@ -258,0 +261,0 @@ } |
@@ -1,2 +0,2 @@ | ||
import { ListedRecord, PublicRecordKeyPolicy, RecordsStore } from './RecordsStore'; | ||
import { ListedRecord, ListedStudio, ListedStudioAssignment, PublicRecordKeyPolicy, RecordsStore, StudioAssignmentRole } from './RecordsStore'; | ||
import { NotAuthorizedError, NotLoggedInError, NotSupportedError, ServerError } from './Errors'; | ||
@@ -13,2 +13,7 @@ import type { ValidateSessionKeyFailure } from './AuthController'; | ||
/** | ||
* Creates a new record. | ||
* @param request The request that should be used to create the record. | ||
*/ | ||
createRecord(request: CreateRecordRequest): Promise<CreateRecordResult>; | ||
/** | ||
* Creates a new public record key for the given bucket name. | ||
@@ -34,8 +39,63 @@ * @param name The name of the record. | ||
/** | ||
* Gets the list of records that the user with the given ID owns. | ||
* Gets the list of records that the user with the given ID has access to. | ||
* @param userId The ID of the user. | ||
*/ | ||
listRecords(userId: string): Promise<ListRecordsResult>; | ||
/** | ||
* Gets the list of records in the given studio that the user with the given ID has access to. | ||
* @param studioId The ID of the studio. | ||
* @param userId The ID of the user that is currently logged in. | ||
*/ | ||
listStudioRecords(studioId: string, userId: string): Promise<ListRecordsResult>; | ||
/** | ||
* Attempts to create a new studio. That is, an entity that can be used to group records. | ||
* @param studioName The name of the studio. | ||
* @param userId The ID of the user that is creating the studio. | ||
*/ | ||
createStudio(studioName: string, userId: string): Promise<CreateStudioResult>; | ||
/** | ||
* Gets the list of studios that the user with the given ID has access to. | ||
* @param userId The ID of the user. | ||
*/ | ||
listStudios(userId: string): Promise<ListStudiosResult>; | ||
/** | ||
* Gets the list of members in a studio. | ||
* @param studioId The ID of the studio. | ||
* @param userId The ID of the user that is currently logged in. | ||
*/ | ||
listStudioMembers(studioId: string, userId: string): Promise<ListStudioMembersResult>; | ||
addStudioMember(request: AddStudioMemberRequest): Promise<AddStudioMemberResult>; | ||
removeStudioMember(request: RemoveStudioMemberRequest): Promise<RemoveStudioMemberResult>; | ||
private _createSalt; | ||
} | ||
/** | ||
* Defines an interface that represents a request to create a record. | ||
*/ | ||
export interface CreateRecordRequest { | ||
/** | ||
* The name of the record that should be created. | ||
*/ | ||
recordName: string; | ||
/** | ||
* The ID of the user that is currently logged in. | ||
*/ | ||
userId: string; | ||
/** | ||
* The ID of the user that should be the owner of the record. | ||
*/ | ||
ownerId?: string; | ||
/** | ||
* The ID of the studio that should own the record. | ||
*/ | ||
studioId?: string; | ||
} | ||
export type CreateRecordResult = CreateRecordSuccess | CreateRecordFailure; | ||
export interface CreateRecordSuccess { | ||
success: true; | ||
} | ||
export interface CreateRecordFailure { | ||
success: false; | ||
errorCode: ServerError | NotLoggedInError | NotAuthorizedError | 'record_already_exists' | 'unacceptable_request'; | ||
errorMessage: string; | ||
} | ||
export type ValidatePublicRecordKeyResult = ValidatePublicRecordKeySuccess | ValidatePublicRecordKeyFailure; | ||
@@ -146,2 +206,7 @@ /** | ||
ownerId: string; | ||
studioId: string; | ||
/** | ||
* The IDs of the members of the studio. | ||
*/ | ||
studioMembers?: ListedStudioAssignment[]; | ||
} | ||
@@ -190,2 +255,103 @@ export interface ValidateRecordNameFailure { | ||
export type InvalidRecordKey = 'invalid_record_key'; | ||
export type CreateStudioResult = CreateStudioSuccess | CreateStudioFailure; | ||
export interface CreateStudioSuccess { | ||
success: true; | ||
studioId: string; | ||
} | ||
export interface CreateStudioFailure { | ||
success: false; | ||
errorCode: NotLoggedInError | NotAuthorizedError | ServerError; | ||
errorMessage: string; | ||
} | ||
export type ListStudiosResult = ListStudiosSuccess | ListStudiosFailure; | ||
export interface ListStudiosSuccess { | ||
success: true; | ||
studios: ListedStudio[]; | ||
} | ||
export interface ListStudiosFailure { | ||
success: false; | ||
errorCode: NotLoggedInError | NotAuthorizedError | ServerError; | ||
errorMessage: string; | ||
} | ||
export type ListStudioMembersResult = ListStudioMembersSuccess | ListStudioMembersFailure; | ||
export interface ListStudioMembersSuccess { | ||
success: true; | ||
members: ListedStudioMember[]; | ||
} | ||
export interface ListedStudioMember { | ||
userId: string; | ||
studioId: string; | ||
role: 'admin' | 'member'; | ||
isPrimaryContact: boolean; | ||
user: ListedStudioMemberUser; | ||
} | ||
export interface ListedStudioMemberUser { | ||
id: string; | ||
name: string; | ||
email?: string; | ||
phoneNumber?: string; | ||
} | ||
export interface ListStudioMembersFailure { | ||
success: false; | ||
errorCode: NotLoggedInError | NotAuthorizedError | ServerError; | ||
errorMessage: string; | ||
} | ||
export interface AddStudioMemberRequest { | ||
/** | ||
* The ID of the studio. | ||
*/ | ||
studioId: string; | ||
/** | ||
* The ID of the user that is currently logged in. | ||
*/ | ||
userId: string; | ||
/** | ||
* The email address of the user that should be added to the studio. | ||
*/ | ||
addedEmail?: string; | ||
/** | ||
* The phone number of the user that should be added to the studio. | ||
*/ | ||
addedPhoneNumber?: string; | ||
/** | ||
* The ID of the user that should be added to the studio. | ||
*/ | ||
addedUserId?: string; | ||
/** | ||
* The role that the added user should have in the studio. | ||
*/ | ||
role: StudioAssignmentRole; | ||
} | ||
export type AddStudioMemberResult = AddStudioMemberSuccess | AddStudioMemberFailure; | ||
export interface AddStudioMemberSuccess { | ||
success: true; | ||
} | ||
export interface AddStudioMemberFailure { | ||
success: false; | ||
errorCode: NotLoggedInError | NotAuthorizedError | ServerError | 'studio_not_found' | 'unacceptable_request' | 'user_not_found'; | ||
errorMessage: string; | ||
} | ||
export interface RemoveStudioMemberRequest { | ||
/** | ||
* The ID of the studio. | ||
*/ | ||
studioId: string; | ||
/** | ||
* The ID of the user that is currently logged in. | ||
*/ | ||
userId: string; | ||
/** | ||
* The ID of the user that should be removed from the studio. | ||
*/ | ||
removedUserId: string; | ||
} | ||
export type RemoveStudioMemberResult = RemoveStudioMemberSuccess | RemoveStudioMemberFailure; | ||
export interface RemoveStudioMemberSuccess { | ||
success: true; | ||
} | ||
export interface RemoveStudioMemberFailure { | ||
success: false; | ||
errorCode: NotLoggedInError | NotAuthorizedError | ServerError; | ||
errorMessage: string; | ||
} | ||
/** | ||
@@ -192,0 +358,0 @@ * The default policy for keys that do not have a specified record key. |
@@ -14,2 +14,3 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
import { fromByteArray } from 'base64-js'; | ||
import { v4 as uuid } from 'uuid'; | ||
/** | ||
@@ -24,2 +25,112 @@ * Defines a class that manages records and their keys. | ||
/** | ||
* Creates a new record. | ||
* @param request The request that should be used to create the record. | ||
*/ | ||
createRecord(request) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
try { | ||
if (!request.userId) { | ||
return { | ||
success: false, | ||
errorCode: 'not_logged_in', | ||
errorMessage: 'The user must be logged in in order to create a record.', | ||
}; | ||
} | ||
const record = yield this._store.getRecordByName(request.recordName); | ||
if (record) { | ||
if (record.name === request.userId && | ||
record.ownerId !== request.userId && | ||
request.ownerId === request.userId) { | ||
console.log(`[RecordsController] [action: record.create recordName: ${record.name}, userId: ${request.userId}] Fixing record owner to match actual owner.`); | ||
record.ownerId = request.userId; | ||
record.studioId = null; | ||
// Clear the hashes and re-create the salt so that access to the record is revoked for any record key that was created before. | ||
record.secretHashes = []; | ||
record.secretSalt = this._createSalt(); | ||
yield this._store.updateRecord(Object.assign({}, record)); | ||
return { | ||
success: true, | ||
}; | ||
} | ||
let existingStudioMembers = yield this._store.listStudioAssignments(record.name); | ||
if (existingStudioMembers.length > 0 && | ||
record.studioId !== record.name && | ||
request.studioId === record.name) { | ||
console.log(`[RecordsController] [action: record.create recordName: ${record.name}, userId: ${request.userId}, studioId: ${request.studioId}] Fixing record owner to match actual owner.`); | ||
record.ownerId = null; | ||
record.studioId = request.studioId; | ||
// Clear the hashes and re-create the salt so that access to the record is revoked for any record key that was created before. | ||
record.secretHashes = []; | ||
record.secretSalt = this._createSalt(); | ||
yield this._store.updateRecord(Object.assign({}, record)); | ||
return { | ||
success: true, | ||
}; | ||
} | ||
return { | ||
success: false, | ||
errorCode: 'record_already_exists', | ||
errorMessage: 'A record with that name already exists.', | ||
}; | ||
} | ||
if (!request.ownerId && !request.studioId) { | ||
return { | ||
success: false, | ||
errorCode: 'unacceptable_request', | ||
errorMessage: 'You must provide an owner ID or a studio ID.', | ||
}; | ||
} | ||
if (request.ownerId) { | ||
if (request.ownerId !== request.userId) { | ||
return { | ||
success: false, | ||
errorCode: 'not_authorized', | ||
errorMessage: 'You are not authorized to create a record for another user.', | ||
}; | ||
} | ||
console.log(`[RecordsController] [action: record.create recordName: ${request.recordName}, userId: ${request.userId}, ownerId: ${request.ownerId}] Creating record.`); | ||
yield this._store.addRecord({ | ||
name: request.recordName, | ||
ownerId: request.ownerId, | ||
secretHashes: [], | ||
secretSalt: this._createSalt(), | ||
studioId: null, | ||
}); | ||
} | ||
else { | ||
const assignments = yield this._store.listStudioAssignments(request.studioId, { | ||
userId: request.userId, | ||
role: 'admin', | ||
}); | ||
if (assignments.length <= 0) { | ||
return { | ||
success: false, | ||
errorCode: 'not_authorized', | ||
errorMessage: 'You are not authorized to create a record for this studio.', | ||
}; | ||
} | ||
console.log(`[RecordsController] [action: record.create recordName: ${request.recordName}, userId: ${request.userId}, studioId: ${request.studioId}] Creating record.`); | ||
yield this._store.addRecord({ | ||
name: request.recordName, | ||
ownerId: null, | ||
secretHashes: [], | ||
secretSalt: this._createSalt(), | ||
studioId: request.studioId, | ||
}); | ||
} | ||
return { | ||
success: true, | ||
}; | ||
} | ||
catch (err) { | ||
console.error('[RecordsController] [createRecord] An error occurred while creating a record:', err); | ||
return { | ||
success: false, | ||
errorCode: 'server_error', | ||
errorMessage: 'A server error occurred.', | ||
}; | ||
} | ||
}); | ||
} | ||
/** | ||
* Creates a new public record key for the given bucket name. | ||
@@ -32,2 +143,3 @@ * @param name The name of the record. | ||
createPublicRecordKey(name, policy, userId) { | ||
var _a; | ||
return __awaiter(this, void 0, void 0, function* () { | ||
@@ -55,11 +167,2 @@ try { | ||
if (record) { | ||
if (record.ownerId !== userId && name !== userId) { | ||
return { | ||
success: false, | ||
errorCode: 'unauthorized_to_create_record_key', | ||
errorMessage: 'Another user has already created this record.', | ||
errorReason: 'record_owned_by_different_user', | ||
}; | ||
} | ||
console.log(`[RecordsController] [action: recordKey.create recordName: ${name}, userId: ${userId}] Creating record key.`); | ||
if (name === userId) { | ||
@@ -70,2 +173,3 @@ // The user is not currently the owner of their own record. | ||
record.ownerId = userId; | ||
record.studioId = null; | ||
// Clear the hashes and re-create the salt so that access to the record is revoked for any record key that was created before. | ||
@@ -76,2 +180,38 @@ record.secretHashes = []; | ||
} | ||
else { | ||
let existingStudioMembers = yield this._store.listStudioAssignments(name); | ||
if (existingStudioMembers.length > 0 && | ||
record.studioId !== name) { | ||
// The studio is not currently the owner of their own record. | ||
// This is an issue that needs to be fixed because studios should always own the record that has the same name as their ID. | ||
console.log(`[RecordsController] [action: recordKey.create recordName: ${name}, userId: ${userId}, studioId: ${name}] Fixing record owner to match actual owner.`); | ||
record.ownerId = null; | ||
record.studioId = record.name; | ||
// Clear the hashes and re-create the salt so that access to the record is revoked for any record key that was created before. | ||
record.secretHashes = []; | ||
record.secretSalt = this._createSalt(); | ||
yield this._store.updateRecord(Object.assign({}, record)); | ||
} | ||
} | ||
if (record.ownerId !== userId && name !== userId) { | ||
let valid = false; | ||
if (record.studioId) { | ||
const assignments = yield this._store.listStudioAssignments(record.studioId, { | ||
userId: userId, | ||
role: 'admin', | ||
}); | ||
if (assignments.length > 0) { | ||
valid = true; | ||
} | ||
} | ||
if (!valid) { | ||
return { | ||
success: false, | ||
errorCode: 'unauthorized_to_create_record_key', | ||
errorMessage: 'Another user has already created this record.', | ||
errorReason: 'record_owned_by_different_user', | ||
}; | ||
} | ||
} | ||
console.log(`[RecordsController] [action: recordKey.create recordName: ${name}, userId: ${userId}] Creating record key.`); | ||
const passwordBytes = randomBytes(16); | ||
@@ -85,3 +225,3 @@ const password = fromByteArray(passwordBytes); // convert to human-readable string | ||
policy: policy !== null && policy !== void 0 ? policy : DEFAULT_RECORD_KEY_POLICY, | ||
creatorId: record.ownerId, | ||
creatorId: (_a = record.ownerId) !== null && _a !== void 0 ? _a : userId, | ||
}); | ||
@@ -115,2 +255,3 @@ return { | ||
ownerId: userId, | ||
studioId: null, | ||
secretHashes: [], | ||
@@ -133,7 +274,7 @@ secretSalt: salt, | ||
catch (err) { | ||
console.error(err); | ||
console.error('[RecordsController] [createPublicRecordKey] An error occurred while creating a public record key:', err); | ||
return { | ||
success: false, | ||
errorCode: 'server_error', | ||
errorMessage: err.toString(), | ||
errorMessage: 'A server error occurred.', | ||
errorReason: 'server_error', | ||
@@ -246,7 +387,7 @@ }; | ||
catch (err) { | ||
console.error(err); | ||
console.error('[RecordsController] [validatePublicRecordKey] An error occurred while creating a public record key:', err); | ||
return { | ||
success: false, | ||
errorCode: 'server_error', | ||
errorMessage: err.toString(), | ||
errorMessage: 'A server error occurred.', | ||
}; | ||
@@ -271,2 +412,3 @@ } | ||
ownerId: userId, | ||
studioId: null, | ||
secretHashes: [], | ||
@@ -279,4 +421,23 @@ secretSalt: this._createSalt(), | ||
ownerId: userId, | ||
studioId: null, | ||
}; | ||
} | ||
let studioMembers = yield this._store.listStudioAssignments(name); | ||
if (studioMembers.length > 0) { | ||
console.log(`[RecordsController] [validateRecordName recordName: ${name}, userId: ${userId}, studioId: ${name}] Creating record for studio.`); | ||
yield this._store.addRecord({ | ||
name, | ||
ownerId: null, | ||
studioId: name, | ||
secretHashes: [], | ||
secretSalt: this._createSalt(), | ||
}); | ||
return { | ||
success: true, | ||
recordName: name, | ||
ownerId: null, | ||
studioId: name, | ||
studioMembers, | ||
}; | ||
} | ||
return { | ||
@@ -300,2 +461,20 @@ success: false, | ||
} | ||
let existingStudioMembers = yield this._store.listStudioAssignments(name); | ||
if (existingStudioMembers.length > 0 && | ||
record.studioId !== name && | ||
record.ownerId !== null) { | ||
console.log(`[RecordsController] [validateRecordName recordName: ${name}, userId: ${userId}, studioId: ${name}] Fixing record studio to match actual studio.`); | ||
record.ownerId = null; | ||
record.studioId = name; | ||
record.secretHashes = []; | ||
record.secretSalt = this._createSalt(); | ||
yield this._store.updateRecord(Object.assign({}, record)); | ||
} | ||
let studioMembers = undefined; | ||
if (existingStudioMembers.length > 0) { | ||
studioMembers = existingStudioMembers; | ||
} | ||
else if (record.studioId) { | ||
studioMembers = yield this._store.listStudioAssignments(record.studioId); | ||
} | ||
return { | ||
@@ -305,10 +484,12 @@ success: true, | ||
ownerId: record.ownerId, | ||
studioId: record.studioId, | ||
studioMembers, | ||
}; | ||
} | ||
catch (err) { | ||
console.error(err); | ||
console.error('[RecordsController] [validateRecordName] An error occurred while creating a public record key:', err); | ||
return { | ||
success: false, | ||
errorCode: 'server_error', | ||
errorMessage: err.toString(), | ||
errorMessage: 'A server error occurred.', | ||
}; | ||
@@ -319,3 +500,3 @@ } | ||
/** | ||
* Gets the list of records that the user with the given ID owns. | ||
* Gets the list of records that the user with the given ID has access to. | ||
* @param userId The ID of the user. | ||
@@ -349,2 +530,254 @@ */ | ||
} | ||
/** | ||
* Gets the list of records in the given studio that the user with the given ID has access to. | ||
* @param studioId The ID of the studio. | ||
* @param userId The ID of the user that is currently logged in. | ||
*/ | ||
listStudioRecords(studioId, userId) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
try { | ||
if (!this._store.listRecordsByStudioIdAndUserId) { | ||
return { | ||
success: false, | ||
errorCode: 'not_supported', | ||
errorMessage: 'This operation is not supported.', | ||
}; | ||
} | ||
const records = yield this._store.listRecordsByStudioIdAndUserId(studioId, userId); | ||
return { | ||
success: true, | ||
records: records, | ||
}; | ||
} | ||
catch (err) { | ||
console.log('[RecordsController] [listStudioRecords] Error listing records: ', err); | ||
return { | ||
success: false, | ||
errorCode: 'server_error', | ||
errorMessage: 'A server error occurred.', | ||
}; | ||
} | ||
}); | ||
} | ||
/** | ||
* Attempts to create a new studio. That is, an entity that can be used to group records. | ||
* @param studioName The name of the studio. | ||
* @param userId The ID of the user that is creating the studio. | ||
*/ | ||
createStudio(studioName, userId) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
try { | ||
const studioId = uuid(); | ||
yield this._store.createStudioForUser({ | ||
id: studioId, | ||
displayName: studioName, | ||
}, userId); | ||
return { | ||
success: true, | ||
studioId: studioId, | ||
}; | ||
} | ||
catch (err) { | ||
console.error('[RecordsController] [createStudio] An error occurred while creating a studio:', err); | ||
return { | ||
success: false, | ||
errorCode: 'server_error', | ||
errorMessage: 'A server error occurred.', | ||
}; | ||
} | ||
}); | ||
} | ||
/** | ||
* Gets the list of studios that the user with the given ID has access to. | ||
* @param userId The ID of the user. | ||
*/ | ||
listStudios(userId) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
try { | ||
const studios = yield this._store.listStudiosForUser(userId); | ||
return { | ||
success: true, | ||
studios: studios.map((s) => ({ | ||
studioId: s.studioId, | ||
displayName: s.displayName, | ||
role: s.role, | ||
isPrimaryContact: s.isPrimaryContact, | ||
})), | ||
}; | ||
} | ||
catch (err) { | ||
console.error('[RecordsController] [listStudios] An error occurred while listing studios:', err); | ||
return { | ||
success: false, | ||
errorCode: 'server_error', | ||
errorMessage: 'A server error occurred.', | ||
}; | ||
} | ||
}); | ||
} | ||
/** | ||
* Gets the list of members in a studio. | ||
* @param studioId The ID of the studio. | ||
* @param userId The ID of the user that is currently logged in. | ||
*/ | ||
listStudioMembers(studioId, userId) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
try { | ||
const members = yield this._store.listStudioAssignments(studioId); | ||
const userAssignment = members.find((m) => m.userId === userId); | ||
if (!userAssignment) { | ||
return { | ||
success: false, | ||
errorCode: 'not_authorized', | ||
errorMessage: 'You are not authorized to access this studio.', | ||
}; | ||
} | ||
if (userAssignment.role === 'admin') { | ||
return { | ||
success: true, | ||
members: members.map((m) => ({ | ||
studioId: m.studioId, | ||
userId: m.userId, | ||
isPrimaryContact: m.isPrimaryContact, | ||
role: m.role, | ||
user: { | ||
id: m.user.id, | ||
name: m.user.name, | ||
email: m.user.email, | ||
phoneNumber: m.user.phoneNumber, | ||
}, | ||
})), | ||
}; | ||
} | ||
return { | ||
success: true, | ||
members: members.map((m) => ({ | ||
studioId: m.studioId, | ||
userId: m.userId, | ||
isPrimaryContact: m.isPrimaryContact, | ||
role: m.role, | ||
user: { | ||
id: m.user.id, | ||
name: m.user.name, | ||
}, | ||
})), | ||
}; | ||
} | ||
catch (err) { | ||
console.error('[RecordsController] [listStudioMembers] An error occurred while listing studio members:', err); | ||
return { | ||
success: false, | ||
errorCode: 'server_error', | ||
errorMessage: 'A server error occurred.', | ||
}; | ||
} | ||
}); | ||
} | ||
addStudioMember(request) { | ||
var _a; | ||
return __awaiter(this, void 0, void 0, function* () { | ||
try { | ||
if (!request.userId) { | ||
return { | ||
success: false, | ||
errorCode: 'not_logged_in', | ||
errorMessage: 'You must be logged in to add a studio member.', | ||
}; | ||
} | ||
const list = yield this._store.listStudioAssignments(request.studioId, { | ||
userId: request.userId, | ||
role: 'admin', | ||
}); | ||
if (list.length <= 0) { | ||
return { | ||
success: false, | ||
errorCode: 'not_authorized', | ||
errorMessage: 'You are not authorized to perform this operation.', | ||
}; | ||
} | ||
let addedUserId = null; | ||
if (request.addedUserId) { | ||
addedUserId = request.addedUserId; | ||
} | ||
else if (request.addedEmail || request.addedPhoneNumber) { | ||
const addedUser = yield this._auth.findUserByAddress((_a = request.addedEmail) !== null && _a !== void 0 ? _a : request.addedPhoneNumber, request.addedEmail ? 'email' : 'phone'); | ||
if (!addedUser) { | ||
return { | ||
success: false, | ||
errorCode: 'user_not_found', | ||
errorMessage: 'The user was not able to be found.', | ||
}; | ||
} | ||
addedUserId = addedUser.id; | ||
} | ||
else { | ||
return { | ||
success: false, | ||
errorCode: 'unacceptable_request', | ||
errorMessage: 'You must provide an email, phone number, or user ID to add a studio member.', | ||
}; | ||
} | ||
yield this._store.addStudioAssignment({ | ||
studioId: request.studioId, | ||
userId: addedUserId, | ||
isPrimaryContact: false, | ||
role: request.role, | ||
}); | ||
return { | ||
success: true, | ||
}; | ||
} | ||
catch (err) { | ||
console.error('[RecordsController] [addStudioMember] An error occurred while adding a studio member:', err); | ||
return { | ||
success: false, | ||
errorCode: 'server_error', | ||
errorMessage: 'A server error occurred.', | ||
}; | ||
} | ||
}); | ||
} | ||
removeStudioMember(request) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
try { | ||
if (!request.userId) { | ||
return { | ||
success: false, | ||
errorCode: 'not_logged_in', | ||
errorMessage: 'You must be logged in to remove a studio member.', | ||
}; | ||
} | ||
if (request.userId === request.removedUserId) { | ||
return { | ||
success: false, | ||
errorCode: 'not_authorized', | ||
errorMessage: 'You are not authorized to perform this operation.', | ||
}; | ||
} | ||
const list = yield this._store.listStudioAssignments(request.studioId, { | ||
userId: request.userId, | ||
role: 'admin', | ||
}); | ||
if (list.length <= 0) { | ||
return { | ||
success: false, | ||
errorCode: 'not_authorized', | ||
errorMessage: 'You are not authorized to perform this operation.', | ||
}; | ||
} | ||
yield this._store.removeStudioAssignment(request.studioId, request.removedUserId); | ||
return { | ||
success: true, | ||
}; | ||
} | ||
catch (err) { | ||
console.error('[RecordsController] [removeStudioMember] An error occurred while removing a studio member:', err); | ||
return { | ||
success: false, | ||
errorCode: 'server_error', | ||
errorMessage: 'A server error occurred.', | ||
}; | ||
} | ||
}); | ||
} | ||
_createSalt() { | ||
@@ -351,0 +784,0 @@ return fromByteArray(randomBytes(16)); |
@@ -109,2 +109,3 @@ import { AuthController } from './AuthController'; | ||
private _listRecords; | ||
private _createRecord; | ||
private _createRecordKey; | ||
@@ -124,2 +125,7 @@ private _policyGrantPermission; | ||
private _aiGenerateImage; | ||
private _postStudio; | ||
private _listStudios; | ||
private _listStudioMembers; | ||
private _addStudioMember; | ||
private _removeStudioMember; | ||
private _listData; | ||
@@ -153,3 +159,5 @@ private _handleRecordFileOptions; | ||
private _getSubscriptionInfo; | ||
private _getSubscriptionInfoV2; | ||
private _manageSubscription; | ||
private _manageSubscriptionV2; | ||
/** | ||
@@ -156,0 +164,0 @@ * Endpoint to retrieve info about a user. |
@@ -38,3 +38,78 @@ /** | ||
*/ | ||
listRecordsByOwnerId?(ownerId: string): Promise<ListedRecord[]>; | ||
listRecordsByOwnerId(ownerId: string): Promise<ListedRecord[]>; | ||
/** | ||
* Gets the list of records that the studio with the given ID owns. | ||
* | ||
* If null or undefined, then this store does not support this method. | ||
* | ||
* @param studioId The ID of the studio that owns the records. | ||
*/ | ||
listRecordsByStudioId(studioId: string): Promise<ListedRecord[]>; | ||
/** | ||
* Gets the list of records that the studio with the given ID owns and that the user with the given ID has access to. | ||
* @param studioId The ID of the studio. | ||
* @param userId The ID of the user. | ||
*/ | ||
listRecordsByStudioIdAndUserId(studioId: string, userId: string): Promise<ListedRecord[]>; | ||
/** | ||
* Adds the given studio to the store. | ||
* @param studio The studio to add. | ||
*/ | ||
addStudio(studio: Studio): Promise<void>; | ||
/** | ||
* Creates a new studio and adds the given user as an admin. | ||
* @param studio The studio to create. | ||
* @param adminId The ID of the admin user. | ||
*/ | ||
createStudioForUser(studio: Studio, adminId: string): Promise<{ | ||
studio: Studio; | ||
assignment: StudioAssignment; | ||
}>; | ||
/** | ||
* Updates the given studio. | ||
* @param studio The studio record that should be updated. | ||
*/ | ||
updateStudio(studio: Studio): Promise<void>; | ||
/** | ||
* Gets the studio with the given ID. | ||
* @param id The ID of the studio. | ||
*/ | ||
getStudioById(id: string): Promise<Studio>; | ||
/** | ||
* Gets the studio that has the given stripe customer ID. | ||
* @param customerId The stripe customer ID for the studio. | ||
*/ | ||
getStudioByStripeCustomerId(customerId: string): Promise<Studio>; | ||
/** | ||
* Gets the list of studios that the user with the given ID has access to. | ||
* @param userId The ID of the user. | ||
*/ | ||
listStudiosForUser(userId: string): Promise<ListedStudio[]>; | ||
/** | ||
* Adds the given studio assignment to the store. | ||
* @param assignment The assignment to add. | ||
*/ | ||
addStudioAssignment(assignment: StudioAssignment): Promise<void>; | ||
/** | ||
* Updates the given studio assignment. | ||
* @param assignment The assignment that should be updated. | ||
*/ | ||
updateStudioAssignment(assignment: StudioAssignment): Promise<void>; | ||
/** | ||
* Removes the given user from the given studio. | ||
* @param studioId The ID of the studio. | ||
* @param userId The ID of the user. | ||
*/ | ||
removeStudioAssignment(studioId: string, userId: string): Promise<void>; | ||
/** | ||
* Gets the list of users that have been assigned to the given studio. | ||
* @param studioId The ID of the studio. | ||
* @param filters The additional filters that should be used. | ||
*/ | ||
listStudioAssignments(studioId: string, filters?: ListStudioAssignmentFilters): Promise<ListedStudioAssignment[]>; | ||
/** | ||
* Gets the list of studio assignments that the user with the given ID has access to. | ||
* @param userId The ID of the user. | ||
*/ | ||
listUserAssignments(userId: string): Promise<ListedUserAssignment[]>; | ||
} | ||
@@ -50,6 +125,12 @@ /** | ||
/** | ||
* The ID of the user that created the record. | ||
* The ID of the user that owns the record. | ||
* Null if the record is owned by a studio. | ||
*/ | ||
ownerId: string; | ||
ownerId: string | null; | ||
/** | ||
* The ID of the studio that owns the record. | ||
* Null if the record is owned by a user. | ||
*/ | ||
studioId: string | null; | ||
/** | ||
* The scrypt hashes of the secrets that allow access to the record. | ||
@@ -72,7 +153,157 @@ */ | ||
/** | ||
* The ID of the user that created the record. | ||
* The ID of the user that owns the record. | ||
* Null if owned by a studio. | ||
*/ | ||
ownerId: string; | ||
ownerId: string | null; | ||
/** | ||
* The ID of the studio that owns the record. | ||
* Null if owned by a user. | ||
*/ | ||
studioId: string | null; | ||
} | ||
/** | ||
* Defines an interface for studio objects. | ||
*/ | ||
export interface Studio { | ||
/** | ||
* The ID of the studio. | ||
*/ | ||
id: string; | ||
/** | ||
* The name of the studio. | ||
*/ | ||
displayName: string; | ||
/** | ||
* The ID of the stripe customer for this studio. | ||
*/ | ||
stripeCustomerId?: string; | ||
/** | ||
* The current subscription status for this studio. | ||
*/ | ||
subscriptionStatus?: string; | ||
/** | ||
* The ID of the stripe subscription that this studio currently has. | ||
*/ | ||
subscriptionId?: string; | ||
} | ||
export type StudioAssignmentRole = 'admin' | 'member'; | ||
/** | ||
* Defines an interface for studio assignment objects. | ||
*/ | ||
export interface StudioAssignment { | ||
/** | ||
* The ID of the studio that this assignment applies to. | ||
*/ | ||
studioId: string; | ||
/** | ||
* The ID of the user that this assignment applies to. | ||
*/ | ||
userId: string; | ||
/** | ||
* Whether the user is the primary contact for this studio. | ||
*/ | ||
isPrimaryContact: boolean; | ||
/** | ||
* The role that this user has in the studio. | ||
*/ | ||
role: StudioAssignmentRole; | ||
} | ||
export interface ListedStudioAssignment { | ||
/** | ||
* The ID of the studio that this assignment applies to. | ||
*/ | ||
studioId: string; | ||
/** | ||
* The ID of the user that this assignment applies to. | ||
*/ | ||
userId: string; | ||
/** | ||
* Whether the user is the primary contact for this studio. | ||
*/ | ||
isPrimaryContact: boolean; | ||
/** | ||
* The role that this user has in the studio. | ||
*/ | ||
role: StudioAssignmentRole; | ||
/** | ||
* The user that this assignment applies to. | ||
*/ | ||
user: ListedStudioAssignmentUser; | ||
} | ||
export interface ListedUserAssignment { | ||
/** | ||
* The name of the studio that this assignment applies to. | ||
*/ | ||
displayName: string; | ||
/** | ||
* The ID of the studio that this assignment applies to. | ||
*/ | ||
studioId: string; | ||
/** | ||
* The ID of the user that this assignment applies to. | ||
*/ | ||
userId: string; | ||
/** | ||
* Whether the user is the primary contact for this studio. | ||
*/ | ||
isPrimaryContact: boolean; | ||
/** | ||
* The role that this user has in the studio. | ||
*/ | ||
role: StudioAssignmentRole; | ||
} | ||
/** | ||
* The user information for a listed studio assignment. | ||
*/ | ||
export interface ListedStudioAssignmentUser { | ||
/** | ||
* The ID of the user. | ||
*/ | ||
id: string; | ||
/** | ||
* The name of the user. | ||
*/ | ||
name: string; | ||
/** | ||
* The email address of the user. | ||
*/ | ||
email: string; | ||
/** | ||
* The phone number of the user. | ||
*/ | ||
phoneNumber: string; | ||
} | ||
export interface ListedStudio { | ||
/** | ||
* The ID of the studio. | ||
*/ | ||
studioId: string; | ||
/** | ||
* The name of the studio. | ||
*/ | ||
displayName: string; | ||
/** | ||
* The role that the user has in the studio. | ||
*/ | ||
role: StudioAssignmentRole; | ||
/** | ||
* Whether the user is the primary contact for this studio. | ||
*/ | ||
isPrimaryContact: boolean; | ||
} | ||
export interface ListStudioAssignmentFilters { | ||
/** | ||
* The ID of the user to filter by. | ||
*/ | ||
userId?: string; | ||
/** | ||
* The role to filter by. | ||
*/ | ||
role?: string; | ||
/** | ||
* Whether to filter by primary contact. | ||
*/ | ||
isPrimaryContact?: boolean; | ||
} | ||
/** | ||
* Defines a type that represents the different kinds of policies that a record key can have. | ||
@@ -79,0 +310,0 @@ * |
@@ -33,2 +33,10 @@ export interface SubscriptionConfiguration { | ||
/** | ||
* Whether the subscription is only purchasable by users. | ||
*/ | ||
userOnly?: boolean; | ||
/** | ||
* Whether the subscription is only purchasable by studios. | ||
*/ | ||
studioOnly?: boolean; | ||
/** | ||
* The tier that the subscription represents. | ||
@@ -35,0 +43,0 @@ * Defaults to "beta". |
@@ -6,2 +6,3 @@ import { AuthController, ValidateSessionKeyFailure } from './AuthController'; | ||
import { SubscriptionConfiguration } from './SubscriptionConfiguration'; | ||
import { RecordsStore } from './RecordsStore'; | ||
/** | ||
@@ -14,4 +15,5 @@ * Defines a class that is able to handle subscriptions. | ||
private _authStore; | ||
private _recordsStore; | ||
private _config; | ||
constructor(stripe: StripeInterface, auth: AuthController, authStore: AuthStore, config: SubscriptionConfiguration); | ||
constructor(stripe: StripeInterface, auth: AuthController, authStore: AuthStore, recordsStore: RecordsStore, config: SubscriptionConfiguration); | ||
/** | ||
@@ -22,2 +24,3 @@ * Gets the status of the given user's scription. | ||
getSubscriptionStatus(request: GetSubscriptionStatusRequest): Promise<GetSubscriptionStatusResult>; | ||
private _getPurchasableSubscriptionsForRole; | ||
private _getPurchasableSubscriptions; | ||
@@ -51,4 +54,8 @@ /** | ||
*/ | ||
userId: string; | ||
userId?: string; | ||
/** | ||
* The ID of the studio that the management session should be created for. | ||
*/ | ||
studioId?: string; | ||
/** | ||
* The subscription that was selected for purcahse by the user. | ||
@@ -80,3 +87,3 @@ */ | ||
*/ | ||
errorCode: ServerError | ValidateSessionKeyFailure['errorCode'] | 'unacceptable_user_id' | 'unacceptable_request' | 'price_does_not_match' | 'not_supported'; | ||
errorCode: ServerError | ValidateSessionKeyFailure['errorCode'] | 'unacceptable_user_id' | 'unacceptable_studio_id' | 'unacceptable_request' | 'price_does_not_match' | 'not_supported'; | ||
/** | ||
@@ -95,3 +102,7 @@ * The error message. | ||
*/ | ||
userId: string; | ||
userId?: string; | ||
/** | ||
* The ID of the studio whose subscrition status should be retrieved. | ||
*/ | ||
studioId?: string; | ||
} | ||
@@ -104,4 +115,8 @@ export type GetSubscriptionStatusResult = GetSubscriptionStatusSuccess | GetSubscriptionStatusFailure; | ||
*/ | ||
userId: string; | ||
userId?: string; | ||
/** | ||
* The ID of the studio. | ||
*/ | ||
studioId?: string; | ||
/** | ||
* The publishable stripe API key. | ||
@@ -234,3 +249,3 @@ */ | ||
*/ | ||
errorCode: ServerError | ValidateSessionKeyFailure['errorCode'] | 'unacceptable_user_id' | 'not_supported'; | ||
errorCode: ServerError | ValidateSessionKeyFailure['errorCode'] | 'unacceptable_user_id' | 'unacceptable_studio_id' | 'unacceptable_request' | 'not_supported'; | ||
/** | ||
@@ -237,0 +252,0 @@ * The error message. |
@@ -16,6 +16,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
export class SubscriptionController { | ||
constructor(stripe, auth, authStore, config) { | ||
constructor(stripe, auth, authStore, recordsStore, config) { | ||
this._stripe = stripe; | ||
this._auth = auth; | ||
this._authStore = authStore; | ||
this._recordsStore = recordsStore; | ||
this._config = config; | ||
@@ -37,23 +38,76 @@ } | ||
try { | ||
if (typeof request.userId !== 'string' || request.userId === '') { | ||
if (request.userId && request.studioId) { | ||
return { | ||
success: false, | ||
errorCode: 'unacceptable_user_id', | ||
errorMessage: 'The given user ID is invalid. It must be a correctly formatted string.', | ||
errorCode: 'unacceptable_request', | ||
errorMessage: 'The given request is invalid. It must not specify both a user ID and a studio ID.', | ||
}; | ||
} | ||
const keyResult = yield this._auth.validateSessionKey(request.sessionKey); | ||
if (keyResult.success === false) { | ||
return keyResult; | ||
if (request.userId) { | ||
if (typeof request.userId !== 'string' || | ||
request.userId === '') { | ||
return { | ||
success: false, | ||
errorCode: 'unacceptable_user_id', | ||
errorMessage: 'The given user ID is invalid. It must be a correctly formatted string.', | ||
}; | ||
} | ||
} | ||
else if (keyResult.userId !== request.userId) { | ||
console.log('[SubscriptionController] [getSubscriptionStatus] Request User ID doesnt match session key User ID!'); | ||
else if (request.studioId) { | ||
if (typeof request.studioId !== 'string' || | ||
request.studioId === '') { | ||
return { | ||
success: false, | ||
errorCode: 'unacceptable_studio_id', | ||
errorMessage: 'The given studio ID is invalid. It must be a correctly formatted string.', | ||
}; | ||
} | ||
} | ||
else { | ||
return { | ||
success: false, | ||
errorCode: 'invalid_key', | ||
errorMessage: INVALID_KEY_ERROR_MESSAGE, | ||
errorCode: 'unacceptable_request', | ||
errorMessage: 'The given request is invalid. It must have a valid user ID or studio ID.', | ||
}; | ||
} | ||
const user = yield this._authStore.findUser(keyResult.userId); | ||
let customerId = user.stripeCustomerId; | ||
const keyResult = yield this._auth.validateSessionKey(request.sessionKey); | ||
let customerId; | ||
let role; | ||
if (keyResult.success === false) { | ||
return keyResult; | ||
} | ||
else { | ||
if (request.userId) { | ||
if (keyResult.userId !== request.userId) { | ||
console.log('[SubscriptionController] [getSubscriptionStatus] Request User ID doesnt match session key User ID!'); | ||
return { | ||
success: false, | ||
errorCode: 'invalid_key', | ||
errorMessage: INVALID_KEY_ERROR_MESSAGE, | ||
}; | ||
} | ||
const user = yield this._authStore.findUser(keyResult.userId); | ||
customerId = user.stripeCustomerId; | ||
role = 'user'; | ||
} | ||
else if (request.studioId) { | ||
const assignments = yield this._recordsStore.listStudioAssignments(request.studioId, { | ||
userId: keyResult.userId, | ||
role: 'admin', | ||
}); | ||
if (assignments.length <= 0) { | ||
console.log('[SubscriptionController] [getSubscriptionStatus] Request user does not have access to studio!'); | ||
return { | ||
success: false, | ||
errorCode: 'invalid_key', | ||
errorMessage: INVALID_KEY_ERROR_MESSAGE, | ||
}; | ||
} | ||
const studio = yield this._recordsStore.getStudioById(request.studioId); | ||
customerId = studio.stripeCustomerId; | ||
role = 'studio'; | ||
} | ||
} | ||
// const user = await this._authStore.findUser(keyResult.userId); | ||
// let customerId = user.stripeCustomerId; | ||
if (!customerId) { | ||
@@ -63,5 +117,6 @@ return { | ||
userId: keyResult.userId, | ||
studioId: request.studioId, | ||
publishableKey: this._stripe.publishableKey, | ||
subscriptions: [], | ||
purchasableSubscriptions: yield this._getPurchasableSubscriptions(), | ||
purchasableSubscriptions: yield this._getPurchasableSubscriptions(role), | ||
}; | ||
@@ -95,6 +150,7 @@ } | ||
? [] | ||
: yield this._getPurchasableSubscriptions(); | ||
: yield this._getPurchasableSubscriptions(role); | ||
return { | ||
success: true, | ||
userId: keyResult.userId, | ||
studioId: request.studioId, | ||
publishableKey: this._stripe.publishableKey, | ||
@@ -115,7 +171,17 @@ subscriptions, | ||
} | ||
_getPurchasableSubscriptions() { | ||
_getPurchasableSubscriptionsForRole(role) { | ||
return this._config.subscriptions.filter((s) => { | ||
var _a, _b, _c; | ||
const isPurchasable = (_a = s.purchasable) !== null && _a !== void 0 ? _a : true; | ||
const isUserOnly = (_b = s.userOnly) !== null && _b !== void 0 ? _b : false; | ||
const isStudioOnly = (_c = s.studioOnly) !== null && _c !== void 0 ? _c : false; | ||
const matchesRole = (isUserOnly && role === 'user') || | ||
(isStudioOnly && role === 'studio') || | ||
(!isUserOnly && !isStudioOnly); | ||
return isPurchasable && matchesRole; | ||
}); | ||
} | ||
_getPurchasableSubscriptions(role) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const promises = this._config.subscriptions | ||
.filter((s) => { var _a; return (_a = s.purchasable) !== null && _a !== void 0 ? _a : true; }) | ||
.map((s) => __awaiter(this, void 0, void 0, function* () { | ||
const promises = this._getPurchasableSubscriptionsForRole(role).map((s) => __awaiter(this, void 0, void 0, function* () { | ||
return ({ | ||
@@ -161,9 +227,36 @@ sub: s, | ||
try { | ||
if (typeof request.userId !== 'string' || request.userId === '') { | ||
if (request.userId && request.studioId) { | ||
return { | ||
success: false, | ||
errorCode: 'unacceptable_user_id', | ||
errorMessage: 'The given user ID is invalid. It must be a correctly formatted string.', | ||
errorCode: 'unacceptable_request', | ||
errorMessage: 'The given request is invalid. It must not specify both a user ID and a studio ID.', | ||
}; | ||
} | ||
if (request.userId) { | ||
if (typeof request.userId !== 'string' || | ||
request.userId === '') { | ||
return { | ||
success: false, | ||
errorCode: 'unacceptable_user_id', | ||
errorMessage: 'The given user ID is invalid. It must be a correctly formatted string.', | ||
}; | ||
} | ||
} | ||
else if (request.studioId) { | ||
if (typeof request.studioId !== 'string' || | ||
request.studioId === '') { | ||
return { | ||
success: false, | ||
errorCode: 'unacceptable_studio_id', | ||
errorMessage: 'The given studio ID is invalid. It must be a correctly formatted string.', | ||
}; | ||
} | ||
} | ||
else { | ||
return { | ||
success: false, | ||
errorCode: 'unacceptable_request', | ||
errorMessage: 'The given request is invalid. It must have a valid user ID or studio ID.', | ||
}; | ||
} | ||
if (!!request.subscriptionId && | ||
@@ -186,23 +279,73 @@ typeof request.subscriptionId !== 'string') { | ||
const keyResult = yield this._auth.validateSessionKey(request.sessionKey); | ||
let customerId; | ||
let customerName; | ||
let customerEmail; | ||
let customerPhone; | ||
let role; | ||
let user; | ||
let studio; | ||
let customerMetadata = {}; | ||
let metadata = {}; | ||
if (keyResult.success === false) { | ||
return keyResult; | ||
} | ||
else if (keyResult.userId !== request.userId) { | ||
console.log('[SubscriptionController] [createManageSubscriptionLink] Request User ID doesnt match session key User ID!'); | ||
return { | ||
success: false, | ||
errorCode: 'invalid_key', | ||
errorMessage: INVALID_KEY_ERROR_MESSAGE, | ||
}; | ||
else if (request.userId) { | ||
if (keyResult.userId !== request.userId) { | ||
console.log('[SubscriptionController] [createManageSubscriptionLink] Request User ID doesnt match session key User ID!'); | ||
return { | ||
success: false, | ||
errorCode: 'invalid_key', | ||
errorMessage: INVALID_KEY_ERROR_MESSAGE, | ||
}; | ||
} | ||
user = yield this._authStore.findUser(keyResult.userId); | ||
customerId = user.stripeCustomerId; | ||
customerName = user.name; | ||
customerEmail = user.email; | ||
customerPhone = user.phoneNumber; | ||
metadata.userId = user.id; | ||
customerMetadata.role = 'user'; | ||
customerMetadata.userId = user.id; | ||
role = 'user'; | ||
console.log(`[SubscriptionController] [createManageSubscriptionLink] Creating a checkout/management session for User (${keyResult.userId}).`); | ||
} | ||
// if (this._config.subscriptions.length <= 0) { | ||
// return { | ||
// success: false, | ||
// errorCode: 'not_supported', | ||
// errorMessage: 'This method is not supported.', | ||
// }; | ||
// } | ||
console.log(`[SubscriptionController] [createManageSubscriptionLink] Creating a checkout/management session for User (${keyResult.userId}).`); | ||
const user = yield this._authStore.findUser(keyResult.userId); | ||
let customerId = user.stripeCustomerId; | ||
else if (request.studioId) { | ||
const assignments = yield this._recordsStore.listStudioAssignments(request.studioId, { | ||
role: 'admin', | ||
}); | ||
const userAssignment = assignments.find((a) => a.userId === keyResult.userId); | ||
if (!userAssignment) { | ||
console.log('[SubscriptionController] [getSubscriptionStatus] Request user does not have access to studio!'); | ||
return { | ||
success: false, | ||
errorCode: 'invalid_key', | ||
errorMessage: INVALID_KEY_ERROR_MESSAGE, | ||
}; | ||
} | ||
studio = yield this._recordsStore.getStudioById(request.studioId); | ||
customerId = studio.stripeCustomerId; | ||
customerName = studio.displayName; | ||
customerMetadata.role = 'studio'; | ||
customerMetadata.studioId = studio.id; | ||
metadata.studioId = studio.id; | ||
let primaryAssignment; | ||
if (userAssignment.isPrimaryContact) { | ||
primaryAssignment = userAssignment; | ||
} | ||
else { | ||
primaryAssignment = assignments.find((a) => a.isPrimaryContact); | ||
} | ||
if (primaryAssignment) { | ||
customerEmail = primaryAssignment.user.email; | ||
customerPhone = primaryAssignment.user.phoneNumber; | ||
metadata.contactUserId = keyResult.userId; | ||
customerMetadata.contactUserId = keyResult.userId; | ||
} | ||
role = 'studio'; | ||
console.log(`[SubscriptionController] [createManageSubscriptionLink] Creating a checkout/management session for Studio (userId: ${keyResult.userId}, studioId: ${studio.id}).`); | ||
} | ||
else { | ||
throw new Error('Should not reach this point'); | ||
} | ||
metadata.subjectId = keyResult.userId; | ||
if (!customerId) { | ||
@@ -218,12 +361,21 @@ if (this._config.subscriptions.length <= 0) { | ||
const result = yield this._stripe.createCustomer({ | ||
name: user.name, | ||
email: user.email, | ||
phone: user.phoneNumber, | ||
name: customerName, | ||
email: customerEmail, | ||
phone: customerPhone, | ||
metadata: customerMetadata, | ||
}); | ||
customerId = user.stripeCustomerId = result.id; | ||
console.log(`[SubscriptionController] [createManageSubscriptionLink] Saving Stripe Customer ID (${customerId}) to User Record (${user.id}).`); | ||
yield this._authStore.saveUser(Object.assign({}, user)); | ||
return yield this._createCheckoutSession(request, user, customerId); | ||
customerId = result.id; | ||
if (user) { | ||
user.stripeCustomerId = customerId; | ||
console.log(`[SubscriptionController] [createManageSubscriptionLink] Saving Stripe Customer ID (${customerId}) to User Record (${user.id}).`); | ||
yield this._authStore.saveUser(Object.assign({}, user)); | ||
} | ||
else if (studio) { | ||
studio.stripeCustomerId = customerId; | ||
console.log(`[SubscriptionController] [createManageSubscriptionLink] Saving Stripe Customer ID (${customerId}) to Studio Record (${studio.id}).`); | ||
yield this._recordsStore.updateStudio(Object.assign({}, studio)); | ||
} | ||
return yield this._createCheckoutSession(request, customerId, metadata, role, user, studio); | ||
} | ||
console.log(`[SubscriptionController] [createManageSubscriptionLink] User (${user.id}) Has Stripe Customer ID (${user.stripeCustomerId}). Checking active subscriptions for customer.`); | ||
console.log(`[SubscriptionController] [createManageSubscriptionLink] Has Stripe Customer ID (${customerId}). Checking active subscriptions for customer.`); | ||
const subs = yield this._stripe.listActiveSubscriptionsForCustomer(customerId); | ||
@@ -245,3 +397,3 @@ const hasSubscription = subs.subscriptions.some((s) => { | ||
console.log(`[SubscriptionController] [createManageSubscriptionLink] Customer has a managable subscription. Creating a portal session.`); | ||
const session = yield this._stripe.createPortalSession(Object.assign(Object.assign({}, ((_a = this._config.portalConfig) !== null && _a !== void 0 ? _a : {})), { customer: customerId, return_url: this._config.returnUrl })); | ||
const session = yield this._stripe.createPortalSession(Object.assign(Object.assign({}, ((_a = this._config.portalConfig) !== null && _a !== void 0 ? _a : {})), { customer: customerId, return_url: returnRoute(this._config.returnUrl, user, studio) })); | ||
console.log(`[SubscriptionController] [createManageSubscriptionLink] Portal session success!`); | ||
@@ -254,6 +406,6 @@ return { | ||
console.log(`[SubscriptionController] [createManageSubscriptionLink] Customer does not have a managable subscription. Creating a checkout session.`); | ||
return yield this._createCheckoutSession(request, user, customerId); | ||
return yield this._createCheckoutSession(request, customerId, metadata, role, user, studio); | ||
} | ||
catch (err) { | ||
console.log('[SubscriptionController] An error occurred while creating a manage subscription link:', err); | ||
console.error('[SubscriptionController] An error occurred while creating a manage subscription link:', err); | ||
return { | ||
@@ -267,8 +419,9 @@ success: false, | ||
} | ||
_createCheckoutSession(request, user, customerId) { | ||
_createCheckoutSession(request, customerId, metadata, role, user, studio) { | ||
var _a; | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const purchasableSubscriptions = this._getPurchasableSubscriptionsForRole(role); | ||
let sub; | ||
if (request.subscriptionId) { | ||
sub = this._config.subscriptions.find((s) => s.id === request.subscriptionId); | ||
sub = purchasableSubscriptions.find((s) => s.id === request.subscriptionId); | ||
if (sub) { | ||
@@ -279,3 +432,3 @@ console.log(`[SubscriptionController] [createManageSubscriptionLink] Using specified subscription (${request.subscriptionId}).`); | ||
if (!sub) { | ||
sub = this._config.subscriptions.find((s) => s.defaultSubscription); | ||
sub = purchasableSubscriptions.find((s) => s.defaultSubscription); | ||
if (sub) { | ||
@@ -286,3 +439,3 @@ console.log(`[SubscriptionController] [createManageSubscriptionLink] Using default subscription.`); | ||
if (!sub) { | ||
sub = this._config.subscriptions[0]; | ||
sub = purchasableSubscriptions[0]; | ||
console.log(`[SubscriptionController] [createManageSubscriptionLink] Using first subscription.`); | ||
@@ -309,3 +462,3 @@ } | ||
console.log(`[SubscriptionController] [createManageSubscriptionLink] Creating Checkout Session.`); | ||
const session = yield this._stripe.createCheckoutSession(Object.assign(Object.assign({}, ((_a = this._config.checkoutConfig) !== null && _a !== void 0 ? _a : {})), { customer: customerId, success_url: this._config.successUrl, cancel_url: this._config.cancelUrl, line_items: [ | ||
const session = yield this._stripe.createCheckoutSession(Object.assign(Object.assign({}, ((_a = this._config.checkoutConfig) !== null && _a !== void 0 ? _a : {})), { customer: customerId, success_url: returnRoute(this._config.successUrl, user, studio), cancel_url: returnRoute(this._config.cancelUrl, user, studio), line_items: [ | ||
{ | ||
@@ -315,5 +468,3 @@ price: productInfo.default_price.id, | ||
}, | ||
], mode: 'subscription', metadata: { | ||
userId: user.id, | ||
} })); | ||
], mode: 'subscription', metadata: metadata })); | ||
console.log(`[SubscriptionController] [createManageSubscriptionLink] Checkout Session Success!`); | ||
@@ -400,13 +551,32 @@ return { | ||
const user = yield this._authStore.findUserByStripeCustomerId(customerId); | ||
if (!user) { | ||
console.log(`[SubscriptionController] [handleStripeWebhook] No user found for Customer ID (${customerId})`); | ||
return { | ||
success: true, | ||
}; | ||
if (user) { | ||
if (user.subscriptionStatus !== status || | ||
user.subscriptionId !== sub.id) { | ||
console.log(`[SubscriptionController] [handleStripeWebhook] User (${user.id}) subscription status doesn't match stored. Updating...`); | ||
yield this._authStore.saveUser(Object.assign(Object.assign({}, user), { subscriptionStatus: status, subscriptionId: sub.id })); | ||
} | ||
else { | ||
return { | ||
success: true, | ||
}; | ||
} | ||
} | ||
if (user.subscriptionStatus !== status || | ||
user.subscriptionId !== sub.id) { | ||
console.log(`[SubscriptionController] [handleStripeWebhook] User subscription status doesn't match stored. Updating...`); | ||
yield this._authStore.saveUser(Object.assign(Object.assign({}, user), { subscriptionStatus: status, subscriptionId: sub.id })); | ||
console.log(`[SubscriptionController] [handleStripeWebhook] No user found for Customer ID (${customerId})`); | ||
const studio = yield this._recordsStore.getStudioByStripeCustomerId(customerId); | ||
if (studio) { | ||
if (studio.subscriptionStatus !== status || | ||
studio.subscriptionId !== sub.id) { | ||
console.log(`[SubscriptionController] [handleStripeWebhook] Studio ((${studio.id})) subscription status doesn't match stored. Updating...`); | ||
yield this._recordsStore.updateStudio(Object.assign(Object.assign({}, studio), { subscriptionStatus: status, subscriptionId: sub.id })); | ||
} | ||
else { | ||
return { | ||
success: true, | ||
}; | ||
} | ||
} | ||
console.log(`[SubscriptionController] [handleStripeWebhook] No studio found for Customer ID (${customerId})`); | ||
return { | ||
success: true, | ||
}; | ||
} | ||
@@ -457,2 +627,13 @@ return { | ||
} | ||
function returnRoute(basePath, user, studio) { | ||
if (user) { | ||
return basePath; | ||
} | ||
else { | ||
return studiosRoute(basePath, studio.id, studio.displayName); | ||
} | ||
} | ||
function studiosRoute(basePath, studioId, studioName) { | ||
return new URL(`/studios/${encodeURIComponent(studioId)}/${encodeURIComponent(studioName)}`, basePath).href; | ||
} | ||
//# sourceMappingURL=SubscriptionController.js.map |
@@ -28,3 +28,3 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
const auth = new AuthController(authStore, authMessenger, subConfig, true); | ||
const recordsStore = new MemoryRecordsStore(); | ||
const recordsStore = new MemoryRecordsStore(authStore); | ||
const records = new RecordsController(recordsStore, authStore); | ||
@@ -31,0 +31,0 @@ const policyStore = new MemoryPolicyStore(); |
@@ -254,2 +254,8 @@ import { fromByteArray, toByteArray } from 'base64-js'; | ||
} | ||
else if (response.errorCode === 'studio_not_found') { | ||
return 404; | ||
} | ||
else if (response.errorCode === 'user_not_found') { | ||
return 404; | ||
} | ||
else if (response.errorCode === 'session_already_revoked') { | ||
@@ -330,2 +336,5 @@ return 200; | ||
} | ||
else if (response.errorCode === 'record_already_exists') { | ||
return 403; | ||
} | ||
else { | ||
@@ -332,0 +341,0 @@ return 400; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
1123823
19462
2
81
41
1