mirror of
https://github.com/standardnotes/app
synced 2026-09-14 21:46:03 -04:00
Compare commits
93
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
343c39e873 | ||
|
|
889f245ab9 | ||
|
|
e8ada146e1 | ||
|
|
c9c5e915ce | ||
|
|
a1ec13d0b0 | ||
|
|
31bb03943d | ||
|
|
2b84c242f7 | ||
|
|
b8bf8ca112 | ||
|
|
c914b88e2d | ||
|
|
0169224bf2 | ||
|
|
4049b19170 | ||
|
|
df4148f824 | ||
|
|
91f3295cff | ||
|
|
c9078f374b | ||
|
|
bb4f1ff099 | ||
|
|
abdaec89b7 | ||
|
|
f559442a67 | ||
|
|
51aed0a518 | ||
|
|
35bb671e31 | ||
|
|
d76b190aba | ||
|
|
d6fd85e451 | ||
|
|
c90087462f | ||
|
|
58838a44cc | ||
|
|
0223800fdf | ||
|
|
318b1fdb0f | ||
|
|
fb517a91b0 | ||
|
|
c7b158eb64 | ||
|
|
d4b63e4ea6 | ||
|
|
cd8596b14e | ||
|
|
6c11766051 | ||
|
|
7f41564328 | ||
|
|
4e576d7526 | ||
|
|
317c6555d2 | ||
|
|
220f62665e | ||
|
|
1dc9086257 | ||
|
|
d149b500ee | ||
|
|
ad70bea368 | ||
|
|
3bcd969f83 | ||
|
|
d4af612fea | ||
|
|
b068b814f2 | ||
|
|
51de012236 | ||
|
|
2694c2f1a9 | ||
|
|
d3609853c6 | ||
|
|
a7ac715499 | ||
|
|
5ac93a6bd8 | ||
|
|
be4cc4e605 | ||
|
|
2fa3fec9ab | ||
|
|
059ee9ea36 | ||
|
|
66d08895fe | ||
|
|
518643dc71 | ||
|
|
5a0c005e8c | ||
|
|
45cdab6f6c | ||
|
|
46245b5569 | ||
|
|
8b103307cb | ||
|
|
a70d1009a3 | ||
|
|
a5b1ff9595 | ||
|
|
d691d7a3b3 | ||
|
|
71675dc6a2 | ||
|
|
6a1b6cef1b | ||
|
|
3d8c12a789 | ||
|
|
370aaecfb8 | ||
|
|
68bfb2ff68 | ||
|
|
a66bb08c3b | ||
|
|
ccde207de8 | ||
|
|
d3262b0f01 | ||
|
|
ffbc984c58 | ||
|
|
979163bae7 | ||
|
|
c928f8a281 | ||
|
|
0e10b347b6 | ||
|
|
caf2c4a876 | ||
|
|
1d22365086 | ||
|
|
bd5ccbfab6 | ||
|
|
46b19d8caf | ||
|
|
a6a0d615c5 | ||
|
|
f3203e2e80 | ||
|
|
e0edabb4b9 | ||
|
|
e68c708628 | ||
|
|
e4f3f26937 | ||
|
|
8575b3e714 | ||
|
|
7c261af230 | ||
|
|
71e20f2a2f | ||
|
|
3855b9f767 | ||
|
|
5c29e22ea4 | ||
|
|
07cb00a815 | ||
|
|
4f2ee4b56f | ||
|
|
90e10c76f7 | ||
|
|
2175c9cdcb | ||
|
|
8c41f0ecc3 | ||
|
|
c8c166d2a0 | ||
|
|
6cacdd8c58 | ||
|
|
b5b6192b60 | ||
|
|
635e2d9150 | ||
|
|
0c23e4d36c |
@@ -64,7 +64,7 @@ jobs:
|
||||
|
||||
- name: Run E2E test suite
|
||||
if: "${{ contains(github.event.head_commit.message, 'skip e2e') == false }}"
|
||||
uses: convictional/trigger-workflow-and-wait@v1.6.1
|
||||
uses: convictional/trigger-workflow-and-wait@master
|
||||
with:
|
||||
owner: standardnotes
|
||||
repo: e2e
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
docker tag standardnotes/snjs:${{ github.sha }} standardnotes/snjs:test
|
||||
docker push standardnotes/snjs:test
|
||||
- name: Run E2E test suite
|
||||
uses: convictional/trigger-workflow-and-wait@v1.6.1
|
||||
uses: convictional/trigger-workflow-and-wait@master
|
||||
with:
|
||||
owner: standardnotes
|
||||
repo: e2e
|
||||
|
||||
@@ -45,10 +45,18 @@ jobs:
|
||||
aws-region: us-east-1
|
||||
- name: Deploy static site to S3 bucket
|
||||
run: aws s3 sync packages/web/dist/ s3://app.standardnotes.com --delete
|
||||
- name: Invalidate CloudFront Cache
|
||||
- name: Invalidate CloudFront Cache on .COM
|
||||
uses: chetan/invalidate-cloudfront-action@master
|
||||
env:
|
||||
DISTRIBUTION: ${{ secrets.WEBAPP_CLOUDFRONT_DISTRIBUTION_ID }}
|
||||
DISTRIBUTION: ${{ secrets.WEBAPP_CLOUDFRONT_COM_DISTRIBUTION_ID }}
|
||||
PATHS: '/*'
|
||||
AWS_REGION: 'us-east-1'
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
- name: Invalidate CloudFront Cache on .ORG
|
||||
uses: chetan/invalidate-cloudfront-action@master
|
||||
env:
|
||||
DISTRIBUTION: ${{ secrets.WEBAPP_CLOUDFRONT_ORG_DISTRIBUTION_ID }}
|
||||
PATHS: '/*'
|
||||
AWS_REGION: 'us-east-1'
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
|
||||
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.20.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.20.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **snjs:** refreshing sessions ([#2106](https://github.com/standardnotes/app/issues/2106)) ([bb4f1ff](https://github.com/standardnotes/app/commit/bb4f1ff0990eb5a6f64e76a699fb95540cffdbb4))
|
||||
|
||||
## [1.20.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.20.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.20.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.20.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
@@ -6,12 +6,4 @@ module.exports = {
|
||||
transform: {
|
||||
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.json' }],
|
||||
},
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 22,
|
||||
functions: 69,
|
||||
lines: 67,
|
||||
statements: 67
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api",
|
||||
"version": "1.20.16",
|
||||
"version": "1.20.21",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -23,7 +23,7 @@
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"lint:fix": "eslint src --ext .ts --fix",
|
||||
"test": "jest spec --coverage"
|
||||
"test": "jest spec --coverage --passWithNoTests"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.2.3",
|
||||
@@ -37,6 +37,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "^1.45.0",
|
||||
"@standardnotes/domain-core": "^1.11.0",
|
||||
"@standardnotes/encryption": "workspace:*",
|
||||
"@standardnotes/models": "workspace:*",
|
||||
"@standardnotes/responses": "workspace:*",
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
import { type Invitation } from '@standardnotes/models'
|
||||
|
||||
import { SubscriptionInviteAcceptResponse } from '../../Response/Subscription/SubscriptionInviteAcceptResponse'
|
||||
import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse'
|
||||
import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse'
|
||||
import { SubscriptionInviteResponse } from '../../Response/Subscription/SubscriptionInviteResponse'
|
||||
import { SubscriptionServerInterface } from '../../Server/Subscription/SubscriptionServerInterface'
|
||||
|
||||
import { SubscriptionApiOperations } from './SubscriptionApiOperations'
|
||||
import { SubscriptionApiService } from './SubscriptionApiService'
|
||||
|
||||
describe('SubscriptionApiService', () => {
|
||||
let subscriptionServer: SubscriptionServerInterface
|
||||
|
||||
const createService = () => new SubscriptionApiService(subscriptionServer)
|
||||
|
||||
beforeEach(() => {
|
||||
subscriptionServer = {} as jest.Mocked<SubscriptionServerInterface>
|
||||
subscriptionServer.invite = jest.fn().mockReturnValue({
|
||||
data: { success: true, sharedSubscriptionInvitationUuid: '1-2-3' },
|
||||
} as jest.Mocked<SubscriptionInviteResponse>)
|
||||
subscriptionServer.cancelInvite = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<SubscriptionInviteCancelResponse>)
|
||||
subscriptionServer.listInvites = jest.fn().mockReturnValue({
|
||||
data: { invitations: [{} as jest.Mocked<Invitation>] },
|
||||
} as jest.Mocked<SubscriptionInviteListResponse>)
|
||||
subscriptionServer.acceptInvite = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<SubscriptionInviteAcceptResponse>)
|
||||
})
|
||||
|
||||
it('should invite a user', async () => {
|
||||
const response = await createService().invite('[email protected]')
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
sharedSubscriptionInvitationUuid: '1-2-3',
|
||||
},
|
||||
})
|
||||
expect(subscriptionServer.invite).toHaveBeenCalledWith({
|
||||
api: '20200115',
|
||||
identifier: '[email protected]',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not invite a user if it is already inviting', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[SubscriptionApiOperations.Inviting, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.invite('[email protected]')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not invite a user if the server fails', async () => {
|
||||
subscriptionServer.invite = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().invite('[email protected]')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should cancel an invite', async () => {
|
||||
const response = await createService().cancelInvite('1-2-3')
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
expect(subscriptionServer.cancelInvite).toHaveBeenCalledWith({
|
||||
api: '20200115',
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not cancel an invite if it is already canceling', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[SubscriptionApiOperations.CancelingInvite, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.cancelInvite('1-2-3')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not cancel an invite if the server fails', async () => {
|
||||
subscriptionServer.cancelInvite = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().cancelInvite('1-2-3')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should list invites', async () => {
|
||||
const response = await createService().listInvites()
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
invitations: [{} as jest.Mocked<Invitation>],
|
||||
},
|
||||
})
|
||||
expect(subscriptionServer.listInvites).toHaveBeenCalledWith({
|
||||
api: '20200115',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not list invitations if it is already listing', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[SubscriptionApiOperations.ListingInvites, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.listInvites()
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not list invites if the server fails', async () => {
|
||||
subscriptionServer.listInvites = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().listInvites()
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should accept an invite', async () => {
|
||||
const response = await createService().acceptInvite('1-2-3')
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
expect(subscriptionServer.acceptInvite).toHaveBeenCalledWith({
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not accept an invite if it is already accepting', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[SubscriptionApiOperations.AcceptingInvite, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.acceptInvite('1-2-3')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not accept an invite if the server fails', async () => {
|
||||
subscriptionServer.acceptInvite = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().acceptInvite('1-2-3')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,197 +0,0 @@
|
||||
import { ProtocolVersion, UserRequestType } from '@standardnotes/common'
|
||||
import { type RootKeyParamsInterface } from '@standardnotes/models'
|
||||
import { UserDeletionResponse } from '../../Response/User/UserDeletionResponse'
|
||||
|
||||
import { UserRegistrationResponse } from '../../Response/User/UserRegistrationResponse'
|
||||
import { UserRequestResponse } from '../../Response/UserRequest/UserRequestResponse'
|
||||
import { UserServerInterface } from '../../Server'
|
||||
import { UserRequestServerInterface } from '../../Server/UserRequest/UserRequestServerInterface'
|
||||
|
||||
import { UserApiOperations } from './UserApiOperations'
|
||||
import { UserApiService } from './UserApiService'
|
||||
|
||||
describe('UserApiService', () => {
|
||||
let userServer: UserServerInterface
|
||||
let userRequestServer: UserRequestServerInterface
|
||||
let keyParams: RootKeyParamsInterface
|
||||
|
||||
const createService = () => new UserApiService(userServer, userRequestServer)
|
||||
|
||||
beforeEach(() => {
|
||||
userServer = {} as jest.Mocked<UserServerInterface>
|
||||
userServer.register = jest.fn().mockReturnValue({
|
||||
data: { user: { email: '[email protected]', uuid: '1-2-3' } },
|
||||
} as jest.Mocked<UserRegistrationResponse>)
|
||||
userServer.deleteAccount = jest.fn().mockReturnValue({
|
||||
data: { message: 'Success' },
|
||||
} as jest.Mocked<UserDeletionResponse>)
|
||||
|
||||
userRequestServer = {} as jest.Mocked<UserRequestServerInterface>
|
||||
userRequestServer.submitUserRequest = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<UserRequestResponse>)
|
||||
|
||||
keyParams = {} as jest.Mocked<RootKeyParamsInterface>
|
||||
keyParams.getPortableValue = jest.fn().mockReturnValue({
|
||||
identifier: '[email protected]',
|
||||
version: ProtocolVersion.V004,
|
||||
})
|
||||
})
|
||||
|
||||
it('should register a user', async () => {
|
||||
const response = await createService().register({
|
||||
email: '[email protected]',
|
||||
serverPassword: 'testpasswd',
|
||||
keyParams,
|
||||
ephemeral: false,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
user: {
|
||||
email: '[email protected]',
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(userServer.register).toHaveBeenCalledWith({
|
||||
api: '20200115',
|
||||
email: '[email protected]',
|
||||
ephemeral: false,
|
||||
identifier: '[email protected]',
|
||||
password: 'testpasswd',
|
||||
version: '004',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not register a user if it is already registering', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[UserApiOperations.Registering, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.register({ email: '[email protected]', serverPassword: 'testpasswd', keyParams, ephemeral: false })
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not register a user if the server fails', async () => {
|
||||
userServer.register = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().register({
|
||||
email: '[email protected]',
|
||||
serverPassword: 'testpasswd',
|
||||
keyParams,
|
||||
ephemeral: false,
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should submit a user request', async () => {
|
||||
const response = await createService().submitUserRequest({
|
||||
userUuid: '1-2-3',
|
||||
requestType: UserRequestType.ExitDiscount,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
expect(userRequestServer.submitUserRequest).toHaveBeenCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
requestType: UserRequestType.ExitDiscount,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not submit a user request if it is already submitting', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[UserApiOperations.SubmittingRequest, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.submitUserRequest({ userUuid: '1-2-3', requestType: UserRequestType.ExitDiscount })
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not submit a user request if the server fails', async () => {
|
||||
userRequestServer.submitUserRequest = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().submitUserRequest({
|
||||
userUuid: '1-2-3',
|
||||
requestType: UserRequestType.ExitDiscount,
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should delete a user', async () => {
|
||||
const response = await createService().deleteAccount('1-2-3')
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
message: 'Success',
|
||||
},
|
||||
})
|
||||
expect(userServer.deleteAccount).toHaveBeenCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not delete a user if it is already deleting', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[UserApiOperations.DeletingAccount, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.deleteAccount('1-2-3')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not delete a user if the server fails', async () => {
|
||||
userServer.deleteAccount = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().deleteAccount('1-2-3')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,61 +0,0 @@
|
||||
import { WebSocketConnectionTokenResponse } from '../../Response'
|
||||
|
||||
import { WebSocketServerInterface } from '../../Server/WebSocket/WebSocketServerInterface'
|
||||
import { WebSocketApiOperations } from './WebSocketApiOperations'
|
||||
|
||||
import { WebSocketApiService } from './WebSocketApiService'
|
||||
|
||||
describe('WebSocketApiService', () => {
|
||||
let webSocketServer: WebSocketServerInterface
|
||||
|
||||
const createService = () => new WebSocketApiService(webSocketServer)
|
||||
|
||||
beforeEach(() => {
|
||||
webSocketServer = {} as jest.Mocked<WebSocketServerInterface>
|
||||
webSocketServer.createConnectionToken = jest.fn().mockReturnValue({
|
||||
data: { token: 'foobar' },
|
||||
} as jest.Mocked<WebSocketConnectionTokenResponse>)
|
||||
})
|
||||
|
||||
it('should create a websocket connection token', async () => {
|
||||
const response = await createService().createConnectionToken()
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
token: 'foobar',
|
||||
},
|
||||
})
|
||||
expect(webSocketServer.createConnectionToken).toHaveBeenCalledWith({})
|
||||
})
|
||||
|
||||
it('should not create a token if it is already creating', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WebSocketApiOperations.CreatingConnectionToken, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.createConnectionToken()
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not create a token if the server fails', async () => {
|
||||
webSocketServer.createConnectionToken = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().createConnectionToken()
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,368 +0,0 @@
|
||||
import { WorkspaceAccessLevel, WorkspaceType } from '@standardnotes/common'
|
||||
|
||||
import { HttpStatusCode } from '../../Http'
|
||||
import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse'
|
||||
import { WorkspaceInvitationAcceptingResponse } from '../../Response/Workspace/WorkspaceInvitationAcceptingResponse'
|
||||
import { WorkspaceInvitationResponse } from '../../Response/Workspace/WorkspaceInvitationResponse'
|
||||
import { WorkspaceListResponse } from '../../Response/Workspace/WorkspaceListResponse'
|
||||
import { WorkspaceUserListResponse } from '../../Response/Workspace/WorkspaceUserListResponse'
|
||||
import { WorkspaceServerInterface } from '../../Server/Workspace/WorkspaceServerInterface'
|
||||
import { WorkspaceKeyshareInitiatingResponse } from '../../Response/Workspace/WorkspaceKeyshareInitiatingResponse'
|
||||
|
||||
import { WorkspaceApiOperations } from './WorkspaceApiOperations'
|
||||
import { WorkspaceApiService } from './WorkspaceApiService'
|
||||
|
||||
describe('WorkspaceApiService', () => {
|
||||
let workspaceServer: WorkspaceServerInterface
|
||||
|
||||
const createService = () => new WorkspaceApiService(workspaceServer)
|
||||
|
||||
beforeEach(() => {
|
||||
workspaceServer = {} as jest.Mocked<WorkspaceServerInterface>
|
||||
workspaceServer.createWorkspace = jest.fn().mockReturnValue({
|
||||
data: { uuid: '1-2-3' },
|
||||
} as jest.Mocked<WorkspaceCreationResponse>)
|
||||
workspaceServer.inviteToWorkspace = jest.fn().mockReturnValue({
|
||||
data: { uuid: 'i-1-2-3' },
|
||||
} as jest.Mocked<WorkspaceInvitationResponse>)
|
||||
workspaceServer.acceptInvite = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<WorkspaceInvitationAcceptingResponse>)
|
||||
workspaceServer.listWorkspaces = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCode.Success,
|
||||
data: { ownedWorkspaces: [], joinedWorkspaces: [] },
|
||||
} as jest.Mocked<WorkspaceListResponse>)
|
||||
workspaceServer.listWorkspaceUsers = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCode.Success,
|
||||
data: { users: [] },
|
||||
} as jest.Mocked<WorkspaceUserListResponse>)
|
||||
workspaceServer.initiateKeyshare = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCode.Success,
|
||||
data: { success: true },
|
||||
} as jest.Mocked<WorkspaceKeyshareInitiatingResponse>)
|
||||
})
|
||||
|
||||
it('should create a workspace', async () => {
|
||||
const response = await createService().createWorkspace({
|
||||
workspaceType: WorkspaceType.Private,
|
||||
encryptedPrivateKey: 'foo',
|
||||
encryptedWorkspaceKey: 'bar',
|
||||
publicKey: 'buzz',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
})
|
||||
expect(workspaceServer.createWorkspace).toHaveBeenCalledWith({
|
||||
encryptedPrivateKey: 'foo',
|
||||
encryptedWorkspaceKey: 'bar',
|
||||
publicKey: 'buzz',
|
||||
workspaceType: 'private',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not create a workspace if it is already creating', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WorkspaceApiOperations.Creating, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.createWorkspace({
|
||||
workspaceType: WorkspaceType.Private,
|
||||
encryptedPrivateKey: 'foo',
|
||||
encryptedWorkspaceKey: 'bar',
|
||||
publicKey: 'buzz',
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not create a workspace if the server fails', async () => {
|
||||
workspaceServer.createWorkspace = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().createWorkspace({
|
||||
workspaceType: WorkspaceType.Private,
|
||||
encryptedPrivateKey: 'foo',
|
||||
encryptedWorkspaceKey: 'bar',
|
||||
publicKey: 'buzz',
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should invite to a workspace', async () => {
|
||||
const response = await createService().inviteToWorkspace({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
inviteeEmail: '[email protected]',
|
||||
accessLevel: WorkspaceAccessLevel.WriteAndRead,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
uuid: 'i-1-2-3',
|
||||
},
|
||||
})
|
||||
expect(workspaceServer.inviteToWorkspace).toHaveBeenCalledWith({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
inviteeEmail: '[email protected]',
|
||||
accessLevel: 'write-and-read',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not invite to a workspace if it is already inviting', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WorkspaceApiOperations.Inviting, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.inviteToWorkspace({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
inviteeEmail: '[email protected]',
|
||||
accessLevel: WorkspaceAccessLevel.WriteAndRead,
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not invite to a workspace if the server fails', async () => {
|
||||
workspaceServer.inviteToWorkspace = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().inviteToWorkspace({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
inviteeEmail: '[email protected]',
|
||||
accessLevel: WorkspaceAccessLevel.WriteAndRead,
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should accept invite to a workspace', async () => {
|
||||
const response = await createService().acceptInvite({
|
||||
userUuid: 'u-1-2-3',
|
||||
inviteUuid: 'i-1-2-3',
|
||||
publicKey: 'foo',
|
||||
encryptedPrivateKey: 'bar',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
expect(workspaceServer.acceptInvite).toHaveBeenCalledWith({
|
||||
userUuid: 'u-1-2-3',
|
||||
inviteUuid: 'i-1-2-3',
|
||||
publicKey: 'foo',
|
||||
encryptedPrivateKey: 'bar',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not accept invite to a workspace if it is already accepting', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WorkspaceApiOperations.Accepting, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.acceptInvite({
|
||||
userUuid: 'u-1-2-3',
|
||||
inviteUuid: 'i-1-2-3',
|
||||
publicKey: 'foo',
|
||||
encryptedPrivateKey: 'bar',
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not accept invite to a workspace if the server fails', async () => {
|
||||
workspaceServer.acceptInvite = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().acceptInvite({
|
||||
userUuid: 'u-1-2-3',
|
||||
inviteUuid: 'i-1-2-3',
|
||||
publicKey: 'foo',
|
||||
encryptedPrivateKey: 'bar',
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should list workspaces', async () => {
|
||||
const response = await createService().listWorkspaces()
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: {
|
||||
ownedWorkspaces: [],
|
||||
joinedWorkspaces: [],
|
||||
},
|
||||
})
|
||||
expect(workspaceServer.listWorkspaces).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not list workspaces if it is already listing them', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WorkspaceApiOperations.ListingWorkspaces, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.listWorkspaces()
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not list workspaces if the server fails', async () => {
|
||||
workspaceServer.listWorkspaces = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().listWorkspaces()
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should list workspace users', async () => {
|
||||
const response = await createService().listWorkspaceUsers({ workspaceUuid: 'w-1-2-3' })
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: {
|
||||
users: [],
|
||||
},
|
||||
})
|
||||
expect(workspaceServer.listWorkspaceUsers).toHaveBeenCalledWith({ workspaceUuid: 'w-1-2-3' })
|
||||
})
|
||||
|
||||
it('should not list workspace users if it is already listing them', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WorkspaceApiOperations.ListingWorkspaceUsers, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.listWorkspaceUsers({ workspaceUuid: 'w-1-2-3' })
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not list workspace users if the server fails', async () => {
|
||||
workspaceServer.listWorkspaceUsers = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().listWorkspaceUsers({ workspaceUuid: 'w-1-2-3' })
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should initiate keyshare in workspace for user', async () => {
|
||||
const response = await createService().initiateKeyshare({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
userUuid: 'u-1-2-3',
|
||||
encryptedWorkspaceKey: 'foobar',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
expect(workspaceServer.initiateKeyshare).toHaveBeenCalledWith({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
userUuid: 'u-1-2-3',
|
||||
encryptedWorkspaceKey: 'foobar',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not initiate keyshare in workspace if it is already initiating', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WorkspaceApiOperations.InitiatingKeyshare, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.initiateKeyshare({ workspaceUuid: 'w-1-2-3', userUuid: 'u-1-2-3', encryptedWorkspaceKey: 'foobar' })
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not initiate keyshare in workspace if the server fails', async () => {
|
||||
workspaceServer.initiateKeyshare = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().initiateKeyshare({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
userUuid: 'u-1-2-3',
|
||||
encryptedWorkspaceKey: 'foobar',
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Environment } from '@standardnotes/models'
|
||||
|
||||
import { HttpResponseMeta } from './HttpResponseMeta'
|
||||
import { HttpService } from './HttpService'
|
||||
|
||||
describe('HttpService', () => {
|
||||
const environment = Environment.Web
|
||||
const appVersion = '1.2.3'
|
||||
const snjsVersion = '2.3.4'
|
||||
const host = 'http://bar'
|
||||
let updateMetaCallback: (meta: HttpResponseMeta) => void
|
||||
|
||||
const createService = () => {
|
||||
const service = new HttpService(environment, appVersion, snjsVersion, updateMetaCallback)
|
||||
service.setHost(host)
|
||||
return service
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
updateMetaCallback = jest.fn()
|
||||
})
|
||||
|
||||
it('should set host', () => {
|
||||
const service = createService()
|
||||
|
||||
expect(service['host']).toEqual('http://bar')
|
||||
|
||||
service.setHost('http://foo')
|
||||
|
||||
expect(service['host']).toEqual('http://foo')
|
||||
})
|
||||
|
||||
it('should set and use the authorization token', () => {
|
||||
const service = createService()
|
||||
|
||||
expect(service['authorizationToken']).toBeUndefined()
|
||||
|
||||
service.setAuthorizationToken('foo-bar')
|
||||
|
||||
expect(service['authorizationToken']).toEqual('foo-bar')
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,7 @@
|
||||
import { isString, joinPaths, sleep } from '@standardnotes/utils'
|
||||
import { Environment } from '@standardnotes/models'
|
||||
import { Session, SessionToken } from '@standardnotes/domain-core'
|
||||
|
||||
import { HttpRequestParams } from './HttpRequestParams'
|
||||
import { HttpVerb } from './HttpVerb'
|
||||
import { HttpRequest } from './HttpRequest'
|
||||
@@ -10,21 +12,26 @@ import { XMLHttpRequestState } from './XMLHttpRequestState'
|
||||
import { ErrorMessage } from '../Error/ErrorMessage'
|
||||
import { HttpResponseMeta } from './HttpResponseMeta'
|
||||
import { HttpErrorResponseBody } from './HttpErrorResponseBody'
|
||||
import { Paths } from '../Server/Auth/Paths'
|
||||
import { SessionRefreshResponse } from '../Response/Auth/SessionRefreshResponse'
|
||||
|
||||
export class HttpService implements HttpServiceInterface {
|
||||
private authorizationToken?: string
|
||||
private session: Session | null
|
||||
private __latencySimulatorMs?: number
|
||||
private host!: string
|
||||
private declare host: string
|
||||
|
||||
constructor(
|
||||
private environment: Environment,
|
||||
private appVersion: string,
|
||||
private snjsVersion: string,
|
||||
private updateMetaCallback: (meta: HttpResponseMeta) => void,
|
||||
) {}
|
||||
private refreshSessionCallback: (session: Session) => void,
|
||||
) {
|
||||
this.session = null
|
||||
}
|
||||
|
||||
setAuthorizationToken(authorizationToken: string): void {
|
||||
this.authorizationToken = authorizationToken
|
||||
setSession(session: Session): void {
|
||||
this.session = session
|
||||
}
|
||||
|
||||
setHost(host: string): void {
|
||||
@@ -36,7 +43,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
url: joinPaths(this.host, path),
|
||||
params,
|
||||
verb: HttpVerb.Get,
|
||||
authentication: authentication ?? this.authorizationToken,
|
||||
authentication: authentication ?? this.session?.accessToken.value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -45,7 +52,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
url: joinPaths(this.host, path),
|
||||
params,
|
||||
verb: HttpVerb.Post,
|
||||
authentication: authentication ?? this.authorizationToken,
|
||||
authentication: authentication ?? this.session?.accessToken.value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -54,7 +61,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
url: joinPaths(this.host, path),
|
||||
params,
|
||||
verb: HttpVerb.Put,
|
||||
authentication: authentication ?? this.authorizationToken,
|
||||
authentication: authentication ?? this.session?.accessToken.value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -63,7 +70,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
url: joinPaths(this.host, path),
|
||||
params,
|
||||
verb: HttpVerb.Patch,
|
||||
authentication: authentication ?? this.authorizationToken,
|
||||
authentication: authentication ?? this.session?.accessToken.value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -72,7 +79,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
url: joinPaths(this.host, path),
|
||||
params,
|
||||
verb: HttpVerb.Delete,
|
||||
authentication: authentication ?? this.authorizationToken,
|
||||
authentication: authentication ?? this.session?.accessToken.value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -89,9 +96,68 @@ export class HttpService implements HttpServiceInterface {
|
||||
this.updateMetaCallback(response.meta)
|
||||
}
|
||||
|
||||
if (response.status === HttpStatusCode.ExpiredAccessToken) {
|
||||
const isSessionRefreshed = await this.refreshSession()
|
||||
if (!isSessionRefreshed) {
|
||||
return response
|
||||
}
|
||||
|
||||
httpRequest.authentication = this.session?.accessToken.value
|
||||
|
||||
return this.runHttp(httpRequest)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
private async refreshSession(): Promise<boolean> {
|
||||
if (this.session === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const response = (await this.post(joinPaths(this.host, Paths.v1.refreshSession), {
|
||||
access_token: this.session.accessToken.value,
|
||||
refresh_token: this.session.refreshToken.value,
|
||||
})) as SessionRefreshResponse
|
||||
|
||||
if (response.data.error) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (response.meta) {
|
||||
this.updateMetaCallback(response.meta)
|
||||
}
|
||||
|
||||
const accessTokenOrError = SessionToken.create(
|
||||
response.data.session.access_token,
|
||||
response.data.session.access_expiration,
|
||||
)
|
||||
if (accessTokenOrError.isFailed()) {
|
||||
return false
|
||||
}
|
||||
const accessToken = accessTokenOrError.getValue()
|
||||
|
||||
const refreshTokenOrError = SessionToken.create(
|
||||
response.data.session.refresh_token,
|
||||
response.data.session.refresh_expiration,
|
||||
)
|
||||
if (refreshTokenOrError.isFailed()) {
|
||||
return false
|
||||
}
|
||||
const refreshToken = refreshTokenOrError.getValue()
|
||||
|
||||
const sessionOrError = Session.create(accessToken, refreshToken, response.data.session.readonly_access)
|
||||
if (sessionOrError.isFailed()) {
|
||||
return false
|
||||
}
|
||||
|
||||
this.setSession(sessionOrError.getValue())
|
||||
|
||||
this.refreshSessionCallback(this.session)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private createRequestBody(httpRequest: HttpRequest): string | Uint8Array | undefined {
|
||||
if (
|
||||
httpRequest.params !== undefined &&
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Session } from '@standardnotes/domain-core'
|
||||
|
||||
import { HttpRequestParams } from './HttpRequestParams'
|
||||
import { HttpResponse } from './HttpResponse'
|
||||
|
||||
export interface HttpServiceInterface {
|
||||
setHost(host: string): void
|
||||
setAuthorizationToken(authorizationToken: string): void
|
||||
setSession(session: Session): void
|
||||
get(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse>
|
||||
post(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse>
|
||||
put(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Either } from '@standardnotes/common'
|
||||
|
||||
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
|
||||
import { HttpResponse } from '../../Http/HttpResponse'
|
||||
import { SessionRefreshResponseBody } from './SessionRefreshResponseBody'
|
||||
|
||||
export interface SessionRefreshResponse extends HttpResponse {
|
||||
data: Either<SessionRefreshResponseBody, HttpErrorResponseBody>
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export type SessionRefreshResponseBody = {
|
||||
session: {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
access_expiration: number
|
||||
refresh_expiration: number
|
||||
readonly_access: boolean
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
const SessionPaths = {
|
||||
refreshSession: '/v1/sessions/refresh',
|
||||
}
|
||||
|
||||
export const Paths = {
|
||||
v1: {
|
||||
...SessionPaths,
|
||||
},
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import { type Invitation } from '@standardnotes/models'
|
||||
|
||||
import { ApiVersion } from '../../Api'
|
||||
import { HttpServiceInterface } from '../../Http'
|
||||
import { SubscriptionInviteResponse } from '../../Response'
|
||||
import { SubscriptionInviteAcceptResponse } from '../../Response/Subscription/SubscriptionInviteAcceptResponse'
|
||||
import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse'
|
||||
import { SubscriptionInviteDeclineResponse } from '../../Response/Subscription/SubscriptionInviteDeclineResponse'
|
||||
import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse'
|
||||
|
||||
import { SubscriptionServer } from './SubscriptionServer'
|
||||
|
||||
describe('SubscriptionServer', () => {
|
||||
let httpService: HttpServiceInterface
|
||||
|
||||
const createServer = () => new SubscriptionServer(httpService)
|
||||
|
||||
beforeEach(() => {
|
||||
httpService = {} as jest.Mocked<HttpServiceInterface>
|
||||
})
|
||||
|
||||
it('should invite a user to a shared subscription', async () => {
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { success: true, sharedSubscriptionInvitationUuid: '1-2-3' },
|
||||
} as jest.Mocked<SubscriptionInviteResponse>)
|
||||
|
||||
const response = await createServer().invite({
|
||||
api: ApiVersion.v0,
|
||||
identifier: '[email protected]',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
sharedSubscriptionInvitationUuid: '1-2-3',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should accept an invite to a shared subscription', async () => {
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<SubscriptionInviteAcceptResponse>)
|
||||
|
||||
const response = await createServer().acceptInvite({
|
||||
api: ApiVersion.v0,
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should decline an invite to a shared subscription', async () => {
|
||||
httpService.get = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<SubscriptionInviteDeclineResponse>)
|
||||
|
||||
const response = await createServer().declineInvite({
|
||||
api: ApiVersion.v0,
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should cancel an invite to a shared subscription', async () => {
|
||||
httpService.delete = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<SubscriptionInviteCancelResponse>)
|
||||
|
||||
const response = await createServer().cancelInvite({
|
||||
api: ApiVersion.v0,
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should list invitations to a shared subscription', async () => {
|
||||
httpService.get = jest.fn().mockReturnValue({
|
||||
data: { invitations: [{} as jest.Mocked<Invitation>] },
|
||||
} as jest.Mocked<SubscriptionInviteListResponse>)
|
||||
|
||||
const response = await createServer().listInvites({
|
||||
api: ApiVersion.v0,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
invitations: [{} as jest.Mocked<Invitation>],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,54 +0,0 @@
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { ApiVersion } from '../../Api'
|
||||
import { HttpServiceInterface } from '../../Http'
|
||||
import { UserDeletionResponse, UserRegistrationResponse } from '../../Response'
|
||||
import { UserServer } from './UserServer'
|
||||
|
||||
describe('UserServer', () => {
|
||||
let httpService: HttpServiceInterface
|
||||
|
||||
const createServer = () => new UserServer(httpService)
|
||||
|
||||
beforeEach(() => {
|
||||
httpService = {} as jest.Mocked<HttpServiceInterface>
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { user: { email: '[email protected]', uuid: '1-2-3' } },
|
||||
} as jest.Mocked<UserRegistrationResponse>)
|
||||
httpService.delete = jest.fn().mockReturnValue({
|
||||
data: { message: 'Success' },
|
||||
} as jest.Mocked<UserDeletionResponse>)
|
||||
})
|
||||
|
||||
it('should register a user', async () => {
|
||||
const response = await createServer().register({
|
||||
password: 'test',
|
||||
api: ApiVersion.v0,
|
||||
email: '[email protected]',
|
||||
ephemeral: false,
|
||||
version: ProtocolVersion.V004,
|
||||
pw_nonce: 'test',
|
||||
identifier: '[email protected]',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
user: {
|
||||
email: '[email protected]',
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should delete a user', async () => {
|
||||
const response = await createServer().deleteAccount({
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
message: 'Success',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
import { UserRequestType } from '@standardnotes/common'
|
||||
|
||||
import { HttpServiceInterface } from '../../Http'
|
||||
import { UserRequestResponse } from '../../Response/UserRequest/UserRequestResponse'
|
||||
|
||||
import { UserRequestServer } from './UserRequestServer'
|
||||
|
||||
describe('UserRequestServer', () => {
|
||||
let httpService: HttpServiceInterface
|
||||
|
||||
const createServer = () => new UserRequestServer(httpService)
|
||||
|
||||
beforeEach(() => {
|
||||
httpService = {} as jest.Mocked<HttpServiceInterface>
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<UserRequestResponse>)
|
||||
})
|
||||
|
||||
it('should submit a user request', async () => {
|
||||
const response = await createServer().submitUserRequest({
|
||||
userUuid: '1-2-3',
|
||||
requestType: UserRequestType.ExitDiscount,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,27 +0,0 @@
|
||||
import { HttpServiceInterface } from '../../Http'
|
||||
import { WebSocketConnectionTokenResponse } from '../../Response'
|
||||
|
||||
import { WebSocketServer } from './WebSocketServer'
|
||||
|
||||
describe('WebSocketServer', () => {
|
||||
let httpService: HttpServiceInterface
|
||||
|
||||
const createServer = () => new WebSocketServer(httpService)
|
||||
|
||||
beforeEach(() => {
|
||||
httpService = {} as jest.Mocked<HttpServiceInterface>
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { token: 'foobar' },
|
||||
} as jest.Mocked<WebSocketConnectionTokenResponse>)
|
||||
})
|
||||
|
||||
it('should create a websocket connection token', async () => {
|
||||
const response = await createServer().createConnectionToken({})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
token: 'foobar',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,124 +0,0 @@
|
||||
import { WorkspaceAccessLevel, WorkspaceType } from '@standardnotes/common'
|
||||
|
||||
import { HttpServiceInterface, HttpStatusCode } from '../../Http'
|
||||
import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse'
|
||||
import { WorkspaceInvitationAcceptingResponse } from '../../Response/Workspace/WorkspaceInvitationAcceptingResponse'
|
||||
import { WorkspaceInvitationResponse } from '../../Response/Workspace/WorkspaceInvitationResponse'
|
||||
import { WorkspaceKeyshareInitiatingResponse } from '../../Response/Workspace/WorkspaceKeyshareInitiatingResponse'
|
||||
import { WorkspaceListResponse } from '../../Response/Workspace/WorkspaceListResponse'
|
||||
import { WorkspaceUserListResponse } from '../../Response/Workspace/WorkspaceUserListResponse'
|
||||
|
||||
import { WorkspaceServer } from './WorkspaceServer'
|
||||
|
||||
describe('WorkspaceServer', () => {
|
||||
let httpService: HttpServiceInterface
|
||||
|
||||
const createServer = () => new WorkspaceServer(httpService)
|
||||
|
||||
beforeEach(() => {
|
||||
httpService = {} as jest.Mocked<HttpServiceInterface>
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { uuid: '1-2-3' },
|
||||
} as jest.Mocked<WorkspaceCreationResponse>)
|
||||
})
|
||||
|
||||
it('should create a workspace', async () => {
|
||||
const response = await createServer().createWorkspace({
|
||||
workspaceType: WorkspaceType.Private,
|
||||
encryptedPrivateKey: 'foo',
|
||||
encryptedWorkspaceKey: 'bar',
|
||||
publicKey: 'buzz',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should inivte to a workspace', async () => {
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { uuid: 'i-1-2-3' },
|
||||
} as jest.Mocked<WorkspaceInvitationResponse>)
|
||||
|
||||
const response = await createServer().inviteToWorkspace({
|
||||
inviteeEmail: '[email protected]',
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
accessLevel: WorkspaceAccessLevel.WriteAndRead,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
uuid: 'i-1-2-3',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should accept invitation to a workspace', async () => {
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<WorkspaceInvitationAcceptingResponse>)
|
||||
|
||||
const response = await createServer().acceptInvite({
|
||||
encryptedPrivateKey: 'foo',
|
||||
inviteUuid: 'i-1-2-3',
|
||||
publicKey: 'bar',
|
||||
userUuid: 'u-1-2-3',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should list workspaces', async () => {
|
||||
httpService.get = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCode.Success,
|
||||
data: { ownedWorkspaces: [], joinedWorkspaces: [] },
|
||||
} as jest.Mocked<WorkspaceListResponse>)
|
||||
|
||||
const response = await createServer().listWorkspaces({})
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: { ownedWorkspaces: [], joinedWorkspaces: [] },
|
||||
})
|
||||
})
|
||||
|
||||
it('should list workspace users', async () => {
|
||||
httpService.get = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCode.Success,
|
||||
data: { users: [] },
|
||||
} as jest.Mocked<WorkspaceUserListResponse>)
|
||||
|
||||
const response = await createServer().listWorkspaceUsers({ workspaceUuid: 'w-1-2-3' })
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: { users: [] },
|
||||
})
|
||||
})
|
||||
|
||||
it('should initiate keyshare for user in a workspace', async () => {
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCode.Success,
|
||||
data: { success: true },
|
||||
} as jest.Mocked<WorkspaceKeyshareInitiatingResponse>)
|
||||
|
||||
const response = await createServer().initiateKeyshare({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
userUuid: 'u-1-2-3',
|
||||
encryptedWorkspaceKey: 'foobar',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.10.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/blocks-editor
|
||||
|
||||
# [1.10.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
### Features
|
||||
|
||||
* add ability to convert selection in Super to bulleted or numbered list ([be4cc4e](https://github.com/standardnotes/app/commit/be4cc4e605100e0a86de214e548e9ed10e2568aa))
|
||||
|
||||
## [1.9.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve variance between Super header font sizes and weights ([8b10330](https://github.com/standardnotes/app/commit/8b103307cba41855713b8627cf5d66a6fef6ff52))
|
||||
|
||||
## [1.9.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "@standardnotes/blocks-editor",
|
||||
"version": "1.9.6",
|
||||
"version": "1.10.1",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
"tsc": "tsc -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lexical/react": "^0.6.0",
|
||||
"@lexical/react": "^0.6.5",
|
||||
"@standardnotes/icons": "workspace:*",
|
||||
"@types/react": "^18.0.25",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"lexical": "^0.6.0",
|
||||
"lexical": "^0.6.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
|
||||
+120
-7
@@ -11,7 +11,11 @@ import './index.css';
|
||||
import {$isCodeHighlightNode} from '@lexical/code';
|
||||
import {$isLinkNode, TOGGLE_LINK_COMMAND} from '@lexical/link';
|
||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
import {mergeRegister} from '@lexical/utils';
|
||||
import {
|
||||
mergeRegister,
|
||||
$findMatchingParent,
|
||||
$getNearestNodeOfType,
|
||||
} from '@lexical/utils';
|
||||
import {
|
||||
$getSelection,
|
||||
$isRangeSelection,
|
||||
@@ -20,7 +24,17 @@ import {
|
||||
FORMAT_TEXT_COMMAND,
|
||||
LexicalEditor,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
$isRootOrShadowRoot,
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
} from 'lexical';
|
||||
import {$isHeadingNode} from '@lexical/rich-text';
|
||||
import {
|
||||
INSERT_UNORDERED_LIST_COMMAND,
|
||||
REMOVE_LIST_COMMAND,
|
||||
$isListNode,
|
||||
ListNode,
|
||||
INSERT_ORDERED_LIST_COMMAND,
|
||||
} from '@lexical/list';
|
||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {createPortal} from 'react-dom';
|
||||
|
||||
@@ -36,9 +50,26 @@ import {
|
||||
LinkIcon,
|
||||
SuperscriptIcon,
|
||||
SubscriptIcon,
|
||||
ListBulleted,
|
||||
ListNumbered,
|
||||
} from '@standardnotes/icons';
|
||||
import {IconComponent} from '../../Theme/IconComponent';
|
||||
|
||||
const blockTypeToBlockName = {
|
||||
bullet: 'Bulleted List',
|
||||
check: 'Check List',
|
||||
code: 'Code Block',
|
||||
h1: 'Heading 1',
|
||||
h2: 'Heading 2',
|
||||
h3: 'Heading 3',
|
||||
h4: 'Heading 4',
|
||||
h5: 'Heading 5',
|
||||
h6: 'Heading 6',
|
||||
number: 'Numbered List',
|
||||
paragraph: 'Normal',
|
||||
quote: 'Quote',
|
||||
};
|
||||
|
||||
const IconSize = 15;
|
||||
|
||||
function TextFormatFloatingToolbar({
|
||||
@@ -52,6 +83,8 @@ function TextFormatFloatingToolbar({
|
||||
isStrikethrough,
|
||||
isSubscript,
|
||||
isSuperscript,
|
||||
isBulletedList,
|
||||
isNumberedList,
|
||||
}: {
|
||||
editor: LexicalEditor;
|
||||
anchorElem: HTMLElement;
|
||||
@@ -63,6 +96,8 @@ function TextFormatFloatingToolbar({
|
||||
isSubscript: boolean;
|
||||
isSuperscript: boolean;
|
||||
isUnderline: boolean;
|
||||
isBulletedList: boolean;
|
||||
isNumberedList: boolean;
|
||||
}): JSX.Element {
|
||||
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -74,6 +109,22 @@ function TextFormatFloatingToolbar({
|
||||
}
|
||||
}, [editor, isLink]);
|
||||
|
||||
const formatBulletList = useCallback(() => {
|
||||
if (!isBulletedList) {
|
||||
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
||||
} else {
|
||||
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
||||
}
|
||||
}, [isBulletedList]);
|
||||
|
||||
const formatNumberedList = useCallback(() => {
|
||||
if (!isNumberedList) {
|
||||
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
||||
} else {
|
||||
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
||||
}
|
||||
}, [isNumberedList]);
|
||||
|
||||
const updateTextFormatFloatingToolbar = useCallback(() => {
|
||||
const selection = $getSelection();
|
||||
|
||||
@@ -226,6 +277,22 @@ function TextFormatFloatingToolbar({
|
||||
<LinkIcon />
|
||||
</IconComponent>
|
||||
</button>
|
||||
<button
|
||||
onClick={formatBulletList}
|
||||
className={'popup-item spaced ' + (isBulletedList ? 'active' : '')}
|
||||
aria-label="Insert bulleted list">
|
||||
<IconComponent size={IconSize}>
|
||||
<ListBulleted />
|
||||
</IconComponent>
|
||||
</button>
|
||||
<button
|
||||
onClick={formatNumberedList}
|
||||
className={'popup-item spaced ' + (isNumberedList ? 'active' : '')}
|
||||
aria-label="Insert numbered list">
|
||||
<IconComponent size={IconSize}>
|
||||
<ListNumbered />
|
||||
</IconComponent>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -236,6 +303,7 @@ function useFloatingTextFormatToolbar(
|
||||
editor: LexicalEditor,
|
||||
anchorElem: HTMLElement,
|
||||
): JSX.Element | null {
|
||||
const [activeEditor, setActiveEditor] = useState(editor);
|
||||
const [isText, setIsText] = useState(false);
|
||||
const [isLink, setIsLink] = useState(false);
|
||||
const [isBold, setIsBold] = useState(false);
|
||||
@@ -245,6 +313,8 @@ function useFloatingTextFormatToolbar(
|
||||
const [isSubscript, setIsSubscript] = useState(false);
|
||||
const [isSuperscript, setIsSuperscript] = useState(false);
|
||||
const [isCode, setIsCode] = useState(false);
|
||||
const [blockType, setBlockType] =
|
||||
useState<keyof typeof blockTypeToBlockName>('paragraph');
|
||||
|
||||
const updatePopup = useCallback(() => {
|
||||
editor.getEditorState().read(() => {
|
||||
@@ -270,6 +340,42 @@ function useFloatingTextFormatToolbar(
|
||||
return;
|
||||
}
|
||||
|
||||
const anchorNode = selection.anchor.getNode();
|
||||
let element =
|
||||
anchorNode.getKey() === 'root'
|
||||
? anchorNode
|
||||
: $findMatchingParent(anchorNode, (e) => {
|
||||
const parent = e.getParent();
|
||||
return parent !== null && $isRootOrShadowRoot(parent);
|
||||
});
|
||||
|
||||
if (element === null) {
|
||||
element = anchorNode.getTopLevelElementOrThrow();
|
||||
}
|
||||
|
||||
const elementKey = element.getKey();
|
||||
const elementDOM = activeEditor.getElementByKey(elementKey);
|
||||
|
||||
if (elementDOM !== null) {
|
||||
if ($isListNode(element)) {
|
||||
const parentList = $getNearestNodeOfType<ListNode>(
|
||||
anchorNode,
|
||||
ListNode,
|
||||
);
|
||||
const type = parentList
|
||||
? parentList.getListType()
|
||||
: element.getListType();
|
||||
setBlockType(type);
|
||||
} else {
|
||||
const type = $isHeadingNode(element)
|
||||
? element.getTag()
|
||||
: element.getType();
|
||||
if (type in blockTypeToBlockName) {
|
||||
setBlockType(type as keyof typeof blockTypeToBlockName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const node = getSelectedNode(selection);
|
||||
|
||||
// Update text format
|
||||
@@ -298,14 +404,19 @@ function useFloatingTextFormatToolbar(
|
||||
setIsText(false);
|
||||
}
|
||||
});
|
||||
}, [editor]);
|
||||
}, [editor, activeEditor]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('selectionchange', updatePopup);
|
||||
return () => {
|
||||
document.removeEventListener('selectionchange', updatePopup);
|
||||
};
|
||||
}, [updatePopup]);
|
||||
return editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
(_payload, newEditor) => {
|
||||
setActiveEditor(newEditor);
|
||||
updatePopup();
|
||||
return false;
|
||||
},
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
);
|
||||
}, [editor, updatePopup]);
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
@@ -336,6 +447,8 @@ function useFloatingTextFormatToolbar(
|
||||
isSuperscript={isSuperscript}
|
||||
isUnderline={isUnderline}
|
||||
isCode={isCode}
|
||||
isBulletedList={blockType === 'bullet'}
|
||||
isNumberedList={blockType === 'number'}
|
||||
/>,
|
||||
anchorElem,
|
||||
);
|
||||
|
||||
@@ -20,19 +20,20 @@
|
||||
padding-left: 16px;
|
||||
}
|
||||
.Lexical__h1 {
|
||||
font-size: 24px;
|
||||
font-size: 26px;
|
||||
color: var(--sn-stylekit-editor-foreground-color);
|
||||
font-weight: 400;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
.Lexical__h2 {
|
||||
font-size: 15px;
|
||||
font-size: 22px;
|
||||
color: var(--sn-stylekit-editor-foreground-color);
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
.Lexical__h3 {
|
||||
font-size: 12px;
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
.Lexical__textBold {
|
||||
|
||||
@@ -3,6 +3,128 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.104.31](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.30](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.29](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.28](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.27](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.26](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.25](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.24](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **desktop:** Fixed issue where the "Record video" modal would error after stopping recording ([d6fd85e](https://github.com/standardnotes/app/commit/d6fd85e451962e539bada9e207aa0ec65a92f511))
|
||||
|
||||
## [3.104.23](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.22](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
default-src 'self' blob:;
|
||||
script-src 'self' 'unsafe-eval';
|
||||
worker-src blob:;
|
||||
connect-src * data:;
|
||||
connect-src * data: blob:;
|
||||
style-src 'unsafe-inline' 'self' http://localhost:* http://127.0.0.1:45653;
|
||||
frame-src * blob:;
|
||||
"
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { MediaManagerInterface } from './MediaManagerInterface'
|
||||
|
||||
const { systemPreferences } = require('electron')
|
||||
|
||||
export class MediaManager implements MediaManagerInterface {
|
||||
async askForMediaAccess(type: 'camera' | 'microphone'): Promise<boolean> {
|
||||
const permission = systemPreferences.getMediaAccessStatus(type)
|
||||
|
||||
if (permission === 'granted') {
|
||||
return true
|
||||
} else if (permission === 'denied') {
|
||||
return false
|
||||
}
|
||||
|
||||
const granted = await systemPreferences.askForMediaAccess(type)
|
||||
return granted
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface MediaManagerInterface {
|
||||
askForMediaAccess(type: 'camera' | 'microphone'): Promise<boolean>
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { MenuManagerInterface } from '../Menus/MenuManagerInterface'
|
||||
import { Component, PackageManagerInterface } from '../Packages/PackageManagerInterface'
|
||||
import { SearchManagerInterface } from '../Search/SearchManagerInterface'
|
||||
import { RemoteDataInterface } from './DataInterface'
|
||||
import { MediaManagerInterface } from '../Media/MediaManagerInterface'
|
||||
|
||||
/**
|
||||
* Read https://github.com/electron/remote to understand how electron/remote works.
|
||||
@@ -34,6 +35,7 @@ export class RemoteBridge implements CrossProcessBridge {
|
||||
private data: RemoteDataInterface,
|
||||
private menus: MenuManagerInterface,
|
||||
private fileBackups: FileBackupsDevice,
|
||||
private media: MediaManagerInterface,
|
||||
) {}
|
||||
|
||||
get exposableValue(): CrossProcessBridge {
|
||||
@@ -73,6 +75,7 @@ export class RemoteBridge implements CrossProcessBridge {
|
||||
openFileBackup: this.openFileBackup.bind(this),
|
||||
getFileBackupReadToken: this.getFileBackupReadToken.bind(this),
|
||||
readNextChunk: this.readNextChunk.bind(this),
|
||||
askForMediaAccess: this.askForMediaAccess.bind(this),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,4 +226,8 @@ export class RemoteBridge implements CrossProcessBridge {
|
||||
public openFileBackup(record: FileBackupRecord): Promise<void> {
|
||||
return this.fileBackups.openFileBackup(record)
|
||||
}
|
||||
|
||||
askForMediaAccess(type: 'camera' | 'microphone'): Promise<boolean> {
|
||||
return this.media.askForMediaAccess(type)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { createBackupsManager } from './Backups/BackupsManager'
|
||||
import { BackupsManagerInterface } from './Backups/BackupsManagerInterface'
|
||||
import { FilesBackupManager } from './FileBackups/FileBackupsManager'
|
||||
import { Keychain } from './Keychain/Keychain'
|
||||
import { MediaManager } from './Media/MediaManager'
|
||||
import { MenuManagerInterface } from './Menus/MenuManagerInterface'
|
||||
import { buildContextMenu, createMenuManager } from './Menus/Menus'
|
||||
import { initializePackageManager } from './Packages/PackageManager'
|
||||
@@ -74,6 +75,7 @@ export async function createWindowState({
|
||||
},
|
||||
services.menuManager,
|
||||
services.fileBackupsManager,
|
||||
services.mediaManager,
|
||||
)
|
||||
|
||||
const shouldOpenUrl = (url: string) => url.startsWith('http') || url.startsWith('mailto')
|
||||
@@ -203,6 +205,7 @@ async function createWindowServices(window: Electron.BrowserWindow, appState: Ap
|
||||
const updateManager = setupUpdates(window, appState, backupsManager)
|
||||
const trayManager = createTrayManager(window, appState.store)
|
||||
const spellcheckerManager = createSpellcheckerManager(appState.store, window.webContents, appLocale)
|
||||
const mediaManager = new MediaManager()
|
||||
|
||||
if (isTesting()) {
|
||||
handleTestMessage(MessageType.SpellCheckerManager, () => spellcheckerManager)
|
||||
@@ -228,6 +231,7 @@ async function createWindowServices(window: Electron.BrowserWindow, appState: Ap
|
||||
packageManager,
|
||||
searchManager,
|
||||
fileBackupsManager,
|
||||
mediaManager,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,4 +49,6 @@ export interface CrossProcessBridge extends FileBackupsDevice {
|
||||
onInitialDataLoad(): void
|
||||
|
||||
destroyAllData(): void
|
||||
|
||||
askForMediaAccess(type: 'camera' | 'microphone'): Promise<boolean>
|
||||
}
|
||||
|
||||
@@ -166,4 +166,8 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn
|
||||
isDeviceDestroyed(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
askForMediaAccess(type: 'camera' | 'microphone'): Promise<boolean> {
|
||||
return this.remoteBridge.askForMediaAccess(type)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,7 @@
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/desktop",
|
||||
"main": "./app/dist/index.js",
|
||||
"version": "3.104.1",
|
||||
"version": "3.104.31",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
@@ -104,6 +104,12 @@
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "./build/entitlements.mac.inherit.plist",
|
||||
"entitlementsInherit": "./build/entitlements.mac.inherit.plist",
|
||||
"extendInfo": {
|
||||
"NSCameraUsageDescription": "Standard Notes requires access to your camera to enable the Moments feature."
|
||||
},
|
||||
"asarUnpack": [
|
||||
"node_modules/keytar"
|
||||
],
|
||||
"target": [
|
||||
"dmg",
|
||||
"zip"
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.2.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/docs
|
||||
|
||||
## [0.2.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/docs
|
||||
|
||||
## [0.2.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/docs
|
||||
|
||||
@@ -90,7 +90,7 @@ _Note that font and font sizes do not apply to mobile; only desktop/web._
|
||||
--sn-stylekit-sans-serif-font: -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||
'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
|
||||
'Helvetica Neue', sans-serif;
|
||||
|
||||
|
||||
--sn-stylekit-grey-1: #72767e;
|
||||
--sn-stylekit-grey-2: #bbbec4;
|
||||
--sn-stylekit-grey-3: #dfe1e4;
|
||||
@@ -98,7 +98,7 @@ _Note that font and font sizes do not apply to mobile; only desktop/web._
|
||||
--sn-stylekit-grey-4-opacity-variant: #bbbec43d;
|
||||
--sn-stylekit-grey-5: #f4f5f7;
|
||||
--sn-stylekit-grey-6: #e5e5e5;
|
||||
|
||||
|
||||
--sn-stylekit-accessory-tint-color-1: #086dd6;
|
||||
--sn-stylekit-accessory-tint-color-2: #ea6595;
|
||||
--sn-stylekit-accessory-tint-color-3: #ebad00;
|
||||
@@ -108,6 +108,53 @@ _Note that font and font sizes do not apply to mobile; only desktop/web._
|
||||
}
|
||||
```
|
||||
|
||||
### Optional Overrides
|
||||
|
||||
```css
|
||||
--modal-background-color: var(--sn-stylekit-background-color);
|
||||
|
||||
--editor-header-bar-background-color: var(--sn-stylekit-background-color);
|
||||
--editor-background-color: var(--sn-stylekit-editor-background-color);
|
||||
--editor-foreground-color: var(--sn-stylekit-editor-foreground-color);
|
||||
--editor-title-bar-border-bottom-color: var(--sn-stylekit-border-color);
|
||||
--editor-title-input-color: var(--sn-stylekit-editor-foreground-color);
|
||||
--editor-pane-background-color: var(--sn-stylekit-background-color);
|
||||
--editor-pane-editor-background-color: var(--sn-stylekit-editor-background-color);
|
||||
--editor-pane-editor-foreground-color: var(--sn-stylekit-editor-foreground-color);
|
||||
--editor-pane-component-stack-item-background-color: var(--sn-stylekit-background-color);
|
||||
|
||||
--text-selection-color: var(--sn-stylekit-info-contrast-color);
|
||||
--text-selection-background-color: var(--sn-stylekit-info-color);
|
||||
|
||||
--note-preview-progress-color: var(--sn-stylekit-info-color);
|
||||
--note-preview-progress-background-color: var(--sn-stylekit-passive-color-4-opacity-variant);
|
||||
|
||||
--note-preview-selected-progress-color: var(--sn-stylekit-secondary-background-color);
|
||||
--note-preview-selected-progress-background-color: var(--sn-stylekit-passive-color-4-opacity-variant);
|
||||
|
||||
--items-column-background-color: var(--sn-stylekit-background-color);
|
||||
--items-column-items-background-color: var(--sn-stylekit-background-color);
|
||||
--items-column-border-left-color: var(--sn-stylekit-border-color);
|
||||
--items-column-border-right-color: var(--sn-stylekit-border-color);
|
||||
--items-column-search-background-color: var(--sn-stylekit-contrast-background-color);
|
||||
--item-cell-selected-background-color: var(--sn-stylekit-contrast-background-color);
|
||||
--item-cell-selected-border-left-color: var(--sn-stylekit-info-color);
|
||||
|
||||
--navigation-column-background-color: var(--sn-stylekit-secondary-background-color);
|
||||
--navigation-section-title-color: var(--sn-stylekit-secondary-foreground-color);
|
||||
--navigation-item-text-color: var(--sn-stylekit-secondary-foreground-color);
|
||||
--navigation-item-count-color: var(--sn-stylekit-neutral-color);
|
||||
--navigation-item-selected-background-color: rgb(253, 253, 253);
|
||||
|
||||
--preferences-navigation-icon-color: var(--sn-stylekit-neutral-color);
|
||||
--preferences-navigation-selected-background-color: var(--sn-stylekit-info-backdrop-color);
|
||||
|
||||
--dropdown-menu-radio-button-inactive-color: var(--sn-stylekit-passive-color-1);
|
||||
|
||||
--panel-resizer-background-color: var(--sn-stylekit-secondary-contrast-background-color);
|
||||
--link-element-color: var(--sn-stylekit-info-color);
|
||||
```
|
||||
|
||||
In order to get SN to display a dock icon for your theme (a circle in the lower right corner of the app that allows you to quickly toggle themes), add the following payload into the your ext.json file when [publishing your theme](/extensions/publishing):
|
||||
|
||||
```json
|
||||
|
||||
@@ -1,303 +0,0 @@
|
||||
---
|
||||
id: auth
|
||||
title: Auth API
|
||||
sidebar_label: Auth
|
||||
description: Authentication portion of the Standard Notes API.
|
||||
keywords:
|
||||
- standard notes
|
||||
- docs
|
||||
- notes app
|
||||
- end-to-end encryption
|
||||
- sync
|
||||
image: /img/logo.png
|
||||
hide_title: false
|
||||
hide_table_of_contents: false
|
||||
---
|
||||
|
||||
### POST /auth
|
||||
|
||||
**Registers a new user**
|
||||
|
||||
| | | |
|
||||
|-------------|---------|---------------------------------------------------------------------------------------------------------------------|
|
||||
| api | `string` | The API version to use. Must not be null, otherwise oldest API version used. Latest value is "20200115" |
|
||||
| created | `string` | Integer timestamp representing the date the client generated the account's encryption keys. Example "1622494310383" |
|
||||
| email | `string` | The account email. |
|
||||
| ephemeral | `boolean` | Whether the initial session created for this account is ephemeral ("Stay signed in" unchecked). |
|
||||
| identifier | `string` | Should equal the account email. |
|
||||
| origination | `string` | Should equal "registration" |
|
||||
| password | `string` | The server password generated by the client from the user's master password. |
|
||||
| pw_nonce | `string` | The nonce generated by the client for the user's encryption key. |
|
||||
| version | `string` | The protocol version the client used to generate the user's encryption key. Latest is "004". |
|
||||
|
||||
#### Response
|
||||
|
||||
```JSON
|
||||
{
|
||||
"session": {
|
||||
"access_token": "1:457b1f4b-88c2-4328-bf32-fe7dd9431d62:WwKgTEDUoEhxECu6",
|
||||
"refresh_token": "1:457b1f4b-88c2-4328-bf32-fe7dd9431d62:G-qEoZsFtoj~RLL-",
|
||||
"access_expiration": 1627678312000,
|
||||
"refresh_expiration": 1654051238000
|
||||
},
|
||||
"key_params": {
|
||||
"created": "1622494310383",
|
||||
"identifier": "[email protected]",
|
||||
"origination": "registration",
|
||||
"pw_nonce": "d97ed41c581fe8c3e0dce7d2ee72afcb63f9f461ae875bae66e30ecf3d952900",
|
||||
"version": "004"
|
||||
},
|
||||
"user": {
|
||||
"uuid": "682f9deb-b75f-4d97-91fa-6fd82a482db1",
|
||||
"email": "[email protected]"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /auth/sign_in
|
||||
|
||||
**Authenticates a user and returns a session.**
|
||||
|
||||
| | | |
|
||||
|-------------|---------|---------------------------------------------------------------------------------------------------------------------|
|
||||
| api | `string` | The API version to use. Must not be null, otherwise oldest API version used. Latest value is "20200115" |
|
||||
| email | `string` | The account email. |
|
||||
| ephemeral | `boolean` | Whether the session created for this account is ephemeral ("Stay signed in" unchecked). |
|
||||
| password | `string` | The server password generated by the client from the user's master password. |
|
||||
|
||||
#### Response
|
||||
|
||||
```JSON
|
||||
{
|
||||
"session": {
|
||||
"access_token": "1:ee03808d-dd05-417c-9c87-d471e49bcc49:Q~-UoIpDhdtkii_t",
|
||||
"refresh_token": "1:ee03808d-dd05-417c-9c87-d471e49bcc49:Jn8UWzeHx2H5nZI5",
|
||||
"access_expiration": 1627678779000,
|
||||
"refresh_expiration": 1654051705000
|
||||
},
|
||||
"key_params": {
|
||||
"created": "1622494310383",
|
||||
"identifier": "[email protected]",
|
||||
"origination": "registration",
|
||||
"pw_nonce": "d97ed41c581fe8c3e0dce7d2ee72afcb63f9f461ae875bae66e30ecf3d952900",
|
||||
"version": "004"
|
||||
},
|
||||
"user": {
|
||||
"uuid": "682f9deb-b75f-4d97-91fa-6fd82a482db1",
|
||||
"email": "[email protected]"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GET /auth/params
|
||||
|
||||
**Queries the parameters used for key generation for an email address. Queried before signing into an account.**
|
||||
|
||||
| | | |
|
||||
|-------------|---------|---------------------------------------------------------------------------------------------------------------------|
|
||||
| api | `string` | The API version to use. Must not be null, otherwise oldest API version used. Latest value is "20200115" |
|
||||
| email | `string` | The account email. |
|
||||
|
||||
#### Response
|
||||
|
||||
```JSON
|
||||
{
|
||||
"identifier": "[email protected]",
|
||||
"pw_nonce": "d97ed41c581fe8c3e0dce7d2ee72afcb63f9f461ae875bae66e30ecf3d952900",
|
||||
"version": "004"
|
||||
}
|
||||
```
|
||||
|
||||
### POST /auth/change_pw
|
||||
|
||||
**Changes a user's password.**
|
||||
|
||||
| | | |
|
||||
|-------------|---------|---------------------------------------------------------------------------------------------------------------------|
|
||||
| api | `string` | The API version to use. Must not be null, otherwise oldest API version used. Latest value is "20200115" |
|
||||
| created | `string` | Integer timestamp representing the date the client generated the account's new encryption keys. Example "1622494310383" |
|
||||
| identifier | `string` | The account email. |
|
||||
| origination | `string` | Should equal "password-change" |
|
||||
| current_password | `string` | The old server password generated by the client from the user's master password. |
|
||||
| new_password | `string` | The new server password generated by the client from the user's master password. |
|
||||
| pw_nonce | `string` | The nonce generated by the client for the user's encryption key. |
|
||||
| version | `string` | The protocol version the client used to generate the user's encryption key. Latest is "004". |
|
||||
|
||||
#### Response
|
||||
|
||||
```JSON
|
||||
{
|
||||
"session": {
|
||||
"access_token": "1:27d4fd8f-b730-4e0a-afd3-1600fb466aaa:HMgmZwV5k5ePt0vj",
|
||||
"refresh_token": "1:27d4fd8f-b730-4e0a-afd3-1600fb466aaa:wHuTeOB-qWGP3AE3",
|
||||
"access_expiration": 1627679129000,
|
||||
"refresh_expiration": 1654052055000
|
||||
},
|
||||
"key_params": {
|
||||
"created": "1622494310383",
|
||||
"identifier": "[email protected]",
|
||||
"origination": "password-change",
|
||||
"pw_nonce": "be1974ff6fb1c541aa8c71fd3c66851b6492cf224b661c72daf44e0bef3096bb",
|
||||
"version": "004"
|
||||
},
|
||||
"user": {
|
||||
"uuid": "682f9deb-b75f-4d97-91fa-6fd82a482db1",
|
||||
"email": "[email protected]"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sessions
|
||||
|
||||
### Session Model
|
||||
|
||||
| Field | Type | Description |
|
||||
|----------------|-----------|---------------------------------------------------|
|
||||
| uuid | `string` | Unique identifier of the session |
|
||||
| user_uuid | `string` | Unique identifier of the user |
|
||||
| user_agent | `string` | The user agent used to create the session |
|
||||
| api_version | `string` | The server API version used to create the session |
|
||||
| access_token | `string` | The access token used to authenticate requests |
|
||||
| refresh_token | `string` | The refresh token used to extend tokens |
|
||||
| access_expiration | `datetime` | The expiration time of the access token |
|
||||
| refresh_expiration | `datetime` | The expiration time of the refresh token |
|
||||
| created_at | `datetime` | Date and time of creation of the session |
|
||||
| updated_at | `datetime` | Last updated date and time of the session |
|
||||
|
||||
- Each `session` includes the API version they were created with. This way we can deny sessions for a given API version if we detect a vulnerability with that version in the future
|
||||
- Sessions are created in the following cases:
|
||||
- When a user signs in
|
||||
- When a user registers a new account
|
||||
|
||||
---
|
||||
|
||||
### Authenticated requests
|
||||
|
||||
The `Authorization` request header field is used by clients to make authenticated request. `Bearer` is the only authentication scheme allowed.
|
||||
|
||||
The client must send the `access` token generated by the session, in the `Authorization` header. For example:
|
||||
|
||||
```
|
||||
GET /sessions HTTP/1.1
|
||||
Host: sync.standardnotes.org
|
||||
Authorization: Bearer <access token>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Below is a list of endpoints related to session management:
|
||||
|
||||
| Method | URL | Params | Description | Successful response code |
|
||||
|-----------|--------------------------|---------------------|--------------------------------------------------------|--------------------------|
|
||||
|`POST` | /auth/sign_out | *None* | Terminates the current session | `204` |
|
||||
|`DELETE` | /session | **uuid** | Terminates the specified session by UUID | `204` |
|
||||
|`DELETE` | /sessions | *None* | Terminates all sessions, except the current one | `204` |
|
||||
|`GET` | /sessions | *None* | Lists all sessions active sessions | `200` |
|
||||
|`POST` | /session/token/refresh | **refresh_token** | Obtains new pair of `access_token` and `refresh_token` | `200` |
|
||||
|
||||
A successful request to `GET /sessions` returns the following JSON response:
|
||||
|
||||
```json
|
||||
{
|
||||
"sessions": [
|
||||
{
|
||||
"uuid": "xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx",
|
||||
"user_agent": "<product> / <product-version> <comment>",
|
||||
"api_version": "xxxxyyzz",
|
||||
"current": "<boolean>",
|
||||
"created_at": "2020-01-01T00:00:00.000Z"
|
||||
}
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Obtaining tokens
|
||||
|
||||
Tokens can be obtained every time the user performs any of the following actions:
|
||||
|
||||
1. When a user signs in
|
||||
1. When a user registers an account
|
||||
1. When the tokens are refreshed
|
||||
|
||||
#### Refreshing tokens
|
||||
|
||||
When an expired `access_token` is provided in the `Authorization` HTTP header, the following JSON response is returned:
|
||||
|
||||
HTTP Status Code: `498 Expired Access Token`
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"tag": "expired-access-token",
|
||||
"message": "The provided access token has expired."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To continue accessing resources, the `access_token` must be refreshed. That is, the current `access_token` is replaced with a new one with an extended expiration date.
|
||||
|
||||
To refresh an `access_token`, a valid `refresh_token` is needed. This `refresh_token` must meet the following requirements:
|
||||
|
||||
- It should belong to the session of the `access_token`
|
||||
- It should not be expired
|
||||
|
||||
Since the `refresh_token` is of single-use, a new `refresh_token` is obtained when the `access_token` is refreshed.
|
||||
|
||||
Refreshing tokens is a process that is transparent to the user, meaning that the client will perform the requests to keep valid tokens without user intervention.
|
||||
|
||||
Here's how to refresh tokens:
|
||||
|
||||
1. Send a `POST` request to `/session/token/refresh`. The body should contain a JSON paylaod with the `refresh_token`:
|
||||
```
|
||||
POST /session/token/refresh HTTP/1.1
|
||||
Host: sync.standardnotes.org
|
||||
Authorization: Bearer <access token>
|
||||
|
||||
{
|
||||
"refresh_token": "R_xxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
1. The `refresh_token` is validated on the server. Depending of the circumstances, there should be two outcomes:
|
||||
1. The provided `refresh_token` is valid. If so, a new pair of tokens is generated and the following JSON response is returned:
|
||||
```json
|
||||
{
|
||||
"token": "xxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx",
|
||||
"session": {
|
||||
"refresh_expiration": 1583020800,
|
||||
"refresh_token": "xxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx",
|
||||
}
|
||||
}
|
||||
```
|
||||
1. The provided `refresh_token` is expired. If so, the following JSON response is returned:
|
||||
|
||||
HTTP Status Code: `400 Bad Request`
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"tag": "expired-refresh-token",
|
||||
"message": "The refresh token has expired."
|
||||
}
|
||||
}
|
||||
```
|
||||
User must start a new session by re-entering their credentials.
|
||||
|
||||
### Expiration
|
||||
|
||||
#### Sessions
|
||||
|
||||
Sessions should be terminated after a period of inactivity. This is for best security practices.
|
||||
|
||||
`Long-lived sessions` are a good choice for our use case, because it can build a better user experience than expiring sessions for a short idle-timeout.
|
||||
|
||||
- A `session` that remains inactive for `1 year` will be terminated, along with all `tokens`
|
||||
|
||||
#### Tokens
|
||||
|
||||
| Name | Type | Expiration |
|
||||
|-----------|-------------|---------------|
|
||||
| `access` | Short-lived | `60 days` |
|
||||
| `refresh` | Long-lived | `1 year` |
|
||||
|
||||
- A `refresh` token is of single-use, while an `access` token can be used as long as it is not expired
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user