[ABANDONED] API server for Flexor social network.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

278 lines
9.1 KiB

5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
  1. import {
  2. FastifyInstance,
  3. Plugin,
  4. DefaultQuery,
  5. DefaultParams,
  6. DefaultHeaders,
  7. DefaultBody,
  8. RouteShorthandOptions,
  9. JSONSchema,
  10. } from 'fastify'
  11. import { Server, IncomingMessage, ServerResponse } from 'http'
  12. import { createAccessToken, createRefreshToken } from '../../lib/authentication'
  13. import { hashPassword, verifyPassword, JWT } from '../../lib/crypto'
  14. import { containerFor, normalize } from '../../lib/database'
  15. import { errorSchema, badRequestError, unauthorizedError } from '../../lib/errors'
  16. import { tokenFromHeader } from '../../lib/http'
  17. import { IUser, IUserToken, IGroup } from '../../types/collections'
  18. interface PluginOptions {
  19. }
  20. const tokenResponseSchema: JSONSchema = {
  21. type: 'object',
  22. properties: {
  23. id: { type: 'string' },
  24. access: { type: 'string' },
  25. refresh: { type: 'string' },
  26. },
  27. }
  28. function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
  29. interface Body {
  30. id: string
  31. name: string
  32. email: string
  33. password: string
  34. groupId: string
  35. }
  36. const options: RouteShorthandOptions = {
  37. schema: {
  38. body: {
  39. type: 'object',
  40. required: ['id', 'email', 'password'],
  41. properties: {
  42. id: { type: 'string' },
  43. name: { type: 'string' },
  44. email: { type: 'string' },
  45. password: { type: 'string' },
  46. groupId: { type: 'string' },
  47. },
  48. },
  49. response: {
  50. 201: tokenResponseSchema,
  51. 400: errorSchema,
  52. },
  53. },
  54. }
  55. server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/register', options, async (request, reply) => {
  56. const { name, email, password, groupId } = request.body
  57. const id = normalize(request.body.id)
  58. if (!id || id === '') return badRequestError(reply, 'id is required', 'id')
  59. if (id.length < 5 || id.length > 20) return badRequestError(reply, 'id must be between 5 and 20 characters', 'id')
  60. if (name && name.length > 40) return badRequestError(reply, 'name must be less than 40 characters', 'name')
  61. if (!email || email === '') return badRequestError(reply, 'email is required', 'email')
  62. if (email.length < 5) return badRequestError(reply, 'email must be at least 5 characters', 'email')
  63. if (!password || password === '') return badRequestError(reply, 'password is required', 'password')
  64. if (password.length < 5) return badRequestError(reply, 'password must be at least 5 characters', 'password')
  65. const userContainer = await containerFor(server.database.client, 'Users')
  66. const { resource: existingUser } = await userContainer.item(id, id).read<IUser>()
  67. if (existingUser) return badRequestError(reply, 'User id already taken', 'id')
  68. let userPending = false
  69. if (groupId) {
  70. const groupContainer = await containerFor(server.database.client, 'Groups')
  71. const { resource: group } = await groupContainer.item(groupId, groupId).read<IGroup>()
  72. if (!group) return badRequestError(reply, 'Group not found', 'groupId')
  73. if (!group.open) return badRequestError(reply, 'Group registration closed', 'groupId')
  74. if (group.requiresApproval) userPending = true
  75. }
  76. const user: IUser = {
  77. id,
  78. partitionKey: id,
  79. type: 'user',
  80. group: groupId,
  81. name,
  82. email,
  83. emailVerified: false,
  84. passwordHash: await hashPassword(password),
  85. installations: [],
  86. points: 0,
  87. subscriberCount: 0,
  88. subscribedCount: 0,
  89. pending: userPending,
  90. privacy: 'public',
  91. paid: false,
  92. about: '',
  93. created: Date.now(),
  94. }
  95. const refreshToken = createRefreshToken(id, request.headers['user-agent'], request.ip)
  96. await userContainer.items.create(user)
  97. await userContainer.items.create(refreshToken)
  98. return {
  99. id,
  100. access: await createAccessToken(id),
  101. refresh: refreshToken.id,
  102. }
  103. })
  104. }
  105. function authenticateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
  106. interface Body {
  107. id?: string
  108. email?: string
  109. password: string
  110. }
  111. const options: RouteShorthandOptions = {
  112. schema: {
  113. body: {
  114. type: 'object',
  115. required: ['id', 'password'],
  116. properties: {
  117. id: { type: 'string' },
  118. password: { type: 'string' },
  119. },
  120. },
  121. response: {
  122. 200: tokenResponseSchema,
  123. 400: errorSchema,
  124. },
  125. },
  126. }
  127. server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/authenticate', options, async (request, reply) => {
  128. const { password } = request.body
  129. const id = normalize(request.body.id)
  130. const container = await containerFor(server.database.client, 'Users')
  131. const { resource: user } = await container.item(id, id).read<IUser>()
  132. if (!user) return badRequestError(reply, 'User not found')
  133. const result = await verifyPassword(user.passwordHash, password)
  134. if (!result) return badRequestError(reply, 'Incorrect credentials')
  135. const refreshToken = createRefreshToken(user.id, request.headers['user-agent'], request.ip)
  136. await container.items.create(refreshToken)
  137. return {
  138. id,
  139. access: await createAccessToken(user.id),
  140. refresh: refreshToken.id,
  141. }
  142. })
  143. }
  144. function refreshRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
  145. interface Headers {
  146. [key: string]: string
  147. authorization: string
  148. }
  149. interface Body {
  150. refresh: string
  151. }
  152. const options: RouteShorthandOptions = {
  153. schema: {
  154. headers: {
  155. type: 'object',
  156. properties: {
  157. authorization: { type: 'string' },
  158. },
  159. },
  160. body: {
  161. type: 'object',
  162. required: ['refresh'],
  163. properties: {
  164. refresh: { type: 'string' },
  165. },
  166. },
  167. response: {
  168. 200: tokenResponseSchema,
  169. 400: errorSchema,
  170. },
  171. },
  172. }
  173. server.post<DefaultQuery, DefaultParams, Headers, Body>('/api/refresh', options, async (request, reply) => {
  174. const tokenString = tokenFromHeader(request.headers.authorization)
  175. if (!tokenString) return badRequestError(reply, 'Access token required')
  176. let userId: string = ''
  177. try {
  178. const tokenData = await JWT.verify(tokenString, {
  179. ignoreExpiration: true,
  180. })
  181. if ((tokenData.exp * 1000) > Date.now()) return badRequestError(reply, 'Token must be expired')
  182. userId = tokenData.sub
  183. } catch (err) {
  184. return badRequestError(reply, 'Invalid token')
  185. }
  186. const container = await containerFor(server.database.client, 'Users')
  187. const tokenItem = container.item(request.body.refresh, userId)
  188. if (!tokenItem) return badRequestError(reply, 'Invalid refresh token')
  189. const { resource: token } = await tokenItem.read<IUserToken>()
  190. if (token.expires < Date.now()) return badRequestError(reply, 'Refresh token expired')
  191. await tokenItem.delete()
  192. const newRefreshToken = createRefreshToken(userId, request.headers['user-agent'], request.ip)
  193. await container.items.create(newRefreshToken)
  194. return {
  195. id: userId,
  196. access: await createAccessToken(userId),
  197. refresh: newRefreshToken.id
  198. }
  199. })
  200. }
  201. function selfRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
  202. const options: RouteShorthandOptions = {
  203. schema: {
  204. response: {
  205. 200: {
  206. type: 'object',
  207. properties: {
  208. id: { type: 'string' },
  209. name: { type: 'string' },
  210. },
  211. },
  212. },
  213. },
  214. }
  215. server.get<DefaultQuery, DefaultParams, DefaultHeaders, DefaultBody>('/api/self', options, async (request, reply) => {
  216. if (!request.viewer) return unauthorizedError(reply)
  217. const container = await containerFor(server.database.client, 'Users')
  218. const { resource: viewer } = await container.item(request.viewer.id, request.viewer.id).read<IUser>()
  219. if (!viewer) return unauthorizedError(reply)
  220. return {
  221. id: viewer.id,
  222. name: viewer.name,
  223. }
  224. })
  225. }
  226. const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = async server => {
  227. registerRoute(server)
  228. authenticateRoute(server)
  229. refreshRoute(server)
  230. selfRoute(server)
  231. }
  232. export default plugin