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