aboutsummaryrefslogtreecommitdiffstats
path: root/app/Controllers/Http/UserController.js
diff options
context:
space:
mode:
authorLibravatar Ricardo <ricardo@cino.io>2023-10-13 14:12:03 +0200
committerLibravatar GitHub <noreply@github.com>2023-10-13 13:12:03 +0100
commite503468660a13760010a94ecda5f0625c6f47f87 (patch)
treefa532f54fc5f091de08d55405ec6339bd2440a02 /app/Controllers/Http/UserController.js
parent1.3.16 [skip ci] (diff)
downloadferdium-server-e503468660a13760010a94ecda5f0625c6f47f87.tar.gz
ferdium-server-e503468660a13760010a94ecda5f0625c6f47f87.tar.zst
ferdium-server-e503468660a13760010a94ecda5f0625c6f47f87.zip
Server re-build with latest AdonisJS framework & Typescript (#47)
* chore: setup first basis structure * chore: ensure styling is loaded correctly * chore: comply to new routing syntax by replace . with / in routes/resource locations * chore: add login controller * chore: correctly use views with slash instead of dot * chore: working login + tests * chore: clean up tests * chore: add password-forgot endpoint and matching test * chore: add delete page test * chore: add logout test * chore: add reset-password route and tests * chore: remove obsolete comment * chore: add account-page and tests * chore: add data page & first step of the test * chore: add transfer/import data feature and tests * chore: add export and basic test * chore: add all static api routes with tests * Regenerate 'pnpm-lock.json' and fix bad merge conflict WIP: - Tests have been commented out since they dont work - Server doesn't start * easier dev and test runs * - remove --require-pragma from reformat-files so formatting works properly - run pnpm reformat-files over codebase - remove .json files from .eslintignore - add invalid.json file to .eslintignore - configure prettier properly in eslint config - add type jsdoc to prettier config - run adonis generate:manifest command to regenerate ace-manifest.json - specify volta in package.json - introduce typecheck npm script - remove unused .mjs extension from npm scripts - install missing type definition dependencies - add pnpm.allowedDeprecatedVersions to package.json - fix invalid extends in tsconfig.json causing TS issues throughout codebase - remove @ts-ignore throughout codebase which is not relevant anymore - enable some of the tsconfig options - remove outdated eslint-disable from codebase - change deprecated faker.company.companyName() to faker.company.name() - fix TS issues inside transfer.spec.ts * - update to latest node and pnpm versions - upgrade all non-major dependencies to latest - install missing @types/luxon dependency - add cuid to pnpm.allowedDeprecatedVersions - add esModuleInterop config option to tsconfig - migrate more deprecated faker methods to new ones - add more temporary ts-ignore to code * - update eslint config - remove trailingComma: all since default in prettier v3 - add typecheck command to prepare-code npm script - upgrade various dependencies to latest major version - update tsconfig to include only useful config options - disable some lint issues and fix others * - add test command to prepare-code - disable strictPropertyInitialization flag in tsconfig which creates issues with adonis models - update precommit hook to excute pnpm prepare-code - remove ts-ignore statements from all models * fix node and pnpm dependency update * add cross env (so that we can develop on windows) * add signup endpoint (TODO: JWT auth) * Add login endpoint * Add me and updateMe endpoints * Add service endpoint * refactor: change endpoints to use jwt * add recipes endpoint * add workspaces endpoint * fix web controllors for login and post import * Update node deps * Change auth middleware (for web) and exempt api from CSRF * Add import endpoint (franz import) * Fix export/import logic * Fix service and workspace data in user/data * Fix partial lint * chore: workaround lint issues * fix: migration naming had two . * Sync back node with recipes repo * Temporarily ignore typescript * Fix adonisrc to handle public folder static assets * Fix issue with production database * add Legacy Password Provider * Fix lint errors * Fix issue on login errors frontend * add Legacy Password Provider * Fix issue with customIcons * Fix issue with auth tokens * Update 'node' to '18.18.0' * make docker work * improve docker entrypoint (test api performance) * Add migration database script * NODE_ENV on recipes * prefer @ts-expect-error over @ts-ignore * small fixes * Update 'pnpm' to '8.7.6' * fix error catch * Automatically generate JWT Public and Private keys * Use custom Adonis5-jwt * Update code to use secret (old way, no breaking changes) * Normalize appKey * Trick to make JWT tokens on client work with new version * Fix error with new JWT logic * Change migration and how we store JWT * Fix 500 response code (needs to be 401) * Improve logic and fix bugs * Fix build and entrypoint logic * Catch error if appKey changes * Add newToken logic * Fix lint (ignore any errors) * Add build for PRs * pnpm reformat-files result * Fix some tests * Fix reset password not working (test failing) * Restore csrfTokens (disabled by accident) * Fix pnpm start command with .env * Disable failing tests on the transfer endpoint (TODO) * Add tests to PR build * Fix build * Remove unnecessary assertStatus * Add typecheck * hash password on UserFactory (fix build) * Add JWT_USE_PEM true by default (increase security) * fix name of github action --------- Co-authored-by: Vijay A <vraravam@users.noreply.github.com> Co-authored-by: Balaji Vijayakumar <kuttibalaji.v6@gmail.com> Co-authored-by: MCMXC <16797721+mcmxcdev@users.noreply.github.com> Co-authored-by: André Oliveira <oliveira.andrerodrigues95@gmail.com>
Diffstat (limited to 'app/Controllers/Http/UserController.js')
-rw-r--r--app/Controllers/Http/UserController.js386
1 files changed, 0 insertions, 386 deletions
diff --git a/app/Controllers/Http/UserController.js b/app/Controllers/Http/UserController.js
deleted file mode 100644
index aef7f01..0000000
--- a/app/Controllers/Http/UserController.js
+++ /dev/null
@@ -1,386 +0,0 @@
1const User = use('App/Models/User');
2const Service = use('App/Models/Service');
3const Workspace = use('App/Models/Workspace');
4const { validateAll } = use('Validator');
5const Env = use('Env');
6
7const atob = require('atob');
8const btoa = require('btoa');
9const fetch = require('node-fetch');
10const { v4: uuid } = require('uuid');
11const crypto = require('crypto');
12
13// TODO: This whole controller needs to be changed such that it can support importing from both Franz and Ferdi
14const franzRequest = (route, method, auth) =>
15 new Promise((resolve, reject) => {
16 const base = 'https://api.franzinfra.com/v1/';
17 const user =
18 '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';
19
20 try {
21 fetch(base + route, {
22 method,
23 headers: {
24 Authorization: `Bearer ${auth}`,
25 'User-Agent': user,
26 },
27 })
28 .then(data => data.json())
29 .then(json => resolve(json));
30 } catch (e) {
31 reject();
32 }
33 });
34
35class UserController {
36 // Register a new user
37 async signup({ request, response, auth }) {
38 if (Env.get('IS_REGISTRATION_ENABLED') == 'false') {
39 // eslint-disable-line eqeqeq
40 return response.status(401).send({
41 message: 'Registration is disabled on this server',
42 status: 401,
43 });
44 }
45
46 // Validate user input
47 const validation = await validateAll(request.all(), {
48 firstname: 'required',
49 lastname: 'required',
50 email: 'required|email|unique:users,email',
51 password: 'required',
52 });
53
54 if (validation.fails()) {
55 return response.status(401).send({
56 message: 'Invalid POST arguments',
57 messages: validation.messages(),
58 status: 401,
59 });
60 }
61
62 const data = request.only(['firstname', 'lastname', 'email', 'password']);
63
64 // Create user in DB
65 let user;
66 try {
67 user = await User.create({
68 email: data.email,
69 password: data.password,
70 username: data.firstname,
71 lastname: data.lastname,
72 });
73 } catch (e) {
74 return response.status(401).send({
75 message: 'E-Mail Address already in use',
76 status: 401,
77 });
78 }
79
80 // Generate new auth token
81 const token = await auth.generate(user);
82
83 return response.send({
84 message: 'Successfully created account',
85 token: token.token,
86 });
87 }
88
89 // Login using an existing user
90 async login({ request, response, auth }) {
91 if (!request.header('Authorization')) {
92 return response.status(401).send({
93 message: 'Please provide authorization',
94 status: 401,
95 });
96 }
97
98 // Get auth data from auth token
99 const authHeader = atob(
100 request.header('Authorization').replace('Basic ', ''),
101 ).split(':');
102
103 // Check if user with email exists
104 const user = await User.query().where('email', authHeader[0]).first();
105 if (!user || !user.email) {
106 return response.status(401).send({
107 message: 'User credentials not valid (Invalid mail)',
108 code: 'invalid-credentials',
109 status: 401,
110 });
111 }
112
113 // Try to login
114 let token;
115 try {
116 token = await auth.attempt(user.email, authHeader[1]);
117 } catch (e) {
118 return response.status(401).send({
119 message: 'User credentials not valid',
120 code: 'invalid-credentials',
121 status: 401,
122 });
123 }
124
125 return response.send({
126 message: 'Successfully logged in',
127 token: token.token,
128 });
129 }
130
131 // Return information about the current user
132 async me({ response, auth }) {
133 try {
134 await auth.getUser();
135 } catch (error) {
136 response.send('Missing or invalid api token');
137 }
138
139 const settings =
140 typeof auth.user.settings === 'string'
141 ? JSON.parse(auth.user.settings)
142 : auth.user.settings;
143
144 return response.send({
145 accountType: 'individual',
146 beta: false,
147 donor: {},
148 email: auth.user.email,
149 emailValidated: true,
150 features: {},
151 firstname: auth.user.username,
152 id: '82c1cf9d-ab58-4da2-b55e-aaa41d2142d8',
153 isPremium: true,
154 isSubscriptionOwner: true,
155 lastname: auth.user.lastname,
156 locale: 'en-US',
157 ...(settings || {}),
158 });
159 }
160
161 async updateMe({ request, response, auth }) {
162 let settings = auth.user.settings || {};
163 if (typeof settings === 'string') {
164 settings = JSON.parse(settings);
165 }
166
167 const newSettings = {
168 ...settings,
169 ...request.all(),
170 };
171
172 // eslint-disable-next-line no-param-reassign
173 auth.user.settings = JSON.stringify(newSettings);
174 await auth.user.save();
175
176 return response.send({
177 data: {
178 accountType: 'individual',
179 beta: false,
180 donor: {},
181 email: auth.user.email,
182 emailValidated: true,
183 features: {},
184 firstname: auth.user.username,
185 id: '82c1cf9d-ab58-4da2-b55e-aaa41d2142d8',
186 isPremium: true,
187 isSubscriptionOwner: true,
188 lastname: auth.user.lastname,
189 locale: 'en-US',
190 ...(newSettings || {}),
191 },
192 status: ['data-updated'],
193 });
194 }
195
196 async import({ request, response, view }) {
197 if (Env.get('IS_REGISTRATION_ENABLED') == 'false') {
198 // eslint-disable-line eqeqeq
199 return response.status(401).send({
200 message: 'Registration is disabled on this server',
201 status: 401,
202 });
203 }
204
205 // Validate user input
206 const validation = await validateAll(request.all(), {
207 email: 'required|email|unique:users,email',
208 password: 'required',
209 });
210 if (validation.fails()) {
211 let errorMessage =
212 'There was an error while trying to import your account:\n';
213 for (const message of validation.messages()) {
214 if (message.validation === 'required') {
215 errorMessage += `- Please make sure to supply your ${message.field}\n`;
216 } else if (message.validation === 'unique') {
217 errorMessage += '- There is already a user with this email.\n';
218 } else {
219 errorMessage += `${message.message}\n`;
220 }
221 }
222 return view.render('others.message', {
223 heading: 'Error while importing',
224 text: errorMessage,
225 });
226 }
227
228 const { email, password } = request.all();
229
230 const hashedPassword = crypto
231 .createHash('sha256')
232 .update(password)
233 .digest('base64');
234
235 if (Env.get('CONNECT_WITH_FRANZ') == 'false') {
236 // eslint-disable-line eqeqeq
237 await User.create({
238 email,
239 password: hashedPassword,
240 username: 'Franz',
241 lastname: 'Franz',
242 });
243
244 return response.send(
245 "Your account has been created but due to this server's configuration, 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.",
246 );
247 }
248
249 const base = 'https://api.franzinfra.com/v1/';
250 const userAgent =
251 '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';
252
253 // Try to get an authentication token
254 let token;
255 try {
256 const basicToken = btoa(`${email}:${hashedPassword}`);
257 const loginBody = {
258 isZendeskLogin: false,
259 };
260
261 const rawResponse = await fetch(`${base}auth/login`, {
262 method: 'POST',
263 body: JSON.stringify(loginBody),
264 headers: {
265 Authorization: `Basic ${basicToken}`,
266 'User-Agent': userAgent,
267 'Content-Type': 'application/json',
268 accept: '*/*',
269 'x-franz-source': 'Web',
270 },
271 });
272 const content = await rawResponse.json();
273
274 if (!content.message || content.message !== 'Successfully logged in') {
275 const errorMessage =
276 'Could not login into Franz with your supplied credentials. Please check and try again';
277 return response.status(401).send(errorMessage);
278 }
279
280 token = content.token;
281 } catch (e) {
282 return response.status(401).send({
283 message: 'Cannot login to Franz',
284 error: e,
285 });
286 }
287
288 // Get user information
289 let userInf = false;
290 try {
291 userInf = await franzRequest('me', 'GET', token);
292 } catch (e) {
293 const errorMessage = `Could not get your user info from Franz. Please check your credentials or try again later.\nError: ${e}`;
294 return response.status(401).send(errorMessage);
295 }
296 if (!userInf) {
297 const errorMessage =
298 'Could not get your user info from Franz. Please check your credentials or try again later';
299 return response.status(401).send(errorMessage);
300 }
301
302 // Create user in DB
303 let user;
304 try {
305 user = await User.create({
306 email: userInf.email,
307 password: hashedPassword,
308 username: userInf.firstname,
309 lastname: userInf.lastname,
310 });
311 } catch (e) {
312 const errorMessage = `Could not create your user in our system.\nError: ${e}`;
313 return response.status(401).send(errorMessage);
314 }
315
316 const serviceIdTranslation = {};
317
318 // Import services
319 try {
320 const services = await franzRequest('me/services', 'GET', token);
321
322 for (const service of services) {
323 // Get new, unused uuid
324 let serviceId;
325 do {
326 serviceId = uuid();
327 } while (
328 (await Service.query().where('serviceId', serviceId).fetch()).rows
329 .length > 0
330 ); // eslint-disable-line no-await-in-loop
331
332 await Service.create({
333 // eslint-disable-line no-await-in-loop
334 userId: user.id,
335 serviceId,
336 name: service.name,
337 recipeId: service.recipeId,
338 settings: JSON.stringify(service),
339 });
340
341 serviceIdTranslation[service.id] = serviceId;
342 }
343 } catch (e) {
344 const errorMessage = `Could not import your services into our system.\nError: ${e}`;
345 return response.status(401).send(errorMessage);
346 }
347
348 // Import workspaces
349 try {
350 const workspaces = await franzRequest('workspace', 'GET', token);
351
352 for (const workspace of workspaces) {
353 let workspaceId;
354 do {
355 workspaceId = uuid();
356 } while (
357 (await Workspace.query().where('workspaceId', workspaceId).fetch())
358 .rows.length > 0
359 ); // eslint-disable-line no-await-in-loop
360
361 const services = workspace.services.map(
362 service => serviceIdTranslation[service],
363 );
364
365 await Workspace.create({
366 // eslint-disable-line no-await-in-loop
367 userId: user.id,
368 workspaceId,
369 name: workspace.name,
370 order: workspace.order,
371 services: JSON.stringify(services),
372 data: JSON.stringify({}),
373 });
374 }
375 } catch (e) {
376 const errorMessage = `Could not import your workspaces into our system.\nError: ${e}`;
377 return response.status(401).send(errorMessage);
378 }
379
380 return response.send(
381 'Your account has been imported. You can now use your Franz/Ferdi account in Ferdium.',
382 );
383 }
384}
385
386module.exports = UserController;