diff --git a/Dockerfile b/Dockerfile index dd2fc3a6..4fd337d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -92,7 +92,8 @@ ENV BASE_URL= \ SMS_API_KEY= \ RATE_LIMITING_WINDOW= \ RATE_LIMITING_NB_REQUESTS= \ - TRUSTED_PROXIES= + TRUSTED_PROXIES= \ + QRCODE_URL= COPY --from=1 /usr/src/app /usr/src/app/ diff --git a/docs/openapi.json b/docs/openapi.json index 2394db39..aeecbc58 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1 +1 @@ -{"openapi":"3.0.0","info":{"title":"Twake on Matrix APIs documentation","version":"0.0.1","description":"This is The documentation of all available APIs of this repository"},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"MatrixError":{"type":"object","properties":{"errcode":{"type":"string","description":"A Matrix error code"},"error":{"type":"string","description":"A human-readable error message"}},"required":["error"]},"ActiveContacts":{"type":"object","description":"the list of active contacts","properties":{"contacts":{"type":"string","description":"active contacts"}}},"MutualRooms":{"type":"array","items":{"type":"object","properties":{"roomId":{"type":"string","description":"the room id"},"name":{"type":"string","description":"the room name"},"topic":{"type":"string","description":"the room topic"},"room_type":{"type":"string","description":"the room type"}}}},"PrivateNote":{"type":"object","properties":{"id":{"type":"string","description":"The private note id"},"content":{"type":"string","description":"The private note content"},"authorId":{"type":"string","description":"The author user id"},"targetId":{"type":"string","description":"The target user id"}}},"CreatePrivateNote":{"type":"object","properties":{"content":{"type":"string","description":"The private note content"},"authorId":{"type":"string","description":"The author user id"},"targetId":{"type":"string","description":"The target user id"}}},"UpdatePrivateNote":{"type":"object","properties":{"id":{"type":"string","description":"The private note id"},"content":{"type":"string","description":"The private note content"}}},"RoomTags":{"type":"object","properties":{"tags":{"description":"the room tags list","type":"array","items":{"type":"string"}}}},"RoomTagCreation":{"type":"object","properties":{"content":{"type":"array","description":"the room tags strings","items":{"type":"string"}},"roomId":{"type":"string","description":"the room id"}}},"RoomTagsUpdate":{"type":"object","properties":{"content":{"type":"array","description":"the room tags strings","items":{"type":"string"}}}},"sms":{"type":"object","properties":{"to":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"text":{"type":"string"}}},"UserInfo":{"type":"object","properties":{"uid":{"type":"string","description":"the user id"},"givenName":{"type":"string","description":"the user given name"},"sn":{"type":"string","description":"the user surname"}}}},"responses":{"InternalServerError":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"The message describing the internal error"}}}}}},"Unauthorized":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_UNAUTHORIZED","error":"Unauthorized"}}}},"BadRequest":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_MISSING_PARAMS","error":"Properties are missing in the request body"}}}},"Forbidden":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_FORBIDDEN","error":"Forbidden"}}}},"Conflict":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"error":"Conflict"}}}},"PermanentRedirect":{"description":"Permanent Redirect","headers":{"Location":{"schema":{"type":"string","description":"URL to use for recdirect"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_UNKNOWN","error":"This non-standard endpoint has been removed"}}}},"NotFound":{"description":"Private note not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_NOT_FOUND","error":"Not Found"}}}},"Unrecognized":{"description":"Unrecognized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_UNRECOGNIZED","error":"Unrecognized"}}}},"Created":{"description":"Created"},"NoContent":{"description":"operation successful and no content returned"},"InternalError":{"description":"Internal error"}},"parameters":{"target_userid":{"name":"target_userid","in":"path","required":true,"description":"the target user id","schema":{"type":"string"}},"user_id":{"name":"user_id","in":"query","description":"the author user id","required":true,"schema":{"type":"string"}},"target_user_id":{"name":"target_user_id","in":"query","description":"the target user id","required":true,"schema":{"type":"string"}},"private_note_id":{"name":"private_note_id","in":"path","description":"the private note id","required":true,"schema":{"type":"string"}},"roomId":{"in":"path","name":"roomId","description":"the room id","required":true,"schema":{"type":"string"}},"userId":{"in":"path","name":"userId","description":"the user id","required":true,"schema":{"type":"string"}}}},"security":[{"bearerAuth":[]}],"paths":{"/_matrix/identity/v2":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2"}},"/_matrix/identity/v2/hash_details":{"get":{"tags":["Federated identity service"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2hash_details"}},"/_matrix/identity/v2/lookup":{"post":{"tags":["Federated identity service"],"description":"Extends https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2lookup to display inactive users and 3PID users","requestBody":{"description":"Object containing hashes of mails/phones to search","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"addresses":{"type":"array","items":{"type":"string","description":"List of (hashed) addresses to lookup"}},"algorithm":{"type":"string","description":"Algorithm the client is using to encode the addresses"},"pepper":{"type":"string","description":"Pepper from '/hash_details'"}},"required":["addresses","algorithm","pepper"]},"example":{"addresses":["4kenr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","nlo35_T5fzSGZzJApqu8lgIudJvmOQtDaHtr-I4rU7I"],"algorithm":"sha256","pepper":"matrixrocks"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"mappings":{"type":"object","additionalProperties":{"type":"string"},"description":"List of active accounts"},"inactive_mappings":{"type":"object","additionalProperties":{"type":"string"},"description":"List of inactive accounts"},"third_party_mappings":{"type":"object","description":"List of hashed addresses by identity server hostname","properties":{"hostname":{"type":"object","properties":{"actives":{"type":"array","items":{"type":"string","description":"List of (hashed) active accounts addresses matching request body addresses"}},"inactives":{"type":"array","items":{"type":"string","description":"List of (hashed) inactive accounts addresses matching request body addresses"}}}}}}}},"example":{"mappings":{"4kenr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc":"@dwho:company.com"},"inactive_mappings":{"nlo35_T5fzSGZzJApqu8lgIudJvmOQtDaHtr-I4rU7I":"@rtyler:company.com"},"third_party_mappings":{"identity1.example.com":{"actives":["78jnr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","gtr42_T5fzSGZzJAmlp5lgIudJvmOQtDaHtr-I4rU7I"],"inactives":["qfgt57N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","lnbc8_T5fzSGZzJAmlp5lgIudJvmOQtDaHtr-I4rU7I"]}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/identity/v2/account":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2account"}},"/_matrix/identity/v2/account/register":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2accountregister"}},"/_matrix/identity/v2/account/logout":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2accountlogout"}},"/_matrix/identity/v2/terms":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2terms"}},"/_matrix/identity/v2/validate/email/requestToken":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2validateemailrequesttoken"}},"/_matrix/identity/v2/validate/email/submitToken":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2validateemailsubmittoken"}},"/_matrix/identity/versions":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityversions"}},"/_twake/identity/v1/lookup/match":{"post":{"tags":["Identity server"],"description":"Looks up the Organization User IDs which match value sent","requestBody":{"description":"Object containing detail for the search and the returned data","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"scope":{"type":"array","items":{"type":"string","description":"List of fields to search in (uid, mail,...)"}},"fields":{"type":"array","items":{"type":"string","description":"List of fields to return for matching users (uid, mail, mobile, displayName, givenName, cn, sn)"}},"val":{"type":"string","description":"Optional value to search"},"limit":{"type":"integer","description":"Optional max number of result to return (default 30)"},"offset":{"type":"integer","description":"Optional offset for pagination"}},"required":["scope","fields"]},"example":{"scope":["mail","uid"],"fields":["uid","displayName","sn","givenName","mobile"],"val":"rtyler","limit":3}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"matches":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string","description":"Matrix address"},"uid":{"type":"string","description":"id of a matching user"},"mail":{"type":"string","description":"email address of a matching user"}}},"description":"List of users that match"}}},"example":{"matches":[{"uid":"dwho","mail":"dwho@badwolf.com"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/_twake/identity/v1/lookup/diff":{"post":{"tags":["Identity server"],"description":"Looks up the Organization User IDs updated since X","requestBody":{"description":"Object containing the timestamp","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"since":{"type":"integer","description":"timestamp"},"fields":{"type":"array","items":{"type":"string","description":"List of fields to return for matching users"}},"limit":{"type":"integer","description":"Optional max number of result to return (default 30)"},"offset":{"type":"integer","description":"Optional offset for pagination"}}},"example":{"since":1685074279,"fields":["uid","mail"],"limit":3}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"matches":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string","description":"Matrix address"},"timestamp":{"type":"integer","description":"current server timestamp"},"uid":{"type":"string","description":"id of a matching user"},"mail":{"type":"string","description":"email address of a matching user"}}},"description":"List of users that match"}}},"example":{"matches":[{"uid":"dwho","mail":"dwho@badwolf.com"}]}}}}}}},"/_twake/recoveryWords":{"get":{"tags":["Vault API"],"description":"Allow for the connected user to retrieve its recovery words","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"words":{"type":"string","description":"Recovery words of the connected user"}}},"example":{"words":"This is the recovery sentence of rtyler"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Connected user has no recovery sentence"}}},"example":{"error":"User has no recovery sentence"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Connected user has multiple recovery sentence"}}},"example":{"error":"User has more than one recovery sentence"}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}},"post":{"tags":["Vault API"],"description":"Store connected user recovery words in database","requestBody":{"description":"Object containing the recovery words of the connected user","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"words":{"type":"string","description":"The recovery words of the connected user"}},"required":["words"]},"example":{"words":"This is the recovery sentence of rtyler"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Message indicating that words have been successfully saved"}},"example":{"message":"Saved recovery words sucessfully"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/InternalServerError"}}},"delete":{"tags":["Vault API"],"description":"Delete the user recovery words in the database","responses":{"204":{"description":"Delete success"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Connected user has no recovery sentence"}}},"example":{"error":"User has no recovery sentence"}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}},"put":{"tags":["Vault API"],"description":"Update stored connected user recovery words in database","requestBody":{"description":"Object containing the recovery words of the connected user","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"words":{"type":"string","description":"The new recovery words of the connected user"}},"required":["words"]},"example":{"words":"This is the updated recovery sentence of rtyler"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Message indicating that words have been successfully updated"}},"example":{"message":"Updated recovery words sucessfully"}}}}},"400":{"description":"Bad request"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/.well-knwon/matrix/client":{"get":{"tags":["Auto configuration"],"description":"Get server metadata for auto configuration","responses":{"200":{"description":"Give server metadata","content":{"application/json":{"schema":{"type":"object","properties":{"m.homeserver":{"type":"object","properties":{"base_url":{"type":"string","description":"Base URL of Matrix server"}}},"m.identity_server":{"type":"object","properties":{"base_url":{"type":"string","description":"Base URL of Identity server"}}},"m.federated_identity_services":{"type":"object","properties":{"base_urls":{"type":"array","items":{"type":"string","description":"Base URL of Federated identity service"},"description":"Available Federated identity services Base URL list"}}},"t.server":{"type":"object","properties":{"base_url":{"type":"string","description":"Base URL of Identity server"},"server_name":{"type":"string","description":"Domain handled by Matrix server"}}},"m.integrations":{"type":"object","properties":{"jitsi":{"type":"object","properties":{"preferredDomain":{"type":"string","description":"Jitsi's preffered domain"},"baseUrl":{"type":"string","description":"URL of Jitsi server"},"useJwt":{"type":"boolean","description":"True if Jitsi server requires a JWT"},"jwt":{"type":"object","properties":{"algorithm":{"type":"string","description":"algorithm used to generate JWT"},"secret":{"type":"string","description":"password of JWTs"},"issuer":{"type":"string","description":"issuer of JWTs"}}}}}}},"m.authentication":{"type":"object","properties":{"issuer":{"type":"string","description":"URL of OIDC issuer"}}}}},"example":{"m.homeserver":{"base_url":"matrix.example.com"},"m.identity_server":{"base_url":"global-id-server.twake.app"},"m.federated_identity_services":{"base_urls":["global-federated_identity_service.twake.app","other-federated-identity-service.twake.app"]},"m.integrations":{"jitsi":{"baseUrl":"https://jitsi.example.com/","preferredDomain":"jitsi.example.com","useJwt":false}},"m.authentication":{"issuer":"https://auth.example.com"},"t.server":{"base_url":"https://tom.example.com","server_name":"example.com"}}}}}}}},"/_matrix/identity/v2/lookups":{"post":{"tags":["Federated identity service"],"description":"Implements https://github.com/guimard/matrix-spec-proposals/blob/unified-identity-service/proposals/4004-unified-identity-service-view.md","requestBody":{"description":"Object containing hashes to store in federated identity service database","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mappings":{"type":"object","description":"List of hashed addresses by identity server hostname","properties":{"hostname":{"type":"array","items":{"type":"object","properties":{"hash":{"type":"string"},"active":{"type":"number"}}}}}},"algorithm":{"type":"string","description":"Algorithm the client is using to encode the addresses"},"pepper":{"type":"string","description":"Pepper from '/hash_details'"}},"required":["addresses","algorithm","pepper"]},"example":{"mappings":{"identity1.example.com":[{"hash":"4kenr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","active":1},{"hash":"nlo35_T5fzSGZzJApqu8lgIudJvmOQtDaHtr-I4rU7I","active":0}]},"algorithm":"sha256","pepper":"matrixrocks"}}}},"responses":{"201":{"description":"Success"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/app/v1/transactions/{txnId}":{"put":{"parameters":[{"in":"path","name":"txnId","required":true,"schema":{"type":"integer"},"description":"The transaction id"}],"tags":["Application server"],"description":"Implements https://spec.matrix.org/v1.6/application-service-api/#put_matrixappv1transactionstxnid","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"308":{"$ref":"#/components/responses/PermanentRedirect"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object"}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/app/v1/users/{userId}":{"get":{"parameters":[{"in":"path","name":"userId","required":true,"schema":{"type":"integer"},"description":"The user id"}],"tags":["Application server"],"description":"Implements https://spec.matrix.org/v1.6/application-service-api/#get_matrixappv1usersuserid","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/app/v1/rooms/{roomAlias}":{"get":{"parameters":[{"in":"path","name":"roomAlias","required":true,"schema":{"type":"integer"},"description":"The room alias"}],"tags":["Application server"],"description":"Implements https://spec.matrix.org/v1.6/application-service-api/#get_matrixappv1roomsroomalias","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/v1/activecontacts":{"get":{"tags":["Active contacts"],"description":"Get the list of active contacts","responses":{"200":{"description":"Active contacts found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActiveContacts"}}}},"401":{"description":"user is unauthorized"},"404":{"description":"Active contacts not found"},"500":{"description":"Internal error"}}},"post":{"tags":["Active contacts"],"description":"Create or update the list of active contacts","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActiveContacts"}}}},"responses":{"201":{"description":"Active contacts saved"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"delete":{"tags":["Active contacts"],"description":"Delete the list of active contacts","responses":{"200":{"description":"Active contacts deleted"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error/"}}}},"/_twake/app/v1/rooms":{"post":{"tags":["Application server"],"description":"Implements https://www.notion.so/Automatic-channels-89ba6f97bc90474ca482a28cf3228d3e","requestBody":{"description":"Object containing room's details","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ldapFilter":{"type":"object","additionalProperties":true,"description":"An object containing keys/values to build a ldap filter"},"aliasName":{"type":"string","description":"The desired room alias local part. If aliasName is equal to foo the complete room alias will be"},"name":{"type":"string","description":"The room name"},"topic":{"type":"string","description":"A short message detailing what is currently being discussed in the room."},"visibility":{"type":"string","enum":["public","private"],"description":"visibility values:\n * `public` - The room will be shown in the published room list\n * `private` - Hide the room from the published room list\n"}},"required":["ldapFilter","aliasName"]},"example":{"ldapFilter":{"mail":"example@test.com","cn":"example"},"aliasName":"exp","name":"Example","topic":"This is an example of a room topic","visibility":"public"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"errcode":{"type":"string"},"error":{"type":"string"}},"additionalProperties":{"type":"string"},"description":"List of users uid not added to the new room due to an error"},"example":[{"uid":"test1","errcode":"M_FORBIDDEN","error":"The user has been banned from the room"},{"uid":"test2","errcode":"M_UNKNOWN","error":"Internal server error"}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"examples":{"example1":{"value":{"error":"Error field: Invalid value (property: name)"}},"example2":{"value":{"errcode":"M_NOT_JSON","error":"Not_json"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"examples":{"example1":{"value":{"error":"This room already exits in Twake database"}},"example2":{"value":{"errcode":"M_ROOM_IN_USE","error":"A room with alias foo already exists in Matrix database"}}}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/mutual_rooms/{target_userid}":{"get":{"tags":["Mutual Rooms"],"description":"Get the list of mutual rooms between two users","parameters":[{"$ref":"#/components/parameters/target_userid"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MutualRooms"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"},"500":{"description":"Internal error"}}}},"/_twake/private_note":{"get":{"tags":["Private Note"],"description":"Get the private note made by the user for a target user","parameters":[{"$ref":"#/components/parameters/user_id"},{"$ref":"#/components/parameters/target_user_id"}],"responses":{"200":{"description":"Private note found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrivateNote"}}}},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"404":{"description":"Private note not found"},"500":{"description":"Internal error"}}},"post":{"tags":["Private Note"],"description":"Create a private note for a target user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePrivateNote"}}}},"responses":{"201":{"description":"Private note created"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"put":{"tags":["Private Note"],"description":"Update a private note","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePrivateNote"}}}},"responses":{"204":{"description":"Private note created"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/private_note/{private_note_id}":{"delete":{"tags":["Private Note"],"description":"Delete a private note","parameters":[{"$ref":"#/components/parameters/private_note_id"}],"responses":{"204":{"description":"Private note deleted"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/v1/room_tags/{roomId}":{"get":{"tags":["Room tags"],"description":"Get room tags","parameters":[{"$ref":"#/components/parameters/roomId"}],"responses":{"200":{"description":"Room tags found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomTags"}}}},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"put":{"tags":["Room tags"],"description":"Update room tags","parameters":[{"$ref":"#/components/parameters/roomId"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomTagsUpdate"}}}},"responses":{"204":{"description":"Room tags updated"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"delete":{"tags":["Room tags"],"description":"delete tags for a room","parameters":[{"$ref":"#/components/parameters/roomId"}],"responses":{"204":{"description":"Room tags deleted"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/v1/room_tags":{"post":{"tags":["Room tags"],"description":"Create room tags","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomTagCreation"}}}},"responses":{"201":{"description":"Room tags created"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/app/v1/search":{"post":{"tags":["Search Engine"],"description":"Search performs with OpenSearch on Tchat messages and rooms","requestBody":{"description":"Object containing search query details","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"searchValue":{"type":"string","description":"Value used to perform the search on rooms and messages data"}},"required":["searchValue"]},"example":{"searchValue":"hello"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"rooms":{"type":"array","description":"List of rooms whose name contains the search value","items":{"type":"object","properties":{"room_id":{"type":"string"},"name":{"type":"string"},"avatar_url":{"type":"string","description":"Url of the room's avatar"}}}},"messages":{"type":"array","description":"List of messages whose content or/and sender display name contain the search value","items":{"type":"object","properties":{"room_id":{"type":"string"},"event_id":{"type":"string","description":"Id of the message"},"content":{"type":"string"},"display_name":{"type":"string","description":"Sender display name"},"avatar_url":{"type":"string","description":"Sender's avatar url if it is a direct chat, otherwise it is the room's avatar url"},"room_name":{"type":"string","description":"Room's name in case of the message is not part of a direct chat"}}}},"mails":{"type":"array","description":"List of mails from Tmail whose meta or content contain the search value","items":{"type":"object","properties":{"attachments":{"type":"array","items":{"type":"object","properties":{"contentDisposition":{"type":"string"},"fileExtension":{"type":"string"},"fileName":{"type":"string"},"mediaType":{"type":"string"},"subtype":{"type":"string"},"textContent":{"type":"string"}}}},"bcc":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"cc":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"date":{"type":"string"},"from":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"hasAttachment":{"type":"boolean"},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}}}},"htmlBody":{"type":"string"},"isAnswered":{"type":"boolean"},"isDeleted":{"type":"boolean"},"isDraft":{"type":"boolean"},"isFlagged":{"type":"boolean"},"isRecent":{"type":"boolean"},"isUnread":{"type":"boolean"},"mailboxId":{"type":"string"},"mediaType":{"type":"string"},"messageId":{"type":"string"},"mimeMessageID":{"type":"string"},"modSeq":{"type":"number"},"saveDate":{"type":"string"},"sentDate":{"type":"string"},"size":{"type":"number"},"subject":{"type":"array","items":{"type":"string"}},"subtype":{"type":"string"},"textBody":{"type":"string"},"threadId":{"type":"string"},"to":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"uid":{"type":"number"},"userFlags":{"type":"array","items":{"type":"string"}}}}}}},"example":{"rooms":[{"room_id":"!dYqMpBXVQgKWETVAtJ:example.com","name":"Hello world room","avatar_url":"mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR"},{"room_id":"!dugSgNYwppGGoeJwYB:example.com","name":"Worldwide room","avatar_url":null}],"messages":[{"room_id":"!dYqMpBXVQgKWETVAtJ:example.com","event_id":"$c0hW6db_GUjk0NRBUuO12IyMpi48LE_tQK6sH3dkd1U","content":"Hello world","display_name":"Anakin Skywalker","avatar_url":"mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR","room_name":"Hello world room"},{"room_id":"!ftGqINYwppGGoeJwYB:example.com","event_id":"$IUzFofxHCvvoHJ-k2nfx7OlWOO8AuPvlHHqkeJLzxJ8","content":"Hello world my friends in direct chat","display_name":"Luke Skywalker","avatar_url":"mxc://matrix.org/wefh34uihSDRGhw34"}],"mails":[{"id":"message1","attachments":[{"contentDisposition":"attachment","fileExtension":"jpg","fileName":"image1.jpg","mediaType":"image/jpeg","textContent":"A beautiful galaxy far, far away."}],"bcc":[{"address":"okenobi@example.com","domain":"example.com","name":"Obi-Wan Kenobi"}],"cc":[{"address":"pamidala@example.com","domain":"example.com","name":"Padme Amidala"}],"date":"2024-02-24T10:15:00Z","from":[{"address":"dmaul@example.com","domain":"example.com","name":"Dark Maul"}],"hasAttachment":true,"headers":[{"name":"Header5","value":"Value5"},{"name":"Header6","value":"Value6"}],"htmlBody":"

A beautiful galaxy far, far away.

","isAnswered":true,"isDeleted":false,"isDraft":false,"isFlagged":true,"isRecent":true,"isUnread":false,"mailboxId":"mailbox3","mediaType":"image/jpeg","messageId":"message3","mimeMessageID":"mimeMessageID3","modSeq":98765,"saveDate":"2024-02-24T10:15:00Z","sentDate":"2024-02-24T10:15:00Z","size":4096,"subject":["Star Wars Message 3"],"subtype":"subtype3","textBody":"A beautiful galaxy far, far away.","threadId":"thread3","to":[{"address":"kren@example.com","domain":"example.com","name":"Kylo Ren"}],"uid":987654,"userFlags":["Flag4","Flag5"]}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/app/v1/opensearch/restore":{"post":{"tags":["Search Engine"],"description":"Restore OpenSearch indexes using Matrix homeserver database","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/sms":{"post":{"requestBody":{"description":"SMS object","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sms"}}}},"tags":["SMS"],"description":"Send an SMS to a phone number","responses":{"200":{"description":"SMS sent successfully"},"400":{"description":"Invalid request"},"401":{"description":"Unauthorized"},"500":{"description":"Internal server error"}}}},"/_twake/v1/user_info/{userId}":{"get":{"tags":["User Info"],"description":"Get user info","parameters":[{"$ref":"#/components/parameters/userId"}],"responses":{"200":{"description":"User info found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInfo"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"404":{"description":"User info not found"},"500":{"description":"Internal server error"}}}}},"tags":[]} \ No newline at end of file +{"openapi":"3.0.0","info":{"title":"Twake on Matrix APIs documentation","version":"0.0.1","description":"This is The documentation of all available APIs of this repository"},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"MatrixError":{"type":"object","properties":{"errcode":{"type":"string","description":"A Matrix error code"},"error":{"type":"string","description":"A human-readable error message"}},"required":["error"]},"ActiveContacts":{"type":"object","description":"the list of active contacts","properties":{"contacts":{"type":"string","description":"active contacts"}}},"MutualRooms":{"type":"array","items":{"type":"object","properties":{"roomId":{"type":"string","description":"the room id"},"name":{"type":"string","description":"the room name"},"topic":{"type":"string","description":"the room topic"},"room_type":{"type":"string","description":"the room type"}}}},"PrivateNote":{"type":"object","properties":{"id":{"type":"string","description":"The private note id"},"content":{"type":"string","description":"The private note content"},"authorId":{"type":"string","description":"The author user id"},"targetId":{"type":"string","description":"The target user id"}}},"CreatePrivateNote":{"type":"object","properties":{"content":{"type":"string","description":"The private note content"},"authorId":{"type":"string","description":"The author user id"},"targetId":{"type":"string","description":"The target user id"}}},"UpdatePrivateNote":{"type":"object","properties":{"id":{"type":"string","description":"The private note id"},"content":{"type":"string","description":"The private note content"}}},"RoomTags":{"type":"object","properties":{"tags":{"description":"the room tags list","type":"array","items":{"type":"string"}}}},"RoomTagCreation":{"type":"object","properties":{"content":{"type":"array","description":"the room tags strings","items":{"type":"string"}},"roomId":{"type":"string","description":"the room id"}}},"RoomTagsUpdate":{"type":"object","properties":{"content":{"type":"array","description":"the room tags strings","items":{"type":"string"}}}},"sms":{"type":"object","properties":{"to":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"text":{"type":"string"}}},"UserInfo":{"type":"object","properties":{"uid":{"type":"string","description":"the user id"},"givenName":{"type":"string","description":"the user given name"},"sn":{"type":"string","description":"the user surname"}}}},"responses":{"InternalServerError":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"The message describing the internal error"}}}}}},"Unauthorized":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_UNAUTHORIZED","error":"Unauthorized"}}}},"BadRequest":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_MISSING_PARAMS","error":"Properties are missing in the request body"}}}},"Forbidden":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_FORBIDDEN","error":"Forbidden"}}}},"Conflict":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"error":"Conflict"}}}},"PermanentRedirect":{"description":"Permanent Redirect","headers":{"Location":{"schema":{"type":"string","description":"URL to use for recdirect"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_UNKNOWN","error":"This non-standard endpoint has been removed"}}}},"NotFound":{"description":"Private note not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_NOT_FOUND","error":"Not Found"}}}},"Unrecognized":{"description":"Unrecognized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_UNRECOGNIZED","error":"Unrecognized"}}}},"Created":{"description":"Created"},"NoContent":{"description":"operation successful and no content returned"},"InternalError":{"description":"Internal error"}},"parameters":{"target_userid":{"name":"target_userid","in":"path","required":true,"description":"the target user id","schema":{"type":"string"}},"user_id":{"name":"user_id","in":"query","description":"the author user id","required":true,"schema":{"type":"string"}},"target_user_id":{"name":"target_user_id","in":"query","description":"the target user id","required":true,"schema":{"type":"string"}},"private_note_id":{"name":"private_note_id","in":"path","description":"the private note id","required":true,"schema":{"type":"string"}},"roomId":{"in":"path","name":"roomId","description":"the room id","required":true,"schema":{"type":"string"}},"userId":{"in":"path","name":"userId","description":"the user id","required":true,"schema":{"type":"string"}}}},"security":[{"bearerAuth":[]}],"paths":{"/_matrix/identity/v2":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2"}},"/_matrix/identity/v2/hash_details":{"get":{"tags":["Federated identity service"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2hash_details"}},"/_matrix/identity/v2/lookup":{"post":{"tags":["Federated identity service"],"description":"Extends https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2lookup to display inactive users and 3PID users","requestBody":{"description":"Object containing hashes of mails/phones to search","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"addresses":{"type":"array","items":{"type":"string","description":"List of (hashed) addresses to lookup"}},"algorithm":{"type":"string","description":"Algorithm the client is using to encode the addresses"},"pepper":{"type":"string","description":"Pepper from '/hash_details'"}},"required":["addresses","algorithm","pepper"]},"example":{"addresses":["4kenr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","nlo35_T5fzSGZzJApqu8lgIudJvmOQtDaHtr-I4rU7I"],"algorithm":"sha256","pepper":"matrixrocks"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"mappings":{"type":"object","additionalProperties":{"type":"string"},"description":"List of active accounts"},"inactive_mappings":{"type":"object","additionalProperties":{"type":"string"},"description":"List of inactive accounts"},"third_party_mappings":{"type":"object","description":"List of hashed addresses by identity server hostname","properties":{"hostname":{"type":"object","properties":{"actives":{"type":"array","items":{"type":"string","description":"List of (hashed) active accounts addresses matching request body addresses"}},"inactives":{"type":"array","items":{"type":"string","description":"List of (hashed) inactive accounts addresses matching request body addresses"}}}}}}}},"example":{"mappings":{"4kenr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc":"@dwho:company.com"},"inactive_mappings":{"nlo35_T5fzSGZzJApqu8lgIudJvmOQtDaHtr-I4rU7I":"@rtyler:company.com"},"third_party_mappings":{"identity1.example.com":{"actives":["78jnr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","gtr42_T5fzSGZzJAmlp5lgIudJvmOQtDaHtr-I4rU7I"],"inactives":["qfgt57N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","lnbc8_T5fzSGZzJAmlp5lgIudJvmOQtDaHtr-I4rU7I"]}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/identity/v2/account":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2account"}},"/_matrix/identity/v2/account/register":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2accountregister"}},"/_matrix/identity/v2/account/logout":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2accountlogout"}},"/_matrix/identity/v2/terms":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2terms"}},"/_matrix/identity/v2/validate/email/requestToken":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2validateemailrequesttoken"}},"/_matrix/identity/v2/validate/email/submitToken":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2validateemailsubmittoken"}},"/_matrix/identity/versions":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityversions"}},"/_twake/identity/v1/lookup/match":{"post":{"tags":["Identity server"],"description":"Looks up the Organization User IDs which match value sent","requestBody":{"description":"Object containing detail for the search and the returned data","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"scope":{"type":"array","items":{"type":"string","description":"List of fields to search in (uid, mail,...)"}},"fields":{"type":"array","items":{"type":"string","description":"List of fields to return for matching users (uid, mail, mobile, displayName, givenName, cn, sn)"}},"val":{"type":"string","description":"Optional value to search"},"limit":{"type":"integer","description":"Optional max number of result to return (default 30)"},"offset":{"type":"integer","description":"Optional offset for pagination"}},"required":["scope","fields"]},"example":{"scope":["mail","uid"],"fields":["uid","displayName","sn","givenName","mobile"],"val":"rtyler","limit":3}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"matches":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string","description":"Matrix address"},"uid":{"type":"string","description":"id of a matching user"},"mail":{"type":"string","description":"email address of a matching user"}}},"description":"List of users that match"}}},"example":{"matches":[{"uid":"dwho","mail":"dwho@badwolf.com"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/_twake/identity/v1/lookup/diff":{"post":{"tags":["Identity server"],"description":"Looks up the Organization User IDs updated since X","requestBody":{"description":"Object containing the timestamp","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"since":{"type":"integer","description":"timestamp"},"fields":{"type":"array","items":{"type":"string","description":"List of fields to return for matching users"}},"limit":{"type":"integer","description":"Optional max number of result to return (default 30)"},"offset":{"type":"integer","description":"Optional offset for pagination"}}},"example":{"since":1685074279,"fields":["uid","mail"],"limit":3}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"matches":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string","description":"Matrix address"},"timestamp":{"type":"integer","description":"current server timestamp"},"uid":{"type":"string","description":"id of a matching user"},"mail":{"type":"string","description":"email address of a matching user"}}},"description":"List of users that match"}}},"example":{"matches":[{"uid":"dwho","mail":"dwho@badwolf.com"}]}}}}}}},"/_twake/recoveryWords":{"get":{"tags":["Vault API"],"description":"Allow for the connected user to retrieve its recovery words","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"words":{"type":"string","description":"Recovery words of the connected user"}}},"example":{"words":"This is the recovery sentence of rtyler"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Connected user has no recovery sentence"}}},"example":{"error":"User has no recovery sentence"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Connected user has multiple recovery sentence"}}},"example":{"error":"User has more than one recovery sentence"}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}},"post":{"tags":["Vault API"],"description":"Store connected user recovery words in database","requestBody":{"description":"Object containing the recovery words of the connected user","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"words":{"type":"string","description":"The recovery words of the connected user"}},"required":["words"]},"example":{"words":"This is the recovery sentence of rtyler"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Message indicating that words have been successfully saved"}},"example":{"message":"Saved recovery words sucessfully"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/InternalServerError"}}},"delete":{"tags":["Vault API"],"description":"Delete the user recovery words in the database","responses":{"204":{"description":"Delete success"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Connected user has no recovery sentence"}}},"example":{"error":"User has no recovery sentence"}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}},"put":{"tags":["Vault API"],"description":"Update stored connected user recovery words in database","requestBody":{"description":"Object containing the recovery words of the connected user","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"words":{"type":"string","description":"The new recovery words of the connected user"}},"required":["words"]},"example":{"words":"This is the updated recovery sentence of rtyler"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Message indicating that words have been successfully updated"}},"example":{"message":"Updated recovery words sucessfully"}}}}},"400":{"description":"Bad request"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/.well-knwon/matrix/client":{"get":{"tags":["Auto configuration"],"description":"Get server metadata for auto configuration","responses":{"200":{"description":"Give server metadata","content":{"application/json":{"schema":{"type":"object","properties":{"m.homeserver":{"type":"object","properties":{"base_url":{"type":"string","description":"Base URL of Matrix server"}}},"m.identity_server":{"type":"object","properties":{"base_url":{"type":"string","description":"Base URL of Identity server"}}},"m.federated_identity_services":{"type":"object","properties":{"base_urls":{"type":"array","items":{"type":"string","description":"Base URL of Federated identity service"},"description":"Available Federated identity services Base URL list"}}},"t.server":{"type":"object","properties":{"base_url":{"type":"string","description":"Base URL of Identity server"},"server_name":{"type":"string","description":"Domain handled by Matrix server"}}},"m.integrations":{"type":"object","properties":{"jitsi":{"type":"object","properties":{"preferredDomain":{"type":"string","description":"Jitsi's preffered domain"},"baseUrl":{"type":"string","description":"URL of Jitsi server"},"useJwt":{"type":"boolean","description":"True if Jitsi server requires a JWT"},"jwt":{"type":"object","properties":{"algorithm":{"type":"string","description":"algorithm used to generate JWT"},"secret":{"type":"string","description":"password of JWTs"},"issuer":{"type":"string","description":"issuer of JWTs"}}}}}}},"m.authentication":{"type":"object","properties":{"issuer":{"type":"string","description":"URL of OIDC issuer"}}}}},"example":{"m.homeserver":{"base_url":"matrix.example.com"},"m.identity_server":{"base_url":"global-id-server.twake.app"},"m.federated_identity_services":{"base_urls":["global-federated_identity_service.twake.app","other-federated-identity-service.twake.app"]},"m.integrations":{"jitsi":{"baseUrl":"https://jitsi.example.com/","preferredDomain":"jitsi.example.com","useJwt":false}},"m.authentication":{"issuer":"https://auth.example.com"},"t.server":{"base_url":"https://tom.example.com","server_name":"example.com"}}}}}}}},"/_matrix/identity/v2/lookups":{"post":{"tags":["Federated identity service"],"description":"Implements https://github.com/guimard/matrix-spec-proposals/blob/unified-identity-service/proposals/4004-unified-identity-service-view.md","requestBody":{"description":"Object containing hashes to store in federated identity service database","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mappings":{"type":"object","description":"List of hashed addresses by identity server hostname","properties":{"hostname":{"type":"array","items":{"type":"object","properties":{"hash":{"type":"string"},"active":{"type":"number"}}}}}},"algorithm":{"type":"string","description":"Algorithm the client is using to encode the addresses"},"pepper":{"type":"string","description":"Pepper from '/hash_details'"}},"required":["addresses","algorithm","pepper"]},"example":{"mappings":{"identity1.example.com":[{"hash":"4kenr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","active":1},{"hash":"nlo35_T5fzSGZzJApqu8lgIudJvmOQtDaHtr-I4rU7I","active":0}]},"algorithm":"sha256","pepper":"matrixrocks"}}}},"responses":{"201":{"description":"Success"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/app/v1/transactions/{txnId}":{"put":{"parameters":[{"in":"path","name":"txnId","required":true,"schema":{"type":"integer"},"description":"The transaction id"}],"tags":["Application server"],"description":"Implements https://spec.matrix.org/v1.6/application-service-api/#put_matrixappv1transactionstxnid","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"308":{"$ref":"#/components/responses/PermanentRedirect"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object"}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/app/v1/users/{userId}":{"get":{"parameters":[{"in":"path","name":"userId","required":true,"schema":{"type":"integer"},"description":"The user id"}],"tags":["Application server"],"description":"Implements https://spec.matrix.org/v1.6/application-service-api/#get_matrixappv1usersuserid","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/app/v1/rooms/{roomAlias}":{"get":{"parameters":[{"in":"path","name":"roomAlias","required":true,"schema":{"type":"integer"},"description":"The room alias"}],"tags":["Application server"],"description":"Implements https://spec.matrix.org/v1.6/application-service-api/#get_matrixappv1roomsroomalias","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/v1/activecontacts":{"get":{"tags":["Active contacts"],"description":"Get the list of active contacts","responses":{"200":{"description":"Active contacts found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActiveContacts"}}}},"401":{"description":"user is unauthorized"},"404":{"description":"Active contacts not found"},"500":{"description":"Internal error"}}},"post":{"tags":["Active contacts"],"description":"Create or update the list of active contacts","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActiveContacts"}}}},"responses":{"201":{"description":"Active contacts saved"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"delete":{"tags":["Active contacts"],"description":"Delete the list of active contacts","responses":{"200":{"description":"Active contacts deleted"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error/"}}}},"/_twake/app/v1/rooms":{"post":{"tags":["Application server"],"description":"Implements https://www.notion.so/Automatic-channels-89ba6f97bc90474ca482a28cf3228d3e","requestBody":{"description":"Object containing room's details","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ldapFilter":{"type":"object","additionalProperties":true,"description":"An object containing keys/values to build a ldap filter"},"aliasName":{"type":"string","description":"The desired room alias local part. If aliasName is equal to foo the complete room alias will be"},"name":{"type":"string","description":"The room name"},"topic":{"type":"string","description":"A short message detailing what is currently being discussed in the room."},"visibility":{"type":"string","enum":["public","private"],"description":"visibility values:\n * `public` - The room will be shown in the published room list\n * `private` - Hide the room from the published room list\n"}},"required":["ldapFilter","aliasName"]},"example":{"ldapFilter":{"mail":"example@test.com","cn":"example"},"aliasName":"exp","name":"Example","topic":"This is an example of a room topic","visibility":"public"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"errcode":{"type":"string"},"error":{"type":"string"}},"additionalProperties":{"type":"string"},"description":"List of users uid not added to the new room due to an error"},"example":[{"uid":"test1","errcode":"M_FORBIDDEN","error":"The user has been banned from the room"},{"uid":"test2","errcode":"M_UNKNOWN","error":"Internal server error"}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"examples":{"example1":{"value":{"error":"Error field: Invalid value (property: name)"}},"example2":{"value":{"errcode":"M_NOT_JSON","error":"Not_json"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"examples":{"example1":{"value":{"error":"This room already exits in Twake database"}},"example2":{"value":{"errcode":"M_ROOM_IN_USE","error":"A room with alias foo already exists in Matrix database"}}}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/mutual_rooms/{target_userid}":{"get":{"tags":["Mutual Rooms"],"description":"Get the list of mutual rooms between two users","parameters":[{"$ref":"#/components/parameters/target_userid"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MutualRooms"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"},"500":{"description":"Internal error"}}}},"/_twake/private_note":{"get":{"tags":["Private Note"],"description":"Get the private note made by the user for a target user","parameters":[{"$ref":"#/components/parameters/user_id"},{"$ref":"#/components/parameters/target_user_id"}],"responses":{"200":{"description":"Private note found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrivateNote"}}}},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"404":{"description":"Private note not found"},"500":{"description":"Internal error"}}},"post":{"tags":["Private Note"],"description":"Create a private note for a target user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePrivateNote"}}}},"responses":{"201":{"description":"Private note created"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"put":{"tags":["Private Note"],"description":"Update a private note","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePrivateNote"}}}},"responses":{"204":{"description":"Private note created"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/private_note/{private_note_id}":{"delete":{"tags":["Private Note"],"description":"Delete a private note","parameters":[{"$ref":"#/components/parameters/private_note_id"}],"responses":{"204":{"description":"Private note deleted"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/v1/qrcode":{"get":{"tags":["QR Code"],"description":"Get access QR Code","responses":{"200":{"description":"QR code generated","content":{"image/svg+xml":{"schema":{"type":"string"}}}},"400":{"description":"Access token is missing"},"401":{"description":"Unauthorized"},"500":{"description":"Internal server error"}}}},"/_twake/v1/room_tags/{roomId}":{"get":{"tags":["Room tags"],"description":"Get room tags","parameters":[{"$ref":"#/components/parameters/roomId"}],"responses":{"200":{"description":"Room tags found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomTags"}}}},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"put":{"tags":["Room tags"],"description":"Update room tags","parameters":[{"$ref":"#/components/parameters/roomId"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomTagsUpdate"}}}},"responses":{"204":{"description":"Room tags updated"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"delete":{"tags":["Room tags"],"description":"delete tags for a room","parameters":[{"$ref":"#/components/parameters/roomId"}],"responses":{"204":{"description":"Room tags deleted"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/v1/room_tags":{"post":{"tags":["Room tags"],"description":"Create room tags","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomTagCreation"}}}},"responses":{"201":{"description":"Room tags created"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/app/v1/search":{"post":{"tags":["Search Engine"],"description":"Search performs with OpenSearch on Tchat messages and rooms","requestBody":{"description":"Object containing search query details","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"searchValue":{"type":"string","description":"Value used to perform the search on rooms and messages data"}},"required":["searchValue"]},"example":{"searchValue":"hello"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"rooms":{"type":"array","description":"List of rooms whose name contains the search value","items":{"type":"object","properties":{"room_id":{"type":"string"},"name":{"type":"string"},"avatar_url":{"type":"string","description":"Url of the room's avatar"}}}},"messages":{"type":"array","description":"List of messages whose content or/and sender display name contain the search value","items":{"type":"object","properties":{"room_id":{"type":"string"},"event_id":{"type":"string","description":"Id of the message"},"content":{"type":"string"},"display_name":{"type":"string","description":"Sender display name"},"avatar_url":{"type":"string","description":"Sender's avatar url if it is a direct chat, otherwise it is the room's avatar url"},"room_name":{"type":"string","description":"Room's name in case of the message is not part of a direct chat"}}}},"mails":{"type":"array","description":"List of mails from Tmail whose meta or content contain the search value","items":{"type":"object","properties":{"attachments":{"type":"array","items":{"type":"object","properties":{"contentDisposition":{"type":"string"},"fileExtension":{"type":"string"},"fileName":{"type":"string"},"mediaType":{"type":"string"},"subtype":{"type":"string"},"textContent":{"type":"string"}}}},"bcc":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"cc":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"date":{"type":"string"},"from":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"hasAttachment":{"type":"boolean"},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}}}},"htmlBody":{"type":"string"},"isAnswered":{"type":"boolean"},"isDeleted":{"type":"boolean"},"isDraft":{"type":"boolean"},"isFlagged":{"type":"boolean"},"isRecent":{"type":"boolean"},"isUnread":{"type":"boolean"},"mailboxId":{"type":"string"},"mediaType":{"type":"string"},"messageId":{"type":"string"},"mimeMessageID":{"type":"string"},"modSeq":{"type":"number"},"saveDate":{"type":"string"},"sentDate":{"type":"string"},"size":{"type":"number"},"subject":{"type":"array","items":{"type":"string"}},"subtype":{"type":"string"},"textBody":{"type":"string"},"threadId":{"type":"string"},"to":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"uid":{"type":"number"},"userFlags":{"type":"array","items":{"type":"string"}}}}}}},"example":{"rooms":[{"room_id":"!dYqMpBXVQgKWETVAtJ:example.com","name":"Hello world room","avatar_url":"mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR"},{"room_id":"!dugSgNYwppGGoeJwYB:example.com","name":"Worldwide room","avatar_url":null}],"messages":[{"room_id":"!dYqMpBXVQgKWETVAtJ:example.com","event_id":"$c0hW6db_GUjk0NRBUuO12IyMpi48LE_tQK6sH3dkd1U","content":"Hello world","display_name":"Anakin Skywalker","avatar_url":"mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR","room_name":"Hello world room"},{"room_id":"!ftGqINYwppGGoeJwYB:example.com","event_id":"$IUzFofxHCvvoHJ-k2nfx7OlWOO8AuPvlHHqkeJLzxJ8","content":"Hello world my friends in direct chat","display_name":"Luke Skywalker","avatar_url":"mxc://matrix.org/wefh34uihSDRGhw34"}],"mails":[{"id":"message1","attachments":[{"contentDisposition":"attachment","fileExtension":"jpg","fileName":"image1.jpg","mediaType":"image/jpeg","textContent":"A beautiful galaxy far, far away."}],"bcc":[{"address":"okenobi@example.com","domain":"example.com","name":"Obi-Wan Kenobi"}],"cc":[{"address":"pamidala@example.com","domain":"example.com","name":"Padme Amidala"}],"date":"2024-02-24T10:15:00Z","from":[{"address":"dmaul@example.com","domain":"example.com","name":"Dark Maul"}],"hasAttachment":true,"headers":[{"name":"Header5","value":"Value5"},{"name":"Header6","value":"Value6"}],"htmlBody":"

A beautiful galaxy far, far away.

","isAnswered":true,"isDeleted":false,"isDraft":false,"isFlagged":true,"isRecent":true,"isUnread":false,"mailboxId":"mailbox3","mediaType":"image/jpeg","messageId":"message3","mimeMessageID":"mimeMessageID3","modSeq":98765,"saveDate":"2024-02-24T10:15:00Z","sentDate":"2024-02-24T10:15:00Z","size":4096,"subject":["Star Wars Message 3"],"subtype":"subtype3","textBody":"A beautiful galaxy far, far away.","threadId":"thread3","to":[{"address":"kren@example.com","domain":"example.com","name":"Kylo Ren"}],"uid":987654,"userFlags":["Flag4","Flag5"]}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/app/v1/opensearch/restore":{"post":{"tags":["Search Engine"],"description":"Restore OpenSearch indexes using Matrix homeserver database","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/sms":{"post":{"requestBody":{"description":"SMS object","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sms"}}}},"tags":["SMS"],"description":"Send an SMS to a phone number","responses":{"200":{"description":"SMS sent successfully"},"400":{"description":"Invalid request"},"401":{"description":"Unauthorized"},"500":{"description":"Internal server error"}}}},"/_twake/v1/user_info/{userId}":{"get":{"tags":["User Info"],"description":"Get user info","parameters":[{"$ref":"#/components/parameters/userId"}],"responses":{"200":{"description":"User info found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInfo"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"404":{"description":"User info not found"},"500":{"description":"Internal server error"}}}}},"tags":[]} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fce35302..62f70a59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5904,6 +5904,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/qrcode": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz", + "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.15", "dev": true, @@ -9533,7 +9542,6 @@ }, "node_modules/decamelize": { "version": "1.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9847,6 +9855,11 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, "node_modules/dir-glob": { "version": "3.0.1", "dev": true, @@ -19511,6 +19524,14 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "license": "MIT", @@ -20148,6 +20169,118 @@ ], "license": "MIT" }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.11.0", "license": "BSD-3-Clause", @@ -20922,6 +21055,11 @@ "node": "*" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, "node_modules/requireindex": { "version": "1.2.0", "dev": true, @@ -21415,7 +21553,6 @@ }, "node_modules/set-blocking": { "version": "2.0.0", - "devOptional": true, "license": "ISC" }, "node_modules/set-cookie-parser": { @@ -25239,6 +25376,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, "node_modules/which-typed-array": { "version": "1.1.15", "license": "MIT", @@ -25334,7 +25476,6 @@ }, "node_modules/wrap-ansi": { "version": "6.2.0", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -25949,9 +26090,13 @@ "@twake/matrix-identity-server": "*", "@twake/utils": "*", "lodash": "^4.17.21", + "qrcode": "^1.5.4", "redis": "^4.6.6", "validator": "^13.11.0" }, + "devDependencies": { + "@types/qrcode": "^1.5.5" + }, "optionalDependencies": { "@crowdsec/express-bouncer": "^0.1.0", "ldapjs": "^2.3.3", diff --git a/packages/matrix-client-server/src/config.json b/packages/matrix-client-server/src/config.json index 7c530dc0..7d14fd50 100644 --- a/packages/matrix-client-server/src/config.json +++ b/packages/matrix-client-server/src/config.json @@ -115,5 +115,6 @@ } ], "sms_folder": "./src/__testData__/sms", - "is_registration_enabled": true + "is_registration_enabled": true, + "qr_code_url": "twake.chat://login" } diff --git a/packages/tom-server/package.json b/packages/tom-server/package.json index 2f7f8409..3aabd8c3 100644 --- a/packages/tom-server/package.json +++ b/packages/tom-server/package.json @@ -45,6 +45,7 @@ "@twake/matrix-identity-server": "*", "@twake/utils": "*", "lodash": "^4.17.21", + "qrcode": "^1.5.4", "redis": "^4.6.6", "validator": "^13.11.0" }, @@ -53,5 +54,8 @@ "ldapjs": "^2.3.3", "pg": "^8.10.0", "sqlite3": "^5.1.6" + }, + "devDependencies": { + "@types/qrcode": "^1.5.5" } } diff --git a/packages/tom-server/src/config.json b/packages/tom-server/src/config.json index 4937b204..4fa77f9f 100644 --- a/packages/tom-server/src/config.json +++ b/packages/tom-server/src/config.json @@ -89,5 +89,6 @@ "sms_api_url": "", "sms_api_login": "", "sms_api_key": "", - "trust_x_forwarded_for": false + "trust_x_forwarded_for": false, + "qr_code_url": "twake.chat://login" } diff --git a/packages/tom-server/src/index.test.ts b/packages/tom-server/src/index.test.ts index 78214706..b4707ae7 100644 --- a/packages/tom-server/src/index.test.ts +++ b/packages/tom-server/src/index.test.ts @@ -34,7 +34,8 @@ describe('Tom-server', () => { userdb_host: userDb, sms_api_key: '', sms_api_login: '', - sms_api_url: '' + sms_api_url: '', + qr_code_url: 'http://example.com/' } if (process.env.TEST_PG === 'yes') { conf.database_engine = 'pg' diff --git a/packages/tom-server/src/index.ts b/packages/tom-server/src/index.ts index 676e1a66..b180e308 100644 --- a/packages/tom-server/src/index.ts +++ b/packages/tom-server/src/index.ts @@ -21,6 +21,7 @@ import userInfoAPIRouter from './user-info-api' import VaultServer from './vault-api' import WellKnown from './wellKnown' import ActiveContacts from './active-contacts-api' +import QRCode from './qrcode-api' export default class TwakeServer { conf: Config @@ -145,6 +146,7 @@ export default class TwakeServer { this.idServer.authenticate, this.logger ) + const qrCodeApi = QRCode(this.idServer, this.conf, this.logger) this.endpoints.use(privateNoteApi) this.endpoints.use(mutualRoolsApi) @@ -153,6 +155,7 @@ export default class TwakeServer { this.endpoints.use(userInfoApi) this.endpoints.use(smsApi) this.endpoints.use(activeContactsApi) + this.endpoints.use(qrCodeApi) if ( this.conf.opensearch_is_activated != null && diff --git a/packages/tom-server/src/qrcode-api/controllers/index.ts b/packages/tom-server/src/qrcode-api/controllers/index.ts new file mode 100644 index 00000000..8ffcbb55 --- /dev/null +++ b/packages/tom-server/src/qrcode-api/controllers/index.ts @@ -0,0 +1,58 @@ +import { type TwakeLogger } from '@twake/logger' +import { + type IQRCodeTokenService, + type IQRCodeApiController, + type IQRCodeService +} from '../types' +import { type Response, type NextFunction } from 'express' +import type { Config, AuthRequest } from '../../types' +import { QRCodeService, QRCodeTokenService } from '../services' + +class QRCodeApiController implements IQRCodeApiController { + private readonly qrCodeService: IQRCodeService + private readonly qrCodeTokenService: IQRCodeTokenService + + constructor(private readonly logger: TwakeLogger, config: Config) { + this.qrCodeService = new QRCodeService(config, logger) + this.qrCodeTokenService = new QRCodeTokenService(config, logger) + } + + /** + * Get the QR code for the connected user. + * + * @param {AuthRequest} req - The request object. + * @param {Response} res - The response object. + * @param {NextFunction} next - The next function. + */ + get = async ( + req: AuthRequest, + res: Response, + next: NextFunction + ): Promise => { + try { + const cookies = req.headers.cookie + + if (cookies === undefined) { + res.status(400).json({ error: 'Cookies are missing' }) + return + } + + const accessToken = await this.qrCodeTokenService.getAccessToken(cookies) + + if (accessToken === null) { + res.status(400).json({ error: 'Invalid access token' }) + return + } + + const qrcode = await this.qrCodeService.getImage(accessToken) + + res.setHeader('Content-Type', 'image/svg+xml') + res.send(qrcode) + } catch (error) { + this.logger.error('Failed to generate QR Code', { error }) + next(error) + } + } +} + +export default QRCodeApiController diff --git a/packages/tom-server/src/qrcode-api/index.ts b/packages/tom-server/src/qrcode-api/index.ts new file mode 100644 index 00000000..eec21749 --- /dev/null +++ b/packages/tom-server/src/qrcode-api/index.ts @@ -0,0 +1 @@ +export { default } from './routes' diff --git a/packages/tom-server/src/qrcode-api/routes/index.ts b/packages/tom-server/src/qrcode-api/routes/index.ts new file mode 100644 index 00000000..6ec20bfc --- /dev/null +++ b/packages/tom-server/src/qrcode-api/routes/index.ts @@ -0,0 +1,49 @@ +/* eslint-disable @typescript-eslint/no-misused-promises */ +import { + getLogger, + type TwakeLogger, + type Config as LoggerConfig +} from '@twake/logger' +import type IdServer from '../../identity-server' +import { type Config } from '../../types' +import { Router } from 'express' +import authMiddleware from '../../utils/middlewares/auth.middleware' +import QRCodeApiController from '../controllers' + +export const PATH = '/_twake/v1/qrcode' + +export default ( + idServer: IdServer, + config: Config, + defaultLogger?: TwakeLogger +): Router => { + const logger = defaultLogger ?? getLogger(config as unknown as LoggerConfig) + const router = Router() + const authenticator = authMiddleware(idServer.authenticate, logger) + const qrCodeController = new QRCodeApiController(logger, config) + + /** + * @openapi + * /_twake/v1/qrcode: + * get: + * tags: + * - QR Code + * description: Get access QR Code + * responses: + * 200: + * description: QR code generated + * content: + * image/svg+xml: + * schema: + * type: string + * 400: + * description: Access token is missing + * 500: + * description: Internal server error + * 401: + * description: Unauthorized + */ + router.get(PATH, authenticator, qrCodeController.get) + + return router +} diff --git a/packages/tom-server/src/qrcode-api/services/index.ts b/packages/tom-server/src/qrcode-api/services/index.ts new file mode 100644 index 00000000..c92f5273 --- /dev/null +++ b/packages/tom-server/src/qrcode-api/services/index.ts @@ -0,0 +1,2 @@ +export * from './qrcode' +export * from './token' diff --git a/packages/tom-server/src/qrcode-api/services/qrcode.ts b/packages/tom-server/src/qrcode-api/services/qrcode.ts new file mode 100644 index 00000000..bcdbc561 --- /dev/null +++ b/packages/tom-server/src/qrcode-api/services/qrcode.ts @@ -0,0 +1,35 @@ +import QRCode from 'qrcode' +import type { IQRCodeService } from '../types' +import type { TwakeLogger } from '@twake/logger' +import type { Config } from '../../types' + +export class QRCodeService implements IQRCodeService { + constructor( + private readonly config: Config, + private readonly logger: TwakeLogger + ) {} + + /** + * Generates a QR code as a string in SVG format. + * + * @param {string} token - The token to be encoded in the QR code. + * @returns {Promise} - The QR code as an SVG string or null if an error occurs. + */ + getImage = async (token: string): Promise => { + try { + const url = this.config.qr_code_url + + if (url === undefined) { + throw new Error('QR code URL is not defined in the configuration.') + } + + const text = `${url}?access_token=${token}` + + return await QRCode.toString(text, { type: 'svg' }) + } catch (error) { + this.logger.error('Failed to generate QRCode', { error }) + + return null + } + } +} diff --git a/packages/tom-server/src/qrcode-api/services/token.ts b/packages/tom-server/src/qrcode-api/services/token.ts new file mode 100644 index 00000000..8a899a9a --- /dev/null +++ b/packages/tom-server/src/qrcode-api/services/token.ts @@ -0,0 +1,213 @@ +import { type TwakeLogger } from '@twake/logger' +import { type Config } from '../../types' +import { + type TokenLoginPayload, + type IQRCodeTokenService, + type TokenLoginResponse, + type loginFlowsResponse, + type OIDCRedirectResponse +} from '../types' + +export class QRCodeTokenService implements IQRCodeTokenService { + JSON_HEADERS = { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + + matrixUrl: string + + constructor( + private readonly config: Config, + private readonly logger: TwakeLogger + ) { + this.matrixUrl = `https://${this.config.matrix_server}/_matrix/client/v3` + } + + getAccessToken = async (authCookies: string): Promise => { + try { + const provider = await this.getOidcProvider() + + if (provider === null) { + throw new Error('Failed to get OIDC provider') + } + + const redirectionResponse = await this.getOidcRedirectLocation(provider) + + if (redirectionResponse === null) { + throw new Error('Failed to get OIDC redirect location') + } + + const { cookies, location } = redirectionResponse + + const loginToken = await this.getLoginToken( + location, + cookies, + authCookies + ) + + if (loginToken === null) { + throw new Error('Failed to get login token') + } + + const accessToken = await this.requestAccessToken(loginToken) + + if (accessToken === null) { + throw new Error('Failed to get access token') + } + + return accessToken + } catch (error) { + this.logger.error('Failed to fetch access_token', { error }) + return null + } + } + + /** + * Fetches the access token from the Matrix server using the provided login token. + * + * @param {string} loginToken - The login token to be used for authentication. + * @returns {Promise} The access token or null if an error occurs. + */ + requestAccessToken = async (loginToken: string): Promise => { + try { + const response = await fetch(`${this.matrixUrl}/login`, { + method: 'POST', + headers: this.JSON_HEADERS, + body: JSON.stringify({ + token: loginToken, + type: 'm.login.token', + initial_device_display_name: 'QR Code Login' + } satisfies TokenLoginPayload) + }) + + const data = (await response.json()) as TokenLoginResponse + + if (data.error !== undefined || data.access_token === undefined) { + throw new Error('No access_token found in the response') + } + + return data.access_token + } catch (error) { + this.logger.error('Failed to fetch access_token', { error }) + return null + } + } + + /** + * Fetches the OIDC provider from the Matrix server. + * + * @returns {Promise} The OIDC provider or null if an error occurs. + */ + getOidcProvider = async (): Promise => { + try { + const response = await fetch(`${this.matrixUrl}/login`, { + method: 'GET', + headers: this.JSON_HEADERS + }) + + const data = (await response.json()) as loginFlowsResponse + + if (data.error !== undefined || data.flows.length < 1) { + throw new Error('No OIDC provider found in the response', { + cause: data.error + }) + } + + const oidcProvider = data.flows.find( + (flow) => + flow.type === 'm.login.sso' && flow.identity_providers !== undefined + ) + + if ( + oidcProvider === undefined || + oidcProvider.identity_providers === undefined + ) { + throw new Error('No OIDC provider found in the response') + } + + return oidcProvider.identity_providers[0].id + } catch (error) { + this.logger.error('Failed to fetch OIDC login provider', { error }) + + return null + } + } + + /** + * Retrieves the OIDC redirect location and session cookies from the Matrix server. + * + * @param {string} oidcProvider - The OIDC provider to use for the redirect. + * @returns {Promise} The OIDC redirect location and cookies or null if an error occurs. + */ + getOidcRedirectLocation = async ( + oidcProvider: string + ): Promise => { + try { + const response = await fetch( + `${this.matrixUrl}/login/sso/redirect/${oidcProvider}?redirectUrl=http://localhost:9876`, + { + method: 'GET', + headers: this.JSON_HEADERS, + redirect: 'manual' + } + ) + + const location = response.headers.get('location') + const cookies = response.headers.get('set-cookie') + + if (location === null) { + throw new Error('No location found in the response') + } + + if (cookies === null) { + throw new Error('No session cookies found in the response') + } + + return { location, cookies } + } catch (error) { + this.logger.error('Failed to fetch access_token', { error }) + return null + } + } + + /** + * Fetches the login token using SSO + * + * @param {string} location - The location to fetch the login token from. + * @param {string} sessionCookies - The session cookies to be used for authentication. + * @param {string} authCookie - The auth cookie to be used for authentication ( ex lemonldap ). + * @returns {Promise} The login token or null if an error occurs. + * @memberof QRCodeTokenService + * @example + * const loginToken = await getLoginToken(location, sessionCookies, authCookie); + */ + getLoginToken = async ( + location: string, + sessionCookies: string, + authCookie: string + ): Promise => { + try { + const response = await fetch(location, { + headers: { + Cookie: `${sessionCookies}; ${authCookie}` + } + }) + + const responseText = await response.text() + const loginTokenMatch = responseText.match(/loginToken=(.+?)['"]/) + + if (loginTokenMatch === null) { + throw new Error('No LoginToken found in the response') + } + + if (loginTokenMatch[1] === undefined) { + throw new Error('invalid LoginToken') + } + + return loginTokenMatch[1] + } catch (error) { + this.logger.error('Failed to fetch LoginToken', { error }) + return null + } + } +} diff --git a/packages/tom-server/src/qrcode-api/test/controller.test.ts b/packages/tom-server/src/qrcode-api/test/controller.test.ts new file mode 100644 index 00000000..f7ca88a1 --- /dev/null +++ b/packages/tom-server/src/qrcode-api/test/controller.test.ts @@ -0,0 +1,97 @@ +import express, { type Response } from 'express' +import supertest from 'supertest' +import type { Config } from '../../types' +import type TwakeIdentityServer from '../../identity-server' +import router, { PATH } from '../routes' +import { type TwakeLogger } from '@twake/logger' + +const app = express() +const getAccessTokenMock = jest.fn() +const getImageMock = jest.fn() +const loggerMock = { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn() +} + +jest.mock('../services', () => { + return { + QRCodeTokenService: jest.fn().mockImplementation(() => { + return { + getAccessToken: getAccessTokenMock + } + }), + QRCodeService: jest.fn().mockImplementation(() => { + return { + getImage: getImageMock + } + }) + } +}) + +const idServerMock = { + db: {}, + userDb: {}, + authenticate: jest + .fn() + .mockImplementation((_req: Request, _res: Response, callbackMethod) => { + callbackMethod('test', 'test') + }) +} + +app.use( + router( + idServerMock as unknown as TwakeIdentityServer, + { + qr_code_url: 'https://example.com/' + } as unknown as Config, + loggerMock as unknown as TwakeLogger + ) +) + +beforeEach(() => { + jest.restoreAllMocks() +}) + +describe('the QRCode API controller', () => { + it('should return a QRCode', async () => { + getAccessTokenMock.mockResolvedValue('test') + getImageMock.mockResolvedValue('test') + + const response = await supertest(app).get(PATH).set('cookie', 'lemon=test') + + expect(response.status).toBe(200) + expect(response.body).toEqual(Buffer.from('test')) + }) + + it('should return 400 if auth cookies were missing', async () => { + const response = await supertest(app).get(PATH) + + expect(response.status).toBe(400) + }) + + it('should return 500 if something wrong happens while generating the SVG', async () => { + getAccessTokenMock.mockResolvedValue('test') + getImageMock.mockRejectedValue(new Error('test')) + + const result = await supertest(app).get(PATH).set('cookie', 'lemon=test') + + expect(result.status).toBe(500) + }) + + it('should return 500 if something wrong happens while fetching the access token', async () => { + getAccessTokenMock.mockRejectedValue(new Error('test')) + + const result = await supertest(app).get(PATH).set('cookie', 'lemon=test') + + expect(result.status).toBe(500) + }) + + it('should return 400 if the access token is invalid', async () => { + getAccessTokenMock.mockResolvedValue(null) + + const result = await supertest(app).get(PATH).set('cookie', 'lemon=test') + + expect(result.status).toBe(400) + }) +}) diff --git a/packages/tom-server/src/qrcode-api/test/qrcode.service.test.ts b/packages/tom-server/src/qrcode-api/test/qrcode.service.test.ts new file mode 100644 index 00000000..f9df6518 --- /dev/null +++ b/packages/tom-server/src/qrcode-api/test/qrcode.service.test.ts @@ -0,0 +1,61 @@ +import { type TwakeLogger } from '@twake/logger' +import { QRCodeService } from '../services' +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import QRCode from 'qrcode' +import type { Config } from '../../types' + +const testSvg = ` +` + +afterEach(() => { + jest.restoreAllMocks() +}) + +describe('the QRCode service', () => { + const loggerMock = { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn() + } + + const configMock = { + qr_code_url: 'https://example.com/' + } as unknown as Config + + const qrCodeService = new QRCodeService( + configMock, + loggerMock as unknown as TwakeLogger + ) + + it('should generate a QRCode', async () => { + const result = await qrCodeService.getImage('test') + + expect(result).not.toBeNull() + + const resultBuffer = Buffer.from(result as string) + const testSvgBuffer = Buffer.from(testSvg) + + expect(resultBuffer).toEqual(testSvgBuffer) + }) + + it('should return null if something wrong happens', async () => { + jest.spyOn(QRCode, 'toString').mockImplementation(() => { + throw new Error('test') + }) + + const result = await qrCodeService.getImage('test') + + expect(result).toBeNull() + }) + + it('should return null if qrcode_url config is not set', async () => { + const failingQrCodeService = new QRCodeService( + {} as unknown as Config, + loggerMock as unknown as TwakeLogger + ) + + const result = await failingQrCodeService.getImage('test') + + expect(result).toBeNull() + }) +}) diff --git a/packages/tom-server/src/qrcode-api/test/router.test.ts b/packages/tom-server/src/qrcode-api/test/router.test.ts new file mode 100644 index 00000000..dbf5b42f --- /dev/null +++ b/packages/tom-server/src/qrcode-api/test/router.test.ts @@ -0,0 +1,105 @@ +import { type ConfigDescription } from '@twake/config-parser' +import { IdentityServerDb, type MatrixDB } from '@twake/matrix-identity-server' +import express from 'express' +import fs from 'fs' +import path from 'path' +import supertest, { type Response } from 'supertest' +import JEST_PROCESS_ROOT_PATH from '../../../jest.globals' +import IdServer from '../../identity-server' +import type { Config } from '../../types' +import router, { PATH } from '../routes' + +const app = express() + +jest + .spyOn(IdentityServerDb.prototype, 'get') + .mockResolvedValue([{ data: '"test"' }]) + +const idServer = new IdServer( + { + get: jest.fn() + } as unknown as MatrixDB, + { + qr_code_url: 'https://example.com/' + } as unknown as Config, + { + database_engine: 'sqlite', + database_host: 'test.db', + rate_limiting_window: 5000, + rate_limiting_nb_requests: 10, + template_dir: `${JEST_PROCESS_ROOT_PATH}/templates`, + userdb_host: './tokens.db', + qr_code_url: 'https://example.com' + } as unknown as ConfigDescription +) + +beforeEach(() => { + jest.spyOn(console, 'warn').mockImplementation(() => {}) +}) + +const controllerGetSpy = jest.fn().mockImplementation((_req, res, _next) => { + res.status(200).send('OK') +}) + +jest.mock('../controllers/index.ts', () => { + return function () { + return { + get: controllerGetSpy + } + } +}) + +describe('the QRCode API router', () => { + beforeAll((done) => { + idServer.ready + .then(() => { + app.use(router(idServer, {} as unknown as Config)) + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterAll(() => { + idServer.cleanJobs() + const pathFilesToDelete = [ + path.join(JEST_PROCESS_ROOT_PATH, 'test.db'), + path.join(JEST_PROCESS_ROOT_PATH, 'tokens.db') + ] + pathFilesToDelete.forEach((path) => { + if (fs.existsSync(path)) fs.unlinkSync(path) + }) + }) + + it('should reject if rate limit is exceeded', async () => { + let response + + for (let i = 0; i < 11; i++) { + response = await supertest(app) + .get(PATH) + .set('Authorization', 'Bearer test') + } + + expect((response as unknown as Response).status).toEqual(429) + await new Promise((resolve) => setTimeout(resolve, 6000)) + }) + + it('should call the controller if the user is authenticated via Bearer', async () => { + await supertest(app).get(PATH).set('Authorization', 'Bearer test') + + expect(controllerGetSpy).toHaveBeenCalled() + }) + + it('should call the controller if the user is authenticated via access_token', async () => { + await supertest(app).get(PATH).query({ access_token: 'test' }) + + expect(controllerGetSpy).toHaveBeenCalled() + }) + + it('should not call the controller if no bearer or access_token is provided', async () => { + await supertest(app).get(PATH) + + expect(controllerGetSpy).not.toHaveBeenCalled() + }) +}) diff --git a/packages/tom-server/src/qrcode-api/test/token.service.test.ts b/packages/tom-server/src/qrcode-api/test/token.service.test.ts new file mode 100644 index 00000000..ab53eda6 --- /dev/null +++ b/packages/tom-server/src/qrcode-api/test/token.service.test.ts @@ -0,0 +1,410 @@ +import { type TwakeLogger } from '@twake/logger' +import { type Config } from '../../types' +import { QRCodeTokenService } from '../services' +import type { LoginFlow } from '../types' + +global.fetch = jest.fn( + async () => + await Promise.resolve({ + json: async () => await Promise.resolve({ access_token: 'demo_token' }) + } as unknown as Response) +) + +describe('the Token service', () => { + const loggerMock = { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn() + } + + const configMock = { + matrix_server: 'example.com' + } as unknown as Config + + const tokenService = new QRCodeTokenService( + configMock, + loggerMock as unknown as TwakeLogger + ) + + describe('the getOidcProvider method', () => { + it('should return the oidc provider', async () => { + global.fetch = jest.fn( + async () => + await Promise.resolve({ + json: async () => + await Promise.resolve({ + flows: [ + { + type: 'm.login.sso', + identity_providers: [ + { id: 'test-oidc-provider', name: 'test oidc provider' } + ] + }, + { + type: 'm.login.token' + } + ] satisfies LoginFlow[] + }) + } as unknown as Response) + ) + const result = await tokenService.getOidcProvider() + + expect(result).toBe('test-oidc-provider') + }) + + it('should return null if no oidc provider is found', async () => { + global.fetch = jest.fn( + async () => + await Promise.resolve({ + json: async () => + await Promise.resolve({ + flows: [ + { + type: 'm.login.token' + } + ] satisfies LoginFlow[] + }) + } as unknown as Response) + ) + const result = await tokenService.getOidcProvider() + + expect(result).toBeNull() + }) + + it("should return null if the sso login flow doesn't have any providers", async () => { + global.fetch = jest.fn( + async () => + await Promise.resolve({ + json: async () => + await Promise.resolve({ + flows: [ + { + type: 'm.login.sso' + } + ] satisfies LoginFlow[] + }) + } as unknown as Response) + ) + + const result = await tokenService.getOidcProvider() + expect(result).toBeNull() + }) + + it('should return null if the response had an error', async () => { + global.fetch = jest.fn( + async () => + await Promise.resolve({ + json: async () => + await Promise.resolve({ + error: 'something unusual' + }) + } as unknown as Response) + ) + const result = await tokenService.getOidcProvider() + + expect(result).toBeNull() + }) + + it('should return null if something wrong happens while fetching', async () => { + global.fetch = jest.fn( + async () => await Promise.reject(new Error('API is down')) + ) + const result = await tokenService.getOidcProvider() + + expect(result).toBeNull() + }) + }) + + describe('the getOidcRedirectLocation method', () => { + it('should return the location and cookies headers from the redirection response', async () => { + global.fetch = jest.fn( + async () => + await Promise.resolve({ + headers: { + get: (header: string) => { + if (header === 'location') { + return 'https://auth.example.com/login' + } else if (header === 'set-cookie') { + return 'cookie1=value1; cookie2=value2' + } + } + } + } as unknown as Response) + ) + + const result = await tokenService.getOidcRedirectLocation('oidc_provider') + + expect(result).toEqual({ + location: 'https://auth.example.com/login', + cookies: 'cookie1=value1; cookie2=value2' + }) + }) + + it('should return null if something wrong happens', async () => { + global.fetch = jest.fn( + async () => await Promise.reject(new Error('API is down')) + ) + + const result = await tokenService.getOidcRedirectLocation('oidc_provider') + + expect(result).toBeNull() + }) + + it('should return null if session cookies were missing from the headers', async () => { + global.fetch = jest.fn( + async () => + await Promise.resolve({ + headers: { + get: (headers: string) => { + if (headers === 'location') { + return 'https://auth.example.com/login' + } + + return null + } + } + } as unknown as Response) + ) + + const result = await tokenService.getOidcRedirectLocation('oidc_provider') + + expect(result).toBeNull() + }) + + it('should return null if location header was missing', async () => { + global.fetch = jest.fn( + async () => + await Promise.resolve({ + headers: { + get: (headers: string) => { + if (headers === 'set-cookie') { + return 'cookie1=value1; cookie2=value2' + } + + return null + } + } + } as unknown as Response) + ) + + const result = await tokenService.getOidcRedirectLocation('oidc_provider') + + expect(result).toBeNull() + }) + }) + + describe('the getLoginToken method', () => { + it('should extract the login token from the response body', async () => { + global.fetch = jest.fn( + async () => + await Promise.resolve({ + text: async () => + await Promise.resolve( + ` +
+ + continue + +
+ ` + ) + } as unknown as Response) + ) + + const result = await tokenService.getLoginToken( + 'oidc_provider', + 'session-cookie', + 'auth-cookie' + ) + + expect(result).toBe('123456') + }) + + it('should return null if something wrong happens', async () => { + global.fetch = jest.fn( + async () => await Promise.reject(new Error('API is down')) + ) + + const result = await tokenService.getLoginToken( + 'oidc_provider', + 'session-cookie', + 'auth-cookie' + ) + + expect(result).toBeNull() + }) + + it("should return null if the response body didn't include a loginToken", async () => { + global.fetch = jest.fn( + async () => + await Promise.resolve({ + text: async () => + await Promise.resolve( + 'continue' + ) + } as unknown as Response) + ) + + const result = await tokenService.getLoginToken( + 'oidc_provider', + 'session-cookie', + 'auth-cookie' + ) + + expect(result).toBeNull() + }) + }) + + describe('the requestAccessToken method', () => { + it('should request an access_token using the provided loginToken', async () => { + global.fetch = jest.fn( + async () => + await Promise.resolve({ + json: async () => + await Promise.resolve({ access_token: 'demo_token' }) + } as unknown as Response) + ) + + const result = await tokenService.requestAccessToken('loginToken') + + expect(result).toBe('demo_token') + }) + + it('should call the login API using the correct payload', async () => { + global.fetch = jest.fn( + async () => + await Promise.resolve({ + json: async () => + await Promise.resolve({ access_token: 'demo_token' }) + } as unknown as Response) + ) + + await tokenService.requestAccessToken('123456') + + expect(global.fetch).toHaveBeenCalledWith( + 'https://example.com/_matrix/client/v3/login', + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + token: '123456', + type: 'm.login.token', + initial_device_display_name: 'QR Code Login' + }) + } + ) + }) + + it('should return null if something wrong happens', async () => { + global.fetch = jest.fn( + async () => await Promise.reject(new Error('API is down')) + ) + + const result = await tokenService.requestAccessToken('loginToken') + + expect(result).toBeNull() + }) + + it("should return null if the response didn't include an access_token", async () => { + global.fetch = jest.fn( + async () => + await Promise.resolve({ + json: async () => + await Promise.resolve({ something_else: 'demo_token' }) + } as unknown as Response) + ) + + const result = await tokenService.requestAccessToken('loginToken') + + expect(result).toBeNull() + }) + + it('should return null if the response had an error', async () => { + global.fetch = jest.fn( + async () => + await Promise.resolve({ + json: async () => await Promise.resolve({ error: 'something' }) + } as unknown as Response) + ) + + const result = await tokenService.requestAccessToken('loginToken') + + expect(result).toBeNull() + }) + }) + + describe('the getAccessToken method', () => { + it('should return the access_token using SSO', async () => { + jest + .spyOn(tokenService, 'getOidcProvider') + .mockResolvedValue('test-oidc-provider') + jest.spyOn(tokenService, 'getOidcRedirectLocation').mockResolvedValue({ + location: 'https://auth.example.com/login', + cookies: 'cookie1=value1; cookie2=value2' + }) + jest.spyOn(tokenService, 'getLoginToken').mockResolvedValue('123456') + jest + .spyOn(tokenService, 'requestAccessToken') + .mockResolvedValue('demo_token') + + const result = await tokenService.getAccessToken('loginToken') + + expect(result).toBe('demo_token') + }) + + it('should return null if something wrong happens while fetching the OIDC provider', async () => { + jest.spyOn(tokenService, 'getOidcProvider').mockResolvedValue(null) + + const result = await tokenService.getAccessToken('loginToken') + + expect(result).toBeNull() + }) + + it("should return null if it couldn't follow the OIDC redirection", async () => { + jest + .spyOn(tokenService, 'getOidcProvider') + .mockResolvedValue('test-oidc-provider') + jest + .spyOn(tokenService, 'getOidcRedirectLocation') + .mockResolvedValue(null) + + const result = await tokenService.getAccessToken('loginToken') + + expect(result).toBeNull() + }) + + it("should return null if it couldn't fetch the loginToken", async () => { + jest + .spyOn(tokenService, 'getOidcProvider') + .mockResolvedValue('test-oidc-provider') + jest.spyOn(tokenService, 'getOidcRedirectLocation').mockResolvedValue({ + location: 'https://auth.example.com/login', + cookies: 'cookie1=value1; cookie2=value2' + }) + jest.spyOn(tokenService, 'getLoginToken').mockResolvedValue(null) + + const result = await tokenService.getAccessToken('loginToken') + + expect(result).toBeNull() + }) + + it("should return null if it couldn't request an access token", async () => { + jest + .spyOn(tokenService, 'getOidcProvider') + .mockResolvedValue('test-oidc-provider') + jest.spyOn(tokenService, 'getOidcRedirectLocation').mockResolvedValue({ + location: 'https://auth.example.com/login', + cookies: 'cookie1=value1; cookie2=value2' + }) + jest.spyOn(tokenService, 'getLoginToken').mockResolvedValue('123456') + jest.spyOn(tokenService, 'requestAccessToken').mockResolvedValue(null) + + const result = await tokenService.getAccessToken('loginToken') + + expect(result).toBeNull() + }) + }) +}) diff --git a/packages/tom-server/src/qrcode-api/types.ts b/packages/tom-server/src/qrcode-api/types.ts new file mode 100644 index 00000000..dc16a31d --- /dev/null +++ b/packages/tom-server/src/qrcode-api/types.ts @@ -0,0 +1,64 @@ +import { type Response, type NextFunction } from 'express' +import type { AuthRequest } from '../types' + +export interface IQRCodeApiController { + get: (req: AuthRequest, res: Response, next: NextFunction) => Promise +} + +export interface IQRCodeService { + getImage: (token: string) => Promise +} + +export interface IQRCodeTokenService { + getAccessToken: (cookies: string) => Promise + requestAccessToken: (loginToken: string) => Promise + getOidcProvider: () => Promise + getOidcRedirectLocation: ( + oidcProvider: string + ) => Promise + getLoginToken: ( + location: string, + sessionCookies: string, + authCookie: string + ) => Promise +} + +export interface TokenLoginPayload { + initial_device_display_name: string + token: string + type: string +} + +export interface GenericResponse { + errcode?: string + error?: string +} + +export interface TokenLoginResponse extends GenericResponse { + access_token: string + device_id: string + expires_in_ms: number + home_server: string + refresh_token: string + user_id: string + well_known?: object +} + +export interface loginFlowsResponse extends GenericResponse { + flows: LoginFlow[] +} + +export interface LoginFlow { + type: string + identity_providers?: IdentityProvider[] +} + +export interface IdentityProvider { + name: string + id: string +} + +export interface OIDCRedirectResponse { + location: string + cookies: string +} diff --git a/packages/tom-server/src/types.ts b/packages/tom-server/src/types.ts index 5f99a084..7467d003 100644 --- a/packages/tom-server/src/types.ts +++ b/packages/tom-server/src/types.ts @@ -38,6 +38,7 @@ export type Config = MConfig & sms_api_key?: string sms_api_login?: string sms_api_url?: string + qr_code_url?: string } export interface AuthRequest extends Request { diff --git a/server.mjs b/server.mjs index c41ccc03..1da12e4d 100644 --- a/server.mjs +++ b/server.mjs @@ -79,7 +79,8 @@ let conf = { userdb_engine: 'ldap', sms_api_key: process.env.SMS_API_KEY, sms_api_login: process.env.SMS_API_LOGIN, - sms_api_url: process.env.SMS_API_URL + sms_api_url: process.env.SMS_API_URL, + qr_code_url: process.env.QRCODE_URL ?? 'twake.chat://login' } if (process.argv[2] === 'generate') {