Skip to content

Commit 89a3333

Browse files
Merge pull request #79 from learnsoftwaredevelopment/integrate-with-frontend-web-app
Integrate with frontend web app
2 parents 18fbd97 + 8ce4e0b commit 89a3333

30 files changed

+9698
-1757
lines changed

.github/workflows/node.js-with-docker-db.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ jobs:
2424

2525
strategy:
2626
matrix:
27-
node-version: [14]
27+
node-version: ['lts/*']
28+
check-latest: true
2829

2930
# If the Pull Request is coming from a fork (pull_request_target), ensure it's opened by "dependabot[bot]". Otherwise, clone it normally.
3031
# References:

.github/workflows/node.js.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ jobs:
2222

2323
strategy:
2424
matrix:
25-
node-version: [14.x]
25+
node-version: ['lts/*']
26+
check-latest: true
2627

2728
steps:
2829
- uses: actions/[email protected]

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,6 @@ dist
104104
.tern-port
105105

106106
# custom
107-
*firebase-adminsdk*.json
107+
*firebase-adminsdk*.json
108+
.gcloudignore
109+
app.yaml

app.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@ const morgan = require('morgan');
44
const cors = require('cors');
55
const mongoose = require('mongoose');
66
const config = require('./utils/config');
7-
8-
const rootRouter = require('./routes/root');
9-
const usersRouter = require('./routes/api/users');
10-
117
const middleware = require('./utils/middleware');
128
const logger = require('./utils/logger');
9+
const rootRouter = require('./routes/root');
10+
const usersRouter = require('./routes/api/users');
1311
const authRouter = require('./routes/api/auth');
1412
const softwareRouter = require('./routes/api/software');
13+
const searchRouter = require('./routes/api/search');
1514

1615
const app = express();
1716

@@ -56,6 +55,7 @@ app.use(rootRouter);
5655
app.use('/api/users', usersRouter);
5756
app.use('/api/auth', authRouter);
5857
app.use('/api/software', softwareRouter);
58+
app.use('/api/search', searchRouter);
5959

6060
app.use(middleware.unknownEndPoint);
6161
app.use(middleware.errorHandler);

controllers/api/searchController.js

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const Software = require('../../models/software');
2+
3+
const searchSoftware = async (req, res) => {
4+
const page = parseInt(req.query.page, 10) || 0;
5+
const perPage = parseInt(req.query.per_page, 10) || 30;
6+
const query = {
7+
name: {
8+
$regex: req.query.q,
9+
$options: 'i',
10+
},
11+
};
12+
13+
const queryResponse = await Software.find(query)
14+
.sort({ name: 'asc' })
15+
.skip(page * perPage)
16+
.limit(perPage)
17+
.populate('meta.addedByUser', { username: 1, name: 1 })
18+
.populate('meta.updatedByUser', { username: 1, name: 1 });
19+
20+
const totalQueryResultCount = await Software.find(query).countDocuments();
21+
22+
return res.status(200).json({
23+
totalQueryResultCount,
24+
queryResponse,
25+
});
26+
};
27+
28+
module.exports = {
29+
searchSoftware,
30+
};

controllers/api/softwareController.js

+27
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const postSoftware = async (req, res) => {
2525
const softwareObject = {
2626
...body,
2727
meta: {
28+
...body.meta,
2829
addedByUser: userId,
2930
updatedByUser: userId,
3031
},
@@ -146,10 +147,36 @@ const deleteSoftwareById = async (req, res) => {
146147
return res.status(204).end();
147148
};
148149

150+
const getRecentAddedSoftware = async (req, res) => {
151+
const count = parseInt(req.query.count, 10) || 5;
152+
153+
const response = await Software.find({})
154+
.sort({ createdAt: 'desc' })
155+
.limit(count)
156+
.populate('meta.addedByUser', { username: 1, name: 1 })
157+
.populate('meta.updatedByUser', { username: 1, name: 1 });
158+
159+
return res.status(200).json(response);
160+
};
161+
162+
const getRecentUpdatedSoftware = async (req, res) => {
163+
const count = parseInt(req.query.count, 10) || 5;
164+
165+
const response = await Software.find({})
166+
.sort({ updatedAt: 'desc' })
167+
.limit(count)
168+
.populate('meta.addedByUser', { username: 1, name: 1 })
169+
.populate('meta.updatedByUser', { username: 1, name: 1 });
170+
171+
return res.status(200).json(response);
172+
};
173+
149174
module.exports = {
150175
getSoftware,
151176
postSoftware,
152177
patchSoftwareById,
153178
getSoftwareById,
154179
deleteSoftwareById,
180+
getRecentAddedSoftware,
181+
getRecentUpdatedSoftware,
155182
};

controllers/api/usersController.js

+41-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const User = require('../../models/user');
22
const firebaseAdmin = require('../../utils/firebaseConfig');
33
const jwtUtils = require('../../utils/jwtUtils');
4+
const { checkUsernameValidity } = require('../../utils/api/usersUtils');
45

56
const getUsers = async (req, res) => {
67
const users = await User.find({}).populate([
@@ -17,17 +18,24 @@ const getUsers = async (req, res) => {
1718
};
1819

1920
const postUsers = async (req, res) => {
20-
const { body } = req;
21+
const { username } = req.body;
2122

2223
const authToken = jwtUtils.getReqAuthToken(req);
24+
2325
const decodedToken = await jwtUtils.verifyAuthToken(authToken, false);
2426

27+
if (!decodedToken) {
28+
return res.status(401).json({
29+
error: 'Missing/Invalid Token',
30+
});
31+
}
32+
2533
const response = await firebaseAdmin.auth().getUser(decodedToken.uid);
2634

2735
const userRecord = response.toJSON();
2836

2937
const user = new User({
30-
username: body.username,
38+
username: !username ? null : username,
3139
name: userRecord.displayName,
3240
email: userRecord.email,
3341
firebaseUid: userRecord.uid,
@@ -36,14 +44,42 @@ const postUsers = async (req, res) => {
3644
const savedUser = await user.save();
3745

3846
// Set custom claim with backend user id (different from firebase user id)
39-
await firebaseAdmin
40-
.auth()
41-
.setCustomUserClaims(decodedToken.uid, { backendId: savedUser.id });
47+
await firebaseAdmin.auth().setCustomUserClaims(decodedToken.uid, {
48+
backendId: savedUser.id,
49+
username: savedUser.username,
50+
});
4251

4352
return res.status(201).json(savedUser);
4453
};
4554

55+
const postUserAvailability = async (req, res) => {
56+
const { username } = req.body;
57+
58+
if (!username) {
59+
return res.status(400).json({
60+
error: 'Missing username in request.',
61+
});
62+
}
63+
64+
const isValidUsername = checkUsernameValidity(username);
65+
66+
if (!isValidUsername) {
67+
return res.status(400).json({
68+
error: 'Invalid username.',
69+
});
70+
}
71+
72+
const response = await User.findOne({
73+
username,
74+
}).lean();
75+
76+
return res.json({
77+
usernameStatus: response === null ? 'Available' : 'Not Available',
78+
});
79+
};
80+
4681
module.exports = {
4782
getUsers,
4883
postUsers,
84+
postUserAvailability,
4985
};

models/software.js

+53-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const mongoose = require('mongoose');
22
const { isURL } = require('validator');
33
const uniqueValidator = require('mongoose-unique-validator');
4+
const { ALLOWED_VIDEO_HOST_WHITELIST } = require('../utils/config');
45

56
const softwareSchema = new mongoose.Schema(
67
{
@@ -26,6 +27,12 @@ const softwareSchema = new mongoose.Schema(
2627
trim: true,
2728
default: '0.0.0',
2829
},
30+
shortDescription: {
31+
type: String,
32+
trim: true,
33+
maxlength: 100,
34+
required: [true, 'Software short description is required'],
35+
},
2936
description: {
3037
type: String,
3138
trim: true,
@@ -34,9 +41,10 @@ const softwareSchema = new mongoose.Schema(
3441
homePage: {
3542
type: String,
3643
validate: [
37-
(value) => isURL(value, {
38-
protocols: ['http', 'https'],
39-
}),
44+
(value) =>
45+
isURL(value, {
46+
protocols: ['http', 'https'],
47+
}),
4048
'A valid url is required',
4149
],
4250
required: [true, 'Software homepage url is required'],
@@ -51,6 +59,12 @@ const softwareSchema = new mongoose.Schema(
5159
type: Boolean,
5260
required: true,
5361
},
62+
pricing: {
63+
type: String,
64+
trim: true,
65+
lowercase: true,
66+
required: [true, 'Software pricing is required'],
67+
},
5468
buildOn: {
5569
type: [
5670
{
@@ -64,21 +78,45 @@ const softwareSchema = new mongoose.Schema(
6478
developedBy: {
6579
type: [
6680
{
67-
type: mongoose.Schema.Types.ObjectId,
68-
ref: 'User',
81+
type: String,
82+
trim: true,
83+
lowercase: true,
6984
},
7085
],
7186
default: [],
7287
},
7388
maintainedBy: {
7489
type: [
7590
{
76-
type: mongoose.Schema.Types.ObjectId,
77-
ref: 'User',
91+
type: String,
92+
trim: true,
93+
lowercase: true,
7894
},
7995
],
8096
default: [],
8197
},
98+
videoLink: {
99+
type: String,
100+
validate: [
101+
(value) => {
102+
if (value) {
103+
return isURL(value, {
104+
protocols: ['http', 'https'],
105+
host_whitelist: ALLOWED_VIDEO_HOST_WHITELIST,
106+
});
107+
}
108+
return true;
109+
},
110+
'A valid video url is required (Only youtube.com or vimeo.com are supported)',
111+
],
112+
default: '',
113+
},
114+
twitterUsername: {
115+
type: String,
116+
trim: true,
117+
lowercase: true,
118+
default: '',
119+
},
82120
query: {
83121
isEnabled: {
84122
type: Boolean,
@@ -87,19 +125,21 @@ const softwareSchema = new mongoose.Schema(
87125
updateUrl: {
88126
type: String,
89127
validate: [
90-
(value) => isURL(value, {
91-
protocols: ['http', 'https'],
92-
}) || value === '',
128+
(value) =>
129+
isURL(value, {
130+
protocols: ['http', 'https'],
131+
}) || value === '',
93132
'A valid update url is required',
94133
],
95134
default: '',
96135
},
97136
downloadUrl: {
98137
type: String,
99138
validate: [
100-
(value) => isURL(value, {
101-
protocols: ['http', 'https', 'ftp'],
102-
}) || value === '',
139+
(value) =>
140+
isURL(value, {
141+
protocols: ['http', 'https', 'ftp'],
142+
}) || value === '',
103143
'A valid download url is required',
104144
],
105145
default: '',

models/user.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
const mongoose = require('mongoose');
22
const uniqueValidator = require('mongoose-unique-validator');
33
const { isEmail, normalizeEmail } = require('validator');
4+
const { ALLOWED_USERNAME_REGEX } = require('../utils/config');
45

5-
const usernameRegex = RegExp('^[a-z0-9_.-]+$');
6+
const usernameRegex = RegExp(ALLOWED_USERNAME_REGEX);
67

78
const userSchema = new mongoose.Schema(
89
{
910
username: {
1011
type: String,
1112
required: [true, 'Missing username'],
12-
minlength: [6, 'The username should be at least 6 characters long'],
1313
unique: true,
1414
lowercase: true,
1515
trim: true,

0 commit comments

Comments
 (0)