wip: usecases

This commit is contained in:
Mo
2023-05-16 07:36:25 -05:00
parent 8ffeae8642
commit ff030391a4
29 changed files with 285 additions and 164 deletions
@@ -0,0 +1,26 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'
export class AddPkcFields1684172607219 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
'users',
new TableColumn({
name: 'public_key',
type: 'text',
}),
)
await queryRunner.addColumn(
'users',
new TableColumn({
name: 'encrypted_private_key',
type: 'text',
}),
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('users', 'public_key')
await queryRunner.dropColumn('users', 'encrypted_private_key')
}
}
@@ -72,6 +72,8 @@ export class UsersController extends BaseHttpController {
kpOrigination: request.body.origination,
kpCreated: request.body.created,
version: request.body.version,
publicKey: request.body.public_key,
encryptedPrivateKey: request.body.private_key,
})
if (updateResult.success) {
@@ -208,6 +210,8 @@ export class UsersController extends BaseHttpController {
kpOrigination: request.body.origination,
updatedWithUserAgent: <string>request.headers['user-agent'],
protocolVersion: request.body.version,
publicKey: request.body.new_public_key,
encryptedPrivateKey: request.body.new_encrypted_private_key,
})
if (!changeCredentialsResult.success) {
@@ -1,9 +1,5 @@
import { ProtocolVersion } from '@standardnotes/common'
import { SimpleUserProjection } from '../../Projection/SimpleUserProjection'
export interface AuthResponse {
user: {
uuid: string
email: string
protocolVersion: ProtocolVersion
}
user: SimpleUserProjection
}
@@ -4,7 +4,6 @@ import {
TokenEncoderInterface,
} from '@standardnotes/security'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { ProtocolVersion } from '@standardnotes/common'
import { SessionBody } from '@standardnotes/responses'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
@@ -19,6 +18,7 @@ import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterfac
import { AuthResponse20161215 } from './AuthResponse20161215'
import { AuthResponse20200115 } from './AuthResponse20200115'
import { SimpleUserProjection } from '../../Projection/SimpleUserProjection'
@injectable()
export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
@@ -54,11 +54,7 @@ export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
return {
session: sessionPayload,
key_params: this.keyParamsFactory.create(dto.user, true),
user: this.userProjector.projectSimple(dto.user) as {
uuid: string
email: string
protocolVersion: ProtocolVersion
},
user: this.userProjector.projectSimple(dto.user) as SimpleUserProjection,
}
}
@@ -62,6 +62,13 @@ export class ChangeCredentials implements UseCaseInterface {
dto.user.email = newUsername.value
}
if (dto.publicKey) {
dto.user.publicKey = dto.publicKey
}
if (dto.encryptedPrivateKey) {
dto.user.encryptedPrivateKey = dto.encryptedPrivateKey
}
dto.user.pwNonce = dto.pwNonce
if (dto.protocolVersion) {
dto.user.version = dto.protocolVersion
@@ -11,4 +11,6 @@ export type ChangeCredentialsDTO = {
protocolVersion?: string
kpOrigination?: string
kpCreated?: string
publicKey?: string
encryptedPrivateKey?: string
}
@@ -71,6 +71,8 @@ export class Register implements UseCaseInterface {
user.encryptedPassword = await bcrypt.hash(password, User.PASSWORD_HASH_COST)
user.encryptedServerKey = await this.crypter.generateEncryptedUserServerKey()
user.serverEncryptionVersion = User.DEFAULT_ENCRYPTION_VERSION
user.publicKey = dto.publicKey ?? null
user.encryptedPrivateKey = dto.encryptedPrivateKey ?? null
const defaultRole = await this.roleRepository.findOneByName(RoleName.NAMES.CoreUser)
if (defaultRole) {
@@ -10,4 +10,6 @@ export type RegisterDTO = {
kpOrigination?: string
kpCreated?: string
version?: string
publicKey?: string
encryptedPrivateKey?: string
}
@@ -15,4 +15,6 @@ export type UpdateUserDTO = {
kpOrigination?: string
kpCreated?: Date
version?: string
publicKey?: string
encryptedPrivateKey?: string
}
+16
View File
@@ -42,6 +42,22 @@ export class User {
})
declare encryptedServerKey: string | null
@Column({
name: 'public_key',
length: 255,
type: 'varchar',
nullable: true,
})
declare publicKey: string | null
@Column({
name: 'encrypted_private_key',
length: 255,
type: 'varchar',
nullable: true,
})
declare encryptedPrivateKey: string | null
@Column({
name: 'server_encryption_version',
type: 'tinyint',
@@ -0,0 +1,7 @@
export type SimpleUserProjection = {
uuid: string
email: string
protocolVersion: string
publicKey?: string
encryptedPrivateKey?: string
}
@@ -2,14 +2,17 @@ import { injectable } from 'inversify'
import { User } from '../Domain/User/User'
import { ProjectorInterface } from './ProjectorInterface'
import { SimpleUserProjection } from './SimpleUserProjection'
@injectable()
export class UserProjector implements ProjectorInterface<User> {
projectSimple(user: User): Record<string, unknown> {
projectSimple(user: User): SimpleUserProjection {
return {
uuid: user.uuid,
email: user.email,
protocolVersion: user.version,
publicKey: user.publicKey ?? undefined,
encryptedPrivateKey: user.encryptedPrivateKey ?? undefined,
}
}
@@ -0,0 +1,37 @@
import { MigrationInterface, QueryRunner, Table, TableColumn } from 'typeorm'
export class CreateGroup1684173017359 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const table = new Table({
name: 'groups',
columns: [
new TableColumn({
name: 'uuid',
type: 'varchar',
length: '36',
isPrimary: true,
}),
new TableColumn({
name: 'user_uuid',
type: 'varchar',
length: '36',
isNullable: false,
}),
new TableColumn({
name: 'created_at_timestamp',
type: 'bigint',
}),
new TableColumn({
name: 'updated_at_timestamp',
type: 'bigint',
}),
],
})
await queryRunner.createTable(table)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('groups')
}
}
@@ -0,0 +1,53 @@
import { MigrationInterface, QueryRunner, Table, TableColumn } from 'typeorm'
export class CreateGroupUser1684173023883 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const table = new Table({
name: 'group_users',
columns: [
new TableColumn({
name: 'uuid',
type: 'varchar',
length: '36',
isPrimary: true,
}),
new TableColumn({
name: 'group_uuid',
type: 'varchar',
length: '36',
isNullable: false,
}),
new TableColumn({
name: 'user_uuid',
type: 'varchar',
length: '36',
isNullable: false,
}),
new TableColumn({
name: 'encrypted_group_key',
type: 'text',
isNullable: false,
}),
new TableColumn({
name: 'sender_public_key',
type: 'text',
isNullable: false,
}),
new TableColumn({
name: 'created_at_timestamp',
type: 'bigint',
}),
new TableColumn({
name: 'updated_at_timestamp',
type: 'bigint',
}),
],
})
await queryRunner.createTable(table)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('group_users')
}
}
@@ -2,10 +2,11 @@ import { GroupServiceInterface } from './../Domain/Group/Service/GroupServiceInt
import { Request, Response } from 'express'
import { BaseHttpController, controller, httpGet, httpPost, results } from 'inversify-express-utils'
import TYPES from '../Bootstrap/Types'
import { GroupUserServiceInterface } from '../Domain/GroupUser/Service/GroupUserService'
@controller('/groups')
export class GroupsController extends BaseHttpController {
constructor(private groupsService: GroupServiceInterface) {
constructor(private groupsService: GroupServiceInterface, private groupUserService: GroupUserServiceInterface) {
super()
}
@@ -23,6 +24,20 @@ export class GroupsController extends BaseHttpController {
return this.json({ group: result })
}
@httpGet('/', TYPES.AuthMiddleware)
public async getUserGroupKeys(
_request: Request,
response: Response,
): Promise<results.NotFoundResult | results.JsonResult> {
const result = await this.groupUserService.getUserGroupKeys({ userUuid: response.locals.user.uuid })
if (!result) {
return this.errorResponse(500, 'Could not get user groups')
}
return this.json({ groups: result })
}
@httpPost('/:groupUuid/users', TYPES.AuthMiddleware)
public async addUserToGroup(
request: Request,
@@ -31,8 +46,9 @@ export class GroupsController extends BaseHttpController {
const result = await this.groupsService.addUserToGroup({
groupUuid: request.params.groupUuid,
ownerUuid: response.locals.user.uuid,
inviteeUuid: request.body.inviteeUuid,
encryptedGroupKey: request.body.encryptedGroupKey,
inviteeUuid: request.body.invitee_uuid,
encryptedGroupKey: request.body.encrypted_group_key,
senderPublicKey: request.body.sender_public_key,
})
if (!result) {
@@ -42,20 +58,6 @@ export class GroupsController extends BaseHttpController {
return this.json({ groupUser: result })
}
@httpGet('/', TYPES.AuthMiddleware)
public async getUserGroups(
_request: Request,
response: Response,
): Promise<results.NotFoundResult | results.JsonResult> {
const result = await this.groupsService.getUserGroups(response.locals.user.uuid)
if (!result) {
return this.errorResponse(500, 'Could not get user groups')
}
return this.json({ groups: result })
}
private errorResponse(status: number, message?: string, tag?: string) {
return this.json(
{
@@ -1,4 +1,3 @@
import { ItemHash } from './../../Item/ItemHash'
import { GroupUser } from '../../GroupUser/Model/GroupUser'
import { Group } from '../Model/Group'
@@ -10,20 +9,13 @@ import { TimerInterface } from '@standardnotes/time'
import { GroupUserServiceInterface } from '../../GroupUser/Service/GroupUserService'
import { v4 as uuidv4 } from 'uuid'
import { GetItem } from '../../UseCase/GetItem/GetItem'
import { SaveItemUseCase } from '../../UseCase/SaveItem'
import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
import { Item } from '../../Item/Item'
import { ItemProjection } from '../../../Projection/ItemProjection'
export class GroupService implements GroupServiceInterface {
constructor(
private groupRepository: GroupsRepositoryInterface,
private groupFactory: GroupFactoryInterface,
private groupUserSerivce: GroupUserServiceInterface,
private getItem: GetItem,
private saveItem: SaveItemUseCase,
private itemProjector: ProjectorInterface<Item, ItemProjection>,
private groupUserService: GroupUserServiceInterface,
private timer: TimerInterface,
) {}
@@ -48,45 +40,15 @@ export class GroupService implements GroupServiceInterface {
ownerUuid: string
inviteeUuid: string
encryptedGroupKey: string
senderPublicKey: string
}): Promise<GroupUser | null> {
const user = await this.groupUserSerivce.createGroupUser(dto.groupUuid, dto.inviteeUuid, dto.encryptedGroupKey)
const user = await this.groupUserService.createGroupUser({
groupUuid: dto.groupUuid,
userUuid: dto.inviteeUuid,
encryptedGroupKey: dto.encryptedGroupKey,
senderPublicKey: dto.senderPublicKey,
})
return user
}
async addItemToGroup(dto: {
groupUuid: string
userUuid: string
itemUuid: string
apiVersion: string
readOnlyAccess: boolean
sessionUuid: string | null
}): Promise<{ success: boolean }> {
const getItemResult = await this.getItem.execute({ itemUuid: dto.itemUuid, userUuid: dto.userUuid })
if (!getItemResult.success) {
return { success: false }
}
const itemHash = await this.itemProjector.projectFull(getItemResult.item)
itemHash.group_uuid = dto.groupUuid
const saveItemResult = await this.saveItem.execute({
itemHash: <ItemHash>itemHash,
userUuid: dto.userUuid,
apiVersion: dto.apiVersion,
readOnlyAccess: dto.readOnlyAccess,
sessionUuid: dto.sessionUuid,
})
return { success: saveItemResult.success }
}
getGroup(groupUuid: string): Promise<Group | null> {
return this.groupRepository.findByUuid(groupUuid)
}
async getUserGroups(userUuid: string): Promise<Group[]> {
return this.groupRepository.findAll({ userUuid })
}
}
@@ -9,18 +9,6 @@ export interface GroupServiceInterface {
ownerUuid: string
inviteeUuid: string
encryptedGroupKey: string
senderPublicKey: string
}): Promise<GroupUser | null>
addItemToGroup(dto: {
groupUuid: string
userUuid: string
itemUuid: string
apiVersion: string
readOnlyAccess: boolean
sessionUuid: string | null
}): Promise<{ success: boolean }>
getGroup(groupUuid: string): Promise<Group | null>
getUserGroups(userUuid: string): Promise<Group[]>
}
@@ -22,6 +22,11 @@ export class GroupUser {
})
declare encryptedGroupKey: string
@Column({
name: 'sender_public_key',
})
declare senderPublicKey: string
@Column({
name: 'created_at_timestamp',
type: 'bigint',
@@ -0,0 +1,4 @@
export interface GetUserGroupKeysDTO {
userUuid: string
syncToken?: string | null
}
@@ -1,12 +1,26 @@
import { GetUserGroupKeysDTO } from './GetUserGroupKeysDTO'
import { GroupUser } from '../Model/GroupUser'
export interface GroupUserServiceInterface {
createGroupUser(groupUuid: string, userUuid: string, encryptedGroupKey: string): Promise<GroupUser>
getGroupUsers(groupUuid: string): Promise<GroupUser[]>
createGroupUser(dto: {
groupUuid: string
userUuid: string
encryptedGroupKey: string
senderPublicKey: string
}): Promise<GroupUser>
getUsersForGroup(groupUuid: string): Promise<GroupUser[]>
getUserGroupKeys(dto: GetUserGroupKeysDTO): Promise<GroupUser[]>
}
export class GroupUserService implements GroupUserServiceInterface {
createGroupUser(groupUuid: string, userUuid: string, encryptedGroupKey: string): Promise<GroupUser> {}
createGroupUser(dto: {
groupUuid: string
userUuid: string
encryptedGroupKey: string
senderPublicKey: string
}): Promise<GroupUser> {}
getGroupUsers(groupUuid: string): Promise<GroupUser[]> {}
getUsersForGroup(groupUuid: string): Promise<GroupUser[]> {}
getUserGroupKeys(dto: GetUserGroupKeysDTO): Promise<GroupUser[]> {}
}
@@ -2,11 +2,10 @@ import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO'
import { ItemSaveRuleResult } from './ItemSaveRuleResult'
import { ItemSaveRuleInterface } from './ItemSaveRuleInterface'
import { ConflictType } from '@standardnotes/responses'
import { GetUserGroupsUseCase } from '../../UseCase/Groups/GetUserGroupsUseCase'
import { GetGroupItemsUseCase } from '../../UseCase/Groups/GetGroupItemsUseCase'
import { GetUserGroupKeysUseCase } from '../../UseCase/Groups/GetUserGroupKeysUseCase'
export class OwnershipFilter implements ItemSaveRuleInterface {
constructor(private getUserGroupsUseCase: GetUserGroupsUseCase, private getGroupItemsUseCase: GetGroupItemsUseCase) {}
constructor(private getUserGroupsUseCase: GetUserGroupKeysUseCase) {}
async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
const itemBelongsToADifferentUser = dto.existingItem !== null && dto.existingItem.userUuid !== dto.userUuid
@@ -1,3 +1,4 @@
import { GroupUserProjection } from '../../../Projection/GroupUserProjection'
import { ItemConflictProjection } from '../../../Projection/ItemConflictProjection'
import { ItemProjection } from '../../../Projection/ItemProjection'
import { SavedItemProjection } from '../../../Projection/SavedItemProjection'
@@ -8,4 +9,5 @@ export type SyncResponse20200115 = {
conflicts: Array<ItemConflictProjection>
sync_token: string
cursor_token?: string
group_keys: Array<GroupUserProjection>
}
@@ -7,12 +7,15 @@ import { ItemProjection } from '../../../Projection/ItemProjection'
import { SyncResponse20200115 } from './SyncResponse20200115'
import { SyncResponseFactoryInterface } from './SyncResponseFactoryInterface'
import { SavedItemProjection } from '../../../Projection/SavedItemProjection'
import { GroupUser } from '../../GroupUser/Model/GroupUser'
import { GroupUserProjection } from '../../../Projection/GroupUserProjection'
export class SyncResponseFactory20200115 implements SyncResponseFactoryInterface {
constructor(
private itemProjector: ProjectorInterface<Item, ItemProjection>,
private itemConflictProjector: ProjectorInterface<ItemConflict, ItemConflictProjection>,
private savedItemProjector: ProjectorInterface<Item, SavedItemProjection>,
private groupKeyProjector: ProjectorInterface<GroupUser, GroupUserProjection>,
) {}
async createResponse(syncItemsResponse: SyncItemsResponse): Promise<SyncResponse20200115> {
@@ -31,12 +34,18 @@ export class SyncResponseFactory20200115 implements SyncResponseFactoryInterface
conflicts.push(<ItemConflictProjection>await this.itemConflictProjector.projectFull(itemConflict))
}
const groupKeys = []
for (const groupKey of syncItemsResponse.groupKeys) {
groupKeys.push(<GroupUserProjection>await this.groupKeyProjector.projectFull(groupKey))
}
return {
retrieved_items: retrievedItems,
saved_items: savedItems,
conflicts,
sync_token: syncItemsResponse.syncToken,
cursor_token: syncItemsResponse.cursorToken,
group_keys: groupKeys,
}
}
}
@@ -1,33 +0,0 @@
import { GroupServiceInterface } from '../../Group/Service/GroupServiceInterface'
import { UseCaseInterface } from '../UseCaseInterface'
import { Group } from '../../Group/Model/Group'
export type GetGroupUseCaseResult =
| {
success: true
group: Group
}
| {
success: false
message: string
}
export class GetGroupUseCase implements UseCaseInterface {
constructor(private groupService: GroupServiceInterface) {}
async execute(dto: { groupUuid: string }): Promise<GetGroupUseCaseResult> {
const result = await this.groupService.getGroup(dto.groupUuid)
if (!result) {
return {
success: false,
message: `Could not get group ${dto.groupUuid}`,
}
}
return {
success: true,
group: result,
}
}
}
@@ -0,0 +1,34 @@
import { GetUserGroupKeysDTO } from './../../GroupUser/Service/GetUserGroupKeysDTO'
import { UseCaseInterface } from '../UseCaseInterface'
import { Group } from '../../Group/Model/Group'
import { GroupUserServiceInterface } from '../../GroupUser/Service/GroupUserService'
export type GetUserGroupKeysUseCaseResult =
| {
success: true
groups: Group[]
}
| {
success: false
message: string
}
export class GetUserGroupKeysUseCase implements UseCaseInterface {
constructor(private groupUserService: GroupUserServiceInterface) {}
async execute(dto: GetUserGroupKeysDTO): Promise<GetUserGroupKeysUseCaseResult> {
const result = await this.groupUserService.getUserGroupKeys(dto)
if (!result) {
return {
success: false,
message: `Could not get user groups for user ${dto.userUuid}`,
}
}
return {
success: true,
groups: result,
}
}
}
@@ -1,33 +0,0 @@
import { GroupServiceInterface } from '../../Group/Service/GroupServiceInterface'
import { UseCaseInterface } from '../UseCaseInterface'
import { Group } from '../../Group/Model/Group'
export type GetUserGroupsUseCaseResult =
| {
success: true
groups: Group[]
}
| {
success: false
message: string
}
export class GetUserGroupsUseCase implements UseCaseInterface {
constructor(private groupService: GroupServiceInterface) {}
async execute(dto: { userUuid: string }): Promise<GetUserGroupsUseCaseResult> {
const result = await this.groupService.getUserGroups(dto.userUuid)
if (!result) {
return {
success: false,
message: `Could not get user groups for user ${dto.userUuid}`,
}
}
return {
success: true,
groups: result,
}
}
}
@@ -1,3 +1,4 @@
import { GroupUserServiceInterface } from '../GroupUser/Service/GroupUserService'
import { Item } from '../Item/Item'
import { ItemConflict } from '../Item/ItemConflict'
import { ItemServiceInterface } from '../Item/ItemServiceInterface'
@@ -6,7 +7,7 @@ import { SyncItemsResponse } from './SyncItemsResponse'
import { UseCaseInterface } from './UseCaseInterface'
export class SyncItems implements UseCaseInterface {
constructor(private itemService: ItemServiceInterface) {}
constructor(private itemService: ItemServiceInterface, private groupUserService: GroupUserServiceInterface) {}
async execute(dto: SyncItemsDTO): Promise<SyncItemsResponse> {
const getItemsResult = await this.itemService.getItems({
@@ -30,12 +31,15 @@ export class SyncItems implements UseCaseInterface {
retrievedItems = await this.itemService.frontLoadKeysItemsToTop(dto.userUuid, retrievedItems)
}
const groupKeys = await this.groupUserService.getUserGroupKeys({ userUuid: dto.userUuid, syncToken: dto.syncToken })
const syncResponse: SyncItemsResponse = {
retrievedItems,
syncToken: saveItemsResult.syncToken,
savedItems: saveItemsResult.savedItems,
conflicts: saveItemsResult.conflicts,
cursorToken: getItemsResult.cursorToken,
groupKeys,
}
return syncResponse
@@ -1,3 +1,4 @@
import { GroupUser } from './../GroupUser/Model/GroupUser'
import { Item } from '../Item/Item'
import { ItemConflict } from '../Item/ItemConflict'
@@ -5,6 +6,7 @@ export type SyncItemsResponse = {
retrievedItems: Array<Item>
savedItems: Array<Item>
conflicts: Array<ItemConflict>
groupKeys: Array<GroupUser>
syncToken: string
cursorToken?: string
}
@@ -0,0 +1,9 @@
export type GroupUserProjection = {
uuid: string
group_uuid: string
user_uuid: string
encrypted_group_key: string
sender_public_key: string
created_at_timestamp: number
updated_at_timestamp: number
}