aboutsummaryrefslogtreecommitdiffstats
path: root/app/Controllers/Http/UserController.ts
diff options
context:
space:
mode:
Diffstat (limited to 'app/Controllers/Http/UserController.ts')
-rw-r--r--app/Controllers/Http/UserController.ts239
1 files changed, 112 insertions, 127 deletions
diff --git a/app/Controllers/Http/UserController.ts b/app/Controllers/Http/UserController.ts
index ef7cfdd..088f7b1 100644
--- a/app/Controllers/Http/UserController.ts
+++ b/app/Controllers/Http/UserController.ts
@@ -1,134 +1,125 @@
1import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; 1import type { HttpContext } from '@adonisjs/core/http'
2import { schema, rules } from '@ioc:Adonis/Core/Validator'; 2import { schema, rules } from '@adonisjs/validator'
3import User from 'App/Models/User'; 3import User from '#app/Models/User'
4import { connectWithFranz, isRegistrationEnabled } from '../../../config/app'; 4import { connectWithFranz, isRegistrationEnabled } from '../../../config/app.js'
5import crypto from 'node:crypto'; 5import crypto from 'node:crypto'
6import { v4 as uuid } from 'uuid'; 6import { v4 as uuid } from 'uuid'
7import Workspace from 'App/Models/Workspace'; 7import Workspace from '#app/Models/Workspace'
8import Service from 'App/Models/Service'; 8import Service from '#app/Models/Service'
9import fetch from 'node-fetch';
10 9
11// TODO: This file needs to be refactored and cleaned up to include types 10// TODO: This file needs to be refactored and cleaned up to include types
12import { handleVerifyAndReHash } from '../../../helpers/PasswordHash'; 11import { handleVerifyAndReHash } from '../../../helpers/PasswordHash.js'
13 12
14const newPostSchema = schema.create({ 13const newPostSchema = schema.create({
15 firstname: schema.string(), 14 firstname: schema.string(),
16 lastname: schema.string(), 15 lastname: schema.string(),
17 email: schema.string([ 16 email: schema.string([rules.email(), rules.unique({ table: 'users', column: 'email' })]),
18 rules.email(),
19 rules.unique({ table: 'users', column: 'email' }),
20 ]),
21 password: schema.string([rules.minLength(8)]), 17 password: schema.string([rules.minLength(8)]),
22}); 18})
23 19
24const franzImportSchema = schema.create({ 20const franzImportSchema = schema.create({
25 email: schema.string([ 21 email: schema.string([rules.email(), rules.unique({ table: 'users', column: 'email' })]),
26 rules.email(),
27 rules.unique({ table: 'users', column: 'email' }),
28 ]),
29 password: schema.string([rules.minLength(8)]), 22 password: schema.string([rules.minLength(8)]),
30}); 23})
31 24
32// // TODO: This whole controller needs to be changed such that it can support importing from both Franz and Ferdi 25// // TODO: This whole controller needs to be changed such that it can support importing from both Franz and Ferdi
33// eslint-disable-next-line @typescript-eslint/no-explicit-any 26// eslint-disable-next-line @typescript-eslint/no-explicit-any
34const franzRequest = (route: any, method: any, auth: any) => 27const franzRequest = (route: any, method: any, auth: any) =>
35 new Promise((resolve, reject) => { 28 new Promise((resolve, reject) => {
36 const base = 'https://api.franzinfra.com/v1/'; 29 const base = 'https://api.franzinfra.com/v1/'
37 const user = 30 const user =
38 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Franz/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36'; 31 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Franz/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36'
39 32
40 try { 33 try {
41 fetch(base + route, { 34 fetch(base + route, {
42 method, 35 method,
43 headers: { 36 headers: {
44 Authorization: `Bearer ${auth}`, 37 'Authorization': `Bearer ${auth}`,
45 'User-Agent': user, 38 'User-Agent': user,
46 }, 39 },
47 }) 40 })
48 .then(data => data.json()) 41 .then((data) => data.json())
49 .then(json => resolve(json)); 42 .then((json) => resolve(json))
50 } catch { 43 } catch {
51 reject(); 44 reject()
52 } 45 }
53 }); 46 })
54 47
55export default class UsersController { 48export default class UsersController {
56 // Register a new user 49 // Register a new user
57 public async signup({ request, response, auth }: HttpContextContract) { 50 public async signup({ request, response, auth }: HttpContext) {
58 if (isRegistrationEnabled === 'false') { 51 if (isRegistrationEnabled === 'false') {
59 return response.status(401).send({ 52 return response.status(401).send({
60 message: 'Registration is disabled on this server', 53 message: 'Registration is disabled on this server',
61 status: 401, 54 status: 401,
62 }); 55 })
63 } 56 }
64 57
65 // Validate user input 58 // Validate user input
66 let data; 59 let data
67 try { 60 try {
68 data = await request.validate({ schema: newPostSchema }); 61 data = await request.validate({ schema: newPostSchema })
69 } catch (error) { 62 } catch (error) {
70 return response.status(401).send({ 63 return response.status(401).send({
71 message: 'Invalid POST arguments', 64 message: 'Invalid POST arguments',
72 messages: error.messages, 65 messages: error.messages,
73 status: 401, 66 status: 401,
74 }); 67 })
75 } 68 }
76 69
77 // Create user in DB 70 // Create user in DB
78 let user; 71 let user
79 try { 72 try {
80 user = await User.create({ 73 user = await User.create({
81 email: data.email, 74 email: data.email,
82 password: data.password, 75 password: data.password,
83 username: data.firstname, 76 username: data.firstname,
84 lastname: data.lastname, 77 lastname: data.lastname,
85 }); 78 })
86 } catch { 79 } catch {
87 return response.status(401).send({ 80 return response.status(401).send({
88 message: 'E-Mail address already in use', 81 message: 'E-Mail address already in use',
89 status: 401, 82 status: 401,
90 }); 83 })
91 } 84 }
92 85
93 // Generate new auth token 86 // Generate new auth token
94 const token = await auth.use('jwt').login(user, { payload: {} }); 87 const token = await auth.use('jwt').login(user, { payload: {} })
95 88
96 return response.send({ 89 return response.send({
97 message: 'Successfully created account', 90 message: 'Successfully created account',
98 token: token.accessToken, 91 token: token.accessToken,
99 }); 92 })
100 } 93 }
101 94
102 // Login using an existing user 95 // Login using an existing user
103 public async login({ request, response, auth }: HttpContextContract) { 96 public async login({ request, response, auth }: HttpContext) {
104 if (!request.header('Authorization')) { 97 if (!request.header('Authorization')) {
105 return response.status(401).send({ 98 return response.status(401).send({
106 message: 'Please provide authorization', 99 message: 'Please provide authorization',
107 status: 401, 100 status: 401,
108 }); 101 })
109 } 102 }
110 103
111 // Get auth data from auth token 104 // Get auth data from auth token
112 const authHeader = atob( 105 const authHeader = atob(request.header('Authorization')!.replace('Basic ', '')).split(':')
113 request.header('Authorization')!.replace('Basic ', ''),
114 ).split(':');
115 106
116 // Check if user with email exists 107 // Check if user with email exists
117 const user = await User.query().where('email', authHeader[0]).first(); 108 const user = await User.query().where('email', authHeader[0]).first()
118 if (!user?.email) { 109 if (!user?.email) {
119 return response.status(401).send({ 110 return response.status(401).send({
120 message: 'User credentials not valid', 111 message: 'User credentials not valid',
121 code: 'invalid-credentials', 112 code: 'invalid-credentials',
122 status: 401, 113 status: 401,
123 }); 114 })
124 } 115 }
125 116
126 // Verify password 117 // Verify password
127 let isMatchedPassword = false; 118 let isMatchedPassword = false
128 try { 119 try {
129 isMatchedPassword = await handleVerifyAndReHash(user, authHeader[1]); 120 isMatchedPassword = await handleVerifyAndReHash(user, authHeader[1])
130 } catch (error) { 121 } catch (error) {
131 return response.internalServerError({ message: error.message }); 122 return response.internalServerError({ message: error.message })
132 } 123 }
133 124
134 if (!isMatchedPassword) { 125 if (!isMatchedPassword) {
@@ -136,31 +127,28 @@ export default class UsersController {
136 message: 'User credentials not valid', 127 message: 'User credentials not valid',
137 code: 'invalid-credentials', 128 code: 'invalid-credentials',
138 status: 401, 129 status: 401,
139 }); 130 })
140 } 131 }
141 132
142 // Generate token 133 // Generate token
143 const token = await auth.use('jwt').login(user, { payload: {} }); 134 const token = await auth.use('jwt').login(user, { payload: {} })
144 135
145 return response.send({ 136 return response.send({
146 message: 'Successfully logged in', 137 message: 'Successfully logged in',
147 token: token.accessToken, 138 token: token.accessToken,
148 }); 139 })
149 } 140 }
150 141
151 // Return information about the current user 142 // Return information about the current user
152 public async me({ request, response, auth }: HttpContextContract) { 143 public async me({ request, response, auth }: HttpContext) {
153 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'. 144 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'.
154 const user = auth.user ?? request.user; 145 const user = auth.user ?? request.user
155 146
156 if (!user) { 147 if (!user) {
157 return response.send('Missing or invalid api token'); 148 return response.send('Missing or invalid api token')
158 } 149 }
159 150
160 const settings = 151 const settings = typeof user.settings === 'string' ? JSON.parse(user.settings) : user.settings
161 typeof user.settings === 'string'
162 ? JSON.parse(user.settings)
163 : user.settings;
164 152
165 return response.send({ 153 return response.send({
166 accountType: 'individual', 154 accountType: 'individual',
@@ -176,29 +164,29 @@ export default class UsersController {
176 lastname: user.lastname, 164 lastname: user.lastname,
177 locale: 'en-US', 165 locale: 'en-US',
178 ...settings, 166 ...settings,
179 }); 167 })
180 } 168 }
181 169
182 public async updateMe({ request, response, auth }: HttpContextContract) { 170 public async updateMe({ request, response, auth }: HttpContext) {
183 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'. 171 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'.
184 const user = auth.user ?? request.user; 172 const user = auth.user ?? request.user
185 173
186 if (!user) { 174 if (!user) {
187 return response.send('Missing or invalid api token'); 175 return response.send('Missing or invalid api token')
188 } 176 }
189 177
190 let settings = user.settings || {}; 178 let settings = user.settings || {}
191 if (typeof settings === 'string') { 179 if (typeof settings === 'string') {
192 settings = JSON.parse(settings); 180 settings = JSON.parse(settings)
193 } 181 }
194 182
195 const newSettings = { 183 const newSettings = {
196 ...settings, 184 ...settings,
197 ...request.all(), 185 ...request.all(),
198 }; 186 }
199 187
200 user.settings = JSON.stringify(newSettings); 188 user.settings = JSON.stringify(newSettings)
201 await user.save(); 189 await user.save()
202 190
203 return response.send({ 191 return response.send({
204 data: { 192 data: {
@@ -217,140 +205,137 @@ export default class UsersController {
217 ...newSettings, 205 ...newSettings,
218 }, 206 },
219 status: ['data-updated'], 207 status: ['data-updated'],
220 }); 208 })
221 } 209 }
222 210
223 public async newToken({ request, response, auth }: HttpContextContract) { 211 public async newToken({ request, response, auth }: HttpContext) {
224 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'. 212 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'.
225 const user = auth.user ?? request.user; 213 const user = auth.user ?? request.user
226 214
227 if (!user) { 215 if (!user) {
228 return response.send('Missing or invalid api token'); 216 return response.send('Missing or invalid api token')
229 } 217 }
230 218
231 const token = await auth.use('jwt').generate(user, { payload: {} }); 219 const token = await auth.use('jwt').generate(user, { payload: {} })
232 220
233 return response.send({ 221 return response.send({
234 token: token.accessToken, 222 token: token.accessToken,
235 }); 223 })
236 } 224 }
237 225
238 public async import({ request, response, view }: HttpContextContract) { 226 public async import({ request, response, view }: HttpContext) {
239 if (isRegistrationEnabled === 'false') { 227 if (isRegistrationEnabled === 'false') {
240 return response.status(401).send({ 228 return response.status(401).send({
241 message: 'Registration is disabled on this server', 229 message: 'Registration is disabled on this server',
242 status: 401, 230 status: 401,
243 }); 231 })
244 } 232 }
245 233
246 if (connectWithFranz === 'false') { 234 if (connectWithFranz === 'false') {
247 return response.send( 235 return response.send(
248 'We could not import your Franz account data.\n\nIf you are the server owner, please set CONNECT_WITH_FRANZ to true to enable account imports.', 236 'We could not import your Franz account data.\n\nIf you are the server owner, please set CONNECT_WITH_FRANZ to true to enable account imports.'
249 ); 237 )
250 } 238 }
251 239
252 // Validate user input 240 // Validate user input
253 let data; 241 let data
254 try { 242 try {
255 data = await request.validate({ schema: franzImportSchema }); 243 data = await request.validate({ schema: franzImportSchema })
256 } catch (error) { 244 } catch (error) {
257 return view.render('others.message', { 245 return view.render('others.message', {
258 heading: 'Error while importing', 246 heading: 'Error while importing',
259 text: error.messages, 247 text: error.messages,
260 }); 248 })
261 } 249 }
262 250
263 const { email, password } = data; 251 const { email, password } = data
264 252
265 const hashedPassword = crypto 253 const hashedPassword = crypto.createHash('sha256').update(password).digest('base64')
266 .createHash('sha256')
267 .update(password)
268 .digest('base64');
269 254
270 const base = 'https://api.franzinfra.com/v1/'; 255 const base = 'https://api.franzinfra.com/v1/'
271 const userAgent = 256 const userAgent =
272 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Franz/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36'; 257 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Franz/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36'
273 258
274 // Try to get an authentication token 259 // Try to get an authentication token
275 let token; 260 let token
276 try { 261 try {
277 const basicToken = btoa(`${email}:${hashedPassword}`); 262 const basicToken = btoa(`${email}:${hashedPassword}`)
278 const loginBody = { 263 const loginBody = {
279 isZendeskLogin: false, 264 isZendeskLogin: false,
280 }; 265 }
281 266
282 const rawResponse = await fetch(`${base}auth/login`, { 267 const rawResponse = await fetch(`${base}auth/login`, {
283 method: 'POST', 268 method: 'POST',
284 body: JSON.stringify(loginBody), 269 body: JSON.stringify(loginBody),
285 headers: { 270 headers: {
286 Authorization: `Basic ${basicToken}`, 271 'Authorization': `Basic ${basicToken}`,
287 'User-Agent': userAgent, 272 'User-Agent': userAgent,
288 'Content-Type': 'application/json', 273 'Content-Type': 'application/json',
289 accept: '*/*', 274 'accept': '*/*',
290 'x-franz-source': 'Web', 275 'x-franz-source': 'Web',
291 }, 276 },
292 }); 277 })
293 const content = await rawResponse.json(); 278 const content = await rawResponse.json()
294 279
295 if (!content.message || content.message !== 'Successfully logged in') { 280 if (!content.message || content.message !== 'Successfully logged in') {
296 const errorMessage = 281 const errorMessage =
297 'Could not login into Franz with your supplied credentials. Please check and try again'; 282 'Could not login into Franz with your supplied credentials. Please check and try again'
298 return response.status(401).send(errorMessage); 283 return response.status(401).send(errorMessage)
299 } 284 }
300 285
301 token = content.token; 286 token = content.token
302 } catch (error) { 287 } catch (error) {
303 return response.status(401).send({ 288 return response.status(401).send({
304 message: 'Cannot login to Franz', 289 message: 'Cannot login to Franz',
305 error: error, 290 error: error,
306 }); 291 })
307 } 292 }
308 293
309 // Get user information 294 // Get user information
310 // eslint-disable-next-line @typescript-eslint/no-explicit-any 295 // eslint-disable-next-line @typescript-eslint/no-explicit-any
311 let userInf: any = false; 296 let userInf: any = false
312 try { 297 try {
313 userInf = await franzRequest('me', 'GET', token); 298 userInf = await franzRequest('me', 'GET', token)
314 } catch (error) { 299 } catch (error) {
315 const errorMessage = `Could not get your user info from Franz. Please check your credentials or try again later.\nError: ${error}`; 300 const errorMessage = `Could not get your user info from Franz. Please check your credentials or try again later.\nError: ${error}`
316 return response.status(401).send(errorMessage); 301 return response.status(401).send(errorMessage)
317 } 302 }
318 if (!userInf) { 303 if (!userInf) {
319 const errorMessage = 304 const errorMessage =
320 'Could not get your user info from Franz. Please check your credentials or try again later'; 305 'Could not get your user info from Franz. Please check your credentials or try again later'
321 return response.status(401).send(errorMessage); 306 return response.status(401).send(errorMessage)
322 } 307 }
323 308
324 // Create user in DB 309 // Create user in DB
325 let user; 310 let user
326 try { 311 try {
327 user = await User.create({ 312 user = await User.create({
328 email: userInf.email, 313 email: userInf.email,
329 password: hashedPassword, 314 password: hashedPassword,
330 username: userInf.firstname, 315 username: userInf.firstname,
331 lastname: userInf.lastname, 316 lastname: userInf.lastname,
332 }); 317 })
333 } catch (error) { 318 } catch (error) {
334 const errorMessage = `Could not create your user in our system.\nError: ${error}`; 319 const errorMessage = `Could not create your user in our system.\nError: ${error}`
335 return response.status(401).send(errorMessage); 320 return response.status(401).send(errorMessage)
336 } 321 }
337 322
338 const serviceIdTranslation = {}; 323 const serviceIdTranslation = {}
339 324
340 // Import services 325 // Import services
341 try { 326 try {
342 const services = await franzRequest('me/services', 'GET', token); 327 const services = await franzRequest('me/services', 'GET', token)
343 328
344 // @ts-expect-error 329 // @ts-expect-error
345 for (const service of services) { 330 for (const service of services) {
346 // Get new, unused uuid 331 // Get new, unused uuid
347 let serviceId; 332 let serviceId
348 do { 333 do {
349 serviceId = uuid(); 334 serviceId = uuid()
350 } while ( 335 } while (
351 // eslint-disable-next-line no-await-in-loop, unicorn/no-await-expression-member 336 // eslint-disable-next-line no-await-in-loop, unicorn/no-await-expression-member
352 (await Service.query().where('serviceId', serviceId)).length > 0 337 (await Service.query().where('serviceId', serviceId)).length > 0
353 ); 338 )
354 339
355 // eslint-disable-next-line no-await-in-loop 340 // eslint-disable-next-line no-await-in-loop
356 await Service.create({ 341 await Service.create({
@@ -359,34 +344,34 @@ export default class UsersController {
359 name: service.name, 344 name: service.name,
360 recipeId: service.recipeId, 345 recipeId: service.recipeId,
361 settings: JSON.stringify(service), 346 settings: JSON.stringify(service),
362 }); 347 })
363 348
364 // @ts-expect-error 349 // @ts-expect-error
365 serviceIdTranslation[service.id] = serviceId; 350 serviceIdTranslation[service.id] = serviceId
366 } 351 }
367 } catch (error) { 352 } catch (error) {
368 const errorMessage = `Could not import your services into our system.\nError: ${error}`; 353 const errorMessage = `Could not import your services into our system.\nError: ${error}`
369 return response.status(401).send(errorMessage); 354 return response.status(401).send(errorMessage)
370 } 355 }
371 356
372 // Import workspaces 357 // Import workspaces
373 try { 358 try {
374 const workspaces = await franzRequest('workspace', 'GET', token); 359 const workspaces = await franzRequest('workspace', 'GET', token)
375 360
376 // @ts-expect-error 361 // @ts-expect-error
377 for (const workspace of workspaces) { 362 for (const workspace of workspaces) {
378 let workspaceId; 363 let workspaceId
379 do { 364 do {
380 workspaceId = uuid(); 365 workspaceId = uuid()
381 } while ( 366 } while (
382 // eslint-disable-next-line unicorn/no-await-expression-member, no-await-in-loop 367 // eslint-disable-next-line unicorn/no-await-expression-member, no-await-in-loop
383 (await Workspace.query().where('workspaceId', workspaceId)).length > 0 368 (await Workspace.query().where('workspaceId', workspaceId)).length > 0
384 ); 369 )
385 370
386 const services = workspace.services.map( 371 const services = workspace.services.map(
387 // @ts-expect-error 372 // @ts-expect-error
388 service => serviceIdTranslation[service], 373 (service) => serviceIdTranslation[service]
389 ); 374 )
390 375
391 // eslint-disable-next-line no-await-in-loop 376 // eslint-disable-next-line no-await-in-loop
392 await Workspace.create({ 377 await Workspace.create({
@@ -396,15 +381,15 @@ export default class UsersController {
396 order: workspace.order, 381 order: workspace.order,
397 services: JSON.stringify(services), 382 services: JSON.stringify(services),
398 data: JSON.stringify({}), 383 data: JSON.stringify({}),
399 }); 384 })
400 } 385 }
401 } catch (error) { 386 } catch (error) {
402 const errorMessage = `Could not import your workspaces into our system.\nError: ${error}`; 387 const errorMessage = `Could not import your workspaces into our system.\nError: ${error}`
403 return response.status(401).send(errorMessage); 388 return response.status(401).send(errorMessage)
404 } 389 }
405 390
406 return response.send( 391 return response.send(
407 'Your account has been imported. You can now use your Franz/Ferdi account in Ferdium.', 392 'Your account has been imported. You can now use your Franz/Ferdi account in Ferdium.'
408 ); 393 )
409 } 394 }
410} 395}