aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorLibravatar vantezzen <properly@protonmail.com>2019-08-22 11:12:36 +0200
committerLibravatar vantezzen <properly@protonmail.com>2019-08-22 11:12:36 +0200
commitb018adf240679ec59a7344e30be39400f1ecd8af (patch)
treec076635761667dad302716b498088f1047281e46 /app
downloadferdium-server-b018adf240679ec59a7344e30be39400f1ecd8af.tar.gz
ferdium-server-b018adf240679ec59a7344e30be39400f1ecd8af.tar.zst
ferdium-server-b018adf240679ec59a7344e30be39400f1ecd8af.zip
Initial commit
Diffstat (limited to 'app')
-rw-r--r--app/Controllers/Http/ServiceController.js94
-rw-r--r--app/Controllers/Http/StaticController.js224
-rw-r--r--app/Controllers/Http/UserController.js94
-rw-r--r--app/Middleware/ConvertEmptyStringsToNull.js17
-rw-r--r--app/Models/Service.js12
-rw-r--r--app/Models/Token.js9
-rw-r--r--app/Models/Traits/NoTimestamp.js16
-rw-r--r--app/Models/User.js43
8 files changed, 509 insertions, 0 deletions
diff --git a/app/Controllers/Http/ServiceController.js b/app/Controllers/Http/ServiceController.js
new file mode 100644
index 0000000..47a8a6f
--- /dev/null
+++ b/app/Controllers/Http/ServiceController.js
@@ -0,0 +1,94 @@
1'use strict'
2
3const User = use('App/Models/User');
4const Service = use('App/Models/Service');
5
6class ServiceController {
7 // Create a new service for user
8 async create({
9 request,
10 response,
11 auth
12 }) {
13 try {
14 await auth.getUser()
15 } catch (error) {
16 return response.send('Missing or invalid api token')
17 }
18
19 const data = request.all();
20
21 const service = await Service.create({
22 userId: auth.user.id,
23 name: data.name,
24 recipeId: data.recipeId,
25 settings: JSON.stringify(data)
26 });
27
28 return response.send({
29 "data": {
30 userId: auth.user.id,
31 id: service.id,
32 "isEnabled": true,
33 "isNotificationEnabled": true,
34 "isBadgeEnabled": true,
35 "isMuted": false,
36 "isDarkModeEnabled": "",
37 "spellcheckerLanguage": "",
38 "order": 1,
39 "customRecipe": false,
40 "hasCustomIcon": false,
41 "workspaces": [],
42 "iconUrl": null,
43 ...data,
44 },
45 "status": ["created"]
46 })
47 }
48
49 // List all services a user has created
50 async list({
51 request,
52 response,
53 auth
54 }) {
55 try {
56 await auth.getUser()
57 } catch (error) {
58 return response.send('Missing or invalid api token')
59 }
60
61
62 const services = (await auth.user.services().fetch()).rows;
63 // Convert to array with all data Franz wants
64 const servicesArray = services.map(service => ({
65 "customRecipe": false,
66 "hasCustomIcon": false,
67 "isBadgeEnabled": true,
68 "isDarkModeEnabled": "",
69 "isEnabled": true,
70 "isMuted": false,
71 "isNotificationEnabled": true,
72 "order": 1,
73 "spellcheckerLanguage": "",
74 "workspaces": [],
75 "iconUrl": null,
76 ...JSON.parse(service.settings),
77 "id": service.id,
78 "name": service.name,
79 "recipeId": service.recipeId,
80 "userId": auth.user.id,
81 }))
82
83 return response.send(servicesArray)
84 }
85
86 // Download a recipe (currently simply redirects to Franz's API)
87 download({ request, response, params }) {
88 const service = params.recipe;
89
90 response.redirect('https://api.franzinfra.com/v1/recipes/download/' + service)
91 }
92}
93
94module.exports = ServiceController
diff --git a/app/Controllers/Http/StaticController.js b/app/Controllers/Http/StaticController.js
new file mode 100644
index 0000000..70f2d7f
--- /dev/null
+++ b/app/Controllers/Http/StaticController.js
@@ -0,0 +1,224 @@
1'use strict'
2/**
3 * Controller for routes with static responses
4 */
5
6class StaticController {
7 // Enable all features
8 features({
9 response
10 }) {
11 return response.send({
12 "needToWaitToProceed": false,
13 "isSpellcheckerPremiumFeature": true,
14 "isServiceProxyEnabled": true,
15 "isServiceProxyPremiumFeature": true,
16 "isWorkspacePremiumFeature": true,
17 "isWorkspaceEnabled": true,
18 "isAnnouncementsEnabled": true,
19 "isSettingsWSEnabled": false,
20 "isServiceLimitEnabled": false,
21 "serviceLimitCount": 0,
22 "isCommunityRecipesPremiumFeature": false
23 })
24 }
25
26 // Return an empty array
27 emptyArray({
28 response
29 }) {
30 return response.send([])
31 }
32
33 // Payment plans availible
34 plans({
35 response
36 }) {
37 return response.send({
38 "month": {
39 "id": "franz-supporter-license",
40 "price": 99
41 },
42 "year": {
43 "id": "franz-supporter-license-year-2019",
44 "price": 99
45 }
46 })
47 }
48
49 // Return list of popular recipes (copy of the response Franz's API is returning)
50 popularRecipes({
51 response
52 }) {
53 return response.send([{
54 "author": "Stefan Malzner <stefan@adlk.io>",
55 "featured": false,
56 "id": "slack",
57 "name": "Slack",
58 "version": "1.0.4",
59 "icons": {
60 "png": "https://cdn.franzinfra.com/recipes/dist/slack/src/icon.png",
61 "svg": "https://cdn.franzinfra.com/recipes/dist/slack/src/icon.svg"
62 }
63 }, {
64 "author": "Stefan Malzner <stefan@adlk.io>",
65 "featured": false,
66 "id": "whatsapp",
67 "name": "WhatsApp",
68 "version": "1.0.1",
69 "icons": {
70 "png": "https://cdn.franzinfra.com/recipes/dist/whatsapp/src/icon.png",
71 "svg": "https://cdn.franzinfra.com/recipes/dist/whatsapp/src/icon.svg"
72 }
73 }, {
74 "author": "Stefan Malzner <stefan@adlk.io>",
75 "featured": false,
76 "id": "messenger",
77 "name": "Messenger",
78 "version": "1.0.6",
79 "icons": {
80 "png": "https://cdn.franzinfra.com/recipes/dist/messenger/src/icon.png",
81 "svg": "https://cdn.franzinfra.com/recipes/dist/messenger/src/icon.svg"
82 }
83 }, {
84 "author": "Stefan Malzner <stefan@adlk.io>",
85 "featured": false,
86 "id": "telegram",
87 "name": "Telegram",
88 "version": "1.0.0",
89 "icons": {
90 "png": "https://cdn.franzinfra.com/recipes/dist/telegram/src/icon.png",
91 "svg": "https://cdn.franzinfra.com/recipes/dist/telegram/src/icon.svg"
92 }
93 }, {
94 "author": "Stefan Malzner <stefan@adlk.io>",
95 "featured": false,
96 "id": "gmail",
97 "name": "Gmail",
98 "version": "1.0.0",
99 "icons": {
100 "png": "https://cdn.franzinfra.com/recipes/dist/gmail/src/icon.png",
101 "svg": "https://cdn.franzinfra.com/recipes/dist/gmail/src/icon.svg"
102 }
103 }, {
104 "author": "Stefan Malzner <stefan@adlk.io>",
105 "featured": false,
106 "id": "skype",
107 "name": "Skype",
108 "version": "1.0.0",
109 "icons": {
110 "png": "https://cdn.franzinfra.com/recipes/dist/skype/src/icon.png",
111 "svg": "https://cdn.franzinfra.com/recipes/dist/skype/src/icon.svg"
112 }
113 }, {
114 "author": "Stefan Malzner <stefan@adlk.io>",
115 "featured": false,
116 "id": "hangouts",
117 "name": "Hangouts",
118 "version": "1.0.0",
119 "icons": {
120 "png": "https://cdn.franzinfra.com/recipes/dist/hangouts/src/icon.png",
121 "svg": "https://cdn.franzinfra.com/recipes/dist/hangouts/src/icon.svg"
122 }
123 }, {
124 "author": "Stefan Malzner <stefan@adlk.io>",
125 "featured": false,
126 "id": "discord",
127 "name": "Discord",
128 "version": "1.0.0",
129 "icons": {
130 "png": "https://cdn.franzinfra.com/recipes/dist/discord/src/icon.png",
131 "svg": "https://cdn.franzinfra.com/recipes/dist/discord/src/icon.svg"
132 }
133 }, {
134 "author": "Stefan Malzner <stefan@adlk.io>",
135 "featured": false,
136 "id": "tweetdeck",
137 "name": "Tweetdeck",
138 "version": "1.0.1",
139 "icons": {
140 "png": "https://cdn.franzinfra.com/recipes/dist/tweetdeck/src/icon.png",
141 "svg": "https://cdn.franzinfra.com/recipes/dist/tweetdeck/src/icon.svg"
142 }
143 }, {
144 "author": "Stefan Malzner <stefan@adlk.io>",
145 "featured": false,
146 "id": "hipchat",
147 "name": "HipChat",
148 "version": "1.0.1",
149 "icons": {
150 "png": "https://cdn.franzinfra.com/recipes/dist/hipchat/src/icon.png",
151 "svg": "https://cdn.franzinfra.com/recipes/dist/hipchat/src/icon.svg"
152 }
153 }, {
154 "author": "Stefan Malzner <stefan@adlk.io>",
155 "featured": false,
156 "id": "gmailinbox",
157 "name": "Inbox by Gmail",
158 "version": "1.0.0",
159 "icons": {
160 "png": "https://cdn.franzinfra.com/recipes/dist/gmailinbox/src/icon.png",
161 "svg": "https://cdn.franzinfra.com/recipes/dist/gmailinbox/src/icon.svg"
162 }
163 }, {
164 "author": "Stefan Malzner <stefan@adlk.io>",
165 "featured": false,
166 "id": "rocketchat",
167 "name": "Rocket.Chat",
168 "version": "1.0.1",
169 "icons": {
170 "png": "https://cdn.franzinfra.com/recipes/dist/rocketchat/src/icon.png",
171 "svg": "https://cdn.franzinfra.com/recipes/dist/rocketchat/src/icon.svg"
172 }
173 }, {
174 "author": "Brian Gilbert <brian@briangilbert.net>",
175 "featured": false,
176 "id": "gitter",
177 "name": "Gitter",
178 "version": "1.0.0",
179 "icons": {
180 "png": "https://cdn.franzinfra.com/recipes/dist/gitter/src/icon.png",
181 "svg": "https://cdn.franzinfra.com/recipes/dist/gitter/src/icon.svg"
182 }
183 }, {
184 "author": "Stefan Malzner <stefan@adlk.io>",
185 "featured": false,
186 "id": "mattermost",
187 "name": "Mattermost",
188 "version": "1.0.0",
189 "icons": {
190 "png": "https://cdn.franzinfra.com/recipes/dist/mattermost/src/icon.png",
191 "svg": "https://cdn.franzinfra.com/recipes/dist/mattermost/src/icon.svg"
192 }
193 }, {
194 "author": "Franz <recipe@meetfranz.com>",
195 "featured": false,
196 "id": "toggl",
197 "name": "toggl",
198 "version": "1.0.0",
199 "icons": {
200 "png": "https://cdn.franzinfra.com/recipes/dist/toggl/src/icon.png",
201 "svg": "https://cdn.franzinfra.com/recipes/dist/toggl/src/icon.svg"
202 }
203 }, {
204 "author": "Stuart Clark <stuart@realityloop.com>",
205 "featured": false,
206 "id": "twist",
207 "name": "twist",
208 "version": "1.0.0",
209 "icons": {
210 "png": "https://cdn.franzinfra.com/recipes/dist/twist/src/icon.png",
211 "svg": "https://cdn.franzinfra.com/recipes/dist/twist/src/icon.svg"
212 }
213 }])
214 }
215
216 // Show announcements
217 announcement({ response, params }) {
218 return response.send({
219 'en-US': 'You are using an unofficial Franz Server.'
220 });
221 }
222}
223
224module.exports = StaticController
diff --git a/app/Controllers/Http/UserController.js b/app/Controllers/Http/UserController.js
new file mode 100644
index 0000000..88f7ecd
--- /dev/null
+++ b/app/Controllers/Http/UserController.js
@@ -0,0 +1,94 @@
1'use strict'
2
3const User = use('App/Models/User');
4const atob = require('atob');
5
6class UserController {
7
8 // Register a new user
9 async signup({
10 request,
11 response,
12 auth,
13 session
14 }) {
15 const data = request.only(['firstname', 'email', 'password']);
16 const user = await User.create({
17 email: data.email,
18 password: data.password,
19 username: data.firstname
20 });
21 const token = await auth.generate(user)
22
23 return response.send({
24 "message": "Successfully created account",
25 "token": token.token
26 });
27 }
28
29 // Login using an existing user
30 async login({
31 request,
32 response,
33 auth
34 }) {
35 const authHeader = atob(request.header('Authorization')).split(':');
36
37 let user = (await User.query().where('email', authHeader[0]).limit(1).fetch()).toJSON();
38 if (!user[0] || !user[0].email) {
39 return response.status(401).send({
40 "message": "User credentials not valid",
41 "code": "invalid-credentials",
42 "status": 401
43 });
44 }
45
46 let token;
47 try {
48 // TODO: Login is currently not working as the password is incorrect
49 token = await auth.attempt(user[0].id, authHeader[1])
50 } catch (e) {
51 return response.status(401).send({
52 "message": "User credentials not valid",
53 "code": "invalid-credentials",
54 "status": 401
55 });
56 }
57
58 return response.send({
59 "message": "Successfully logged in",
60 "token": token.token
61 });
62 }
63
64 // Return information about the current user
65 async me({
66 request,
67 response,
68 auth,
69 session
70 }) {
71 try {
72 await auth.getUser()
73 } catch (error) {
74 response.send('Missing or invalid api token')
75 }
76
77 return response.send({
78 accountType: "individual",
79 beta: false,
80 donor: {},
81 email: auth.user.email,
82 emailValidated: true,
83 features: {},
84 firstname: "Franz",
85 id: "2acd2aa0-0869-4a91-adab-f700ac256dbe",
86 isPremium: true,
87 isSubscriptionOwner: true,
88 lastname: "Franz",
89 locale: "en-US"
90 });
91 }
92}
93
94module.exports = UserController
diff --git a/app/Middleware/ConvertEmptyStringsToNull.js b/app/Middleware/ConvertEmptyStringsToNull.js
new file mode 100644
index 0000000..a5750cc
--- /dev/null
+++ b/app/Middleware/ConvertEmptyStringsToNull.js
@@ -0,0 +1,17 @@
1'use strict'
2
3class ConvertEmptyStringsToNull {
4 async handle ({ request }, next) {
5 if (Object.keys(request.body).length) {
6 request.body = Object.assign(
7 ...Object.keys(request.body).map(key => ({
8 [key]: request.body[key] !== '' ? request.body[key] : null
9 }))
10 )
11 }
12
13 await next()
14 }
15}
16
17module.exports = ConvertEmptyStringsToNull
diff --git a/app/Models/Service.js b/app/Models/Service.js
new file mode 100644
index 0000000..0ca72fd
--- /dev/null
+++ b/app/Models/Service.js
@@ -0,0 +1,12 @@
1'use strict'
2
3/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
4const Model = use('Model')
5
6class Service extends Model {
7 user() {
8 return this.belongsTo('App/Models/User', 'userId', 'id')
9 }
10}
11
12module.exports = Service
diff --git a/app/Models/Token.js b/app/Models/Token.js
new file mode 100644
index 0000000..e089e87
--- /dev/null
+++ b/app/Models/Token.js
@@ -0,0 +1,9 @@
1'use strict'
2
3/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
4const Model = use('Model')
5
6class Token extends Model {
7}
8
9module.exports = Token
diff --git a/app/Models/Traits/NoTimestamp.js b/app/Models/Traits/NoTimestamp.js
new file mode 100644
index 0000000..edd07f0
--- /dev/null
+++ b/app/Models/Traits/NoTimestamp.js
@@ -0,0 +1,16 @@
1'use strict'
2
3class NoTimestamp {
4 register (Model) {
5 Object.defineProperties(Model, {
6 createdAtColumn: {
7 get: () => null,
8 },
9 updatedAtColumn: {
10 get: () => null,
11 }
12 })
13 }
14}
15
16module.exports = NoTimestamp
diff --git a/app/Models/User.js b/app/Models/User.js
new file mode 100644
index 0000000..0bb1547
--- /dev/null
+++ b/app/Models/User.js
@@ -0,0 +1,43 @@
1'use strict'
2
3/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
4const Model = use('Model')
5
6/** @type {import('@adonisjs/framework/src/Hash')} */
7const Hash = use('Hash')
8
9class User extends Model {
10 static boot () {
11 super.boot()
12
13 /**
14 * A hook to hash the user password before saving
15 * it to the database.
16 */
17 this.addHook('beforeSave', async (userInstance) => {
18 if (userInstance.dirty.password) {
19 userInstance.password = await Hash.make(userInstance.password)
20 }
21 })
22 }
23
24 /**
25 * A relationship on tokens is required for auth to
26 * work. Since features like `refreshTokens` or
27 * `rememberToken` will be saved inside the
28 * tokens table.
29 *
30 * @method tokens
31 *
32 * @return {Object}
33 */
34 tokens () {
35 return this.hasMany('App/Models/Token')
36 }
37
38 services () {
39 return this.hasMany('App/Models/Service', 'id', 'userId')
40 }
41}
42
43module.exports = User