Compare commits

..

16 Commits

Author SHA1 Message Date
standardci
148542dd5a chore(release): publish new version
- @standardnotes/home-server@1.11.24
 - @standardnotes/syncing-server@1.51.0
2023-07-05 09:17:53 +00:00
Karol Sójko
d2b2c339f2 feat: add getting shared vaults for a user (#639) 2023-07-05 11:01:21 +02:00
standardci
d2578c48f0 chore(release): publish new version
- @standardnotes/auth-server@1.121.0
 - @standardnotes/home-server@1.11.23
2023-07-05 08:21:19 +00:00
Karol Sójko
fecfd54728 feat(auth): add notifications model (#638) 2023-07-05 10:03:01 +02:00
Karol Sójko
17e4162d3e Revert "fix: unplug node-gyp based deps (#637)"
This reverts commit 742209d773.
2023-07-04 12:56:10 +02:00
Karol Sójko
742209d773 fix: unplug node-gyp based deps (#637) 2023-07-04 10:42:44 +02:00
standardci
1fa4b7cf27 chore(release): publish new version
- @standardnotes/home-server@1.11.22
 - @standardnotes/syncing-server@1.50.0
2023-07-03 17:55:21 +00:00
Karol Sójko
5dc5507039 feat: add invite users to a shared vault. (#636)
Co-authored-by: Mo <mo@standardnotes.com>
2023-07-03 19:40:36 +02:00
standardci
3035a20b9f chore(release): publish new version
- @standardnotes/home-server@1.11.21
 - @standardnotes/syncing-server@1.49.0
2023-07-03 16:58:01 +00:00
Karol Sójko
04b3bb034f feat: add creating shared vault file valet tokens. (#635)
Co-authored-by: Mo <mo@standardnotes.com>
2023-07-03 18:43:32 +02:00
standardci
bf84be0136 chore(release): publish new version
- @standardnotes/home-server@1.11.20
 - @standardnotes/syncing-server@1.48.0
2023-07-03 10:14:10 +00:00
Karol Sójko
890cf48749 feat: add shared vault invite model. (#634)
Co-authored-by: Mo <mo@standardnotes.com>
2023-07-03 11:54:52 +02:00
standardci
2b3436c6ce chore(release): publish new version
- @standardnotes/home-server@1.11.19
 - @standardnotes/syncing-server@1.47.0
2023-06-30 13:25:38 +00:00
Karol Sójko
4df8c3b2e5 feat: add use case for creating shared vaults and adding users to it. (#633)
Co-authored-by: Mo <mo@standardnotes.com>
2023-06-30 15:11:12 +02:00
standardci
25a2696c32 chore(release): publish new version
- @standardnotes/home-server@1.11.18
 - @standardnotes/syncing-server@1.46.0
2023-06-30 11:33:49 +00:00
Karol Sójko
52f879f842 feat: add shared vaults user model. (#632)
Co-authored-by: Mo <mo@standardnotes.com>
2023-06-30 13:19:31 +02:00
57 changed files with 2076 additions and 21 deletions

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.121.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.120.2...@standardnotes/auth-server@1.121.0) (2023-07-05)
### Features
* **auth:** add notifications model ([#638](https://github.com/standardnotes/server/issues/638)) ([fecfd54](https://github.com/standardnotes/server/commit/fecfd5472824b5adae708db95d351e4ad65ee87b))
## [1.120.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.120.1...@standardnotes/auth-server@1.120.2) (2023-06-30)
**Note:** Version bump only for package @standardnotes/auth-server

Binary file not shown.

View File

@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddNotifications1688540448427 implements MigrationInterface {
name = 'AddNotifications1688540448427'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE `notifications` (`uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `type` varchar(36) NOT NULL, `payload` text NOT NULL, `created_at_timestamp` bigint NOT NULL, `updated_at_timestamp` bigint NOT NULL, INDEX `index_notifications_on_user_uuid` (`user_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `index_notifications_on_user_uuid` ON `notifications`')
await queryRunner.query('DROP TABLE `notifications`')
}
}

View File

@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddNotifications1688540623272 implements MigrationInterface {
name = 'AddNotifications1688540623272'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE "notifications" ("uuid" varchar PRIMARY KEY NOT NULL, "user_uuid" varchar(36) NOT NULL, "type" varchar(36) NOT NULL, "payload" text NOT NULL, "created_at_timestamp" bigint NOT NULL, "updated_at_timestamp" bigint NOT NULL)',
)
await queryRunner.query('CREATE INDEX "index_notifications_on_user_uuid" ON "notifications" ("user_uuid") ')
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX "index_notifications_on_user_uuid"')
await queryRunner.query('DROP TABLE "notifications"')
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.120.2",
"version": "1.121.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -18,21 +18,26 @@ import { TypeORMEmergencyAccessInvitation } from '../Infra/TypeORM/TypeORMEmerge
import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace'
import { Env } from './Env'
import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'
import { TypeORMNotification } from '../Infra/TypeORM/TypeORMNotification'
export class AppDataSource {
private dataSource: DataSource | undefined
private _dataSource: DataSource | undefined
constructor(private env: Env) {}
getRepository<Entity extends ObjectLiteral>(target: EntityTarget<Entity>): Repository<Entity> {
if (!this.dataSource) {
if (!this._dataSource) {
throw new Error('DataSource not initialized')
}
return this.dataSource.getRepository(target)
return this._dataSource.getRepository(target)
}
async initialize(): Promise<void> {
await this.dataSource.initialize()
}
get dataSource(): DataSource {
this.env.load()
const isConfiguredForMySQL = this.env.get('DB_TYPE') === 'mysql'
@@ -60,6 +65,7 @@ export class AppDataSource {
TypeORMAuthenticatorChallenge,
TypeORMEmergencyAccessInvitation,
TypeORMCacheEntry,
TypeORMNotification,
],
migrations: [`${__dirname}/../../migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
migrationsRun: true,
@@ -104,7 +110,7 @@ export class AppDataSource {
database: inReplicaMode ? undefined : this.env.get('DB_DATABASE'),
}
this.dataSource = new DataSource(mySQLDataSourceOptions)
this._dataSource = new DataSource(mySQLDataSourceOptions)
} else {
const sqliteDataSourceOptions: SqliteConnectionOptions = {
...commonDataSourceOptions,
@@ -112,9 +118,9 @@ export class AppDataSource {
database: this.env.get('DB_SQLITE_DATABASE_PATH'),
}
this.dataSource = new DataSource(sqliteDataSourceOptions)
this._dataSource = new DataSource(sqliteDataSourceOptions)
}
await this.dataSource.initialize()
return this._dataSource
}
}

View File

@@ -0,0 +1,7 @@
import { AppDataSource } from './DataSource'
import { Env } from './Env'
const env: Env = new Env()
env.load()
export const MigrationsDataSource = new AppDataSource(env).dataSource

View File

@@ -0,0 +1,18 @@
import { Timestamps, Uuid } from '@standardnotes/domain-core'
import { Notification } from './Notification'
import { NotificationType } from './NotificationType'
describe('Notification', () => {
it('should create an entity', () => {
const entityOrError = Notification.create({
timestamps: Timestamps.create(123456789, 123456789).getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
payload: 'payload',
type: NotificationType.create(NotificationType.TYPES.SharedVaultItemRemoved).getValue(),
})
expect(entityOrError.isFailed()).toBeFalsy()
expect(entityOrError.getValue().id).not.toBeNull()
})
})

View File

@@ -0,0 +1,17 @@
import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
import { NotificationProps } from './NotificationProps'
export class Notification extends Entity<NotificationProps> {
get id(): UniqueEntityId {
return this._id
}
private constructor(props: NotificationProps, id?: UniqueEntityId) {
super(props, id)
}
static create(props: NotificationProps, id?: UniqueEntityId): Result<Notification> {
return Result.ok<Notification>(new Notification(props, id))
}
}

View File

@@ -0,0 +1,9 @@
import { Timestamps, Uuid } from '@standardnotes/domain-core'
import { NotificationType } from './NotificationType'
export interface NotificationProps {
userUuid: Uuid
type: NotificationType
payload: string
timestamps: Timestamps
}

View File

@@ -0,0 +1,16 @@
import { NotificationType } from './NotificationType'
describe('NotificationType', () => {
it('should create a value object', () => {
const valueOrError = NotificationType.create(NotificationType.TYPES.SharedVaultItemRemoved)
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().value).toEqual('shared_vault_item_removed')
})
it('should not create an invalid value object', () => {
const valueOrError = NotificationType.create('TEST')
expect(valueOrError.isFailed()).toBeTruthy()
})
})

View File

@@ -0,0 +1,27 @@
import { Result, ValueObject } from '@standardnotes/domain-core'
import { NotificationTypeProps } from './NotificationTypeProps'
export class NotificationType extends ValueObject<NotificationTypeProps> {
static readonly TYPES = {
SharedVaultItemRemoved: 'shared_vault_item_removed',
RemovedFromSharedVault: 'removed_from_shared_vault',
}
get value(): string {
return this.props.value
}
private constructor(props: NotificationTypeProps) {
super(props)
}
static create(notificationType: string): Result<NotificationType> {
const isValidPermission = Object.values(this.TYPES).includes(notificationType)
if (!isValidPermission) {
return Result.fail<NotificationType>(`Invalid shared vault user permission ${notificationType}`)
} else {
return Result.ok<NotificationType>(new NotificationType({ value: notificationType }))
}
}
}

View File

@@ -0,0 +1,3 @@
export interface NotificationTypeProps {
value: string
}

View File

@@ -0,0 +1,38 @@
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
@Entity({ name: 'notifications' })
export class TypeORMNotification {
@PrimaryGeneratedColumn('uuid')
declare uuid: string
@Column({
name: 'user_uuid',
length: 36,
})
@Index('index_notifications_on_user_uuid')
declare userUuid: string
@Column({
name: 'type',
length: 36,
})
declare type: string
@Column({
name: 'payload',
type: 'text',
})
declare payload: string
@Column({
name: 'created_at_timestamp',
type: 'bigint',
})
declare createdAtTimestamp: number
@Column({
name: 'updated_at_timestamp',
type: 'bigint',
})
declare updatedAtTimestamp: number
}

View File

@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.11.24](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.23...@standardnotes/home-server@1.11.24) (2023-07-05)
**Note:** Version bump only for package @standardnotes/home-server
## [1.11.23](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.22...@standardnotes/home-server@1.11.23) (2023-07-05)
**Note:** Version bump only for package @standardnotes/home-server
## [1.11.22](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.21...@standardnotes/home-server@1.11.22) (2023-07-03)
**Note:** Version bump only for package @standardnotes/home-server
## [1.11.21](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.20...@standardnotes/home-server@1.11.21) (2023-07-03)
**Note:** Version bump only for package @standardnotes/home-server
## [1.11.20](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.19...@standardnotes/home-server@1.11.20) (2023-07-03)
**Note:** Version bump only for package @standardnotes/home-server
## [1.11.19](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.18...@standardnotes/home-server@1.11.19) (2023-06-30)
**Note:** Version bump only for package @standardnotes/home-server
## [1.11.18](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.17...@standardnotes/home-server@1.11.18) (2023-06-30)
**Note:** Version bump only for package @standardnotes/home-server
## [1.11.17](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.16...@standardnotes/home-server@1.11.17) (2023-06-30)
**Note:** Version bump only for package @standardnotes/home-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/home-server",
"version": "1.11.17",
"version": "1.11.24",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,42 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.51.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.50.0...@standardnotes/syncing-server@1.51.0) (2023-07-05)
### Features
* add getting shared vaults for a user ([#639](https://github.com/standardnotes/syncing-server-js/issues/639)) ([d2b2c33](https://github.com/standardnotes/syncing-server-js/commit/d2b2c339f2089ea5d538ee40118af083983be5ef))
# [1.50.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.49.0...@standardnotes/syncing-server@1.50.0) (2023-07-03)
### Features
* add invite users to a shared vault. ([#636](https://github.com/standardnotes/syncing-server-js/issues/636)) ([5dc5507](https://github.com/standardnotes/syncing-server-js/commit/5dc5507039c0dfb9df82a85377846651fef73c57))
# [1.49.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.48.0...@standardnotes/syncing-server@1.49.0) (2023-07-03)
### Features
* add creating shared vault file valet tokens. ([#635](https://github.com/standardnotes/syncing-server-js/issues/635)) ([04b3bb0](https://github.com/standardnotes/syncing-server-js/commit/04b3bb034fb5bf6f9d00d5b2e8a1abc4832c5417))
# [1.48.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.47.0...@standardnotes/syncing-server@1.48.0) (2023-07-03)
### Features
* add shared vault invite model. ([#634](https://github.com/standardnotes/syncing-server-js/issues/634)) ([890cf48](https://github.com/standardnotes/syncing-server-js/commit/890cf48749b120212080563e6d3070bd43641f1a))
# [1.47.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.46.0...@standardnotes/syncing-server@1.47.0) (2023-06-30)
### Features
* add use case for creating shared vaults and adding users to it. ([#633](https://github.com/standardnotes/syncing-server-js/issues/633)) ([4df8c3b](https://github.com/standardnotes/syncing-server-js/commit/4df8c3b2e5ba4b7d510849ac71b19ed1749f098c))
# [1.46.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.45.0...@standardnotes/syncing-server@1.46.0) (2023-06-30)
### Features
* add shared vaults user model. ([#632](https://github.com/standardnotes/syncing-server-js/issues/632)) ([52f879f](https://github.com/standardnotes/syncing-server-js/commit/52f879f84216084d8affcb3522b1d99cb1135104))
# [1.45.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.44.6...@standardnotes/syncing-server@1.45.0) (2023-06-30)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.45.0",
"version": "1.51.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -0,0 +1,17 @@
import { Timestamps, Uuid } from '@standardnotes/domain-core'
import { SharedVault } from './SharedVault'
describe('SharedVault', () => {
it('should create an entity', () => {
const entityOrError = SharedVault.create({
fileUploadBytesLimit: 1_000_000,
fileUploadBytesUsed: 0,
timestamps: Timestamps.create(123456789, 123456789).getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
})
expect(entityOrError.isFailed()).toBeFalsy()
expect(entityOrError.getValue().id).not.toBeNull()
})
})

View File

@@ -0,0 +1,9 @@
import { Uuid } from '@standardnotes/domain-core'
import { SharedVault } from './SharedVault'
export interface SharedVaultRepositoryInterface {
findByUuid(uuid: Uuid): Promise<SharedVault | null>
findByUuids(uuids: Uuid[], lastSyncTime?: number): Promise<SharedVault[]>
save(sharedVault: SharedVault): Promise<void>
remove(sharedVault: SharedVault): Promise<void>
}

View File

@@ -1,7 +0,0 @@
import { SharedVault } from './SharedVault'
export interface SharedVaultsRepositoryInterface {
findByUuid(uuid: string): Promise<SharedVault | null>
save(sharedVault: SharedVault): Promise<void>
remove(sharedVault: SharedVault): Promise<void>
}

View File

@@ -0,0 +1,20 @@
import { Timestamps, Uuid } from '@standardnotes/domain-core'
import { SharedVaultInvite } from './SharedVaultInvite'
import { SharedVaultUserPermission } from '../SharedVaultUserPermission'
describe('SharedVaultInvite', () => {
it('should create an entity', () => {
const entityOrError = SharedVaultInvite.create({
permission: SharedVaultUserPermission.create('read').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
senderUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
encryptedMessage: 'encryptedMessage',
timestamps: Timestamps.create(123456789, 123456789).getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
})
expect(entityOrError.isFailed()).toBeFalsy()
expect(entityOrError.getValue().id).not.toBeNull()
})
})

View File

@@ -0,0 +1,17 @@
import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
import { SharedVaultInviteProps } from './SharedVaultInviteProps'
export class SharedVaultInvite extends Entity<SharedVaultInviteProps> {
get id(): UniqueEntityId {
return this._id
}
private constructor(props: SharedVaultInviteProps, id?: UniqueEntityId) {
super(props, id)
}
static create(props: SharedVaultInviteProps, id?: UniqueEntityId): Result<SharedVaultInvite> {
return Result.ok<SharedVaultInvite>(new SharedVaultInvite(props, id))
}
}

View File

@@ -0,0 +1,11 @@
import { Timestamps, Uuid } from '@standardnotes/domain-core'
import { SharedVaultUserPermission } from '../SharedVaultUserPermission'
export interface SharedVaultInviteProps {
sharedVaultUuid: Uuid
userUuid: Uuid
senderUuid: Uuid
encryptedMessage: string
permission: SharedVaultUserPermission
timestamps: Timestamps
}

View File

@@ -0,0 +1,10 @@
import { Uuid } from '@standardnotes/domain-core'
import { SharedVaultInvite } from './SharedVaultInvite'
export interface SharedVaultInviteRepositoryInterface {
findByUuid(sharedVaultInviteUuid: Uuid): Promise<SharedVaultInvite | null>
save(sharedVaultInvite: SharedVaultInvite): Promise<void>
remove(sharedVaultInvite: SharedVaultInvite): Promise<void>
findByUserUuidAndSharedVaultUuid(dto: { userUuid: Uuid; sharedVaultUuid: Uuid }): Promise<SharedVaultInvite | null>
}

View File

@@ -0,0 +1,18 @@
import { Timestamps, Uuid } from '@standardnotes/domain-core'
import { SharedVaultUser } from './SharedVaultUser'
import { SharedVaultUserPermission } from './SharedVaultUserPermission'
describe('SharedVaultUser', () => {
it('should create an entity', () => {
const entityOrError = SharedVaultUser.create({
permission: SharedVaultUserPermission.create('read').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123456789, 123456789).getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
})
expect(entityOrError.isFailed()).toBeFalsy()
expect(entityOrError.getValue().id).not.toBeNull()
})
})

View File

@@ -0,0 +1,17 @@
import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
import { SharedVaultUserProps } from './SharedVaultUserProps'
export class SharedVaultUser extends Entity<SharedVaultUserProps> {
get id(): UniqueEntityId {
return this._id
}
private constructor(props: SharedVaultUserProps, id?: UniqueEntityId) {
super(props, id)
}
static create(props: SharedVaultUserProps, id?: UniqueEntityId): Result<SharedVaultUser> {
return Result.ok<SharedVaultUser>(new SharedVaultUser(props, id))
}
}

View File

@@ -0,0 +1,16 @@
import { SharedVaultUserPermission } from './SharedVaultUserPermission'
describe('SharedVaultUserPermission', () => {
it('should create a value object', () => {
const valueOrError = SharedVaultUserPermission.create('read')
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().value).toEqual('read')
})
it('should not create an invalid value object', () => {
const valueOrError = SharedVaultUserPermission.create('TEST')
expect(valueOrError.isFailed()).toBeTruthy()
})
})

View File

@@ -0,0 +1,28 @@
import { Result, ValueObject } from '@standardnotes/domain-core'
import { SharedVaultUserPermissionProps } from './SharedVaultUserPermissionProps'
export class SharedVaultUserPermission extends ValueObject<SharedVaultUserPermissionProps> {
static readonly PERMISSIONS = {
Read: 'read',
Write: 'write',
Admin: 'admin',
}
get value(): string {
return this.props.value
}
private constructor(props: SharedVaultUserPermissionProps) {
super(props)
}
static create(sharedVaultUserPermission: string): Result<SharedVaultUserPermission> {
const isValidPermission = Object.values(this.PERMISSIONS).includes(sharedVaultUserPermission)
if (!isValidPermission) {
return Result.fail<SharedVaultUserPermission>(`Invalid shared vault user permission ${sharedVaultUserPermission}`)
} else {
return Result.ok<SharedVaultUserPermission>(new SharedVaultUserPermission({ value: sharedVaultUserPermission }))
}
}
}

View File

@@ -0,0 +1,3 @@
export interface SharedVaultUserPermissionProps {
value: string
}

View File

@@ -0,0 +1,10 @@
import { Timestamps, Uuid } from '@standardnotes/domain-core'
import { SharedVaultUserPermission } from './SharedVaultUserPermission'
export interface SharedVaultUserProps {
sharedVaultUuid: Uuid
userUuid: Uuid
permission: SharedVaultUserPermission
timestamps: Timestamps
}

View File

@@ -0,0 +1,11 @@
import { Uuid } from '@standardnotes/domain-core'
import { SharedVaultUser } from './SharedVaultUser'
export interface SharedVaultUserRepositoryInterface {
findByUuid(sharedVaultUserUuid: Uuid): Promise<SharedVaultUser | null>
findByUserUuid(userUuid: Uuid): Promise<SharedVaultUser[]>
save(sharedVaultUser: SharedVaultUser): Promise<void>
remove(sharedVault: SharedVaultUser): Promise<void>
findByUserUuidAndSharedVaultUuid(dto: { userUuid: Uuid; sharedVaultUuid: Uuid }): Promise<SharedVaultUser | null>
}

View File

@@ -0,0 +1,139 @@
import { TimerInterface } from '@standardnotes/time'
import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
import { SharedVaultUserRepositoryInterface } from '../../SharedVault/User/SharedVaultUserRepositoryInterface'
import { RemoveUserEvents } from '../RemoveUserEvents/RemoveUserEvents'
import { AddUserToSharedVault } from './AddUserToSharedVault'
import { Result } from '@standardnotes/domain-core'
import { SharedVault } from '../../SharedVault/SharedVault'
import { SharedVaultUser } from '../../SharedVault/User/SharedVaultUser'
describe('AddUserToSharedVault', () => {
let removeUserEvents: RemoveUserEvents
let sharedVaultRepository: SharedVaultRepositoryInterface
let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
let timer: TimerInterface
let sharedVault: SharedVault
const validUuid = '00000000-0000-0000-0000-000000000000'
const createUseCase = () =>
new AddUserToSharedVault(removeUserEvents, sharedVaultRepository, sharedVaultUserRepository, timer)
beforeEach(() => {
removeUserEvents = {} as jest.Mocked<RemoveUserEvents>
removeUserEvents.execute = jest.fn().mockResolvedValue(Result.ok())
sharedVault = {} as jest.Mocked<SharedVault>
sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
sharedVaultUserRepository.save = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
})
it('should return a failure result if the shared vault uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: 'invalid-uuid',
userUuid: validUuid,
permission: 'read',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Given value is not a valid uuid: invalid-uuid')
})
it('should return a failure result if the user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: validUuid,
userUuid: 'invalid-uuid',
permission: 'read',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Given value is not a valid uuid: invalid-uuid')
})
it('should return a failure result if the permission is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: validUuid,
userUuid: validUuid,
permission: 'test',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Invalid shared vault user permission test')
})
it('should return a failure result if the shared vault does not exist', async () => {
const useCase = createUseCase()
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValueOnce(null)
const result = await useCase.execute({
sharedVaultUuid: validUuid,
userUuid: validUuid,
permission: 'read',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Attempting to add a shared vault user to a non-existent shared vault')
})
it('should return a failure result if removing user events fails', async () => {
const useCase = createUseCase()
removeUserEvents.execute = jest.fn().mockResolvedValueOnce(Result.fail('test'))
const result = await useCase.execute({
sharedVaultUuid: validUuid,
userUuid: validUuid,
permission: 'read',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('test')
})
it('should return a failure result if creating the shared vault user fails', async () => {
const useCase = createUseCase()
const mockSharedVaultUser = jest.spyOn(SharedVaultUser, 'create')
mockSharedVaultUser.mockImplementation(() => {
return Result.fail('Oops')
})
const result = await useCase.execute({
sharedVaultUuid: validUuid,
userUuid: validUuid,
permission: 'read',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Oops')
mockSharedVaultUser.mockRestore()
})
it('should add a user to a shared vault', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: validUuid,
userUuid: validUuid,
permission: 'read',
})
expect(result.isFailed()).toBe(false)
expect(sharedVaultUserRepository.save).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,71 @@
import { Result, Timestamps, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
import { AddUserToSharedVaultDTO } from './AddUserToSharedVaultDTO'
import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
import { SharedVaultUser } from '../../SharedVault/User/SharedVaultUser'
import { SharedVaultUserRepositoryInterface } from '../../SharedVault/User/SharedVaultUserRepositoryInterface'
import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
import { RemoveUserEvents } from '../RemoveUserEvents/RemoveUserEvents'
export class AddUserToSharedVault implements UseCaseInterface<SharedVaultUser> {
constructor(
private removeUserEvents: RemoveUserEvents,
private sharedVaultRepository: SharedVaultRepositoryInterface,
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
private timer: TimerInterface,
) {}
async execute(dto: AddUserToSharedVaultDTO): Promise<Result<SharedVaultUser>> {
const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
if (sharedVaultUuidOrError.isFailed()) {
return Result.fail(sharedVaultUuidOrError.getError())
}
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
const sharedVault = await this.sharedVaultRepository.findByUuid(sharedVaultUuid)
if (!sharedVault) {
return Result.fail('Attempting to add a shared vault user to a non-existent shared vault')
}
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
const permissionOrError = SharedVaultUserPermission.create(dto.permission)
if (permissionOrError.isFailed()) {
return Result.fail(permissionOrError.getError())
}
const permission = permissionOrError.getValue()
const removingEventsResult = await this.removeUserEvents.execute({
sharedVaultUuid: sharedVaultUuid.value,
userUuid: userUuid.value,
})
if (removingEventsResult.isFailed()) {
return Result.fail(removingEventsResult.getError())
}
const timestamps = Timestamps.create(
this.timer.getTimestampInMicroseconds(),
this.timer.getTimestampInMicroseconds(),
).getValue()
const sharedVaultUserOrError = SharedVaultUser.create({
userUuid,
sharedVaultUuid,
permission,
timestamps,
})
if (sharedVaultUserOrError.isFailed()) {
return Result.fail(sharedVaultUserOrError.getError())
}
const sharedVaultUser = sharedVaultUserOrError.getValue()
await this.sharedVaultUserRepository.save(sharedVaultUser)
return Result.ok(sharedVaultUser)
}
}

View File

@@ -0,0 +1,5 @@
export interface AddUserToSharedVaultDTO {
sharedVaultUuid: string
userUuid: string
permission: string
}

View File

@@ -0,0 +1,83 @@
import { TimerInterface } from '@standardnotes/time'
import { Result } from '@standardnotes/domain-core'
import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
import { AddUserToSharedVault } from '../AddUserToSharedVault/AddUserToSharedVault'
import { CreateSharedVault } from './CreateSharedVault'
import { SharedVault } from '../../SharedVault/SharedVault'
describe('CreateSharedVault', () => {
let addUserToSharedVault: AddUserToSharedVault
let sharedVaultRepository: SharedVaultRepositoryInterface
let timer: TimerInterface
const createUseCase = () => new CreateSharedVault(addUserToSharedVault, sharedVaultRepository, timer)
beforeEach(() => {
addUserToSharedVault = {} as jest.Mocked<AddUserToSharedVault>
addUserToSharedVault.execute = jest.fn().mockResolvedValue(Result.ok())
sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
sharedVaultRepository.save = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
})
it('should return a failure result if the user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'invalid-uuid',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Given value is not a valid uuid: invalid-uuid')
})
it('should return a failure result if the shared vault could not be created', async () => {
const useCase = createUseCase()
const mockSharedVault = jest.spyOn(SharedVault, 'create')
mockSharedVault.mockImplementation(() => {
return Result.fail('Oops')
})
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Oops')
mockSharedVault.mockRestore()
})
it('should return a failure result if the user could not be added to the shared vault', async () => {
const useCase = createUseCase()
addUserToSharedVault.execute = jest.fn().mockResolvedValue(Result.fail('Oops'))
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Oops')
})
it('should create a shared vault', async () => {
const useCase = createUseCase()
await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(addUserToSharedVault.execute).toHaveBeenCalledWith({
sharedVaultUuid: expect.any(String),
userUuid: '00000000-0000-0000-0000-000000000000',
permission: 'admin',
})
expect(sharedVaultRepository.save).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,55 @@
import { Result, Timestamps, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { CreateSharedVaultResult } from './CreateSharedVaultResult'
import { CreateSharedVaultDTO } from './CreateSharedVaultDTO'
import { TimerInterface } from '@standardnotes/time'
import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
import { AddUserToSharedVault } from '../AddUserToSharedVault/AddUserToSharedVault'
import { SharedVault } from '../../SharedVault/SharedVault'
export class CreateSharedVault implements UseCaseInterface<CreateSharedVaultResult> {
private readonly FILE_UPLOAD_BYTES_LIMIT = 1_000_000
constructor(
private addUserToSharedVault: AddUserToSharedVault,
private sharedVaultRepository: SharedVaultRepositoryInterface,
private timer: TimerInterface,
) {}
async execute(dto: CreateSharedVaultDTO): Promise<Result<CreateSharedVaultResult>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
const timestamps = Timestamps.create(
this.timer.getTimestampInMicroseconds(),
this.timer.getTimestampInMicroseconds(),
).getValue()
const sharedVaultOrError = SharedVault.create({
fileUploadBytesLimit: this.FILE_UPLOAD_BYTES_LIMIT,
fileUploadBytesUsed: 0,
userUuid,
timestamps,
})
if (sharedVaultOrError.isFailed()) {
return Result.fail(sharedVaultOrError.getError())
}
const sharedVault = sharedVaultOrError.getValue()
await this.sharedVaultRepository.save(sharedVault)
const sharedVaultUserOrError = await this.addUserToSharedVault.execute({
sharedVaultUuid: sharedVault.id.toString(),
userUuid: dto.userUuid,
permission: 'admin',
})
if (sharedVaultUserOrError.isFailed()) {
return Result.fail(sharedVaultUserOrError.getError())
}
const sharedVaultUser = sharedVaultUserOrError.getValue()
return Result.ok({ sharedVault, sharedVaultUser })
}
}

View File

@@ -0,0 +1,3 @@
export interface CreateSharedVaultDTO {
userUuid: string
}

View File

@@ -0,0 +1,7 @@
import { SharedVault } from '../../SharedVault/SharedVault'
import { SharedVaultUser } from '../../SharedVault/User/SharedVaultUser'
export interface CreateSharedVaultResult {
sharedVaultUser: SharedVaultUser
sharedVault: SharedVault
}

View File

@@ -0,0 +1,363 @@
import { SharedVaultValetTokenData, TokenEncoderInterface, ValetTokenOperation } from '@standardnotes/security'
import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
import { SharedVaultUserRepositoryInterface } from '../../SharedVault/User/SharedVaultUserRepositoryInterface'
import { CreateSharedVaultFileValetToken } from './CreateSharedVaultFileValetToken'
import { SharedVault } from '../../SharedVault/SharedVault'
import { SharedVaultUser } from '../../SharedVault/User/SharedVaultUser'
import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
import { Timestamps, Uuid } from '@standardnotes/domain-core'
describe('CreateSharedVaultFileValetToken', () => {
let sharedVaultRepository: SharedVaultRepositoryInterface
let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
let tokenEncoder: TokenEncoderInterface<SharedVaultValetTokenData>
const valetTokenTTL = 3600
let sharedVault: SharedVault
let sharedVaultUser: SharedVaultUser
const createUseCase = () =>
new CreateSharedVaultFileValetToken(sharedVaultRepository, sharedVaultUserRepository, tokenEncoder, valetTokenTTL)
beforeEach(() => {
sharedVault = SharedVault.create({
fileUploadBytesLimit: 100,
fileUploadBytesUsed: 2,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
sharedVaultUser = SharedVaultUser.create({
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
tokenEncoder = {} as jest.Mocked<TokenEncoderInterface<SharedVaultValetTokenData>>
tokenEncoder.encodeExpirableToken = jest.fn().mockReturnValue('encoded-token')
})
it('should return error when shared vault uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: 'invalid-uuid',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Read,
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Given value is not a valid uuid: invalid-uuid')
})
it('should return error when user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'invalid-uuid',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Read,
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Given value is not a valid uuid: invalid-uuid')
})
it('should return error when shared vault is not found', async () => {
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(null)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Read,
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Shared vault not found')
})
it('should return error when shared vault user is not found', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(null)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Read,
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Shared vault user not found')
})
it('should return error when shared vault user does not have permission', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Write,
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('User does not have permission to perform this operation')
})
it('should create a shared vault file valet token', async () => {
sharedVaultUser = SharedVaultUser.create({
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Write).getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Write,
})
expect(result.isFailed()).toBe(false)
expect(result.getValue()).toBe('encoded-token')
})
describe('move operation', () => {
beforeEach(() => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
.fn()
.mockReturnValueOnce(
SharedVaultUser.create({
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Write).getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
)
.mockReturnValueOnce(
SharedVaultUser.create({
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
)
})
it('should return error when move operation type is not specified', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Move,
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Move operation type is required')
})
it('should return error when target uuid is missing on a shared-vault-to-shared-vault move operation', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Move,
moveOperationType: 'shared-vault-to-shared-vault',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Shared vault to shared vault move target uuid is required')
})
it('should return error when target uuid is invalid on a shared-vault-to-shared-vault move operation', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Move,
moveOperationType: 'shared-vault-to-shared-vault',
sharedVaultToSharedVaultMoveTargetUuid: 'invalid-uuid',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Given value is not a valid uuid: invalid-uuid')
})
it('should return error when target shared vault user is not found on a shared-vault-to-shared-vault move operation', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
.fn()
.mockReturnValueOnce(
SharedVaultUser.create({
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Write).getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
)
.mockReturnValueOnce(null)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Move,
moveOperationType: 'shared-vault-to-shared-vault',
sharedVaultToSharedVaultMoveTargetUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Shared vault target user not found')
})
it('should return error when target shared vault user does not have permission on a shared-vault-to-shared-vault move operation', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
.fn()
.mockReturnValueOnce(
SharedVaultUser.create({
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Write).getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
)
.mockReturnValueOnce(
SharedVaultUser.create({
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Move,
moveOperationType: 'shared-vault-to-shared-vault',
sharedVaultToSharedVaultMoveTargetUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('User does not have permission to perform this operation')
})
it('should create move valet token for shared-vault-to-shared-vault operation', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
.fn()
.mockReturnValueOnce(
SharedVaultUser.create({
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Write).getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
)
.mockReturnValueOnce(
SharedVaultUser.create({
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Write).getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Move,
moveOperationType: 'shared-vault-to-shared-vault',
sharedVaultToSharedVaultMoveTargetUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBe(false)
expect(result.getValue()).toBe('encoded-token')
})
it('should create move valet token for shared-vault-to-user operation', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
.fn()
.mockReturnValueOnce(
SharedVaultUser.create({
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Write).getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
)
.mockReturnValueOnce(
SharedVaultUser.create({
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Write).getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Move,
moveOperationType: 'shared-vault-to-user',
sharedVaultToSharedVaultMoveTargetUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBe(false)
expect(result.getValue()).toBe('encoded-token')
})
it('should create move valet token for user-to-shared-vault operation', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
.fn()
.mockReturnValueOnce(
SharedVaultUser.create({
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Write).getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
)
.mockReturnValueOnce(
SharedVaultUser.create({
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Write).getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Move,
moveOperationType: 'user-to-shared-vault',
sharedVaultToSharedVaultMoveTargetUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBe(false)
expect(result.getValue()).toBe('encoded-token')
})
})
})

View File

@@ -0,0 +1,124 @@
import { SharedVaultValetTokenData, TokenEncoderInterface, ValetTokenOperation } from '@standardnotes/security'
import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { SharedVaultUserRepositoryInterface } from '../../SharedVault/User/SharedVaultUserRepositoryInterface'
import { CreateSharedVaultFileValetTokenDTO } from './CreateSharedVaultFileValetTokenDTO'
import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
export class CreateSharedVaultFileValetToken implements UseCaseInterface<string> {
constructor(
private sharedVaultRepository: SharedVaultRepositoryInterface,
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
private tokenEncoder: TokenEncoderInterface<SharedVaultValetTokenData>,
private valetTokenTTL: number,
) {}
async execute(dto: CreateSharedVaultFileValetTokenDTO): Promise<Result<string>> {
const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
if (sharedVaultUuidOrError.isFailed()) {
return Result.fail(sharedVaultUuidOrError.getError())
}
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
const sharedVault = await this.sharedVaultRepository.findByUuid(sharedVaultUuid)
if (!sharedVault) {
return Result.fail('Shared vault not found')
}
const sharedVaultUser = await this.sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid({
userUuid: userUuid,
sharedVaultUuid: sharedVaultUuid,
})
if (!sharedVaultUser) {
return Result.fail('Shared vault user not found')
}
if (
sharedVaultUser.props.permission.value === SharedVaultUserPermission.PERMISSIONS.Read &&
dto.operation !== ValetTokenOperation.Read
) {
return Result.fail('User does not have permission to perform this operation')
}
if (dto.operation === ValetTokenOperation.Move) {
if (!dto.moveOperationType) {
return Result.fail('Move operation type is required')
}
if (dto.moveOperationType === 'shared-vault-to-shared-vault') {
if (!dto.sharedVaultToSharedVaultMoveTargetUuid) {
return Result.fail('Shared vault to shared vault move target uuid is required')
}
const sharedVaultTargetUuidOrError = Uuid.create(dto.sharedVaultToSharedVaultMoveTargetUuid)
if (sharedVaultTargetUuidOrError.isFailed()) {
return Result.fail(sharedVaultTargetUuidOrError.getError())
}
const sharedVaultTargetUuid = sharedVaultTargetUuidOrError.getValue()
const toSharedVaultUser = await this.sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid({
userUuid: userUuid,
sharedVaultUuid: sharedVaultTargetUuid,
})
if (!toSharedVaultUser) {
return Result.fail('Shared vault target user not found')
}
if (toSharedVaultUser.props.permission.value === SharedVaultUserPermission.PERMISSIONS.Read) {
return Result.fail('User does not have permission to perform this operation')
}
}
}
const tokenData: SharedVaultValetTokenData = {
sharedVaultUuid: dto.sharedVaultUuid,
permittedOperation: dto.operation,
remoteIdentifier: dto.remoteIdentifier,
uploadBytesUsed: sharedVault.props.fileUploadBytesUsed,
uploadBytesLimit: sharedVault.props.fileUploadBytesLimit,
unencryptedFileSize: dto.unencryptedFileSize,
moveOperation: this.createMoveOperationData(dto),
}
const valetToken = this.tokenEncoder.encodeExpirableToken(tokenData, this.valetTokenTTL)
return Result.ok(valetToken)
}
private createMoveOperationData(dto: CreateSharedVaultFileValetTokenDTO): SharedVaultValetTokenData['moveOperation'] {
if (!dto.moveOperationType) {
return undefined
}
let fromUuid: string
let toUuid: string
switch (dto.moveOperationType) {
case 'shared-vault-to-user':
fromUuid = dto.sharedVaultUuid
toUuid = dto.userUuid
break
case 'user-to-shared-vault':
fromUuid = dto.userUuid
toUuid = dto.sharedVaultUuid
break
case 'shared-vault-to-shared-vault':
fromUuid = dto.sharedVaultUuid
toUuid = dto.sharedVaultToSharedVaultMoveTargetUuid as string
break
}
return {
type: dto.moveOperationType,
fromUuid,
toUuid,
}
}
}

View File

@@ -0,0 +1,12 @@
import { SharedVaultMoveType, ValetTokenOperation } from '@standardnotes/security'
export interface CreateSharedVaultFileValetTokenDTO {
userUuid: string
sharedVaultUuid: string
fileUuid?: string
remoteIdentifier: string
operation: ValetTokenOperation
unencryptedFileSize?: number
moveOperationType?: SharedVaultMoveType
sharedVaultToSharedVaultMoveTargetUuid?: string
}

View File

@@ -0,0 +1,68 @@
import { Timestamps, Uuid } from '@standardnotes/domain-core'
import { SharedVault } from '../../SharedVault/SharedVault'
import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
import { SharedVaultUser } from '../../SharedVault/User/SharedVaultUser'
import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
import { SharedVaultUserRepositoryInterface } from '../../SharedVault/User/SharedVaultUserRepositoryInterface'
import { GetSharedVaults } from './GetSharedVaults'
describe('GetSharedVaults', () => {
let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
let sharedVaultRepository: SharedVaultRepositoryInterface
let sharedVault: SharedVault
let sharedVaultUser: SharedVaultUser
const createUseCase = () => new GetSharedVaults(sharedVaultUserRepository, sharedVaultRepository)
beforeEach(() => {
sharedVaultUser = SharedVaultUser.create({
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Admin).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
sharedVaultUserRepository.findByUserUuid = jest.fn().mockResolvedValue([sharedVaultUser])
sharedVault = SharedVault.create({
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
fileUploadBytesLimit: 123,
fileUploadBytesUsed: 123,
}).getValue()
sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
sharedVaultRepository.findByUuids = jest.fn().mockResolvedValue([sharedVault])
})
it('returns shared vaults', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.getValue()).toEqual([sharedVault])
})
it('returns empty array if no shared vaults found', async () => {
sharedVaultUserRepository.findByUserUuid = jest.fn().mockResolvedValue([])
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.getValue()).toEqual([])
})
it('returns error if user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'invalid',
})
expect(result.isFailed()).toBeTruthy()
})
})

View File

@@ -0,0 +1,35 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { SharedVault } from '../../SharedVault/SharedVault'
import { GetSharedVaultsDTO } from './GetSharedVaultsDTO'
import { SharedVaultUserRepositoryInterface } from '../../SharedVault/User/SharedVaultUserRepositoryInterface'
import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
export class GetSharedVaults implements UseCaseInterface<SharedVault[]> {
constructor(
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
private sharedVaultRepository: SharedVaultRepositoryInterface,
) {}
async execute(dto: GetSharedVaultsDTO): Promise<Result<SharedVault[]>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
const ownedSharedVaultsAssociations = await this.sharedVaultUserRepository.findByUserUuid(userUuid)
const sharedVaultUuids = ownedSharedVaultsAssociations.map(
(sharedVaultUser) => sharedVaultUser.props.sharedVaultUuid,
)
if (sharedVaultUuids.length === 0) {
return Result.ok([])
}
const sharedVaults = await this.sharedVaultRepository.findByUuids(sharedVaultUuids, dto.lastSyncTime)
return Result.ok(sharedVaults)
}
}

View File

@@ -0,0 +1,4 @@
export interface GetSharedVaultsDTO {
userUuid: string
lastSyncTime?: number
}

View File

@@ -0,0 +1,191 @@
import { TimerInterface } from '@standardnotes/time'
import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
import { SharedVaultInviteRepositoryInterface } from '../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
import { InviteUserToSharedVault } from './InviteUserToSharedVault'
import { SharedVault } from '../../SharedVault/SharedVault'
import { SharedVaultInvite } from '../../SharedVault/User/Invite/SharedVaultInvite'
import { Uuid, Timestamps, Result } from '@standardnotes/domain-core'
import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
describe('InviteUserToSharedVault', () => {
let sharedVaultRepository: SharedVaultRepositoryInterface
let sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface
let timer: TimerInterface
let sharedVault: SharedVault
const createUseCase = () => new InviteUserToSharedVault(sharedVaultRepository, sharedVaultInviteRepository, timer)
beforeEach(() => {
sharedVault = SharedVault.create({
fileUploadBytesLimit: 100,
fileUploadBytesUsed: 2,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
sharedVaultInviteRepository = {} as jest.Mocked<SharedVaultInviteRepositoryInterface>
sharedVaultInviteRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(null)
sharedVaultInviteRepository.save = jest.fn()
sharedVaultInviteRepository.remove = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
})
it('should return a failure result if the shared vault uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: 'invalid',
senderUuid: '00000000-0000-0000-0000-000000000000',
recipientUuid: '00000000-0000-0000-0000-000000000000',
permission: SharedVaultUserPermission.PERMISSIONS.Read,
encryptedMessage: 'encryptedMessage',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Given value is not a valid uuid: invalid')
})
it('should return a failure result if the shared vault does not exist', async () => {
const useCase = createUseCase()
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(undefined)
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
senderUuid: '00000000-0000-0000-0000-000000000000',
recipientUuid: '00000000-0000-0000-0000-000000000000',
permission: SharedVaultUserPermission.PERMISSIONS.Read,
encryptedMessage: 'encryptedMessage',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Attempting to invite a user to a non-existent shared vault')
})
it('should return a failure result if the sender uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
senderUuid: 'invalid',
recipientUuid: '00000000-0000-0000-0000-000000000000',
permission: SharedVaultUserPermission.PERMISSIONS.Read,
encryptedMessage: 'encryptedMessage',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Given value is not a valid uuid: invalid')
})
it('should return a failure result if the recipient uuid is invalid', async () => {
const useCase = createUseCase()
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
senderUuid: '00000000-0000-0000-0000-000000000000',
recipientUuid: 'invalid',
permission: SharedVaultUserPermission.PERMISSIONS.Read,
encryptedMessage: 'encryptedMessage',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Given value is not a valid uuid: invalid')
})
it('should remove an already existing invite', async () => {
const useCase = createUseCase()
sharedVaultInviteRepository.findByUserUuidAndSharedVaultUuid = jest
.fn()
.mockResolvedValue({} as jest.Mocked<SharedVaultInvite>)
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
senderUuid: '00000000-0000-0000-0000-000000000000',
recipientUuid: '00000000-0000-0000-0000-000000000000',
permission: SharedVaultUserPermission.PERMISSIONS.Read,
encryptedMessage: 'encryptedMessage',
})
expect(result.isFailed()).toBe(false)
expect(sharedVaultInviteRepository.remove).toHaveBeenCalled()
})
it('should create a shared vault invite', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
senderUuid: '00000000-0000-0000-0000-000000000000',
recipientUuid: '00000000-0000-0000-0000-000000000000',
permission: SharedVaultUserPermission.PERMISSIONS.Read,
encryptedMessage: 'encryptedMessage',
})
expect(result.isFailed()).toBe(false)
expect(result.getValue().props.sharedVaultUuid.value).toBe('00000000-0000-0000-0000-000000000000')
})
it('should return a failure if the permission is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
senderUuid: '00000000-0000-0000-0000-000000000000',
recipientUuid: '00000000-0000-0000-0000-000000000000',
permission: 'invalid',
encryptedMessage: 'encryptedMessage',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Invalid shared vault user permission invalid')
})
it('should return a failure if the sender is not the owner of the shared vault', async () => {
const useCase = createUseCase()
sharedVault = SharedVault.create({
fileUploadBytesLimit: 100,
fileUploadBytesUsed: 2,
userUuid: Uuid.create('10000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
senderUuid: '00000000-0000-0000-0000-000000000001',
recipientUuid: '00000000-0000-0000-0000-000000000000',
permission: SharedVaultUserPermission.PERMISSIONS.Read,
encryptedMessage: 'encryptedMessage',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Only the owner of a shared vault can invite users to it')
})
it('should return a failure if the shared vault invite could not be created', async () => {
const useCase = createUseCase()
const mockSharedVaultInvite = jest.spyOn(SharedVaultInvite, 'create')
mockSharedVaultInvite.mockImplementation(() => {
return Result.fail('Oops')
})
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
senderUuid: '00000000-0000-0000-0000-000000000000',
recipientUuid: '00000000-0000-0000-0000-000000000000',
permission: SharedVaultUserPermission.PERMISSIONS.Read,
encryptedMessage: 'encryptedMessage',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Oops')
mockSharedVaultInvite.mockRestore()
})
})

View File

@@ -0,0 +1,77 @@
import { Result, Timestamps, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { SharedVaultInvite } from '../../SharedVault/User/Invite/SharedVaultInvite'
import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
import { InviteUserToSharedVaultDTO } from './InviteUserToSharedVaultDTO'
import { SharedVaultInviteRepositoryInterface } from '../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
import { TimerInterface } from '@standardnotes/time'
import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
export class InviteUserToSharedVault implements UseCaseInterface<SharedVaultInvite> {
constructor(
private sharedVaultRepository: SharedVaultRepositoryInterface,
private sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface,
private timer: TimerInterface,
) {}
async execute(dto: InviteUserToSharedVaultDTO): Promise<Result<SharedVaultInvite>> {
const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
if (sharedVaultUuidOrError.isFailed()) {
return Result.fail(sharedVaultUuidOrError.getError())
}
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
const senderUuidOrError = Uuid.create(dto.senderUuid)
if (senderUuidOrError.isFailed()) {
return Result.fail(senderUuidOrError.getError())
}
const senderUuid = senderUuidOrError.getValue()
const recipientUuidOrError = Uuid.create(dto.recipientUuid)
if (recipientUuidOrError.isFailed()) {
return Result.fail(recipientUuidOrError.getError())
}
const recipientUuid = recipientUuidOrError.getValue()
const permissionOrError = SharedVaultUserPermission.create(dto.permission)
if (permissionOrError.isFailed()) {
return Result.fail(permissionOrError.getError())
}
const permission = permissionOrError.getValue()
const sharedVault = await this.sharedVaultRepository.findByUuid(sharedVaultUuid)
if (!sharedVault) {
return Result.fail('Attempting to invite a user to a non-existent shared vault')
}
if (sharedVault.props.userUuid.value !== senderUuid.value) {
return Result.fail('Only the owner of a shared vault can invite users to it')
}
const existingInvite = await this.sharedVaultInviteRepository.findByUserUuidAndSharedVaultUuid({
userUuid: recipientUuid,
sharedVaultUuid,
})
if (existingInvite) {
await this.sharedVaultInviteRepository.remove(existingInvite)
}
const sharedVaultInviteOrError = SharedVaultInvite.create({
encryptedMessage: dto.encryptedMessage,
userUuid: recipientUuid,
sharedVaultUuid,
senderUuid,
permission,
timestamps: Timestamps.create(
this.timer.getTimestampInMicroseconds(),
this.timer.getTimestampInMicroseconds(),
).getValue(),
})
if (sharedVaultInviteOrError.isFailed()) {
return Result.fail(sharedVaultInviteOrError.getError())
}
const sharedVaultInvite = sharedVaultInviteOrError.getValue()
await this.sharedVaultInviteRepository.save(sharedVaultInvite)
return Result.ok(sharedVaultInvite)
}
}

View File

@@ -0,0 +1,7 @@
export interface InviteUserToSharedVaultDTO {
sharedVaultUuid: string
senderUuid: string
recipientUuid: string
encryptedMessage: string
permission: string
}

View File

@@ -0,0 +1,9 @@
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { RemoveUserEventsDTO } from './RemoveUserEventsDTO'
export class RemoveUserEvents implements UseCaseInterface<void> {
async execute(_dto: RemoveUserEventsDTO): Promise<Result<void>> {
throw new Error('Method not implemented.')
}
}

View File

@@ -0,0 +1,4 @@
export interface RemoveUserEventsDTO {
sharedVaultUuid: string
userUuid: string
}

View File

@@ -0,0 +1,50 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
@Entity({ name: 'shared_vault_invites' })
export class TypeORMSharedVaultInvite {
@PrimaryGeneratedColumn('uuid')
declare uuid: string
@Column({
name: 'shared_vault_uuid',
length: 36,
})
declare sharedVaultUuid: string
@Column({
name: 'user_uuid',
length: 36,
})
declare userUuid: string
@Column({
name: 'sender_uuid',
length: 36,
})
declare senderUuid: string
@Column({
name: 'encrypted_message',
type: 'text',
})
declare encryptedMessage: string
@Column({
name: 'permission',
type: 'varchar',
length: 24,
})
declare permission: string
@Column({
name: 'created_at_timestamp',
type: 'bigint',
})
declare createdAtTimestamp: number
@Column({
name: 'updated_at_timestamp',
type: 'bigint',
})
declare updatedAtTimestamp: number
}

View File

@@ -0,0 +1,59 @@
import { Repository } from 'typeorm'
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
import { TypeORMSharedVaultInvite } from './TypeORMSharedVaultInvite'
import { SharedVaultInvite } from '../../Domain/SharedVault/User/Invite/SharedVaultInvite'
import { SharedVaultInviteRepositoryInterface } from '../../Domain/SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
export class TypeORMSharedVaultInviteRepository implements SharedVaultInviteRepositoryInterface {
constructor(
private ormRepository: Repository<TypeORMSharedVaultInvite>,
private mapper: MapperInterface<SharedVaultInvite, TypeORMSharedVaultInvite>,
) {}
async findByUserUuidAndSharedVaultUuid(dto: {
userUuid: Uuid
sharedVaultUuid: Uuid
}): Promise<SharedVaultInvite | null> {
const persistence = await this.ormRepository
.createQueryBuilder('shared_vault_invite')
.where('shared_vault_invite.user_uuid = :uuid', {
uuid: dto.userUuid.value,
})
.andWhere('shared_vault_invite.shared_vault_uuid = :sharedVaultUuid', {
sharedVaultUuid: dto.sharedVaultUuid.value,
})
.getOne()
if (persistence === null) {
return null
}
return this.mapper.toDomain(persistence)
}
async save(sharedVaultInvite: SharedVaultInvite): Promise<void> {
const persistence = this.mapper.toProjection(sharedVaultInvite)
await this.ormRepository.save(persistence)
}
async findByUuid(uuid: Uuid): Promise<SharedVaultInvite | null> {
const persistence = await this.ormRepository
.createQueryBuilder('shared_vault_invite')
.where('shared_vault_invite.uuid = :uuid', {
uuid: uuid.value,
})
.getOne()
if (persistence === null) {
return null
}
return this.mapper.toDomain(persistence)
}
async remove(sharedVaultInvite: SharedVaultInvite): Promise<void> {
await this.ormRepository.remove(this.mapper.toProjection(sharedVaultInvite))
}
}

View File

@@ -1,27 +1,41 @@
import { Repository } from 'typeorm'
import { MapperInterface } from '@standardnotes/domain-core'
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
import { SharedVaultsRepositoryInterface } from '../../Domain/SharedVault/SharedVaultsRepositoryInterface'
import { SharedVaultRepositoryInterface } from '../../Domain/SharedVault/SharedVaultRepositoryInterface'
import { TypeORMSharedVault } from './TypeORMSharedVault'
import { SharedVault } from '../../Domain/SharedVault/SharedVault'
export class TypeORMSharedVaultRepository implements SharedVaultsRepositoryInterface {
export class TypeORMSharedVaultRepository implements SharedVaultRepositoryInterface {
constructor(
private ormRepository: Repository<TypeORMSharedVault>,
private mapper: MapperInterface<SharedVault, TypeORMSharedVault>,
) {}
async findByUuids(uuids: Uuid[], lastSyncTime?: number | undefined): Promise<SharedVault[]> {
const queryBuilder = await this.ormRepository
.createQueryBuilder('shared_vault')
.where('shared_vault.uuid IN (:...sharedVaultUuids)', { sharedVaultUuids: uuids.map((uuid) => uuid.value) })
if (lastSyncTime !== undefined) {
queryBuilder.andWhere('shared_vault.updated_at_timestamp > :lastSyncTime', { lastSyncTime })
}
const persistence = await queryBuilder.getMany()
return persistence.map((p) => this.mapper.toDomain(p))
}
async save(sharedVault: SharedVault): Promise<void> {
const persistence = this.mapper.toProjection(sharedVault)
await this.ormRepository.save(persistence)
}
async findByUuid(uuid: string): Promise<SharedVault | null> {
async findByUuid(uuid: Uuid): Promise<SharedVault | null> {
const persistence = await this.ormRepository
.createQueryBuilder('shared_vault')
.where('shared_vault.uuid = :uuid', {
uuid,
uuid: uuid.value,
})
.getOne()

View File

@@ -0,0 +1,38 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
@Entity({ name: 'shared_vault_users' })
export class TypeORMSharedVaultUser {
@PrimaryGeneratedColumn('uuid')
declare uuid: string
@Column({
name: 'shared_vault_uuid',
length: 36,
})
declare sharedVaultUuid: string
@Column({
name: 'user_uuid',
length: 36,
})
declare userUuid: string
@Column({
name: 'permission',
type: 'varchar',
length: 24,
})
declare permission: string
@Column({
name: 'created_at_timestamp',
type: 'bigint',
})
declare createdAtTimestamp: number
@Column({
name: 'updated_at_timestamp',
type: 'bigint',
})
declare updatedAtTimestamp: number
}

View File

@@ -0,0 +1,70 @@
import { Repository } from 'typeorm'
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
import { TypeORMSharedVaultUser } from './TypeORMSharedVaultUser'
import { SharedVaultUser } from '../../Domain/SharedVault/User/SharedVaultUser'
import { SharedVaultUserRepositoryInterface } from '../../Domain/SharedVault/User/SharedVaultUserRepositoryInterface'
export class TypeORMSharedVaultUserRepository implements SharedVaultUserRepositoryInterface {
constructor(
private ormRepository: Repository<TypeORMSharedVaultUser>,
private mapper: MapperInterface<SharedVaultUser, TypeORMSharedVaultUser>,
) {}
async findByUserUuid(userUuid: Uuid): Promise<SharedVaultUser[]> {
const persistence = await this.ormRepository
.createQueryBuilder('shared_vault_user')
.where('shared_vault_user.user_uuid = :userUuid', {
userUuid: userUuid.value,
})
.getMany()
return persistence.map((p) => this.mapper.toDomain(p))
}
async findByUserUuidAndSharedVaultUuid(dto: {
userUuid: Uuid
sharedVaultUuid: Uuid
}): Promise<SharedVaultUser | null> {
const persistence = await this.ormRepository
.createQueryBuilder('shared_vault_user')
.where('shared_vault_user.user_uuid = :userUuid', {
userUuid: dto.userUuid.value,
})
.andWhere('shared_vault_user.shared_vault_uuid = :sharedVaultUuid', {
sharedVaultUuid: dto.sharedVaultUuid.value,
})
.getOne()
if (persistence === null) {
return null
}
return this.mapper.toDomain(persistence)
}
async save(sharedVaultUser: SharedVaultUser): Promise<void> {
const persistence = this.mapper.toProjection(sharedVaultUser)
await this.ormRepository.save(persistence)
}
async findByUuid(uuid: Uuid): Promise<SharedVaultUser | null> {
const persistence = await this.ormRepository
.createQueryBuilder('shared_vault_user')
.where('shared_vault_user.uuid = :uuid', {
uuid: uuid.value,
})
.getOne()
if (persistence === null) {
return null
}
return this.mapper.toDomain(persistence)
}
async remove(sharedVaultUser: SharedVaultUser): Promise<void> {
await this.ormRepository.remove(this.mapper.toProjection(sharedVaultUser))
}
}

View File

@@ -0,0 +1,81 @@
import { Timestamps, MapperInterface, UniqueEntityId, Uuid, Validator } from '@standardnotes/domain-core'
import { SharedVaultInvite } from '../Domain/SharedVault/User/Invite/SharedVaultInvite'
import { TypeORMSharedVaultInvite } from '../Infra/TypeORM/TypeORMSharedVaultInvite'
import { SharedVaultUserPermission } from '../Domain/SharedVault/User/SharedVaultUserPermission'
export class SharedVaultInvitePersistenceMapper
implements MapperInterface<SharedVaultInvite, TypeORMSharedVaultInvite>
{
toDomain(projection: TypeORMSharedVaultInvite): SharedVaultInvite {
const userUuidOrError = Uuid.create(projection.userUuid)
if (userUuidOrError.isFailed()) {
throw new Error(`Failed to create shared vault invite from projection: ${userUuidOrError.getError()}`)
}
const userUuid = userUuidOrError.getValue()
const senderUuidOrError = Uuid.create(projection.senderUuid)
if (senderUuidOrError.isFailed()) {
throw new Error(`Failed to create shared vault invite from projection: ${senderUuidOrError.getError()}`)
}
const senderUuid = senderUuidOrError.getValue()
const sharedVaultUuidOrError = Uuid.create(projection.sharedVaultUuid)
if (sharedVaultUuidOrError.isFailed()) {
throw new Error(`Failed to create shared vault invite from projection: ${sharedVaultUuidOrError.getError()}`)
}
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
const timestampsOrError = Timestamps.create(projection.createdAtTimestamp, projection.updatedAtTimestamp)
if (timestampsOrError.isFailed()) {
throw new Error(`Failed to create shared vault invite from projection: ${timestampsOrError.getError()}`)
}
const timestamps = timestampsOrError.getValue()
const permissionOrError = SharedVaultUserPermission.create(projection.permission)
if (permissionOrError.isFailed()) {
throw new Error(`Failed to create shared vault invite from projection: ${permissionOrError.getError()}`)
}
const permission = permissionOrError.getValue()
const notEmptyMessageValidationResult = Validator.isNotEmpty(projection.encryptedMessage)
if (notEmptyMessageValidationResult.isFailed()) {
throw new Error(
`Failed to create shared vault invite from projection: ${notEmptyMessageValidationResult.getError()}`,
)
}
const sharedVaultInviteOrError = SharedVaultInvite.create(
{
userUuid,
sharedVaultUuid,
senderUuid,
permission,
timestamps,
encryptedMessage: projection.encryptedMessage,
},
new UniqueEntityId(projection.uuid),
)
if (sharedVaultInviteOrError.isFailed()) {
throw new Error(`Failed to create shared vault invite from projection: ${sharedVaultInviteOrError.getError()}`)
}
const sharedVaultInvite = sharedVaultInviteOrError.getValue()
return sharedVaultInvite
}
toProjection(domain: SharedVaultInvite): TypeORMSharedVaultInvite {
const typeorm = new TypeORMSharedVaultInvite()
typeorm.uuid = domain.id.toString()
typeorm.sharedVaultUuid = domain.props.sharedVaultUuid.value
typeorm.userUuid = domain.props.userUuid.value
typeorm.permission = domain.props.permission.value
typeorm.senderUuid = domain.props.senderUuid.value
typeorm.encryptedMessage = domain.props.encryptedMessage
typeorm.createdAtTimestamp = domain.props.timestamps.createdAt
typeorm.updatedAtTimestamp = domain.props.timestamps.updatedAt
return typeorm
}
}

View File

@@ -0,0 +1,62 @@
import { Timestamps, MapperInterface, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
import { SharedVaultUser } from '../Domain/SharedVault/User/SharedVaultUser'
import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser'
import { SharedVaultUserPermission } from '../Domain/SharedVault/User/SharedVaultUserPermission'
export class SharedVaultUserPersistenceMapper implements MapperInterface<SharedVaultUser, TypeORMSharedVaultUser> {
toDomain(projection: TypeORMSharedVaultUser): SharedVaultUser {
const userUuidOrError = Uuid.create(projection.userUuid)
if (userUuidOrError.isFailed()) {
throw new Error(`Failed to create shared vault user from projection: ${userUuidOrError.getError()}`)
}
const userUuid = userUuidOrError.getValue()
const sharedVaultUuidOrError = Uuid.create(projection.sharedVaultUuid)
if (sharedVaultUuidOrError.isFailed()) {
throw new Error(`Failed to create shared vault user from projection: ${sharedVaultUuidOrError.getError()}`)
}
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
const timestampsOrError = Timestamps.create(projection.createdAtTimestamp, projection.updatedAtTimestamp)
if (timestampsOrError.isFailed()) {
throw new Error(`Failed to create shared vault user from projection: ${timestampsOrError.getError()}`)
}
const timestamps = timestampsOrError.getValue()
const permissionOrError = SharedVaultUserPermission.create(projection.permission)
if (permissionOrError.isFailed()) {
throw new Error(`Failed to create shared vault user from projection: ${permissionOrError.getError()}`)
}
const permission = permissionOrError.getValue()
const sharedVaultUserOrError = SharedVaultUser.create(
{
userUuid,
sharedVaultUuid,
permission,
timestamps,
},
new UniqueEntityId(projection.uuid),
)
if (sharedVaultUserOrError.isFailed()) {
throw new Error(`Failed to create shared vault user from projection: ${sharedVaultUserOrError.getError()}`)
}
const sharedVaultUser = sharedVaultUserOrError.getValue()
return sharedVaultUser
}
toProjection(domain: SharedVaultUser): TypeORMSharedVaultUser {
const typeorm = new TypeORMSharedVaultUser()
typeorm.uuid = domain.id.toString()
typeorm.sharedVaultUuid = domain.props.sharedVaultUuid.value
typeorm.userUuid = domain.props.userUuid.value
typeorm.permission = domain.props.permission.value
typeorm.createdAtTimestamp = domain.props.timestamps.createdAt
typeorm.updatedAtTimestamp = domain.props.timestamps.updatedAt
return typeorm
}
}