2020-11-05 18:58:23 +02:00
import { PaginationOrderDir } from '../../models/utils/types' ;
import { ErrorMethodNotAllowed , ErrorForbidden , ErrorBadRequest , ErrorNotFound } from './utils/errors' ;
import route_folders from './routes/folders' ;
import route_notes from './routes/notes' ;
import route_resources from './routes/resources' ;
import route_tags from './routes/tags' ;
import route_master_keys from './routes/master_keys' ;
import route_search from './routes/search' ;
import route_ping from './routes/ping' ;
2021-06-22 20:57:04 +02:00
import route_auth from './routes/auth' ;
2021-08-30 19:53:24 +02:00
import route_events from './routes/events' ;
2023-10-06 16:03:32 +02:00
import route_revisions from './routes/revisions' ;
2020-11-05 18:58:23 +02:00
const { ltrimSlashes } = require ( '../../path-utils' ) ;
const md5 = require ( 'md5' ) ;
export enum RequestMethod {
GET = 'GET' ,
POST = 'POST' ,
PUT = 'PUT' ,
DELETE = 'DELETE' ,
}
2021-06-22 20:57:04 +02:00
export interface RequestFile {
2020-11-12 21:29:22 +02:00
path : string ;
2020-11-05 18:58:23 +02:00
}
interface RequestQuery {
2020-11-12 21:29:22 +02:00
fields? : string [ ] | string ;
token? : string ;
nounce? : string ;
page? : number ;
2020-11-05 18:58:23 +02:00
// Search engine query
2020-11-12 21:29:22 +02:00
query? : string ;
type ? : string ; // Model type as a string (eg. "note", "folder")
2020-11-05 18:58:23 +02:00
2020-11-12 21:29:22 +02:00
as_tree? : number ;
2020-11-05 18:58:23 +02:00
// Pagination
2020-11-12 21:29:22 +02:00
limit? : number ;
order_dir? : PaginationOrderDir ;
order_by? : string ;
2021-06-22 20:57:04 +02:00
// Auth token
auth_token? : string ;
2021-08-30 19:53:24 +02:00
// Event cursor
cursor? : string ;
2024-03-02 16:25:27 +02:00
// For note deletion
permanent? : string ;
2024-05-03 17:14:04 +02:00
include_deleted? : string ;
include_conflicts? : string ;
2020-11-05 18:58:23 +02:00
}
export interface Request {
2020-11-12 21:29:22 +02:00
method : RequestMethod ;
path : string ;
query : RequestQuery ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-12 21:29:22 +02:00
body : any ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-12 21:29:22 +02:00
bodyJson_ : any ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-12 21:29:22 +02:00
bodyJson : any ;
files : RequestFile [ ] ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-12 21:29:22 +02:00
params : any [ ] ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-12 21:29:22 +02:00
action? : any ;
2020-11-05 18:58:23 +02:00
}
2021-06-22 20:57:04 +02:00
export enum AuthTokenStatus {
Waiting = 'waiting' ,
Accepted = 'accepted' ,
Rejected = 'rejected' ,
}
interface AuthToken {
value : string ;
status : AuthTokenStatus ;
}
export interface RequestContext {
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2021-06-22 20:57:04 +02:00
dispatch : Function ;
authToken : AuthToken ;
token : string ;
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-06-22 20:57:04 +02:00
type RouteFunction = ( request : Request , id : string , link : string , context : RequestContext ) = > Promise < any | void > ;
2020-11-05 18:58:23 +02:00
interface ResourceNameToRoute {
2020-11-12 21:13:28 +02:00
[ key : string ] : RouteFunction ;
2020-11-05 18:58:23 +02:00
}
export default class Api {
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2020-11-12 21:13:28 +02:00
private token_ : string | Function ;
2021-06-22 20:57:04 +02:00
private authToken_ : AuthToken = null ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-12 21:13:28 +02:00
private knownNounces_ : any = { } ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-12 21:13:28 +02:00
private actionApi_ : any ;
private resourceNameToRoute_ : ResourceNameToRoute = { } ;
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2021-06-22 20:57:04 +02:00
private dispatch_ : Function ;
2020-11-05 18:58:23 +02:00
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
2021-06-22 20:57:04 +02:00
public constructor ( token : string | Function = null , dispatch : Function = null , actionApi : any = null ) {
2020-11-05 18:58:23 +02:00
this . token_ = token ;
this . actionApi_ = actionApi ;
2021-06-22 20:57:04 +02:00
this . dispatch_ = dispatch ;
2020-11-05 18:58:23 +02:00
this . resourceNameToRoute_ = {
ping : route_ping ,
notes : route_notes ,
folders : route_folders ,
tags : route_tags ,
resources : route_resources ,
master_keys : route_master_keys ,
search : route_search ,
services : this.action_services.bind ( this ) ,
2021-06-22 20:57:04 +02:00
auth : route_auth ,
2021-08-30 19:53:24 +02:00
events : route_events ,
2023-10-06 16:03:32 +02:00
revisions : route_revisions ,
2020-11-05 18:58:23 +02:00
} ;
2021-06-22 20:57:04 +02:00
this . dispatch = this . dispatch . bind ( this ) ;
2020-11-05 18:58:23 +02:00
}
2021-06-22 20:57:04 +02:00
public get token ( ) : string {
2020-11-05 18:58:23 +02:00
return typeof this . token_ === 'function' ? this . token_ ( ) : this . token_ ;
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-06-22 20:57:04 +02:00
private dispatch ( action : any ) {
if ( action . type === 'API_AUTH_TOKEN_SET' ) {
this . authToken_ = {
value : action.value ,
status : AuthTokenStatus.Waiting ,
} ;
this . dispatch_ ( {
type : 'API_NEED_AUTH_SET' ,
value : true ,
} ) ;
return ;
}
return this . dispatch_ ( action ) ;
}
public acceptAuthToken ( accept : boolean ) {
2021-07-19 10:46:32 +02:00
if ( ! this . authToken_ ) throw new Error ( 'Auth token is not set' ) ;
2021-06-22 20:57:04 +02:00
this . authToken_ . status = accept ? AuthTokenStatus.Accepted : AuthTokenStatus.Rejected ;
this . dispatch_ ( {
type : 'API_NEED_AUTH_SET' ,
value : false ,
} ) ;
}
2020-11-12 21:13:28 +02:00
private parsePath ( path : string ) {
2020-11-05 18:58:23 +02:00
path = ltrimSlashes ( path ) ;
if ( ! path ) return { fn : null , params : [ ] } ;
const pathParts = path . split ( '/' ) ;
const callSuffix = pathParts . splice ( 0 , 1 ) [ 0 ] ;
const fn = this . resourceNameToRoute_ [ callSuffix ] ;
return {
fn : fn ,
params : pathParts ,
} ;
}
// Response can be any valid JSON object, so a string, and array or an object (key/value pairs).
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-12 21:13:28 +02:00
public async route ( method : RequestMethod , path : string , query : RequestQuery = null , body : any = null , files : RequestFile [ ] = null ) : Promise < any > {
2020-11-05 18:58:23 +02:00
if ( ! files ) files = [ ] ;
if ( ! query ) query = { } ;
const parsedPath = this . parsePath ( path ) ;
if ( ! parsedPath . fn ) throw new ErrorNotFound ( ) ; // Nothing at the root yet
if ( query && query . nounce ) {
const requestMd5 = md5 ( JSON . stringify ( [ method , path , body , query , files . length ] ) ) ;
if ( this . knownNounces_ [ query . nounce ] === requestMd5 ) {
throw new ErrorBadRequest ( 'Duplicate Nounce' ) ;
}
this . knownNounces_ [ query . nounce ] = requestMd5 ;
}
let id = null ;
let link = null ;
const params = parsedPath . params ;
if ( params . length >= 1 ) {
id = params [ 0 ] ;
params . splice ( 0 , 1 ) ;
if ( params . length >= 1 ) {
link = params [ 0 ] ;
params . splice ( 0 , 1 ) ;
}
}
2020-11-12 21:13:28 +02:00
const request : Request = {
2020-11-05 18:58:23 +02:00
method ,
path : ltrimSlashes ( path ) ,
query : query ? query : { } ,
body ,
bodyJson_ : null ,
2020-11-12 21:13:28 +02:00
bodyJson : function ( disallowedProperties : string [ ] = null ) {
2020-11-05 18:58:23 +02:00
if ( ! this . bodyJson_ ) this . bodyJson_ = JSON . parse ( this . body ) ;
if ( disallowedProperties ) {
2023-06-01 13:02:36 +02:00
const filteredBody = { . . . this . bodyJson_ } ;
2020-11-05 18:58:23 +02:00
for ( let i = 0 ; i < disallowedProperties . length ; i ++ ) {
const n = disallowedProperties [ i ] ;
delete filteredBody [ n ] ;
}
return filteredBody ;
}
return this . bodyJson_ ;
} ,
files ,
params ,
} ;
this . checkToken_ ( request ) ;
2021-06-22 20:57:04 +02:00
const context : RequestContext = {
dispatch : this.dispatch ,
token : this.token ,
authToken : this.authToken_ ,
} ;
2020-11-05 18:58:23 +02:00
try {
2021-06-22 20:57:04 +02:00
return await parsedPath . fn ( request , id , link , context ) ;
2020-11-05 18:58:23 +02:00
} catch ( error ) {
if ( ! error . httpCode ) error . httpCode = 500 ;
throw error ;
}
}
2020-11-12 21:13:28 +02:00
private checkToken_ ( request : Request ) {
2020-11-05 18:58:23 +02:00
// For now, whitelist some calls to allow the web clipper to work
// without an extra auth step
2021-06-22 20:57:04 +02:00
// const whiteList = [['GET', 'ping'], ['GET', 'tags'], ['GET', 'folders'], ['POST', 'notes']];
const whiteList = [
[ 'GET' , 'ping' ] ,
[ 'GET' , 'auth' ] ,
[ 'POST' , 'auth' ] ,
[ 'GET' , 'auth/check' ] ,
] ;
2020-11-05 18:58:23 +02:00
for ( let i = 0 ; i < whiteList . length ; i ++ ) {
if ( whiteList [ i ] [ 0 ] === request . method && whiteList [ i ] [ 1 ] === request . path ) return ;
}
2021-06-22 20:57:04 +02:00
// If the API has been initialized without a token, it means no auth is
// needed. This is for example when it is used as the plugin data API.
2020-11-05 18:58:23 +02:00
if ( ! this . token ) return ;
2021-06-22 20:57:04 +02:00
2020-11-05 18:58:23 +02:00
if ( ! request . query || ! request . query . token ) throw new ErrorForbidden ( 'Missing "token" parameter' ) ;
if ( request . query . token !== this . token ) throw new ErrorForbidden ( 'Invalid "token" parameter' ) ;
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-12 21:13:28 +02:00
private async execServiceActionFromRequest_ ( externalApi : any , request : Request ) {
2020-11-05 18:58:23 +02:00
const action = externalApi [ request . action ] ;
if ( ! action ) throw new ErrorNotFound ( ` Invalid action: ${ request . action } ` ) ;
2023-06-01 13:02:36 +02:00
const args = { . . . request } ;
2020-11-05 18:58:23 +02:00
delete args . action ;
return action ( args ) ;
}
2020-11-12 21:13:28 +02:00
private async action_services ( request : Request , serviceName : string ) {
2020-11-05 18:58:23 +02:00
if ( request . method !== RequestMethod . POST ) throw new ErrorMethodNotAllowed ( ) ;
if ( ! this . actionApi_ ) throw new ErrorNotFound ( 'No action API has been setup!' ) ;
if ( ! this . actionApi_ [ serviceName ] ) throw new ErrorNotFound ( ` No such service: ${ serviceName } ` ) ;
const externalApi = this . actionApi_ [ serviceName ] ( ) ;
return this . execServiceActionFromRequest_ ( externalApi , JSON . parse ( request . body ) ) ;
}
}