mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-13 22:54:42 -04:00
fix: Move content into to frontend directory
Doing this BEFORE the merge prevents a lot of merge conflicts. Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
4e18ce38f3
commit
762a0a850e
1051 changed files with 0 additions and 35 deletions
54
frontend/src/api/alias/index.ts
Normal file
54
frontend/src/api/alias/index.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { Alias, NewAliasDto, PrimaryAliasDto } from './types'
|
||||
import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder'
|
||||
import { PutApiRequestBuilder } from '../common/api-request-builder/put-api-request-builder'
|
||||
import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-api-request-builder'
|
||||
|
||||
/**
|
||||
* Adds an alias to an existing note.
|
||||
*
|
||||
* @param noteIdOrAlias The note id or an existing alias for a note.
|
||||
* @param newAlias The new alias.
|
||||
* @return Information about the newly created alias.
|
||||
* @throws {Error} when the api request wasn't successfull
|
||||
*/
|
||||
export const addAlias = async (noteIdOrAlias: string, newAlias: string): Promise<Alias> => {
|
||||
const response = await new PostApiRequestBuilder<Alias, NewAliasDto>('alias')
|
||||
.withJsonBody({
|
||||
noteIdOrAlias,
|
||||
newAlias
|
||||
})
|
||||
.sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a given alias as the primary one for a note.
|
||||
* The former primary alias should be marked as non-primary by the backend automatically.
|
||||
*
|
||||
* @param alias The alias to mark as primary for its corresponding note.
|
||||
* @return The updated information about the alias.
|
||||
* @throws {Error} when the api request wasn't successfull
|
||||
*/
|
||||
export const markAliasAsPrimary = async (alias: string): Promise<Alias> => {
|
||||
const response = await new PutApiRequestBuilder<Alias, PrimaryAliasDto>('alias/' + alias)
|
||||
.withJsonBody({
|
||||
primaryAlias: true
|
||||
})
|
||||
.sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a given alias from its corresponding note.
|
||||
*
|
||||
* @param alias The alias to remove from its note.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const deleteAlias = async (alias: string): Promise<void> => {
|
||||
await new DeleteApiRequestBuilder('alias/' + alias).sendRequest()
|
||||
}
|
19
frontend/src/api/alias/types.ts
Normal file
19
frontend/src/api/alias/types.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
export interface Alias {
|
||||
name: string
|
||||
primaryAlias: boolean
|
||||
noteId: string
|
||||
}
|
||||
|
||||
export interface NewAliasDto {
|
||||
noteIdOrAlias: string
|
||||
newAlias: string
|
||||
}
|
||||
|
||||
export interface PrimaryAliasDto {
|
||||
primaryAlias: boolean
|
||||
}
|
15
frontend/src/api/auth/index.ts
Normal file
15
frontend/src/api/auth/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-api-request-builder'
|
||||
|
||||
/**
|
||||
* Requests to log out the current user.
|
||||
*
|
||||
* @throws {Error} if logout is not possible.
|
||||
*/
|
||||
export const doLogout = async (): Promise<void> => {
|
||||
await new DeleteApiRequestBuilder('auth/logout').sendRequest()
|
||||
}
|
30
frontend/src/api/auth/ldap.ts
Normal file
30
frontend/src/api/auth/ldap.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { LoginDto } from './types'
|
||||
import { AuthError } from './types'
|
||||
import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder'
|
||||
|
||||
/**
|
||||
* Requests to log in a user via LDAP credentials.
|
||||
*
|
||||
* @param provider The identifier of the LDAP provider with which to login.
|
||||
* @param username The username with which to try the login.
|
||||
* @param password The password of the user.
|
||||
* @throws {AuthError.INVALID_CREDENTIALS} if the LDAP provider denied the given credentials.
|
||||
* @throws {Error} when the api request wasn't successfull
|
||||
*/
|
||||
export const doLdapLogin = async (provider: string, username: string, password: string): Promise<void> => {
|
||||
await new PostApiRequestBuilder<void, LoginDto>('auth/ldap/' + provider)
|
||||
.withJsonBody({
|
||||
username: username,
|
||||
password: password
|
||||
})
|
||||
.withStatusCodeErrorMapping({
|
||||
401: AuthError.INVALID_CREDENTIALS
|
||||
})
|
||||
.sendRequest()
|
||||
}
|
75
frontend/src/api/auth/local.ts
Normal file
75
frontend/src/api/auth/local.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { ChangePasswordDto, LoginDto, RegisterDto } from './types'
|
||||
import { AuthError, RegisterError } from './types'
|
||||
import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder'
|
||||
import { PutApiRequestBuilder } from '../common/api-request-builder/put-api-request-builder'
|
||||
|
||||
/**
|
||||
* Requests to do a local login with a provided username and password.
|
||||
*
|
||||
* @param username The username for which the login should be tried.
|
||||
* @param password The password which should be used to log in.
|
||||
* @throws {AuthError.INVALID_CREDENTIALS} when the username or password is wrong.
|
||||
* @throws {AuthError.LOGIN_DISABLED} when the local login is disabled on the backend.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const doLocalLogin = async (username: string, password: string): Promise<void> => {
|
||||
await new PostApiRequestBuilder<void, LoginDto>('auth/local/login')
|
||||
.withJsonBody({
|
||||
username,
|
||||
password
|
||||
})
|
||||
.withStatusCodeErrorMapping({
|
||||
400: AuthError.LOGIN_DISABLED,
|
||||
401: AuthError.INVALID_CREDENTIALS
|
||||
})
|
||||
.sendRequest()
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests to register a new local user in the backend.
|
||||
*
|
||||
* @param username The username of the new user.
|
||||
* @param displayName The display name of the new user.
|
||||
* @param password The password of the new user.
|
||||
* @throws {RegisterError.USERNAME_EXISTING} when there is already an existing user with the same username.
|
||||
* @throws {RegisterError.REGISTRATION_DISABLED} when the registration of local users has been disabled on the backend.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const doLocalRegister = async (username: string, displayName: string, password: string): Promise<void> => {
|
||||
await new PostApiRequestBuilder<void, RegisterDto>('auth/local')
|
||||
.withJsonBody({
|
||||
username,
|
||||
displayName,
|
||||
password
|
||||
})
|
||||
.withStatusCodeErrorMapping({
|
||||
400: RegisterError.REGISTRATION_DISABLED,
|
||||
409: RegisterError.USERNAME_EXISTING
|
||||
})
|
||||
.sendRequest()
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests to update the user's current password to a new one.
|
||||
* @param currentPassword The current password of the user for confirmation.
|
||||
* @param newPassword The new password of the user.
|
||||
* @throws {AuthError.INVALID_CREDENTIALS} when the current password is wrong.
|
||||
* @throws {AuthError.LOGIN_DISABLED} when local login is disabled on the backend.
|
||||
*/
|
||||
export const doLocalPasswordChange = async (currentPassword: string, newPassword: string): Promise<void> => {
|
||||
await new PutApiRequestBuilder<void, ChangePasswordDto>('auth/local')
|
||||
.withJsonBody({
|
||||
currentPassword,
|
||||
newPassword
|
||||
})
|
||||
.withStatusCodeErrorMapping({
|
||||
400: AuthError.LOGIN_DISABLED,
|
||||
401: AuthError.INVALID_CREDENTIALS
|
||||
})
|
||||
.sendRequest()
|
||||
}
|
33
frontend/src/api/auth/types.ts
Normal file
33
frontend/src/api/auth/types.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
export enum AuthError {
|
||||
INVALID_CREDENTIALS = 'invalidCredentials',
|
||||
LOGIN_DISABLED = 'loginDisabled',
|
||||
OPENID_ERROR = 'openIdError',
|
||||
OTHER = 'other'
|
||||
}
|
||||
|
||||
export enum RegisterError {
|
||||
USERNAME_EXISTING = 'usernameExisting',
|
||||
REGISTRATION_DISABLED = 'registrationDisabled',
|
||||
OTHER = 'other'
|
||||
}
|
||||
|
||||
export interface LoginDto {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface RegisterDto {
|
||||
username: string
|
||||
password: string
|
||||
displayName: string
|
||||
}
|
||||
|
||||
export interface ChangePasswordDto {
|
||||
currentPassword: string
|
||||
newPassword: string
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { ApiRequestBuilder } from './api-request-builder'
|
||||
|
||||
/**
|
||||
* Builder to construct and execute a call to the HTTP API that contains a body payload.
|
||||
*
|
||||
* @param RequestBodyType The type of the request body if applicable.
|
||||
*/
|
||||
export abstract class ApiRequestBuilderWithBody<ResponseType, RequestBodyType> extends ApiRequestBuilder<ResponseType> {
|
||||
/**
|
||||
* Adds a body part to the API request. If this is called multiple times, only the body of the last invocation will be
|
||||
* used during the execution of the request.
|
||||
*
|
||||
* @param bodyData The data to use as request body.
|
||||
* @return The API request instance itself for chaining.
|
||||
*/
|
||||
withBody(bodyData: BodyInit): this {
|
||||
this.requestBody = bodyData
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a JSON-encoded body part to the API request. This method will set the content-type header appropriately.
|
||||
*
|
||||
* @param bodyData The data to use as request body. Will get stringified to JSON.
|
||||
* @return The API request instance itself for chaining.
|
||||
* @see withBody
|
||||
*/
|
||||
withJsonBody(bodyData: RequestBodyType): this {
|
||||
this.withHeader('Content-Type', 'application/json')
|
||||
return this.withBody(JSON.stringify(bodyData))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import deepmerge from 'deepmerge'
|
||||
import { defaultConfig, defaultHeaders } from '../default-config'
|
||||
import { ApiResponse } from '../api-response'
|
||||
|
||||
/**
|
||||
* Builder to construct and execute a call to the HTTP API.
|
||||
*
|
||||
* @param ResponseType The type of the response if applicable.
|
||||
*/
|
||||
export abstract class ApiRequestBuilder<ResponseType> {
|
||||
private readonly targetUrl: string
|
||||
private overrideExpectedResponseStatus: number | undefined
|
||||
private customRequestOptions = defaultConfig
|
||||
private customRequestHeaders = new Headers(defaultHeaders)
|
||||
private customStatusCodeErrorMapping: Record<number, string> | undefined
|
||||
protected requestBody: BodyInit | undefined
|
||||
|
||||
/**
|
||||
* Initializes a new API call with the default request options.
|
||||
*
|
||||
* @param endpoint The target endpoint without a leading slash.
|
||||
*/
|
||||
constructor(endpoint: string) {
|
||||
this.targetUrl = `api/private/${endpoint}`
|
||||
}
|
||||
|
||||
protected async sendRequestAndVerifyResponse(
|
||||
httpMethod: RequestInit['method'],
|
||||
defaultExpectedStatus: number
|
||||
): Promise<ApiResponse<ResponseType>> {
|
||||
const response = await fetch(this.targetUrl, {
|
||||
...this.customRequestOptions,
|
||||
method: httpMethod,
|
||||
headers: this.customRequestHeaders,
|
||||
body: this.requestBody
|
||||
})
|
||||
|
||||
if (this.customStatusCodeErrorMapping && this.customStatusCodeErrorMapping[response.status]) {
|
||||
throw new Error(this.customStatusCodeErrorMapping[response.status])
|
||||
}
|
||||
|
||||
const expectedStatus = this.overrideExpectedResponseStatus
|
||||
? this.overrideExpectedResponseStatus
|
||||
: defaultExpectedStatus
|
||||
if (response.status !== expectedStatus) {
|
||||
throw new Error(`Expected response status code ${expectedStatus} but received ${response.status}.`)
|
||||
}
|
||||
|
||||
return new ApiResponse(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an HTTP header to the API request. Previous headers with the same name will get overridden on subsequent calls
|
||||
* with the same name.
|
||||
*
|
||||
* @param name The name of the HTTP header to add. Example: 'Content-Type'
|
||||
* @param value The value of the HTTP header to add. Example: 'text/markdown'
|
||||
* @return The API request instance itself for chaining.
|
||||
*/
|
||||
withHeader(name: string, value: string): this {
|
||||
this.customRequestHeaders.set(name, value)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds custom request options for the underlying fetch request by merging them with the existing options.
|
||||
*
|
||||
* @param options The options to set for the fetch request.
|
||||
* @return The API request instance itself for chaining.
|
||||
*/
|
||||
withCustomOptions(options: Partial<Omit<RequestInit, 'method' | 'headers' | 'body'>>): this {
|
||||
this.customRequestOptions = deepmerge(this.customRequestOptions, options)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a mapping from response status codes to error messages. An error with the specified message will be thrown
|
||||
* when the status code of the response matches one of the defined ones.
|
||||
*
|
||||
* @param mapping The mapping from response status codes to error messages.
|
||||
* @return The API request instance itself for chaining.
|
||||
*/
|
||||
withStatusCodeErrorMapping(mapping: Record<number, string>): this {
|
||||
this.customStatusCodeErrorMapping = mapping
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the expected status code of the response. Can be used to override the default expected status code.
|
||||
* An error will be thrown when the status code of the response does not match the expected one.
|
||||
*
|
||||
* @param expectedCode The expected status code of the response.
|
||||
* @return The API request instance itself for chaining.
|
||||
*/
|
||||
withExpectedStatusCode(expectedCode: number): this {
|
||||
this.overrideExpectedResponseStatus = expectedCode
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the prepared API call as a GET request. A default status code of 200 is expected.
|
||||
*
|
||||
* @return The API response.
|
||||
* @throws {Error} when the status code does not match the expected one or is defined as in the custom status code
|
||||
* error mapping.
|
||||
*/
|
||||
abstract sendRequest(): Promise<ApiResponse<ResponseType>>
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { expectFetch } from './test-utils/expect-fetch'
|
||||
import { DeleteApiRequestBuilder } from './delete-api-request-builder'
|
||||
|
||||
describe('DeleteApiRequestBuilder', () => {
|
||||
let originalFetch: typeof global['fetch']
|
||||
|
||||
beforeAll(() => {
|
||||
originalFetch = global.fetch
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
global.fetch = originalFetch
|
||||
})
|
||||
describe('sendRequest without body', () => {
|
||||
it('without headers', async () => {
|
||||
expectFetch('api/private/test', 204, { method: 'DELETE' })
|
||||
await new DeleteApiRequestBuilder<string, undefined>('test').sendRequest()
|
||||
})
|
||||
|
||||
it('with single header', async () => {
|
||||
const expectedHeaders = new Headers()
|
||||
expectedHeaders.append('test', 'true')
|
||||
expectFetch('api/private/test', 204, {
|
||||
method: 'DELETE',
|
||||
headers: expectedHeaders
|
||||
})
|
||||
await new DeleteApiRequestBuilder<string, undefined>('test').withHeader('test', 'true').sendRequest()
|
||||
})
|
||||
|
||||
it('with overriding single header', async () => {
|
||||
const expectedHeaders = new Headers()
|
||||
expectedHeaders.append('test', 'false')
|
||||
expectFetch('api/private/test', 204, {
|
||||
method: 'DELETE',
|
||||
headers: expectedHeaders
|
||||
})
|
||||
await new DeleteApiRequestBuilder<string, undefined>('test')
|
||||
.withHeader('test', 'true')
|
||||
.withHeader('test', 'false')
|
||||
.sendRequest()
|
||||
})
|
||||
|
||||
it('with multiple different headers', async () => {
|
||||
const expectedHeaders = new Headers()
|
||||
expectedHeaders.append('test', 'true')
|
||||
expectedHeaders.append('test2', 'false')
|
||||
expectFetch('api/private/test', 204, {
|
||||
method: 'DELETE',
|
||||
headers: expectedHeaders
|
||||
})
|
||||
await new DeleteApiRequestBuilder<string, undefined>('test')
|
||||
.withHeader('test', 'true')
|
||||
.withHeader('test2', 'false')
|
||||
.sendRequest()
|
||||
})
|
||||
})
|
||||
|
||||
it('sendRequest with JSON body', async () => {
|
||||
const expectedHeaders = new Headers()
|
||||
expectedHeaders.append('Content-Type', 'application/json')
|
||||
|
||||
expectFetch('api/private/test', 204, {
|
||||
method: 'DELETE',
|
||||
headers: expectedHeaders,
|
||||
body: '{"test":true,"foo":"bar"}'
|
||||
})
|
||||
await new DeleteApiRequestBuilder('test')
|
||||
.withJsonBody({
|
||||
test: true,
|
||||
foo: 'bar'
|
||||
})
|
||||
.sendRequest()
|
||||
})
|
||||
|
||||
it('sendRequest with other body', async () => {
|
||||
expectFetch('api/private/test', 204, {
|
||||
method: 'DELETE',
|
||||
body: 'HedgeDoc'
|
||||
})
|
||||
await new DeleteApiRequestBuilder('test').withBody('HedgeDoc').sendRequest()
|
||||
})
|
||||
|
||||
it('sendRequest with expected status code', async () => {
|
||||
expectFetch('api/private/test', 200, { method: 'DELETE' })
|
||||
await new DeleteApiRequestBuilder<string, undefined>('test').withExpectedStatusCode(200).sendRequest()
|
||||
})
|
||||
|
||||
describe('sendRequest with custom options', () => {
|
||||
it('with one option', async () => {
|
||||
expectFetch('api/private/test', 204, {
|
||||
method: 'DELETE',
|
||||
cache: 'force-cache'
|
||||
})
|
||||
await new DeleteApiRequestBuilder<string, undefined>('test')
|
||||
.withCustomOptions({
|
||||
cache: 'force-cache'
|
||||
})
|
||||
.sendRequest()
|
||||
})
|
||||
|
||||
it('overriding single option', async () => {
|
||||
expectFetch('api/private/test', 204, {
|
||||
method: 'DELETE',
|
||||
cache: 'no-store'
|
||||
})
|
||||
await new DeleteApiRequestBuilder<string, undefined>('test')
|
||||
.withCustomOptions({
|
||||
cache: 'force-cache'
|
||||
})
|
||||
.withCustomOptions({
|
||||
cache: 'no-store'
|
||||
})
|
||||
.sendRequest()
|
||||
})
|
||||
|
||||
it('with multiple options', async () => {
|
||||
expectFetch('api/private/test', 204, {
|
||||
method: 'DELETE',
|
||||
cache: 'force-cache',
|
||||
integrity: 'test'
|
||||
})
|
||||
await new DeleteApiRequestBuilder<string, undefined>('test')
|
||||
.withCustomOptions({
|
||||
cache: 'force-cache',
|
||||
integrity: 'test'
|
||||
})
|
||||
.sendRequest()
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendRequest with custom error map', () => {
|
||||
it('for valid status code', async () => {
|
||||
expectFetch('api/private/test', 204, { method: 'DELETE' })
|
||||
await new DeleteApiRequestBuilder<string, undefined>('test')
|
||||
.withStatusCodeErrorMapping({
|
||||
400: 'noooooo',
|
||||
401: 'not you!'
|
||||
})
|
||||
.sendRequest()
|
||||
})
|
||||
|
||||
it('for invalid status code 1', async () => {
|
||||
expectFetch('api/private/test', 400, { method: 'DELETE' })
|
||||
const request = new DeleteApiRequestBuilder<string, undefined>('test')
|
||||
.withStatusCodeErrorMapping({
|
||||
400: 'noooooo',
|
||||
401: 'not you!'
|
||||
})
|
||||
.sendRequest()
|
||||
await expect(request).rejects.toThrow('noooooo')
|
||||
})
|
||||
|
||||
it('for invalid status code 2', async () => {
|
||||
expectFetch('api/private/test', 401, { method: 'DELETE' })
|
||||
const request = new DeleteApiRequestBuilder<string, undefined>('test')
|
||||
.withStatusCodeErrorMapping({
|
||||
400: 'noooooo',
|
||||
401: 'not you!'
|
||||
})
|
||||
.sendRequest()
|
||||
await expect(request).rejects.toThrow('not you!')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { ApiResponse } from '../api-response'
|
||||
import { ApiRequestBuilderWithBody } from './api-request-builder-with-body'
|
||||
|
||||
/**
|
||||
* Builder to construct a DELETE request to the API.
|
||||
*
|
||||
* @param ResponseType The type of the expected response. Defaults to no response body.
|
||||
* @param RequestBodyType The type of the request body. Defaults to no request body.
|
||||
* @see ApiRequestBuilder
|
||||
*/
|
||||
export class DeleteApiRequestBuilder<ResponseType = void, RequestBodyType = unknown> extends ApiRequestBuilderWithBody<
|
||||
ResponseType,
|
||||
RequestBodyType
|
||||
> {
|
||||
/**
|
||||
* @see ApiRequestBuilder#sendRequest
|
||||
*/
|
||||
sendRequest(): Promise<ApiResponse<ResponseType>> {
|
||||
return this.sendRequestAndVerifyResponse('DELETE', 204)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { expectFetch } from './test-utils/expect-fetch'
|
||||
import { GetApiRequestBuilder } from './get-api-request-builder'
|
||||
|
||||
describe('GetApiRequestBuilder', () => {
|
||||
let originalFetch: typeof global['fetch']
|
||||
|
||||
beforeAll(() => {
|
||||
originalFetch = global.fetch
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
global.fetch = originalFetch
|
||||
})
|
||||
|
||||
describe('sendRequest', () => {
|
||||
it('without headers', async () => {
|
||||
expectFetch('api/private/test', 200, { method: 'GET' })
|
||||
await new GetApiRequestBuilder<string>('test').sendRequest()
|
||||
})
|
||||
|
||||
it('with single header', async () => {
|
||||
const expectedHeaders = new Headers()
|
||||
expectedHeaders.append('test', 'true')
|
||||
expectFetch('api/private/test', 200, {
|
||||
method: 'GET',
|
||||
headers: expectedHeaders
|
||||
})
|
||||
await new GetApiRequestBuilder<string>('test').withHeader('test', 'true').sendRequest()
|
||||
})
|
||||
|
||||
it('with overriding single header', async () => {
|
||||
const expectedHeaders = new Headers()
|
||||
expectedHeaders.append('test', 'false')
|
||||
expectFetch('api/private/test', 200, {
|
||||
method: 'GET',
|
||||
headers: expectedHeaders
|
||||
})
|
||||
await new GetApiRequestBuilder<string>('test')
|
||||
.withHeader('test', 'true')
|
||||
.withHeader('test', 'false')
|
||||
.sendRequest()
|
||||
})
|
||||
|
||||
it('with multiple different headers', async () => {
|
||||
const expectedHeaders = new Headers()
|
||||
expectedHeaders.append('test', 'true')
|
||||
expectedHeaders.append('test2', 'false')
|
||||
expectFetch('api/private/test', 200, {
|
||||
method: 'GET',
|
||||
headers: expectedHeaders
|
||||
})
|
||||
await new GetApiRequestBuilder<string>('test')
|
||||
.withHeader('test', 'true')
|
||||
.withHeader('test2', 'false')
|
||||
.sendRequest()
|
||||
})
|
||||
})
|
||||
|
||||
it('sendRequest with expected status code', async () => {
|
||||
expectFetch('api/private/test', 200, { method: 'GET' })
|
||||
await new GetApiRequestBuilder<string>('test').withExpectedStatusCode(200).sendRequest()
|
||||
})
|
||||
|
||||
describe('sendRequest with custom options', () => {
|
||||
it('with one option', async () => {
|
||||
expectFetch('api/private/test', 200, {
|
||||
method: 'GET',
|
||||
cache: 'force-cache'
|
||||
})
|
||||
await new GetApiRequestBuilder<string>('test')
|
||||
.withCustomOptions({
|
||||
cache: 'force-cache'
|
||||
})
|
||||
.sendRequest()
|
||||
})
|
||||
|
||||
it('overriding single option', async () => {
|
||||
expectFetch('api/private/test', 200, {
|
||||
method: 'GET',
|
||||
cache: 'no-store'
|
||||
})
|
||||
await new GetApiRequestBuilder<string>('test')
|
||||
.withCustomOptions({
|
||||
cache: 'force-cache'
|
||||
})
|
||||
.withCustomOptions({
|
||||
cache: 'no-store'
|
||||
})
|
||||
.sendRequest()
|
||||
})
|
||||
|
||||
it('with multiple options', async () => {
|
||||
expectFetch('api/private/test', 200, {
|
||||
method: 'GET',
|
||||
cache: 'force-cache',
|
||||
integrity: 'test'
|
||||
})
|
||||
await new GetApiRequestBuilder<string>('test')
|
||||
.withCustomOptions({
|
||||
cache: 'force-cache',
|
||||
integrity: 'test'
|
||||
})
|
||||
.sendRequest()
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendRequest with custom error map', () => {
|
||||
it('for valid status code', async () => {
|
||||
expectFetch('api/private/test', 200, { method: 'GET' })
|
||||
await new GetApiRequestBuilder<string>('test')
|
||||
.withStatusCodeErrorMapping({
|
||||
400: 'noooooo',
|
||||
401: 'not you!'
|
||||
})
|
||||
.sendRequest()
|
||||
})
|
||||
|
||||
it('for invalid status code 1', async () => {
|
||||
expectFetch('api/private/test', 400, { method: 'GET' })
|
||||
const request = new GetApiRequestBuilder<string>('test')
|
||||
.withStatusCodeErrorMapping({
|
||||
400: 'noooooo',
|
||||
401: 'not you!'
|
||||
})
|
||||
.sendRequest()
|
||||
await expect(request).rejects.toThrow('noooooo')
|
||||
})
|
||||
|
||||
it('for invalid status code 2', async () => {
|
||||
expectFetch('api/private/test', 401, { method: 'GET' })
|
||||
const request = new GetApiRequestBuilder<string>('test')
|
||||
.withStatusCodeErrorMapping({
|
||||
400: 'noooooo',
|
||||
401: 'not you!'
|
||||
})
|
||||
.sendRequest()
|
||||
await expect(request).rejects.toThrow('not you!')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { ApiRequestBuilder } from './api-request-builder'
|
||||
import type { ApiResponse } from '../api-response'
|
||||
|
||||
/**
|
||||
* Builder to construct a GET request to the API.
|
||||
*
|
||||
* @param ResponseType The type of the expected response.
|
||||
* @see ApiRequestBuilder
|
||||
*/
|
||||
export class GetApiRequestBuilder<ResponseType> extends ApiRequestBuilder<ResponseType> {
|
||||
/**
|
||||
* @see ApiRequestBuilder#sendRequest
|
||||
*/
|
||||
sendRequest(): Promise<ApiResponse<ResponseType>> {
|
||||
return this.sendRequestAndVerifyResponse('GET', 200)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { PostApiRequestBuilder } from './post-api-request-builder'
|
||||
import { expectFetch } from './test-utils/expect-fetch'
|
||||
|
||||
describe('PostApiRequestBuilder', () => {
|
||||
let originalFetch: typeof global['fetch']
|
||||
|
||||
beforeAll(() => {
|
||||
originalFetch = global.fetch
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
global.fetch = originalFetch
|
||||
})
|
||||
|
||||
describe('sendRequest without body', () => {
|
||||
it('without headers', async () => {
|
||||
expectFetch('api/private/test', 201, { method: 'POST' })
|
||||
await new PostApiRequestBuilder<string, undefined>('test').sendRequest()
|
||||
})
|
||||
|
||||
it('with single header', async () => {
|
||||
const expectedHeaders = new Headers()
|
||||
expectedHeaders.append('test', 'true')
|
||||
expectFetch('api/private/test', 201, {
|
||||
method: 'POST',
|
||||
headers: expectedHeaders
|
||||
})
|
||||
await new PostApiRequestBuilder<string, undefined>('test').withHeader('test', 'true').sendRequest()
|
||||
})
|
||||
|
||||
it('with overriding single header', async () => {
|
||||
const expectedHeaders = new Headers()
|
||||
expectedHeaders.append('test', 'false')
|
||||
expectFetch('api/private/test', 201, {
|
||||
method: 'POST',
|
||||
headers: expectedHeaders
|
||||
})
|
||||
await new PostApiRequestBuilder<string, undefined>('test')
|
||||
.withHeader('test', 'true')
|
||||
.withHeader('test', 'false')
|
||||
.sendRequest()
|
||||
})
|
||||
|
||||
it('with multiple different headers', async () => {
|
||||
const expectedHeaders = new Headers()
|
||||
expectedHeaders.append('test', 'true')
|
||||
expectedHeaders.append('test2', 'false')
|
||||
expectFetch('api/private/test', 201, {
|
||||
method: 'POST',
|
||||
headers: expectedHeaders
|
||||
})
|
||||
await new PostApiRequestBuilder<string, undefined>('test')
|
||||
.withHeader('test', 'true')
|
||||
.withHeader('test2', 'false')
|
||||
.sendRequest()
|
||||
})
|
||||
})
|
||||
|
||||
it('sendRequest with JSON body', async () => {
|
||||
const expectedHeaders = new Headers()
|
||||
expectedHeaders.append('Content-Type', 'application/json')
|
||||
|
||||
expectFetch('api/private/test', 201, {
|
||||
method: 'POST',
|
||||
headers: expectedHeaders,
|
||||
body: '{"test":true,"foo":"bar"}'
|
||||
})
|
||||
await new PostApiRequestBuilder('test')
|
||||
.withJsonBody({
|
||||
test: true,
|
||||
foo: 'bar'
|
||||
})
|
||||
.sendRequest()
|
||||
})
|
||||
|
||||
it('sendRequest with other body', async () => {
|
||||
expectFetch('api/private/test', 201, {
|
||||
method: 'POST',
|
||||
body: 'HedgeDoc'
|
||||
})
|
||||
await new PostApiRequestBuilder('test').withBody('HedgeDoc').sendRequest()
|
||||
})
|
||||
|
||||
it('sendRequest with expected status code', async () => {
|
||||
expectFetch('api/private/test', 200, { method: 'POST' })
|
||||
await new PostApiRequestBuilder<string, undefined>('test').withExpectedStatusCode(200).sendRequest()
|
||||
})
|
||||
|
||||
describe('sendRequest with custom options', () => {
|
||||
it('with one option', async () => {
|
||||
expectFetch('api/private/test', 201, {
|
||||
method: 'POST',
|
||||
cache: 'force-cache'
|
||||
})
|
||||
await new PostApiRequestBuilder<string, undefined>('test')
|
||||
.withCustomOptions({
|
||||
cache: 'force-cache'
|
||||
})
|
||||
.sendRequest()
|
||||
})
|
||||
|
||||
it('overriding single option', async () => {
|
||||
expectFetch('api/private/test', 201, {
|
||||
method: 'POST',
|
||||
cache: 'no-store'
|
||||
})
|
||||
await new PostApiRequestBuilder<string, undefined>('test')
|
||||
.withCustomOptions({
|
||||
cache: 'force-cache'
|
||||
})
|
||||
.withCustomOptions({
|
||||
cache: 'no-store'
|
||||
})
|
||||
.sendRequest()
|
||||
})
|
||||
|
||||
it('with multiple options', async () => {
|
||||
expectFetch('api/private/test', 201, {
|
||||
method: 'POST',
|
||||
cache: 'force-cache',
|
||||
integrity: 'test'
|
||||
})
|
||||
await new PostApiRequestBuilder<string, undefined>('test')
|
||||
.withCustomOptions({
|
||||
cache: 'force-cache',
|
||||
integrity: 'test'
|
||||
})
|
||||
.sendRequest()
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendRequest with custom error map', () => {
|
||||
it('for valid status code', async () => {
|
||||
expectFetch('api/private/test', 201, { method: 'POST' })
|
||||
await new PostApiRequestBuilder<string, undefined>('test')
|
||||
.withStatusCodeErrorMapping({
|
||||
400: 'noooooo',
|
||||
401: 'not you!'
|
||||
})
|
||||
.sendRequest()
|
||||
})
|
||||
|
||||
it('for invalid status code 1', async () => {
|
||||
expectFetch('api/private/test', 400, { method: 'POST' })
|
||||
const request = new PostApiRequestBuilder<string, undefined>('test')
|
||||
.withStatusCodeErrorMapping({
|
||||
400: 'noooooo',
|
||||
401: 'not you!'
|
||||
})
|
||||
.sendRequest()
|
||||
await expect(request).rejects.toThrow('noooooo')
|
||||
})
|
||||
|
||||
it('for invalid status code 2', async () => {
|
||||
expectFetch('api/private/test', 401, { method: 'POST' })
|
||||
const request = new PostApiRequestBuilder<string, undefined>('test')
|
||||
.withStatusCodeErrorMapping({
|
||||
400: 'noooooo',
|
||||
401: 'not you!'
|
||||
})
|
||||
.sendRequest()
|
||||
await expect(request).rejects.toThrow('not you!')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { ApiResponse } from '../api-response'
|
||||
import { ApiRequestBuilderWithBody } from './api-request-builder-with-body'
|
||||
|
||||
/**
|
||||
* Builder to construct a POST request to the API.
|
||||
*
|
||||
* @param ResponseType The type of the expected response.
|
||||
* @param RequestBodyType The type of the request body
|
||||
* @see ApiRequestBuilder
|
||||
*/
|
||||
export class PostApiRequestBuilder<ResponseType, RequestBodyType> extends ApiRequestBuilderWithBody<
|
||||
ResponseType,
|
||||
RequestBodyType
|
||||
> {
|
||||
/**
|
||||
* @see ApiRequestBuilder#sendRequest
|
||||
*/
|
||||
sendRequest(): Promise<ApiResponse<ResponseType>> {
|
||||
return this.sendRequestAndVerifyResponse('POST', 201)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { expectFetch } from './test-utils/expect-fetch'
|
||||
import { PutApiRequestBuilder } from './put-api-request-builder'
|
||||
|
||||
describe('PutApiRequestBuilder', () => {
|
||||
let originalFetch: typeof global['fetch']
|
||||
|
||||
beforeAll(() => {
|
||||
originalFetch = global.fetch
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
global.fetch = originalFetch
|
||||
})
|
||||
|
||||
describe('sendRequest without body', () => {
|
||||
it('without headers', async () => {
|
||||
expectFetch('api/private/test', 200, { method: 'PUT' })
|
||||
await new PutApiRequestBuilder<string, undefined>('test').sendRequest()
|
||||
})
|
||||
|
||||
it('with single header', async () => {
|
||||
const expectedHeaders = new Headers()
|
||||
expectedHeaders.append('test', 'true')
|
||||
expectFetch('api/private/test', 200, {
|
||||
method: 'PUT',
|
||||
headers: expectedHeaders
|
||||
})
|
||||
await new PutApiRequestBuilder<string, undefined>('test').withHeader('test', 'true').sendRequest()
|
||||
})
|
||||
|
||||
it('with overriding single header', async () => {
|
||||
const expectedHeaders = new Headers()
|
||||
expectedHeaders.append('test', 'false')
|
||||
expectFetch('api/private/test', 200, {
|
||||
method: 'PUT',
|
||||
headers: expectedHeaders
|
||||
})
|
||||
await new PutApiRequestBuilder<string, undefined>('test')
|
||||
.withHeader('test', 'true')
|
||||
.withHeader('test', 'false')
|
||||
.sendRequest()
|
||||
})
|
||||
|
||||
it('with multiple different headers', async () => {
|
||||
const expectedHeaders = new Headers()
|
||||
expectedHeaders.append('test', 'true')
|
||||
expectedHeaders.append('test2', 'false')
|
||||
expectFetch('api/private/test', 200, {
|
||||
method: 'PUT',
|
||||
headers: expectedHeaders
|
||||
})
|
||||
await new PutApiRequestBuilder<string, undefined>('test')
|
||||
.withHeader('test', 'true')
|
||||
.withHeader('test2', 'false')
|
||||
.sendRequest()
|
||||
})
|
||||
})
|
||||
|
||||
it('sendRequest with JSON body', async () => {
|
||||
const expectedHeaders = new Headers()
|
||||
expectedHeaders.append('Content-Type', 'application/json')
|
||||
|
||||
expectFetch('api/private/test', 200, {
|
||||
method: 'PUT',
|
||||
headers: expectedHeaders,
|
||||
body: '{"test":true,"foo":"bar"}'
|
||||
})
|
||||
await new PutApiRequestBuilder('test')
|
||||
.withJsonBody({
|
||||
test: true,
|
||||
foo: 'bar'
|
||||
})
|
||||
.sendRequest()
|
||||
})
|
||||
|
||||
it('sendRequest with other body', async () => {
|
||||
expectFetch('api/private/test', 200, {
|
||||
method: 'PUT',
|
||||
body: 'HedgeDoc'
|
||||
})
|
||||
await new PutApiRequestBuilder('test').withBody('HedgeDoc').sendRequest()
|
||||
})
|
||||
|
||||
it('sendRequest with expected status code', async () => {
|
||||
expectFetch('api/private/test', 200, { method: 'PUT' })
|
||||
await new PutApiRequestBuilder<string, undefined>('test').withExpectedStatusCode(200).sendRequest()
|
||||
})
|
||||
|
||||
describe('sendRequest with custom options', () => {
|
||||
it('with one option', async () => {
|
||||
expectFetch('api/private/test', 200, {
|
||||
method: 'PUT',
|
||||
cache: 'force-cache'
|
||||
})
|
||||
await new PutApiRequestBuilder<string, undefined>('test')
|
||||
.withCustomOptions({
|
||||
cache: 'force-cache'
|
||||
})
|
||||
.sendRequest()
|
||||
})
|
||||
|
||||
it('overriding single option', async () => {
|
||||
expectFetch('api/private/test', 200, {
|
||||
method: 'PUT',
|
||||
cache: 'no-store'
|
||||
})
|
||||
await new PutApiRequestBuilder<string, undefined>('test')
|
||||
.withCustomOptions({
|
||||
cache: 'force-cache'
|
||||
})
|
||||
.withCustomOptions({
|
||||
cache: 'no-store'
|
||||
})
|
||||
.sendRequest()
|
||||
})
|
||||
|
||||
it('with multiple options', async () => {
|
||||
expectFetch('api/private/test', 200, {
|
||||
method: 'PUT',
|
||||
cache: 'force-cache',
|
||||
integrity: 'test'
|
||||
})
|
||||
await new PutApiRequestBuilder<string, undefined>('test')
|
||||
.withCustomOptions({
|
||||
cache: 'force-cache',
|
||||
integrity: 'test'
|
||||
})
|
||||
.sendRequest()
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendRequest with custom error map', () => {
|
||||
it('for valid status code', async () => {
|
||||
expectFetch('api/private/test', 200, { method: 'PUT' })
|
||||
await new PutApiRequestBuilder<string, undefined>('test')
|
||||
.withStatusCodeErrorMapping({
|
||||
400: 'noooooo',
|
||||
401: 'not you!'
|
||||
})
|
||||
.sendRequest()
|
||||
})
|
||||
|
||||
it('for invalid status code 1', async () => {
|
||||
expectFetch('api/private/test', 400, { method: 'PUT' })
|
||||
const request = new PutApiRequestBuilder<string, undefined>('test')
|
||||
.withStatusCodeErrorMapping({
|
||||
400: 'noooooo',
|
||||
401: 'not you!'
|
||||
})
|
||||
.sendRequest()
|
||||
await expect(request).rejects.toThrow('noooooo')
|
||||
})
|
||||
|
||||
it('for invalid status code 2', async () => {
|
||||
expectFetch('api/private/test', 401, { method: 'PUT' })
|
||||
const request = new PutApiRequestBuilder<string, undefined>('test')
|
||||
.withStatusCodeErrorMapping({
|
||||
400: 'noooooo',
|
||||
401: 'not you!'
|
||||
})
|
||||
.sendRequest()
|
||||
await expect(request).rejects.toThrow('not you!')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { ApiResponse } from '../api-response'
|
||||
import { ApiRequestBuilderWithBody } from './api-request-builder-with-body'
|
||||
|
||||
/**
|
||||
* Builder to construct a PUT request to the API.
|
||||
*
|
||||
* @param ResponseType The type of the expected response.
|
||||
* @param RequestBodyType The type of the request body
|
||||
* @see ApiRequestBuilder
|
||||
*/
|
||||
export class PutApiRequestBuilder<ResponseType, RequestBodyType> extends ApiRequestBuilderWithBody<
|
||||
ResponseType,
|
||||
RequestBodyType
|
||||
> {
|
||||
/**
|
||||
* @see ApiRequestBuilder#sendRequest
|
||||
*/
|
||||
sendRequest(): Promise<ApiResponse<ResponseType>> {
|
||||
return this.sendRequestAndVerifyResponse('PUT', 200)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defaultConfig } from '../../default-config'
|
||||
import { Mock } from 'ts-mockery'
|
||||
|
||||
/**
|
||||
* Mock fetch api for tests.
|
||||
* Check that the given url and options are present in the request and return the given status code.
|
||||
*
|
||||
* @param expectedUrl the url that should be requested
|
||||
* @param requestStatusCode the status code the mocked request should return
|
||||
* @param expectedOptions additional options
|
||||
*/
|
||||
export const expectFetch = (expectedUrl: string, requestStatusCode: number, expectedOptions: RequestInit): void => {
|
||||
global.fetch = jest.fn((fetchUrl: RequestInfo | URL, fetchOptions?: RequestInit): Promise<Response> => {
|
||||
expect(fetchUrl).toEqual(expectedUrl)
|
||||
expect(fetchOptions).toStrictEqual({
|
||||
...defaultConfig,
|
||||
body: undefined,
|
||||
headers: new Headers(),
|
||||
...expectedOptions
|
||||
})
|
||||
return Promise.resolve(
|
||||
Mock.of<Response>({
|
||||
status: requestStatusCode
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
55
frontend/src/api/common/api-response.test.ts
Normal file
55
frontend/src/api/common/api-response.test.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Mock } from 'ts-mockery'
|
||||
import { ApiResponse } from './api-response'
|
||||
|
||||
describe('ApiResponse', () => {
|
||||
it('getResponse returns input response', () => {
|
||||
const mockResponse = Mock.of<Response>()
|
||||
const responseObj = new ApiResponse(mockResponse)
|
||||
expect(responseObj.getResponse()).toEqual(mockResponse)
|
||||
})
|
||||
|
||||
it('asBlob', async () => {
|
||||
const mockBlob = Mock.of<Blob>()
|
||||
const mockResponse = Mock.of<Response>({
|
||||
blob(): Promise<Blob> {
|
||||
return Promise.resolve(mockBlob)
|
||||
}
|
||||
})
|
||||
const responseObj = new ApiResponse(mockResponse)
|
||||
await expect(responseObj.asBlob()).resolves.toEqual(mockBlob)
|
||||
})
|
||||
|
||||
describe('asParsedJsonObject with', () => {
|
||||
it('invalid header', async () => {
|
||||
const mockHeaders = new Headers()
|
||||
mockHeaders.set('Content-Type', 'text/invalid')
|
||||
const mockResponse = Mock.of<Response>({
|
||||
headers: mockHeaders
|
||||
})
|
||||
const responseObj = new ApiResponse(mockResponse)
|
||||
await expect(responseObj.asParsedJsonObject()).rejects.toThrow('Response body does not seem to be JSON encoded')
|
||||
})
|
||||
|
||||
it('valid header', async () => {
|
||||
const mockHeaders = new Headers()
|
||||
mockHeaders.set('Content-Type', 'application/json')
|
||||
const mockBody = {
|
||||
Hedgy: '🦔'
|
||||
}
|
||||
const mockResponse = Mock.of<Response>({
|
||||
headers: mockHeaders,
|
||||
json(): Promise<typeof mockBody> {
|
||||
return Promise.resolve(mockBody)
|
||||
}
|
||||
})
|
||||
const responseObj = new ApiResponse(mockResponse)
|
||||
await expect(responseObj.asParsedJsonObject()).resolves.toEqual(mockBody)
|
||||
})
|
||||
})
|
||||
})
|
54
frontend/src/api/common/api-response.ts
Normal file
54
frontend/src/api/common/api-response.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class that represents the response of an {@link ApiRequestBuilder}.
|
||||
*/
|
||||
export class ApiResponse<ResponseType> {
|
||||
private readonly response: Response
|
||||
|
||||
/**
|
||||
* Initializes a new API response instance based on an HTTP response.
|
||||
*
|
||||
* @param response The HTTP response from the fetch call.
|
||||
*/
|
||||
constructor(response: Response) {
|
||||
this.response = response
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw response from the fetch call.
|
||||
*
|
||||
* @return The response from the fetch call.
|
||||
*/
|
||||
getResponse(): Response {
|
||||
return this.response
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the response as parsed JSON. An error will be thrown if the response is not JSON encoded.
|
||||
*
|
||||
* @return The parsed JSON response.
|
||||
* @throws {Error} if the response is not JSON encoded.
|
||||
*/
|
||||
async asParsedJsonObject(): Promise<ResponseType> {
|
||||
if (!this.response.headers.get('Content-Type')?.startsWith('application/json')) {
|
||||
throw new Error('Response body does not seem to be JSON encoded.')
|
||||
}
|
||||
// TODO Responses should better be type validated
|
||||
// see https://github.com/hedgedoc/react-client/issues/1219
|
||||
return (await this.response.json()) as ResponseType
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the response as a Blob.
|
||||
*
|
||||
* @return The response body as a blob.
|
||||
*/
|
||||
async asBlob(): Promise<Blob> {
|
||||
return await this.response.blob()
|
||||
}
|
||||
}
|
15
frontend/src/api/common/default-config.ts
Normal file
15
frontend/src/api/common/default-config.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const defaultHeaders: HeadersInit = {}
|
||||
|
||||
export const defaultConfig: Partial<RequestInit> = {
|
||||
mode: 'cors',
|
||||
cache: 'no-cache',
|
||||
credentials: 'same-origin',
|
||||
redirect: 'follow',
|
||||
referrerPolicy: 'no-referrer'
|
||||
}
|
19
frontend/src/api/config/index.ts
Normal file
19
frontend/src/api/config/index.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Config } from './types'
|
||||
import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
|
||||
|
||||
/**
|
||||
* Fetches the frontend config from the backend.
|
||||
*
|
||||
* @return The frontend config.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const getConfig = async (): Promise<Config> => {
|
||||
const response = await new GetApiRequestBuilder<Config>('config').sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
86
frontend/src/api/config/types.ts
Normal file
86
frontend/src/api/config/types.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
allowAnonymous: boolean
|
||||
allowRegister: boolean
|
||||
authProviders: AuthProvider[]
|
||||
branding: BrandingConfig
|
||||
useImageProxy: boolean
|
||||
specialUrls: SpecialUrls
|
||||
version: BackendVersion
|
||||
plantumlServer?: string
|
||||
maxDocumentLength: number
|
||||
}
|
||||
|
||||
export enum AuthProviderType {
|
||||
DROPBOX = 'dropbox',
|
||||
FACEBOOK = 'facebook',
|
||||
GITHUB = 'github',
|
||||
GOOGLE = 'google',
|
||||
TWITTER = 'twitter',
|
||||
GITLAB = 'gitlab',
|
||||
OAUTH2 = 'oauth2',
|
||||
LDAP = 'ldap',
|
||||
SAML = 'saml',
|
||||
LOCAL = 'local'
|
||||
}
|
||||
|
||||
export type AuthProviderTypeWithCustomName =
|
||||
| AuthProviderType.GITLAB
|
||||
| AuthProviderType.OAUTH2
|
||||
| AuthProviderType.LDAP
|
||||
| AuthProviderType.SAML
|
||||
|
||||
export type AuthProviderTypeWithoutCustomName =
|
||||
| AuthProviderType.DROPBOX
|
||||
| AuthProviderType.FACEBOOK
|
||||
| AuthProviderType.GITHUB
|
||||
| AuthProviderType.GOOGLE
|
||||
| AuthProviderType.TWITTER
|
||||
| AuthProviderType.LOCAL
|
||||
|
||||
export const authProviderTypeOneClick = [
|
||||
AuthProviderType.DROPBOX,
|
||||
AuthProviderType.FACEBOOK,
|
||||
AuthProviderType.GITHUB,
|
||||
AuthProviderType.GITLAB,
|
||||
AuthProviderType.GOOGLE,
|
||||
AuthProviderType.OAUTH2,
|
||||
AuthProviderType.SAML,
|
||||
AuthProviderType.TWITTER
|
||||
]
|
||||
|
||||
export interface AuthProviderWithCustomName {
|
||||
type: AuthProviderTypeWithCustomName
|
||||
identifier: string
|
||||
providerName: string
|
||||
}
|
||||
|
||||
export interface AuthProviderWithoutCustomName {
|
||||
type: AuthProviderTypeWithoutCustomName
|
||||
}
|
||||
|
||||
export type AuthProvider = AuthProviderWithCustomName | AuthProviderWithoutCustomName
|
||||
|
||||
export interface BrandingConfig {
|
||||
name?: string
|
||||
logo?: string
|
||||
}
|
||||
|
||||
export interface BackendVersion {
|
||||
major: number
|
||||
minor: number
|
||||
patch: number
|
||||
preRelease?: string
|
||||
commit?: string
|
||||
}
|
||||
|
||||
export interface SpecialUrls {
|
||||
privacy?: string
|
||||
termsOfUse?: string
|
||||
imprint?: string
|
||||
}
|
20
frontend/src/api/group/index.ts
Normal file
20
frontend/src/api/group/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { GroupInfo } from './types'
|
||||
import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
|
||||
|
||||
/**
|
||||
* Retrieves information about a group with a given name.
|
||||
*
|
||||
* @param groupName The name of the group.
|
||||
* @return Information about the group.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const getGroup = async (groupName: string): Promise<GroupInfo> => {
|
||||
const response = await new GetApiRequestBuilder<GroupInfo>('groups/' + groupName).sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
11
frontend/src/api/group/types.ts
Normal file
11
frontend/src/api/group/types.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export interface GroupInfo {
|
||||
name: string
|
||||
displayName: string
|
||||
special: boolean
|
||||
}
|
34
frontend/src/api/history/dto-methods.ts
Normal file
34
frontend/src/api/history/dto-methods.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { HistoryEntry, HistoryEntryPutDto, HistoryEntryWithOrigin } from './types'
|
||||
import { HistoryEntryOrigin } from './types'
|
||||
|
||||
/**
|
||||
* Transform a {@link HistoryEntry} into a {@link HistoryEntryWithOrigin}.
|
||||
*
|
||||
* @param entry the entry to build from
|
||||
* @return the history entry with an origin
|
||||
*/
|
||||
export const addRemoteOriginToHistoryEntry = (entry: HistoryEntry): HistoryEntryWithOrigin => {
|
||||
return {
|
||||
...entry,
|
||||
origin: HistoryEntryOrigin.REMOTE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link HistoryEntryPutDto} from a {@link HistoryEntry}.
|
||||
*
|
||||
* @param entry the entry to build the dto from
|
||||
* @return the dto for the api
|
||||
*/
|
||||
export const historyEntryToHistoryEntryPutDto = (entry: HistoryEntry): HistoryEntryPutDto => {
|
||||
return {
|
||||
pinStatus: entry.pinStatus,
|
||||
lastVisitedAt: entry.lastVisitedAt,
|
||||
note: entry.identifier
|
||||
}
|
||||
}
|
69
frontend/src/api/history/index.ts
Normal file
69
frontend/src/api/history/index.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { ChangePinStatusDto, HistoryEntry, HistoryEntryPutDto } from './types'
|
||||
import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
|
||||
import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder'
|
||||
import { PutApiRequestBuilder } from '../common/api-request-builder/put-api-request-builder'
|
||||
import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-api-request-builder'
|
||||
|
||||
/**
|
||||
* Fetches the remote history for the user from the server.
|
||||
*
|
||||
* @return The remote history entries of the user.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const getRemoteHistory = async (): Promise<HistoryEntry[]> => {
|
||||
const response = await new GetApiRequestBuilder<HistoryEntry[]>('me/history').sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the remote history of the user with the given history entries.
|
||||
*
|
||||
* @param entries The history entries to store remotely.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const setRemoteHistoryEntries = async (entries: HistoryEntryPutDto[]): Promise<void> => {
|
||||
await new PostApiRequestBuilder<void, HistoryEntryPutDto[]>('me/history').withJsonBody(entries).sendRequest()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a remote history entry's pin state.
|
||||
*
|
||||
* @param noteIdOrAlias The note id for which to update the pinning state.
|
||||
* @param pinStatus True when the note should be pinned, false otherwise.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const updateRemoteHistoryEntryPinStatus = async (
|
||||
noteIdOrAlias: string,
|
||||
pinStatus: boolean
|
||||
): Promise<HistoryEntry> => {
|
||||
const response = await new PutApiRequestBuilder<HistoryEntry, ChangePinStatusDto>('me/history/' + noteIdOrAlias)
|
||||
.withJsonBody({
|
||||
pinStatus
|
||||
})
|
||||
.sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a remote history entry.
|
||||
*
|
||||
* @param noteIdOrAlias The note id or alias of the history entry to remove.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const deleteRemoteHistoryEntry = async (noteIdOrAlias: string): Promise<void> => {
|
||||
await new DeleteApiRequestBuilder('me/history/' + noteIdOrAlias).sendRequest()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the complete remote history.
|
||||
*
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const deleteRemoteHistory = async (): Promise<void> => {
|
||||
await new DeleteApiRequestBuilder('me/history').sendRequest()
|
||||
}
|
31
frontend/src/api/history/types.ts
Normal file
31
frontend/src/api/history/types.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
export enum HistoryEntryOrigin {
|
||||
LOCAL = 'local',
|
||||
REMOTE = 'remote'
|
||||
}
|
||||
|
||||
export interface HistoryEntryPutDto {
|
||||
note: string
|
||||
pinStatus: boolean
|
||||
lastVisitedAt: string
|
||||
}
|
||||
|
||||
export interface HistoryEntry {
|
||||
identifier: string
|
||||
title: string
|
||||
lastVisitedAt: string
|
||||
tags: string[]
|
||||
pinStatus: boolean
|
||||
}
|
||||
|
||||
export interface HistoryEntryWithOrigin extends HistoryEntry {
|
||||
origin: HistoryEntryOrigin
|
||||
}
|
||||
|
||||
export interface ChangePinStatusDto {
|
||||
pinStatus: boolean
|
||||
}
|
55
frontend/src/api/me/index.ts
Normal file
55
frontend/src/api/me/index.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { MediaUpload } from '../media/types'
|
||||
import type { ChangeDisplayNameDto, LoginUserInfo } from './types'
|
||||
import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
|
||||
import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-api-request-builder'
|
||||
import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder'
|
||||
|
||||
/**
|
||||
* Returns metadata about the currently signed-in user from the API.
|
||||
*
|
||||
* @return The user metadata.
|
||||
* @throws {Error} when the user is not signed-in.
|
||||
*/
|
||||
export const getMe = async (): Promise<LoginUserInfo> => {
|
||||
const response = await new GetApiRequestBuilder<LoginUserInfo>('me').sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the current user from the server.
|
||||
*
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const deleteUser = async (): Promise<void> => {
|
||||
await new DeleteApiRequestBuilder('me').sendRequest()
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the display name of the current user.
|
||||
*
|
||||
* @param displayName The new display name to set.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const updateDisplayName = async (displayName: string): Promise<void> => {
|
||||
await new PostApiRequestBuilder<void, ChangeDisplayNameDto>('me/profile')
|
||||
.withJsonBody({
|
||||
displayName
|
||||
})
|
||||
.sendRequest()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of media belonging to the user.
|
||||
*
|
||||
* @return List of media object information.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const getMyMedia = async (): Promise<MediaUpload[]> => {
|
||||
const response = await new GetApiRequestBuilder<MediaUpload[]>('me/media').sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
15
frontend/src/api/me/types.ts
Normal file
15
frontend/src/api/me/types.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { UserInfo } from '../users/types'
|
||||
|
||||
export interface LoginUserInfo extends UserInfo {
|
||||
authProvider: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface ChangeDisplayNameDto {
|
||||
displayName: string
|
||||
}
|
53
frontend/src/api/media/index.ts
Normal file
53
frontend/src/api/media/index.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { ImageProxyRequestDto, ImageProxyResponse, MediaUpload } from './types'
|
||||
import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder'
|
||||
import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-api-request-builder'
|
||||
|
||||
/**
|
||||
* Requests an image-proxy URL from the backend for a given image URL.
|
||||
*
|
||||
* @param imageUrl The image URL which should be proxied.
|
||||
* @return The proxy URL for the image.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const getProxiedUrl = async (imageUrl: string): Promise<ImageProxyResponse> => {
|
||||
const response = await new PostApiRequestBuilder<ImageProxyResponse, ImageProxyRequestDto>('media/proxy')
|
||||
.withJsonBody({
|
||||
url: imageUrl
|
||||
})
|
||||
.sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a media file to the backend.
|
||||
*
|
||||
* @param noteIdOrAlias The id or alias of the note from which the media is uploaded.
|
||||
* @param media The binary media content.
|
||||
* @return The URL of the uploaded media object.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const uploadFile = async (noteIdOrAlias: string, media: Blob): Promise<MediaUpload> => {
|
||||
const postData = new FormData()
|
||||
postData.append('file', media)
|
||||
const response = await new PostApiRequestBuilder<MediaUpload, void>('media')
|
||||
.withHeader('Content-Type', 'multipart/form-data')
|
||||
.withHeader('HedgeDoc-Note', noteIdOrAlias)
|
||||
.withBody(postData)
|
||||
.sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes some uploaded media object.
|
||||
*
|
||||
* @param mediaId The identifier of the media object to delete.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const deleteUploadedMedia = async (mediaId: string): Promise<void> => {
|
||||
await new DeleteApiRequestBuilder('media/' + mediaId).sendRequest()
|
||||
}
|
19
frontend/src/api/media/types.ts
Normal file
19
frontend/src/api/media/types.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
export interface MediaUpload {
|
||||
url: string
|
||||
noteId: string | null
|
||||
createdAt: string
|
||||
username: string
|
||||
}
|
||||
|
||||
export interface ImageProxyResponse {
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface ImageProxyRequestDto {
|
||||
url: string
|
||||
}
|
94
frontend/src/api/notes/index.ts
Normal file
94
frontend/src/api/notes/index.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { Note, NoteDeletionOptions, NoteMetadata } from './types'
|
||||
import type { MediaUpload } from '../media/types'
|
||||
import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
|
||||
import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder'
|
||||
import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-api-request-builder'
|
||||
|
||||
/**
|
||||
* Retrieves the content and metadata about the specified note.
|
||||
*
|
||||
* @param noteIdOrAlias The id or alias of the note.
|
||||
* @return Content and metadata of the specified note.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const getNote = async (noteIdOrAlias: string): Promise<Note> => {
|
||||
const response = await new GetApiRequestBuilder<Note>('notes/' + noteIdOrAlias)
|
||||
.withStatusCodeErrorMapping({ 404: 'api.note.notFound', 403: 'api.note.forbidden' })
|
||||
.sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the metadata of the specified note.
|
||||
*
|
||||
* @param noteIdOrAlias The id or alias of the note.
|
||||
* @return Metadata of the specified note.
|
||||
*/
|
||||
export const getNoteMetadata = async (noteIdOrAlias: string): Promise<NoteMetadata> => {
|
||||
const response = await new GetApiRequestBuilder<NoteMetadata>(`notes/${noteIdOrAlias}/metadata`).sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of media objects associated with the specified note.
|
||||
*
|
||||
* @param noteIdOrAlias The id or alias of the note.
|
||||
* @return List of media object metadata associated with specified note.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const getMediaForNote = async (noteIdOrAlias: string): Promise<MediaUpload[]> => {
|
||||
const response = await new GetApiRequestBuilder<MediaUpload[]>(`notes/${noteIdOrAlias}/media`).sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new note with a given markdown content.
|
||||
*
|
||||
* @param markdown The content of the new note.
|
||||
* @return Content and metadata of the new note.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const createNote = async (markdown: string): Promise<Note> => {
|
||||
const response = await new PostApiRequestBuilder<Note, void>('notes')
|
||||
.withHeader('Content-Type', 'text/markdown')
|
||||
.withBody(markdown)
|
||||
.sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new note with a given markdown content and a defined primary alias.
|
||||
*
|
||||
* @param markdown The content of the new note.
|
||||
* @param primaryAlias The primary alias of the new note.
|
||||
* @return Content and metadata of the new note.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const createNoteWithPrimaryAlias = async (markdown: string, primaryAlias: string): Promise<Note> => {
|
||||
const response = await new PostApiRequestBuilder<Note, void>('notes/' + primaryAlias)
|
||||
.withHeader('Content-Type', 'text/markdown')
|
||||
.withBody(markdown)
|
||||
.sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the specified note.
|
||||
*
|
||||
* @param noteIdOrAlias The id or alias of the note to delete.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const deleteNote = async (noteIdOrAlias: string): Promise<void> => {
|
||||
await new DeleteApiRequestBuilder<void, NoteDeletionOptions>('notes/' + noteIdOrAlias)
|
||||
.withJsonBody({
|
||||
keepMedia: false
|
||||
// TODO Ask whether the user wants to keep the media uploaded to the note.
|
||||
// https://github.com/hedgedoc/react-client/issues/2288
|
||||
})
|
||||
.sendRequest()
|
||||
}
|
56
frontend/src/api/notes/types.ts
Normal file
56
frontend/src/api/notes/types.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { Alias } from '../alias/types'
|
||||
|
||||
export interface Note {
|
||||
content: string
|
||||
metadata: NoteMetadata
|
||||
editedByAtPosition: NoteEdit[]
|
||||
}
|
||||
|
||||
export interface NoteMetadata {
|
||||
id: string
|
||||
aliases: Alias[]
|
||||
primaryAddress: string
|
||||
title: string
|
||||
description: string
|
||||
tags: string[]
|
||||
updatedAt: string
|
||||
updateUsername: string | null
|
||||
viewCount: number
|
||||
createdAt: string
|
||||
editedBy: string[]
|
||||
permissions: NotePermissions
|
||||
version: number
|
||||
}
|
||||
|
||||
export interface NoteEdit {
|
||||
username: string | null
|
||||
startPos: number
|
||||
endPos: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface NotePermissions {
|
||||
owner: string | null
|
||||
sharedToUsers: NoteUserPermissionEntry[]
|
||||
sharedToGroups: NoteGroupPermissionEntry[]
|
||||
}
|
||||
|
||||
export interface NoteUserPermissionEntry {
|
||||
username: string
|
||||
canEdit: boolean
|
||||
}
|
||||
|
||||
export interface NoteGroupPermissionEntry {
|
||||
groupName: string
|
||||
canEdit: boolean
|
||||
}
|
||||
|
||||
export interface NoteDeletionOptions {
|
||||
keepMedia: boolean
|
||||
}
|
110
frontend/src/api/permissions/index.ts
Normal file
110
frontend/src/api/permissions/index.ts
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { NotePermissions } from '../notes/types'
|
||||
import type { OwnerChangeDto, PermissionSetDto } from './types'
|
||||
import { PutApiRequestBuilder } from '../common/api-request-builder/put-api-request-builder'
|
||||
import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-api-request-builder'
|
||||
|
||||
/**
|
||||
* Sets the owner of a note.
|
||||
*
|
||||
* @param noteId The id of the note.
|
||||
* @param owner The username of the new owner.
|
||||
* @return The updated {@link NotePermissions}.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const setNoteOwner = async (noteId: string, owner: string): Promise<NotePermissions> => {
|
||||
const response = await new PutApiRequestBuilder<NotePermissions, OwnerChangeDto>(
|
||||
`notes/${noteId}/metadata/permissions/owner`
|
||||
)
|
||||
.withJsonBody({
|
||||
owner
|
||||
})
|
||||
.sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a permission for one user of a note.
|
||||
*
|
||||
* @param noteId The id of the note.
|
||||
* @param username The username of the user to set the permission for.
|
||||
* @param canEdit true if the user should be able to update the note, false otherwise.
|
||||
* @return The updated {@link NotePermissions}.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const setUserPermission = async (
|
||||
noteId: string,
|
||||
username: string,
|
||||
canEdit: boolean
|
||||
): Promise<NotePermissions> => {
|
||||
const response = await new PutApiRequestBuilder<NotePermissions, PermissionSetDto>(
|
||||
`notes/${noteId}/metadata/permissions/users/${username}`
|
||||
)
|
||||
.withJsonBody({
|
||||
canEdit
|
||||
})
|
||||
.sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a permission for one group of a note.
|
||||
*
|
||||
* @param noteId The id of the note.
|
||||
* @param groupName The name of the group to set the permission for.
|
||||
* @param canEdit true if the group should be able to update the note, false otherwise.
|
||||
* @return The updated {@link NotePermissions}.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const setGroupPermission = async (
|
||||
noteId: string,
|
||||
groupName: string,
|
||||
canEdit: boolean
|
||||
): Promise<NotePermissions> => {
|
||||
const response = await new PutApiRequestBuilder<NotePermissions, PermissionSetDto>(
|
||||
`notes/${noteId}/metadata/permissions/groups/${groupName}`
|
||||
)
|
||||
.withJsonBody({
|
||||
canEdit
|
||||
})
|
||||
.sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the permissions of a note for a user.
|
||||
*
|
||||
* @param noteId The id of the note.
|
||||
* @param username The name of the user to remove the permission of.
|
||||
* @return The updated {@link NotePermissions}.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const removeUserPermission = async (noteId: string, username: string): Promise<NotePermissions> => {
|
||||
const response = await new DeleteApiRequestBuilder<NotePermissions>(
|
||||
`notes/${noteId}/metadata/permissions/users/${username}`
|
||||
)
|
||||
.withExpectedStatusCode(200)
|
||||
.sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the permissions of a note for a group.
|
||||
*
|
||||
* @param noteId The id of the note.
|
||||
* @param groupName The name of the group to remove the permission of.
|
||||
* @return The updated {@link NotePermissions}.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const removeGroupPermission = async (noteId: string, groupName: string): Promise<NotePermissions> => {
|
||||
const response = await new DeleteApiRequestBuilder<NotePermissions>(
|
||||
`notes/${noteId}/metadata/permissions/groups/${groupName}`
|
||||
)
|
||||
.withExpectedStatusCode(200)
|
||||
.sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
12
frontend/src/api/permissions/types.ts
Normal file
12
frontend/src/api/permissions/types.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
export interface OwnerChangeDto {
|
||||
owner: string
|
||||
}
|
||||
|
||||
export interface PermissionSetDto {
|
||||
canEdit: boolean
|
||||
}
|
45
frontend/src/api/revisions/index.ts
Normal file
45
frontend/src/api/revisions/index.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { RevisionDetails, RevisionMetadata } from './types'
|
||||
import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
|
||||
import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-api-request-builder'
|
||||
|
||||
/**
|
||||
* Retrieves a note revision while using a cache for often retrieved revisions.
|
||||
*
|
||||
* @param noteId The id of the note for which to fetch the revision.
|
||||
* @param revisionId The id of the revision to fetch.
|
||||
* @return The revision.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const getRevision = async (noteId: string, revisionId: number): Promise<RevisionDetails> => {
|
||||
const response = await new GetApiRequestBuilder<RevisionDetails>(
|
||||
`notes/${noteId}/revisions/${revisionId}`
|
||||
).sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of all revisions stored for a given note.
|
||||
*
|
||||
* @param noteId The id of the note for which to look up the stored revisions.
|
||||
* @return A list of revision ids.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const getAllRevisions = async (noteId: string): Promise<RevisionMetadata[]> => {
|
||||
const response = await new GetApiRequestBuilder<RevisionMetadata[]>(`notes/${noteId}/revisions`).sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all revisions for a note.
|
||||
*
|
||||
* @param noteIdOrAlias The id or alias of the note to delete all revisions for.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const deleteRevisionsForNote = async (noteIdOrAlias: string): Promise<void> => {
|
||||
await new DeleteApiRequestBuilder(`notes/${noteIdOrAlias}/revisions`).sendRequest()
|
||||
}
|
21
frontend/src/api/revisions/types.ts
Normal file
21
frontend/src/api/revisions/types.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { NoteEdit } from '../notes/types'
|
||||
|
||||
export interface RevisionDetails extends RevisionMetadata {
|
||||
content: string
|
||||
patch: string
|
||||
edits: NoteEdit[]
|
||||
}
|
||||
|
||||
export interface RevisionMetadata {
|
||||
id: number
|
||||
createdAt: string
|
||||
length: number
|
||||
authorUsernames: string[]
|
||||
anonymousAuthorCount: number
|
||||
}
|
48
frontend/src/api/tokens/index.ts
Normal file
48
frontend/src/api/tokens/index.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { AccessToken, AccessTokenWithSecret, CreateAccessTokenDto } from './types'
|
||||
import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
|
||||
import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder'
|
||||
import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-api-request-builder'
|
||||
|
||||
/**
|
||||
* Retrieves the access tokens for the current user.
|
||||
*
|
||||
* @return List of access token metadata.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const getAccessTokenList = async (): Promise<AccessToken[]> => {
|
||||
const response = await new GetApiRequestBuilder<AccessToken[]>('tokens').sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new access token for the current user.
|
||||
*
|
||||
* @param label The user-defined label for the new access token.
|
||||
* @param validUntil The user-defined expiry date of the new access token in milliseconds of unix time.
|
||||
* @return The new access token metadata along with its secret.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const postNewAccessToken = async (label: string, validUntil: number): Promise<AccessTokenWithSecret> => {
|
||||
const response = await new PostApiRequestBuilder<AccessTokenWithSecret, CreateAccessTokenDto>('tokens')
|
||||
.withJsonBody({
|
||||
label,
|
||||
validUntil
|
||||
})
|
||||
.sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an access token from the current user account.
|
||||
*
|
||||
* @param keyId The key id of the access token to delete.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const deleteAccessToken = async (keyId: string): Promise<void> => {
|
||||
await new DeleteApiRequestBuilder('tokens/' + keyId).sendRequest()
|
||||
}
|
22
frontend/src/api/tokens/types.ts
Normal file
22
frontend/src/api/tokens/types.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export interface AccessToken {
|
||||
label: string
|
||||
validUntil: string
|
||||
keyId: string
|
||||
createdAt: string
|
||||
lastUsedAt: string | null
|
||||
}
|
||||
|
||||
export interface AccessTokenWithSecret extends AccessToken {
|
||||
secret: string
|
||||
}
|
||||
|
||||
export interface CreateAccessTokenDto {
|
||||
label: string
|
||||
validUntil: number
|
||||
}
|
20
frontend/src/api/users/index.ts
Normal file
20
frontend/src/api/users/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { UserInfo } from './types'
|
||||
import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
|
||||
|
||||
/**
|
||||
* Retrieves information about a specific user while using a cache to avoid many requests for the same username.
|
||||
*
|
||||
* @param username The username of interest.
|
||||
* @return Metadata about the requested user.
|
||||
* @throws {Error} when the api request wasn't successful.
|
||||
*/
|
||||
export const getUser = async (username: string): Promise<UserInfo> => {
|
||||
const response = await new GetApiRequestBuilder<UserInfo>('users/' + username).sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
11
frontend/src/api/users/types.ts
Normal file
11
frontend/src/api/users/types.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export interface UserInfo {
|
||||
username: string
|
||||
displayName: string
|
||||
photo: string
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue