@tiledesk/tiledesk-server 2.4.6 → 2.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.circleci/config.yml +1 -1
- package/app.js +11 -0
- package/config/email.js +1 -1
- package/middleware/passport.js +1 -0
- package/models/auth.js +9 -1
- package/models/user.js +3 -0
- package/package.json +2 -2
- package/pubmodules/apps/listener.js +1 -0
- package/routes/auth.js +44 -3
- package/routes/images.js +3 -1
- package/routes/project.js +12 -0
- package/routes/users.js +2 -0
- package/services/emailService.js +2 -2
- package/template/email/assignedEmailMessage.html +1 -1
- package/template/email/assignedRequest.html +1 -1
- package/template/email/beenInvitedExistingUser.html +4 -4
- package/template/email/beenInvitedNewUser.html +1 -1
- package/template/email/emailDirect.html +1 -1
- package/template/email/newMessage.html +1 -1
- package/template/email/newMessageFollower.html +1 -1
- package/template/email/passwordChanged.html +3 -3
- package/template/email/pooledEmailMessage.html +1 -1
- package/template/email/pooledRequest.html +1 -1
- package/template/email/resetPassword.html +3 -3
- package/template/email/ticket.html +1 -1
- package/template/email/verify.html +4 -4
package/.circleci/config.yml
CHANGED
@@ -16,7 +16,7 @@ jobs:
|
|
16
16
|
# You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub.
|
17
17
|
# A list of available CircleCI Docker Convenience Images are available here: https://circleci.com/developer/images/image/cimg/node
|
18
18
|
docker:
|
19
|
-
- image: cimg/node:
|
19
|
+
- image: cimg/node:16.17.1
|
20
20
|
# Then run your tests!
|
21
21
|
# CircleCI will report the results back to your VCS provider.
|
22
22
|
steps:
|
package/app.js
CHANGED
@@ -132,6 +132,7 @@ var settingDataLoader = require('./services/settingDataLoader');
|
|
132
132
|
var schemaMigrationService = require('./services/schemaMigrationService');
|
133
133
|
var RouterLogger = require('./models/routerLogger');
|
134
134
|
var cacheEnabler = require("./services/cacheEnabler");
|
135
|
+
const session = require('express-session');
|
135
136
|
|
136
137
|
require('./services/mongoose-cache-fn')(mongoose);
|
137
138
|
|
@@ -270,6 +271,16 @@ if (process.env.ENABLE_ACCESSLOG) {
|
|
270
271
|
|
271
272
|
app.use(passport.initialize());
|
272
273
|
|
274
|
+
// After you declare "app"
|
275
|
+
if (process.env.DISABLE_SESSION_STRATEGY==true || process.env.DISABLE_SESSION_STRATEGY=="true" ) {
|
276
|
+
winston.info("Express Session disabled");
|
277
|
+
} else {
|
278
|
+
// https://www.npmjs.com/package/express-session
|
279
|
+
let sessionSecret = process.env.SESSION_SECRET || "tiledesk-session-secret";
|
280
|
+
winston.info("Express Session Secret: " + sessionSecret);
|
281
|
+
app.use(session({ secret: sessionSecret}));
|
282
|
+
app.use(passport.session());
|
283
|
+
}
|
273
284
|
|
274
285
|
//ATTENTION. If you use AWS Api Gateway you need also to configure the cors policy https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-cors-console.html
|
275
286
|
app.use(cors());
|
package/config/email.js
CHANGED
@@ -3,7 +3,7 @@ module.exports = {
|
|
3
3
|
'username': 'postmaster@mg.tiledesk.com',
|
4
4
|
'from': 'Tiledesk Notification <postmaster@mg.tiledesk.com>',
|
5
5
|
'bcc': '',
|
6
|
-
'baseUrl':'
|
6
|
+
'baseUrl':'http://localhost:8081/dashboard',
|
7
7
|
'replyEnabled' : false,
|
8
8
|
'inboundDomain': 'tickets.tiledesk.com'
|
9
9
|
};
|
package/middleware/passport.js
CHANGED
package/models/auth.js
CHANGED
@@ -12,9 +12,17 @@ var AuthSchema = new Schema({
|
|
12
12
|
default: 'password',
|
13
13
|
required: true
|
14
14
|
},
|
15
|
+
subject: {
|
16
|
+
type: String,
|
17
|
+
required: true
|
18
|
+
},
|
19
|
+
email: {
|
20
|
+
type: String,
|
21
|
+
},
|
15
22
|
password: {
|
16
23
|
type: String,
|
17
|
-
required:
|
24
|
+
required: false,
|
25
|
+
// required: true,
|
18
26
|
// https://stackoverflow.com/questions/12096262/how-to-protect-the-password-field-in-mongoose-mongodb-so-it-wont-return-in-a-qu
|
19
27
|
// select: false
|
20
28
|
},
|
package/models/user.js
CHANGED
package/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "@tiledesk/tiledesk-server",
|
3
3
|
"description": "The Tiledesk server module",
|
4
|
-
"version": "2.4.
|
4
|
+
"version": "2.4.8",
|
5
5
|
"scripts": {
|
6
6
|
"start": "node ./bin/www",
|
7
7
|
"pretest": "mongodb-runner start",
|
@@ -44,7 +44,7 @@
|
|
44
44
|
"@tiledesk/tiledesk-kaleyra-proxy": "^0.1.7",
|
45
45
|
"@tiledesk/tiledesk-messenger-connector": "0.1.9",
|
46
46
|
"@tiledesk/tiledesk-rasa-connector": "^1.0.10",
|
47
|
-
"@tiledesk/tiledesk-tybot-connector": "^0.1.
|
47
|
+
"@tiledesk/tiledesk-tybot-connector": "^0.1.78",
|
48
48
|
"@tiledesk/tiledesk-whatsapp-connector": "^0.1.45",
|
49
49
|
"amqplib": "^0.5.5",
|
50
50
|
"app-root-path": "^3.0.0",
|
@@ -23,6 +23,7 @@ class Listener {
|
|
23
23
|
if (config.databaseUri) {
|
24
24
|
winston.debug("apps config databaseUri: " + config.databaseUri);
|
25
25
|
}
|
26
|
+
// console.log("ACCESS_TOKEN_SECRET",process.env.APPS_ACCESS_TOKEN_SECRET || configSecretOrPubicKay);
|
26
27
|
|
27
28
|
apps.startApp({
|
28
29
|
ACCESS_TOKEN_SECRET: process.env.APPS_ACCESS_TOKEN_SECRET || configSecretOrPubicKay,
|
package/routes/auth.js
CHANGED
@@ -452,9 +452,31 @@ function (req, res) {
|
|
452
452
|
});
|
453
453
|
|
454
454
|
|
455
|
+
// http://localhost:3000/auth/google?redirect_url=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fgoogle%2Fcallback%3Ffrom%3Dsignup
|
456
|
+
|
457
|
+
// http://localhost:3000/auth/google?redirect_url=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fgoogle%2Fcallbacks
|
458
|
+
|
459
|
+
// http://localhost:3000/auth/google?redirect_url=%2F%23%2Fproject%2F6452281f6d68c5f419c1c577%2Fhome
|
460
|
+
|
461
|
+
|
455
462
|
|
456
463
|
// Redirect the user to the Google signin page</em>
|
457
|
-
router.get("/google", passport.authenticate("google", { scope: ["email", "profile"] }));
|
464
|
+
// router.get("/google", passport.authenticate("google", { scope: ["email", "profile"] }));
|
465
|
+
router.get("/google", function(req,res,next){
|
466
|
+
winston.info("redirect_url: "+ req.query.redirect_url );
|
467
|
+
req.session.redirect_url = req.query.redirect_url;
|
468
|
+
// req._toParam = 'Hello';
|
469
|
+
passport.authenticate(
|
470
|
+
// 'google', { scope : ["email", "profile"], state: base64url(JSON.stringify({blah: 'text'})) } //custom redirect_url req.query.state
|
471
|
+
'google', { scope : ["email", "profile"] } //custom redirect_url
|
472
|
+
// 'google', { scope : ["email", "profile"], callbackURL: req.query.redirect_url } //custom redirect_url
|
473
|
+
)(req,res,next);
|
474
|
+
});
|
475
|
+
|
476
|
+
// router.get("/google/callbacks", passport.authenticate("google", { session: false }), (req, res) => {
|
477
|
+
// console.log("callback_signup");
|
478
|
+
// res.redirect("/google/callback");
|
479
|
+
// });
|
458
480
|
|
459
481
|
// Retrieve user data using the access token received</em>
|
460
482
|
router.get("/google/callback", passport.authenticate("google", { session: false }), (req, res) => {
|
@@ -462,9 +484,17 @@ router.get("/google/callback", passport.authenticate("google", { session: false
|
|
462
484
|
|
463
485
|
var user = req.user;
|
464
486
|
winston.debug("user", user);
|
487
|
+
// winston.info("req._toParam: "+ req._toParam);
|
488
|
+
// winston.info("req.query.redirect_url: "+ req.query.redirect_url);
|
489
|
+
// winston.info("req.query.state: "+ req.query.state);
|
490
|
+
winston.info("req.session.redirect_url: "+ req.session.redirect_url);
|
491
|
+
|
465
492
|
|
466
493
|
var userJson = user.toObject();
|
467
494
|
|
495
|
+
delete userJson.password;
|
496
|
+
|
497
|
+
|
468
498
|
var signOptions = {
|
469
499
|
issuer: 'https://tiledesk.com',
|
470
500
|
subject: 'user',
|
@@ -485,8 +515,19 @@ router.get("/google/callback", passport.authenticate("google", { session: false
|
|
485
515
|
// return the information including token as JSON
|
486
516
|
// res.json(returnObject);
|
487
517
|
|
488
|
-
|
489
|
-
winston.debug("
|
518
|
+
let dashboard_base_url = process.env.EMAIL_BASEURL || config.baseUrl;
|
519
|
+
winston.debug("Google Redirect dashboard_base_url: ", dashboard_base_url);
|
520
|
+
|
521
|
+
let homeurl = "/#/";
|
522
|
+
|
523
|
+
if (req.session.redirect_url) {
|
524
|
+
homeurl = req.session.redirect_url;
|
525
|
+
}
|
526
|
+
|
527
|
+
var url = dashboard_base_url+homeurl+"?token=JWT "+token;
|
528
|
+
|
529
|
+
|
530
|
+
winston.info("Google Redirect: "+ url);
|
490
531
|
|
491
532
|
res.redirect(url);
|
492
533
|
|
package/routes/images.js
CHANGED
@@ -23,7 +23,9 @@ const fileService = new FileGridFsService("images");
|
|
23
23
|
|
24
24
|
|
25
25
|
const fileFilter = (req, file, cb) => {
|
26
|
-
if (file.mimetype == 'image/jpeg' || file.mimetype == 'image/png'
|
26
|
+
if (file.mimetype == 'image/jpeg' || file.mimetype == 'image/png'
|
27
|
+
|| file.mimetype == 'image/gif'|| file.mimetype == 'image/vnd.microsoft.icon'
|
28
|
+
|| file.mimetype == 'image/webp') {
|
27
29
|
cb(null, true);
|
28
30
|
} else {
|
29
31
|
cb(null, false);
|
package/routes/project.js
CHANGED
@@ -46,6 +46,8 @@ router.put('/:projectid/downgradeplan', [passport.authenticate(['basic', 'jwt'],
|
|
46
46
|
router.delete('/:projectid/physical', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRole('owner')], function (req, res) {
|
47
47
|
winston.debug(req.body);
|
48
48
|
// TODO delete also department, faq_kb, faq, group, label, lead, message, project_users, requests, subscription
|
49
|
+
|
50
|
+
// TODO use findByIdAndRemove otherwise project don't contains label object
|
49
51
|
Project.remove({ _id: req.params.projectid }, function (err, project) {
|
50
52
|
if (err) {
|
51
53
|
winston.error('Error deleting project ', err);
|
@@ -121,12 +123,18 @@ router.put('/:projectid', [passport.authenticate(['basic', 'jwt'], { session: fa
|
|
121
123
|
if (req.body["settings.email.templates.newMessage"]!=undefined) {
|
122
124
|
update["settings.email.templates.newMessage"] = req.body["settings.email.templates.newMessage"];
|
123
125
|
}
|
126
|
+
if (req.body["settings.email.templates.newMessageFollower"]!=undefined) {
|
127
|
+
update["settings.email.templates.newMessageFollower"] = req.body["settings.email.templates.newMessageFollower"];
|
128
|
+
}
|
124
129
|
if (req.body["settings.email.templates.ticket"]!=undefined) {
|
125
130
|
update["settings.email.templates.ticket"] = req.body["settings.email.templates.ticket"];
|
126
131
|
}
|
127
132
|
if (req.body["settings.email.templates.sendTranscript"]!=undefined) {
|
128
133
|
update["settings.email.templates.sendTranscript"] = req.body["settings.email.templates.sendTranscript"];
|
129
134
|
}
|
135
|
+
if (req.body["settings.email.templates.emailDirect"]!=undefined) {
|
136
|
+
update["settings.email.templates.emailDirect"] = req.body["settings.email.templates.emailDirect"];
|
137
|
+
}
|
130
138
|
|
131
139
|
|
132
140
|
if (req.body["settings.email.from"]!=undefined) {
|
@@ -512,6 +520,8 @@ router.get('/:projectid', [passport.authenticate(['basic', 'jwt'], { session: fa
|
|
512
520
|
winston.warn('Project not found ');
|
513
521
|
return res.status(404).send({ success: false, msg: 'Object not found.' });
|
514
522
|
}
|
523
|
+
|
524
|
+
//TODO REMOVE settings from project
|
515
525
|
res.json(project);
|
516
526
|
});
|
517
527
|
});
|
@@ -608,6 +618,8 @@ router.get('/', [passport.authenticate(['basic', 'jwt'], { session: false }), va
|
|
608
618
|
|
609
619
|
project_users.sort((a, b) => (a.id_project && b.id_project && a.id_project.updatedAt > b.id_project.updatedAt) ? 1 : -1)
|
610
620
|
project_users.reverse();
|
621
|
+
|
622
|
+
//TODO REMOVE settings from project
|
611
623
|
res.json(project_users);
|
612
624
|
});
|
613
625
|
});
|
package/routes/users.js
CHANGED
@@ -20,6 +20,7 @@ router.put('/', function (req, res) {
|
|
20
20
|
update.firstname = req.body.firstname;
|
21
21
|
update.lastname = req.body.lastname;
|
22
22
|
update.attributes = req.body.attributes;
|
23
|
+
update.description = req.body.description;
|
23
24
|
|
24
25
|
|
25
26
|
|
@@ -75,6 +76,7 @@ router.delete('/physical', function (req, res) {
|
|
75
76
|
// cancello virtualmente progetti owner
|
76
77
|
winston.debug('delete USER - REQ BODY ', req.body);
|
77
78
|
|
79
|
+
// TODO use findByIdAndRemove otherwise user don't contains label object
|
78
80
|
User.remove({ _id: req.user.id }, function (err, user) {
|
79
81
|
if (err) {
|
80
82
|
winston.error(err);
|
package/services/emailService.js
CHANGED
@@ -1857,7 +1857,7 @@ parseText(text, payload) {
|
|
1857
1857
|
var baseScope = JSON.parse(JSON.stringify(this));
|
1858
1858
|
delete baseScope.pass;
|
1859
1859
|
|
1860
|
-
winston.
|
1860
|
+
winston.debug("parseText text: "+ text);
|
1861
1861
|
|
1862
1862
|
var templateHand = handlebars.compile(text);
|
1863
1863
|
|
@@ -1868,7 +1868,7 @@ parseText(text, payload) {
|
|
1868
1868
|
};
|
1869
1869
|
|
1870
1870
|
var textTemplate = templateHand(replacements);
|
1871
|
-
winston.
|
1871
|
+
winston.debug("parseText textTemplate: "+ textTemplate);
|
1872
1872
|
|
1873
1873
|
return textTemplate;
|
1874
1874
|
|
@@ -5,7 +5,7 @@
|
|
5
5
|
<head>
|
6
6
|
<meta name="viewport" content="width=device-width" />
|
7
7
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
8
|
-
<title>New message ticket from
|
8
|
+
<title>New message ticket from Tiledesk</title>
|
9
9
|
|
10
10
|
<style type="text/css">
|
11
11
|
img {
|
@@ -4,7 +4,7 @@
|
|
4
4
|
<head>
|
5
5
|
<meta name="viewport" content="width=device-width" />
|
6
6
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
7
|
-
<title>New email from
|
7
|
+
<title>New email from Tiledesk</title>
|
8
8
|
|
9
9
|
<style type="text/css">
|
10
10
|
img {
|
@@ -100,13 +100,13 @@
|
|
100
100
|
|
101
101
|
<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top">
|
102
102
|
<h2 style="text-align: center; letter-spacing: 1px; font-weight: 400; line-height:24px ">
|
103
|
-
{{currentUserFirstname}} {{currentUserLastname}} has invited you to the
|
103
|
+
{{currentUserFirstname}} {{currentUserLastname}} has invited you to the Tiledesk project <strong> {{projectName}}</strong>
|
104
104
|
</h2>
|
105
105
|
|
106
106
|
<br> <br><strong style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">Hi {{invitedUserFirstname}} {{invitedUserLastname}},</strong>
|
107
107
|
|
108
108
|
<br> <br>
|
109
|
-
I invited you to take on the role of {{invitedUserRole}} of the
|
109
|
+
I invited you to take on the role of {{invitedUserRole}} of the Tiledesk <strong> {{projectName}}</strong> project
|
110
110
|
|
111
111
|
|
112
112
|
<div style="text-align: center;">
|
@@ -116,7 +116,7 @@
|
|
116
116
|
</a>
|
117
117
|
</div>
|
118
118
|
|
119
|
-
<br><br> Team
|
119
|
+
<br><br> The Tiledesk Team
|
120
120
|
</td>
|
121
121
|
</tr>
|
122
122
|
|
@@ -4,7 +4,7 @@
|
|
4
4
|
<head>
|
5
5
|
<meta name="viewport" content="width=device-width" />
|
6
6
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
7
|
-
<title>New email from
|
7
|
+
<title>New email from Tiledesk</title>
|
8
8
|
|
9
9
|
<style type="text/css">
|
10
10
|
img {
|
@@ -106,11 +106,11 @@
|
|
106
106
|
<br> <br<strong style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">Hi {{userFirstname}} {{userLastname}},</strong>
|
107
107
|
|
108
108
|
<br> <br>
|
109
|
-
The password of your
|
109
|
+
The password of your Tiledesk account {{to}} was just changed.
|
110
110
|
<br><br>If this was you, then you can safely ignore this email.
|
111
111
|
<br><br>If this wasn't you please contact <a href="mailto:info@tiledesk.com">our support team</a>
|
112
112
|
|
113
|
-
<br><br> Team
|
113
|
+
<br><br> The Tiledesk Team
|
114
114
|
</td>
|
115
115
|
</tr>
|
116
116
|
|
@@ -5,7 +5,7 @@
|
|
5
5
|
<head>
|
6
6
|
<meta name="viewport" content="width=device-width" />
|
7
7
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
8
|
-
<title>New message ticket from
|
8
|
+
<title>New message ticket from Tiledesk</title>
|
9
9
|
|
10
10
|
<style type="text/css">
|
11
11
|
img {
|
@@ -4,7 +4,7 @@
|
|
4
4
|
<head>
|
5
5
|
<meta name="viewport" content="width=device-width" />
|
6
6
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
7
|
-
<title>New pooled chat from
|
7
|
+
<title>New pooled chat from Tiledesk</title>
|
8
8
|
|
9
9
|
<style type="text/css">
|
10
10
|
img {
|
@@ -3,7 +3,7 @@
|
|
3
3
|
<head>
|
4
4
|
<meta name="viewport" content="width=device-width" />
|
5
5
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
6
|
-
<title>New email from
|
6
|
+
<title>New email from Tiledesk</title>
|
7
7
|
|
8
8
|
<style type="text/css">
|
9
9
|
img {
|
@@ -102,7 +102,7 @@
|
|
102
102
|
<br> <br<strong style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">Hi {{userFirstname}} {{userLastname}},</strong>
|
103
103
|
|
104
104
|
<br> <br>
|
105
|
-
Seems like you forgot your password for
|
105
|
+
Seems like you forgot your password for Tiledesk. If this is true, click below to reset your password
|
106
106
|
<div style="text-align: center;">
|
107
107
|
<br><br>
|
108
108
|
<a href="{{baseScope.baseUrl}}/#/resetpassword/{{resetPswRequestId}}" style=" background-color: #ff8574 !important; border: none; color: white; padding: 12px 30px; text-align: center; text-decoration: none; display: inline-block; font-size: 16px; font-weight: 600; letter-spacing: 1px; margin: 4px 2px; cursor: pointer;">
|
@@ -113,7 +113,7 @@
|
|
113
113
|
<br><br><span style="font-size:12px; ">If you're having trouble clicking the "Reset My Password" button, copy and paste the URL below into your web browser: </span>
|
114
114
|
<br>
|
115
115
|
<span style="color:#03a5e8; font-size:12px; ">{{baseScope.baseUrl}}/#/resetpassword/{{resetPswRequestId}}</span>
|
116
|
-
<br><br> Team
|
116
|
+
<br><br> The Tiledesk Team
|
117
117
|
</td>
|
118
118
|
</tr>
|
119
119
|
|
@@ -4,7 +4,7 @@
|
|
4
4
|
<head>
|
5
5
|
<meta name="viewport" content="width=device-width" />
|
6
6
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
7
|
-
<title>New email from
|
7
|
+
<title>New email from Tiledesk</title>
|
8
8
|
|
9
9
|
<style type="text/css">
|
10
10
|
img {
|
@@ -109,11 +109,11 @@
|
|
109
109
|
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
110
110
|
<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top">
|
111
111
|
<strong style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">Hi {{savedUser.firstname}} {{savedUser.lastname}},</strong>
|
112
|
-
<!-- <br> welcome on
|
113
|
-
<br><br> Thank you for signin up with
|
112
|
+
<!-- <br> welcome on Tiledesk.com. -->
|
113
|
+
<br><br> Thank you for signin up with Tiledesk.
|
114
114
|
<br><br> To complete the setup, <span><a href="{{baseScope.baseUrl}}/#/verify/email/{{savedUser._id}}"> click here to verify your email address. </a> </span>
|
115
115
|
<br><br>Give us your feedback! We need your advice. Send an email to <a href="mailto:info@tiledesk.com">info@tiledesk.com</a>
|
116
|
-
<br><br> Team
|
116
|
+
<br><br> The Tiledesk Team
|
117
117
|
</td>
|
118
118
|
</tr>
|
119
119
|
|