@opengovsg/mockpass 2.7.9
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/.eslintrc.json +13 -0
- package/.gitattributes +2 -0
- package/.github/dependabot.yml +14 -0
- package/.github/mergify.yml +12 -0
- package/.github/workflows/ci.yml +27 -0
- package/.github/workflows/npmpublish.yml +22 -0
- package/.gitpod.yml +5 -0
- package/.husky/pre-commit +4 -0
- package/.husky/pre-push +4 -0
- package/.prettierrc.js +5 -0
- package/Dockerfile +11 -0
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/commitlint.config.js +7 -0
- package/index.js +87 -0
- package/lib/assertions.js +319 -0
- package/lib/crypto/index.js +61 -0
- package/lib/crypto/myinfo-signature.js +153 -0
- package/lib/express/index.js +6 -0
- package/lib/express/myinfo/consent.js +160 -0
- package/lib/express/myinfo/controllers.js +179 -0
- package/lib/express/myinfo/index.js +10 -0
- package/lib/express/oidc.js +131 -0
- package/lib/express/saml.js +171 -0
- package/lib/express/sgid.js +168 -0
- package/lib/saml-artifact.js +32 -0
- package/package.json +81 -0
- package/public/mockpass/resources/css/animate.css +43 -0
- package/public/mockpass/resources/css/common.css +121 -0
- package/public/mockpass/resources/css/reset.css +95 -0
- package/public/mockpass/resources/css/style-baseline-small-media.css +567 -0
- package/public/mockpass/resources/css/style-baseline.css +1006 -0
- package/public/mockpass/resources/css/style-common-small-media.css +156 -0
- package/public/mockpass/resources/css/style-common.css +510 -0
- package/public/mockpass/resources/css/style-homepage-small-media.css +588 -0
- package/public/mockpass/resources/css/style-homepage.css +674 -0
- package/public/mockpass/resources/css/style-main.css +9 -0
- package/public/mockpass/resources/img/ajax-loader.gif +0 -0
- package/public/mockpass/resources/img/ask_cheryl_tab.png +0 -0
- package/public/mockpass/resources/img/background/large-device/sp_bg.jpg +0 -0
- package/public/mockpass/resources/img/background/medium-device/ipad-bg.jpg +0 -0
- package/public/mockpass/resources/img/background/medium-device/ipad-landscape-sp-bg.jpg +0 -0
- package/public/mockpass/resources/img/background/small-device/mobile-sp-bg.jpg +0 -0
- package/public/mockpass/resources/img/carousel/large-device/how-to-setup-2fa-icon.png +0 -0
- package/public/mockpass/resources/img/carousel/large-device/register-icon.png +0 -0
- package/public/mockpass/resources/img/carousel/large-device/reset-password-icon.png +0 -0
- package/public/mockpass/resources/img/carousel/large-device/setup-2fa-icon.png +0 -0
- package/public/mockpass/resources/img/carousel/large-device/update-acct-icon.png +0 -0
- package/public/mockpass/resources/img/carousel/medium-device/ipad-register-icon.png +0 -0
- package/public/mockpass/resources/img/carousel/medium-device/ipad-reset-password-icon.png +0 -0
- package/public/mockpass/resources/img/carousel/medium-device/ipad-setup-2fa-icon.png +0 -0
- package/public/mockpass/resources/img/carousel/medium-device/ipad-update-acct-icon.png +0 -0
- package/public/mockpass/resources/img/carousel/small-device/mobile-register.png +0 -0
- package/public/mockpass/resources/img/carousel/small-device/mobile-reset-password-icon.png +0 -0
- package/public/mockpass/resources/img/carousel/small-device/mobile-update-acct-icon.png +0 -0
- package/public/mockpass/resources/img/close.png +0 -0
- package/public/mockpass/resources/img/id-pw-icon.png +0 -0
- package/public/mockpass/resources/img/logo/mockpass-logo.png +0 -0
- package/public/mockpass/resources/img/logo/mockpass-placeholder-logo.png +0 -0
- package/public/mockpass/resources/img/logo/mockpass_watermark.png +0 -0
- package/public/mockpass/resources/img/qr-icon.png +0 -0
- package/public/mockpass/resources/img/qr-shadow.png +0 -0
- package/public/mockpass/resources/img/refresh.jpg +0 -0
- package/public/mockpass/resources/img/sidebar-icons.png +0 -0
- package/public/mockpass/resources/img/sp-qr-unavailable.png +0 -0
- package/public/mockpass/resources/img/utility-icon-black.png +0 -0
- package/public/mockpass/resources/js/bootstrap.min.js +7 -0
- package/public/mockpass/resources/js/jquery-3.5.1.js +10872 -0
- package/public/mockpass/resources/js/login-common.js +849 -0
- package/public/mockpass/resources/plugins/bootstrap-3.3.6/css/bootstrap.min.css +6 -0
- package/public/mockpass/resources/plugins/bootstrap-3.3.6/fonts/glyphicons-halflings-regular.woff2 +0 -0
- package/static/certs/csr.pem +17 -0
- package/static/certs/key.pem +28 -0
- package/static/certs/key.pub +9 -0
- package/static/certs/server.crt +21 -0
- package/static/certs/spcp-csr.pem +17 -0
- package/static/certs/spcp-key.pem +28 -0
- package/static/certs/spcp.crt +20 -0
- package/static/html/consent.html +40 -0
- package/static/html/login-page.html +271 -0
- package/static/myinfo/v2.json +6154 -0
- package/static/myinfo/v3.json +29386 -0
- package/static/saml/corppass.xml +21 -0
- package/static/saml/unsigned-assertion.xml +24 -0
- package/static/saml/unsigned-response.xml +19 -0
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global JS variables related to Login Tab
|
|
3
|
+
*/
|
|
4
|
+
var qrCodeTab = "#qrcodeloginli";
|
|
5
|
+
var loginTab = "#loginli";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Global JS variables related to password login
|
|
9
|
+
*/
|
|
10
|
+
var singPassID = "#loginID";
|
|
11
|
+
var password= "#password";
|
|
12
|
+
var captca = "#jCaptcha";
|
|
13
|
+
var captcaImg = "#logincap";
|
|
14
|
+
|
|
15
|
+
var loginBlock = "#LoginForm";
|
|
16
|
+
var logincaptchaBlock = "#loginc";
|
|
17
|
+
var loginerrorMessageBlock= "#errorMessage";
|
|
18
|
+
var captchaErrorMsgBlock= "#captchaErrMsg";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Global JS variables related to QR Code
|
|
22
|
+
*/
|
|
23
|
+
var hasScanned = "has-scanned";
|
|
24
|
+
var isExpired = "is-expired";
|
|
25
|
+
var cantGen = "cant-gen";
|
|
26
|
+
var isUnavailable = "is-unavailable";
|
|
27
|
+
var isSuspended = "is-suspended";
|
|
28
|
+
var isLocked = "is-locked";
|
|
29
|
+
|
|
30
|
+
var isQrCodeGenerated = false;
|
|
31
|
+
var qrCodeInitStartTime = 0;
|
|
32
|
+
|
|
33
|
+
/*******************************************************************************
|
|
34
|
+
*SOFT TOKEN RELATED METHODS START
|
|
35
|
+
******************************************************************************/
|
|
36
|
+
|
|
37
|
+
//this javascript method is used for app link for web kit singpass in esevice login
|
|
38
|
+
function initAppLauncher(spmurl) {
|
|
39
|
+
NativeAppLauncher.init({
|
|
40
|
+
appLauncherElId: 'qrcodelink', // Element Id of App Launcher button.
|
|
41
|
+
notSupportedMessage: 'Sorry, QR Login is not compatible with this browser. Please try another.', // Defaults to 'Not Supported!'
|
|
42
|
+
universalLinkUrl: spmurl,
|
|
43
|
+
appUri: 'https',
|
|
44
|
+
appDeepUri: 'spm',
|
|
45
|
+
androidAppId: 'sg.ndi.sp',
|
|
46
|
+
iOsAppStore: 'https://itunes.apple.com/app/singpass-mobile/id1340660807?ls=1&mt=8',
|
|
47
|
+
debug: false // Optional
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* This method will start the listener for 2 minutes.After 2 minutes.The
|
|
52
|
+
* listener expires.
|
|
53
|
+
*
|
|
54
|
+
* @param succesUrl
|
|
55
|
+
* @param logoutUrl
|
|
56
|
+
* @returns
|
|
57
|
+
*/
|
|
58
|
+
function startTwofaPushNotifResponseListener(succesUrl) {
|
|
59
|
+
|
|
60
|
+
$.ajax({
|
|
61
|
+
url : "/spauth/tfa/userpnreslistener",
|
|
62
|
+
type : "GET",
|
|
63
|
+
cache: false,
|
|
64
|
+
complete : function(response) {
|
|
65
|
+
var responseObj = response.responseJSON;
|
|
66
|
+
if (responseObj.threadCounter < 2){
|
|
67
|
+
startTwofaPushNotifResponseListener(succesUrl);
|
|
68
|
+
} else if (responseObj.listenerStatus === "SUCCESS") {
|
|
69
|
+
window.location.href = succesUrl;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
error : function(xhr, status, error) {
|
|
73
|
+
startTwofaPushNotifResponseListener(succesUrl);
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* This method is execute the scanned listener via ajax call.
|
|
80
|
+
*
|
|
81
|
+
*
|
|
82
|
+
* @returns
|
|
83
|
+
*/
|
|
84
|
+
function startQRCodeScannedResponseListener() {
|
|
85
|
+
|
|
86
|
+
$.ajax({
|
|
87
|
+
url : spauthContextPath + "/login/qrscannedlistener",
|
|
88
|
+
type : "GET",
|
|
89
|
+
cache: false,
|
|
90
|
+
global: false,
|
|
91
|
+
complete : function(response) {
|
|
92
|
+
var responseObj = response.responseJSON;
|
|
93
|
+
// check if the request is timeout and validate the no of times the thread has been called for same transaction id
|
|
94
|
+
if (responseObj.listenerStatus === "SUCCESS" && responseObj.userStatus == null) {
|
|
95
|
+
if (browserLogin !== 'DESKTOP') {
|
|
96
|
+
if (document.visibilityState === 'visible') {
|
|
97
|
+
startQRCodeAcknowledgedResponseListener();
|
|
98
|
+
$('.qr__wrapper').addClass(hasScanned);
|
|
99
|
+
} else {
|
|
100
|
+
document.addEventListener("visibilitychange", function() {
|
|
101
|
+
if (document.visibilityState === 'visible') {
|
|
102
|
+
startQRCodeAcknowledgedResponseListener();
|
|
103
|
+
$('.qr__wrapper').addClass(hasScanned);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
startQRCodeAcknowledgedResponseListener();
|
|
109
|
+
$('.qr__wrapper').addClass(hasScanned);
|
|
110
|
+
}
|
|
111
|
+
} else if (responseObj.listenerStatus === 'RETRY') {
|
|
112
|
+
if (browserLogin !== 'DESKTOP') {
|
|
113
|
+
if (document.visibilityState === 'visible') {
|
|
114
|
+
startQRCodeScannedResponseListener();
|
|
115
|
+
} else {
|
|
116
|
+
document.addEventListener("visibilitychange", function() {
|
|
117
|
+
if (document.visibilityState === 'visible') {
|
|
118
|
+
startQRCodeScannedResponseListener();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
startQRCodeScannedResponseListener();
|
|
124
|
+
}
|
|
125
|
+
} else if (responseObj.listenerStatus === "ERROR") {
|
|
126
|
+
$('.qr__wrapper').addClass(cantGen);
|
|
127
|
+
} else if (responseObj.listenerStatus === "EXPIRED") {
|
|
128
|
+
$('.qr__wrapper').addClass(isExpired);
|
|
129
|
+
} else if (responseObj.userStatus === "SPCP004D" || responseObj.userStatus === "SPCP004E") {
|
|
130
|
+
$('.qr__wrapper').addClass(isLocked);
|
|
131
|
+
} else if (responseObj.userStatus === "SPCP003" || responseObj.userStatus === "SPCP004A" || responseObj.userStatus === "SPCP004B" || responseObj.userStatus === "SPCP004C" || responseObj.userStatus === "SPCP005" || responseObj.userStatus === "SPCP006") {
|
|
132
|
+
$('.qr__wrapper').addClass(isSuspended);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
error : function(xhr, status, error) {
|
|
136
|
+
startQRCodeScannedResponseListener();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* This method is execute the acknowledged listener via ajax call clean the previous
|
|
143
|
+
* active the scanned and acknowledged listener is session storage
|
|
144
|
+
*
|
|
145
|
+
* @returns
|
|
146
|
+
*/
|
|
147
|
+
function startQRCodeAcknowledgedResponseListener() {
|
|
148
|
+
var form = $("#qrcodelogin");
|
|
149
|
+
|
|
150
|
+
$.ajax({
|
|
151
|
+
url : spauthContextPath + "/login/qracknowledgedlistener",
|
|
152
|
+
type : "POST",
|
|
153
|
+
cache: false,
|
|
154
|
+
global: false,
|
|
155
|
+
data: $(form).serialize() + "&wogaaid=" + initialiseWogaaId(),
|
|
156
|
+
complete : function(response) {
|
|
157
|
+
var responseObj = response.responseJSON;
|
|
158
|
+
if (responseObj.listenerStatus === "SUCCESS") {
|
|
159
|
+
setCookie("tabId","qrcodetab");
|
|
160
|
+
|
|
161
|
+
// to send wogaa request
|
|
162
|
+
if (!isSingpassHome) {
|
|
163
|
+
sendWogaaRequest(responseObj.wogaaUrl, responseObj.wogaaMessage);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// to redirect to given page
|
|
167
|
+
doPostRequest("verifyqrcodeauth");
|
|
168
|
+
} else if (responseObj.listenerStatus === 'RETRY') {
|
|
169
|
+
if (browserLogin !== 'DESKTOP') {
|
|
170
|
+
if (document.visibilityState === 'visible') {
|
|
171
|
+
startQRCodeAcknowledgedResponseListener();
|
|
172
|
+
} else {
|
|
173
|
+
document.addEventListener("visibilitychange", function() {
|
|
174
|
+
if (document.visibilityState === 'visible') {
|
|
175
|
+
startQRCodeAcknowledgedResponseListener();
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
startQRCodeAcknowledgedResponseListener();
|
|
181
|
+
}
|
|
182
|
+
} else if (responseObj.listenerStatus === "ERROR") {
|
|
183
|
+
$('.qr__wrapper').addClass(cantGen);
|
|
184
|
+
} else if (responseObj.listenerStatus === "EXPIRED") {
|
|
185
|
+
$('.qr__wrapper').addClass(isExpired);
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
error : function(xhr, status, error) {
|
|
189
|
+
startQRCodeAcknowledgedResponseListener();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function refreshQRCode() {
|
|
195
|
+
isQrCodeGenerated = false;
|
|
196
|
+
generateQRCode();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* This method generate the QR code.
|
|
201
|
+
* Page will reload when the session has time out.
|
|
202
|
+
*
|
|
203
|
+
* @returns
|
|
204
|
+
*/
|
|
205
|
+
function generateQRCode() {
|
|
206
|
+
|
|
207
|
+
if (isQrCodeGenerated) {
|
|
208
|
+
return;
|
|
209
|
+
} else {
|
|
210
|
+
$('.qr__wrapper').removeClass(cantGen);
|
|
211
|
+
$('.qr__wrapper').removeClass(isExpired);
|
|
212
|
+
$('.qr__wrapper').removeClass(isLocked);
|
|
213
|
+
$('.qr__wrapper').removeClass(isSuspended);
|
|
214
|
+
$('.qr__wrapper').removeClass(hasScanned);
|
|
215
|
+
|
|
216
|
+
$.ajax({
|
|
217
|
+
url : spauthContextPath + "/login/generateqrcode",
|
|
218
|
+
type : "POST",
|
|
219
|
+
cache: false,
|
|
220
|
+
success : function(response) {
|
|
221
|
+
var responseObj = response;
|
|
222
|
+
|
|
223
|
+
if (responseObj.error === 136) {
|
|
224
|
+
if (isSingpassHome === true) {
|
|
225
|
+
doPostRequest("/spauth/login/logout");
|
|
226
|
+
// To refresh the page so that user can successfully login
|
|
227
|
+
window.location.reload();
|
|
228
|
+
} else {
|
|
229
|
+
window.location.replace("/spauth/login/eservicelogout");
|
|
230
|
+
}
|
|
231
|
+
} else if (responseObj.qrcode_byte != null) {
|
|
232
|
+
$('#qrcodelink').addClass('flip').delay(500).queue(function(){
|
|
233
|
+
initAppLauncher(responseObj.spm_url);
|
|
234
|
+
$('#qrImage').attr("src", "data:image/png;base64," + responseObj.qrcode_byte);
|
|
235
|
+
$('#qrcodelink').removeClass('flip').dequeue();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
isQrCodeGenerated = true;
|
|
239
|
+
startQRCodeScannedResponseListener();
|
|
240
|
+
qrCodeInitStartTime = Date.now();
|
|
241
|
+
} else if (responseObj.qrcode_is_unavailable) {
|
|
242
|
+
$('.qr__wrapper').addClass(isUnavailable);
|
|
243
|
+
} else {
|
|
244
|
+
$('.qr__wrapper').addClass(cantGen);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
error: function(XMLHttpRequest, textStatus, errorThrown) {
|
|
248
|
+
$('.qr__wrapper').addClass(cantGen);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Helper method to open new tab upon clicking on qr code
|
|
256
|
+
* Close the window after one second ( for user login experience )
|
|
257
|
+
* @param url
|
|
258
|
+
* @returns
|
|
259
|
+
*/
|
|
260
|
+
function redirectToSingPassMobile(url) {
|
|
261
|
+
var spmwindow = window.open(url, "_blank");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* This method will get the QR Validity Time (value for loading screen timeout)
|
|
266
|
+
*
|
|
267
|
+
* @return time in milliseconds that will be used for setting the timeoutQRLoadingScreen global variable.
|
|
268
|
+
* Note that negative values will skip loading screen.
|
|
269
|
+
*/
|
|
270
|
+
function getQRValidityTime() {
|
|
271
|
+
var timeoutQRLoadingScreen = 120000 - (Date.now() - qrCodeInitStartTime);
|
|
272
|
+
if (timeoutQRLoadingScreen == 0) {
|
|
273
|
+
timeoutQRLoadingScreen = -1;
|
|
274
|
+
}
|
|
275
|
+
return timeoutQRLoadingScreen;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/*******************************************************************************
|
|
279
|
+
*SOFT TOKEN RELATED METHODS ENDS
|
|
280
|
+
******************************************************************************/
|
|
281
|
+
|
|
282
|
+
/*******************************************************************************
|
|
283
|
+
*CAPTCHA RELATED METHODS STARTS
|
|
284
|
+
******************************************************************************/
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* This method is called when user clicked cancel button in captcha page
|
|
288
|
+
*
|
|
289
|
+
* @param divIdToHide
|
|
290
|
+
* @param divIdToDisplay
|
|
291
|
+
*/
|
|
292
|
+
function doCancelCaptcha(divToHide, divToDisplay) {
|
|
293
|
+
$(divToHide).hide();
|
|
294
|
+
$(divToDisplay).show();
|
|
295
|
+
$("#jCaptcha").val("");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* This method is called when user Singpass ID / Password retries is more than 3
|
|
300
|
+
* times.
|
|
301
|
+
*
|
|
302
|
+
* @param flow
|
|
303
|
+
* @param captchId
|
|
304
|
+
* @param hideList
|
|
305
|
+
* @param showList
|
|
306
|
+
* @param captchUrl
|
|
307
|
+
* @returns
|
|
308
|
+
*/
|
|
309
|
+
function isUserRetriesReachedMax(captchId, hideList, showList, captchUrl) {
|
|
310
|
+
showElements(showList);
|
|
311
|
+
hideElements(hideList);
|
|
312
|
+
$(captchId).attr("src", captchUrl);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* This method is called when user entered invalid captcha
|
|
317
|
+
* @param flow
|
|
318
|
+
* @param captchId
|
|
319
|
+
* @param errorMessageId
|
|
320
|
+
* @param clearList
|
|
321
|
+
* @param hideList
|
|
322
|
+
* @param showList
|
|
323
|
+
* @param captchUrl
|
|
324
|
+
* @returns
|
|
325
|
+
*/
|
|
326
|
+
function invalCap(captchId, errorMessageId, clearList, hideList, showList, captchUrl){
|
|
327
|
+
clear(clearList);
|
|
328
|
+
showElements(showList);
|
|
329
|
+
$(captchId).attr("src", captchUrl);
|
|
330
|
+
$(errorMessageId).text("Incorrect code. Please try again.");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/*******************************************************************************
|
|
334
|
+
*CAPTCHA RELATED METHODS ENDS
|
|
335
|
+
******************************************************************************/
|
|
336
|
+
|
|
337
|
+
/*******************************************************************************
|
|
338
|
+
* COMMON LOGIN RELATED METHODS STARTS
|
|
339
|
+
******************************************************************************/
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* This is common method to show the elements based on the idlist given as
|
|
343
|
+
* parameter
|
|
344
|
+
*
|
|
345
|
+
* @param {List}
|
|
346
|
+
* IdList
|
|
347
|
+
* @returns
|
|
348
|
+
*/
|
|
349
|
+
function showElements(IdList) {
|
|
350
|
+
var len = IdList.length;
|
|
351
|
+
for (i = 0; i < len; i++) {
|
|
352
|
+
$(IdList[i]).show();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* This is common method to hide the elements based on the idlist given as
|
|
358
|
+
* parameter
|
|
359
|
+
*
|
|
360
|
+
* @param IdList
|
|
361
|
+
* @returns
|
|
362
|
+
*/
|
|
363
|
+
function hideElements(IdList) {
|
|
364
|
+
var len = IdList.length;
|
|
365
|
+
for (i = 0; i < len; i++) {
|
|
366
|
+
$(IdList[i]).hide();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* This is common method to clear the values based on idlist given as the
|
|
372
|
+
* parameter
|
|
373
|
+
*
|
|
374
|
+
* @param IdList
|
|
375
|
+
* @returns
|
|
376
|
+
*/
|
|
377
|
+
function clear(IdList) {
|
|
378
|
+
var len = IdList.length;
|
|
379
|
+
for (i = 0; i < len; i++) {
|
|
380
|
+
$(IdList[i]).val("");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* This is method that trim the value with value given in the parameter
|
|
386
|
+
* @param x
|
|
387
|
+
* @returns x after trim.
|
|
388
|
+
*/
|
|
389
|
+
function myTrim(x) {
|
|
390
|
+
return x.replace(/(^[ \t]*\n)/gm, "");
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* This is generic method called for onkeypress action.
|
|
395
|
+
*
|
|
396
|
+
* @param e
|
|
397
|
+
* @param action
|
|
398
|
+
* @param lflow
|
|
399
|
+
* @returns
|
|
400
|
+
*/
|
|
401
|
+
function doKeyPress(e, action) {
|
|
402
|
+
|
|
403
|
+
var keynum;
|
|
404
|
+
if (window.event) { // IE
|
|
405
|
+
keynum = e.keyCode;
|
|
406
|
+
} else if (e.which) { // Netscape/Firefox/Opera
|
|
407
|
+
keynum = e.which;
|
|
408
|
+
}
|
|
409
|
+
if (keynum == 13) {
|
|
410
|
+
if (action == 'LOGIN' || action == 'captcha') {
|
|
411
|
+
doSubmit(action);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function getCookie(key) {
|
|
418
|
+
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
|
|
419
|
+
return keyValue ? keyValue[2] : null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function setRememberTab(){
|
|
423
|
+
var tabId = getCookie("tabId");
|
|
424
|
+
return tabId;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function setCookie(key, value) {
|
|
428
|
+
var expires = new Date();
|
|
429
|
+
expires.setTime(expires.getTime() + (1 * 24 * 60 * 60 * 1000));
|
|
430
|
+
document.cookie = key + '=' + value + ';expires=' + expires.toUTCString();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* This is generic method to show qr code tab.
|
|
435
|
+
*/
|
|
436
|
+
function showQRCodeTab() {
|
|
437
|
+
$(qrCodeTab).addClass('active');
|
|
438
|
+
$('#sectionB').addClass('active');
|
|
439
|
+
$(loginTab).removeClass('active');
|
|
440
|
+
$('#sectionA').removeClass('active');
|
|
441
|
+
generateQRCode();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* This is generic method to show login tab.
|
|
446
|
+
*/
|
|
447
|
+
function showLoginTab() {
|
|
448
|
+
$(qrCodeTab).removeClass('active');
|
|
449
|
+
$('#sectionB').removeClass('active');
|
|
450
|
+
$(loginTab).addClass('active');
|
|
451
|
+
$('#sectionA').addClass('active in');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function showLoadTab(){
|
|
455
|
+
var tabId = setRememberTab();
|
|
456
|
+
if (tabId == 'qrcodetab'){
|
|
457
|
+
showQRCodeTab();
|
|
458
|
+
} else {
|
|
459
|
+
showLoginTab();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
toggleQRTooltip();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Shows QR Tooltip if login tab is active
|
|
467
|
+
*/
|
|
468
|
+
function toggleQRTooltip() {
|
|
469
|
+
if(!$(qrCodeTab).hasClass('active')) {
|
|
470
|
+
$('#sp-mobile-tooltip').show();
|
|
471
|
+
} else {
|
|
472
|
+
$('#sp-mobile-tooltip').hide();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/*
|
|
477
|
+
* Sets the modal top position to be placed just below the mobile-header
|
|
478
|
+
*/
|
|
479
|
+
function setModalTopPos() {
|
|
480
|
+
var modalTopPos = $('#mobile-header').position().top + $('#mobile-header').outerHeight();
|
|
481
|
+
modalTopPos > 0 ? $('#myModalHorizontal').find('.homepageLogin.modal-dialog').css('top', modalTopPos+'px') : $('#myModalHorizontal').find('.homepageLogin.modal-dialog').attr('style', '');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* This method is to check if the user password is eight character.
|
|
486
|
+
* @param password
|
|
487
|
+
* @returns
|
|
488
|
+
*/
|
|
489
|
+
function isEightChar(password) {
|
|
490
|
+
if (password.length == 8) {
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* This method is called when user clicked cancel button.
|
|
497
|
+
*
|
|
498
|
+
* @param URL
|
|
499
|
+
* @returns
|
|
500
|
+
*/
|
|
501
|
+
function doCancel(URL) {
|
|
502
|
+
window.location = URL;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* This method is to validate singPassID and Password entered is empty
|
|
507
|
+
* @param userId
|
|
508
|
+
* @param password
|
|
509
|
+
* @param errorDivId
|
|
510
|
+
* @returns false if values is is empty
|
|
511
|
+
*/
|
|
512
|
+
function validateUserIdPassword(userId, password, errorDivId){
|
|
513
|
+
if (userId.length == 0 && password.length == 0) {
|
|
514
|
+
$(errorDivId).css("display", "block");
|
|
515
|
+
$(errorDivId).text("Please enter your SingPass ID and Password");
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Method to validate the mandatory fields
|
|
523
|
+
*/
|
|
524
|
+
function validateMandatoryFields(actionType) {
|
|
525
|
+
var userId = $("#loginID").val();
|
|
526
|
+
var password = $("#password").val();
|
|
527
|
+
var captchaVal = $("#jCaptcha").val();
|
|
528
|
+
if (actionType === 'LOGIN' && userId.length == 0 && password.length == 0) {
|
|
529
|
+
document.getElementById('errorMessage').style.display = "block";
|
|
530
|
+
document.getElementById('errorMessage').innerHTML = "Please enter your SingPass ID and Password";
|
|
531
|
+
return false;
|
|
532
|
+
} else if (actionType === 'LOGIN' && userId.length == 0) {
|
|
533
|
+
$('#password').val("");
|
|
534
|
+
document.getElementById('errorMessage').style.display = "block";
|
|
535
|
+
document.getElementById('errorMessage').innerHTML = "Please enter your SingPass ID.";
|
|
536
|
+
return false;
|
|
537
|
+
} else if (actionType === 'LOGIN' && password.length == 0) {
|
|
538
|
+
document.getElementById('errorMessage').style.display = "block";
|
|
539
|
+
document.getElementById('errorMessage').innerHTML = "Please enter your SingPass password.";
|
|
540
|
+
return false;
|
|
541
|
+
} else if (actionType === 'captcha' && captchaVal.length == 0) {
|
|
542
|
+
$('#captchaErrMsg').show();
|
|
543
|
+
$('#captchaErrMsg').text("Enter the code shown above.");
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* This method is to validate singPassID is empty or not
|
|
551
|
+
*
|
|
552
|
+
* @param userId
|
|
553
|
+
* @param errorDivId
|
|
554
|
+
* @param errorMessage
|
|
555
|
+
* @returns false if values is is empty
|
|
556
|
+
*/
|
|
557
|
+
function validateUserId(userId, errorDivId, errorMessage,hideErrorDiv) {
|
|
558
|
+
if (userId.length == 0) {
|
|
559
|
+
$(errorDivId).css("display", "block");
|
|
560
|
+
$(errorDivId).text(errorMessage);
|
|
561
|
+
$("#plLockedErrorMessage").hide();
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* This method is validate password is empty or not
|
|
569
|
+
*
|
|
570
|
+
* @param password
|
|
571
|
+
* @param errorDivId
|
|
572
|
+
* @param errorMessage
|
|
573
|
+
* @returns if values is is empty
|
|
574
|
+
*/
|
|
575
|
+
function validatePassword(password, errorDivId, errorMessage) {
|
|
576
|
+
if (password.length == 0) {
|
|
577
|
+
$(errorDivId).css("display", "block");
|
|
578
|
+
$(errorDivId).text(errorMessage);
|
|
579
|
+
$("#plpLockedErrorMessage").hide();
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* This method to check the mandatory validation
|
|
587
|
+
* @param userId
|
|
588
|
+
* @param password
|
|
589
|
+
* @param errorDivId
|
|
590
|
+
* @param errorMessage
|
|
591
|
+
* @param pErrorMessage
|
|
592
|
+
* @returns
|
|
593
|
+
*/
|
|
594
|
+
function mandatoryValidation(userId, password, errorDivId, errorMessage, pErrorMessage){
|
|
595
|
+
var validation = validateUserIdPassword(userId, password, errorDivId);
|
|
596
|
+
if(validation){
|
|
597
|
+
validation = validateUserId(userId, errorDivId, errorMessage);
|
|
598
|
+
if(validation){
|
|
599
|
+
validation = validatePassword(password, errorDivId, pErrorMessage);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return validation;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* This is a common methods used to set all the rba related details
|
|
607
|
+
*
|
|
608
|
+
* @param flow
|
|
609
|
+
* @param obj
|
|
610
|
+
* @param data
|
|
611
|
+
* @param modulussec
|
|
612
|
+
* @param deviceDetId
|
|
613
|
+
* @param encryptedRbaDeviceId
|
|
614
|
+
* @param rbaDeviceParamId
|
|
615
|
+
* @returns
|
|
616
|
+
*/
|
|
617
|
+
function setRBAData(obj, data, modulussec, deviceDetId, encryptedRbaDeviceId, rbaDeviceParamId) {
|
|
618
|
+
var jsonString;
|
|
619
|
+
var encryptedRbaDevice;
|
|
620
|
+
var rbaDeviceParam2;
|
|
621
|
+
try {
|
|
622
|
+
var Exponent = obj.EXPONENT;
|
|
623
|
+
var Modulus = obj.RSA_PUBLIC_KEY;
|
|
624
|
+
var randomString16 = obj.RANDOM_STRING_16;
|
|
625
|
+
var rsaBlock = encryptVerifyNoUserRSABlock256(Exponent, Modulus, data,randomString16);
|
|
626
|
+
jsonString = JSON.stringify(jsonObj);
|
|
627
|
+
} catch (e) {
|
|
628
|
+
//doNothing
|
|
629
|
+
}
|
|
630
|
+
$(deviceDetId).val(jsonString);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* This is a common method used to set all the randoms details for password encyrpt.
|
|
635
|
+
* @param obj
|
|
636
|
+
* @param password
|
|
637
|
+
* @param randomString16Id
|
|
638
|
+
* @param randomString32Id
|
|
639
|
+
* @param randomString64Id
|
|
640
|
+
* @param rsaBlockId
|
|
641
|
+
* @param rsaBlock1Id
|
|
642
|
+
* @param rsaBlock2Id
|
|
643
|
+
* @returns
|
|
644
|
+
*/
|
|
645
|
+
function setRamdoms(obj, password, randomString16Id, randomString32Id, randomString64Id, rsaBlockId, rsaBlock1Id, rsaBlock2Id) {
|
|
646
|
+
var Exponent = obj.EXPONENT;
|
|
647
|
+
var Modulus = obj.RSA_PUBLIC_KEY;
|
|
648
|
+
var randomString16 = obj.RANDOM_STRING_16;
|
|
649
|
+
var randomString32 = obj.RANDOM_STRING_32;
|
|
650
|
+
var randomString64 = obj.RANDOM_STRING_64;
|
|
651
|
+
|
|
652
|
+
var rsaBlock = encryptVerifyNoUserRSABlock256(Exponent, Modulus, password,randomString16);
|
|
653
|
+
var rsaBlock1 = encryptMigratePwdNoVerifyNoUser256RSABlock512(Exponent,Modulus, password, randomString32, randomString64);
|
|
654
|
+
var rsaBlock2 = encryptVerifyStaticNoUserRSABlock512(Exponent, Modulus,password, randomString64);
|
|
655
|
+
|
|
656
|
+
$(randomString16Id).val(randomString16);
|
|
657
|
+
$(randomString32Id).val(randomString32);
|
|
658
|
+
$(randomString64Id).val(randomString64);
|
|
659
|
+
$(rsaBlockId).val(rsaBlock);
|
|
660
|
+
$(rsaBlock1Id).val(rsaBlock1);
|
|
661
|
+
$(rsaBlock2Id).val(rsaBlock2);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* This method is to hide and show list when user is invalid user
|
|
667
|
+
*
|
|
668
|
+
* @param flow
|
|
669
|
+
* @param errorMessageId
|
|
670
|
+
* @param clearList
|
|
671
|
+
* @param hideList
|
|
672
|
+
* @param showList
|
|
673
|
+
* @param message
|
|
674
|
+
* @returns
|
|
675
|
+
*/
|
|
676
|
+
function invalUsr(flow, errorMessageId, clearList, hideList, showList, message) {
|
|
677
|
+
clear(clearList);
|
|
678
|
+
hideElements(hideList);
|
|
679
|
+
showElements(showList);
|
|
680
|
+
$(errorMessageId).text(message);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* This method is to hide and show list when user is locked/suspend/terminated user
|
|
685
|
+
* @param flow
|
|
686
|
+
* @param errorMessageId
|
|
687
|
+
* @param clearList
|
|
688
|
+
* @param hideList
|
|
689
|
+
* @param showList
|
|
690
|
+
* @param message
|
|
691
|
+
* @returns
|
|
692
|
+
*/
|
|
693
|
+
function commErr(flow, errorMessageId, clearList, hideList, showList, message) {
|
|
694
|
+
clear(clearList);
|
|
695
|
+
hideElements(hideList);
|
|
696
|
+
showElements(showList);
|
|
697
|
+
$(errorMessageId).text(message);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* This method hide and show login and captcha form elements based on the error message return.
|
|
702
|
+
* @param errorMessage
|
|
703
|
+
* @returns
|
|
704
|
+
*/
|
|
705
|
+
function invalidLoginAction(errorMessage, captchaVal) {
|
|
706
|
+
|
|
707
|
+
if (errorMessage == 'invalUsr') {
|
|
708
|
+
// Login Form
|
|
709
|
+
$("#LoginForm").show();
|
|
710
|
+
$('#loginID').val("");
|
|
711
|
+
$('#password').val("");
|
|
712
|
+
$('#errorMessage').show();
|
|
713
|
+
$('#errorMessage').text("You have entered an invalid SingPass ID or Password.");
|
|
714
|
+
// Captcha Form
|
|
715
|
+
$("#loginc").hide();
|
|
716
|
+
$("#jCaptcha").val("");
|
|
717
|
+
$('#captchaErrMsg').hide();
|
|
718
|
+
} else if (errorMessage == 'commErr') {
|
|
719
|
+
// Login Form
|
|
720
|
+
$("#LoginForm").show();
|
|
721
|
+
$('#loginID').val("");
|
|
722
|
+
$('#password').val("");
|
|
723
|
+
$('#errorMessage').show();
|
|
724
|
+
$('#errorMessage').text("We are unable to verify your account. Please reset your password. Alternatively, you can contact the SingPass helpdesk for more information.");
|
|
725
|
+
// Captcha Form
|
|
726
|
+
$("#loginc").hide();
|
|
727
|
+
$("#jCaptcha").val("");
|
|
728
|
+
$('#captchaErrMsg').hide();
|
|
729
|
+
} else if (errorMessage == 'isUserRetriesReachedMax') {
|
|
730
|
+
// Login Form
|
|
731
|
+
$("#LoginForm").hide();
|
|
732
|
+
$('#errorMessage').hide();
|
|
733
|
+
// Captcha Form
|
|
734
|
+
$("#loginc").show();
|
|
735
|
+
$("#jCaptcha").val("");
|
|
736
|
+
$('#captchaErrMsg').hide();
|
|
737
|
+
$('#logincap').attr('src', captchaVal);
|
|
738
|
+
} else if (errorMessage == 'invalCap') {
|
|
739
|
+
// Login Form
|
|
740
|
+
$("#LoginForm").hide();
|
|
741
|
+
$('#errorMessage').hide();
|
|
742
|
+
// Captcha Form
|
|
743
|
+
$("#loginc").show();
|
|
744
|
+
$("#jCaptcha").val("");
|
|
745
|
+
$('#captchaErrMsg').show();
|
|
746
|
+
$('#captchaErrMsg').text("Incorrect code. Please try again.");
|
|
747
|
+
$('#logincap').attr('src', captchaVal);
|
|
748
|
+
} else if (errorMessage == 'notActiveIrasUsr') {
|
|
749
|
+
$("#LoginForm").show();
|
|
750
|
+
$('#loginID').val("");
|
|
751
|
+
$('#password').val("");
|
|
752
|
+
$('#errorMessage').show();
|
|
753
|
+
$('#errorMessage').html("Please set up the <a href='https://singpassmobile.sg/' target='_blank'>SingPass Mobile app</a> on your mobile device to access SingPass or IRAS Digital Services");
|
|
754
|
+
// Captcha Form
|
|
755
|
+
$("#loginc").hide();
|
|
756
|
+
$("#jCaptcha").val("");
|
|
757
|
+
$('#captchaErrMsg').hide();
|
|
758
|
+
} else {
|
|
759
|
+
$("#LoginForm").show();
|
|
760
|
+
$('#loginID').val("");
|
|
761
|
+
$('#password').val("");
|
|
762
|
+
$('#errorMessage').show();
|
|
763
|
+
$('#errorMessage').html(errorMessage);
|
|
764
|
+
// Captcha Form
|
|
765
|
+
$("#loginc").hide();
|
|
766
|
+
$("#jCaptcha").val("");
|
|
767
|
+
$('#captchaErrMsg').hide();
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function hexEncode(str) {
|
|
772
|
+
var result = '';
|
|
773
|
+
for (var i = 0; i < str.length; i++) {
|
|
774
|
+
result += str.charCodeAt(i).toString(16);
|
|
775
|
+
}
|
|
776
|
+
return result;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function hexToBase64(hexString) {
|
|
780
|
+
return btoa(hexString.match(/\w{2}/g).map(function(a) {
|
|
781
|
+
return String.fromCharCode(parseInt(a, 16));
|
|
782
|
+
}).join(''));
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function generateSamlArtFromCustomNric() {
|
|
786
|
+
var customNric = document.getElementById('customNric').value;
|
|
787
|
+
if (customNric.length !== 9) {
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
var hashedPartnerId = document.getElementById('hashedPartnerId').value;
|
|
791
|
+
var artifactDataHex = '00040000' + hashedPartnerId + hexEncode('customNric:' + customNric);
|
|
792
|
+
document.getElementById('customNricSamlArt').value = hexToBase64(artifactDataHex);
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/*******************************************************************************
|
|
797
|
+
* WOGAA RELATED METHODS STARTS
|
|
798
|
+
******************************************************************************/
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* This method will initialise WOGAA ID.
|
|
802
|
+
*
|
|
803
|
+
* @return wogaaId - WOGAA ID.
|
|
804
|
+
*/
|
|
805
|
+
function initialiseWogaaId() {
|
|
806
|
+
var wogaaId = "";
|
|
807
|
+
try {
|
|
808
|
+
wogaaId = _satellite.getVisitorId().getMarketingCloudVisitorID();
|
|
809
|
+
if (wogaaId == null || wogaaId == undefined) {
|
|
810
|
+
wogaaId = "";
|
|
811
|
+
}
|
|
812
|
+
} catch (error) {
|
|
813
|
+
wogaaId = error.message;
|
|
814
|
+
}
|
|
815
|
+
return wogaaId;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* This method will send a POST request to WOGAA.
|
|
820
|
+
*
|
|
821
|
+
* @param wogaaUrl
|
|
822
|
+
* contains WOGAA API URL.
|
|
823
|
+
* @param wogaaMessage
|
|
824
|
+
* contains a string to be sent to WOGAA.
|
|
825
|
+
*/
|
|
826
|
+
function sendWogaaRequest(wogaaUrl, wogaaMessage) {
|
|
827
|
+
|
|
828
|
+
try {
|
|
829
|
+
if (wogaaUrl !== null && wogaaUrl !== undefined) {
|
|
830
|
+
var wogaaRequest = new XMLHttpRequest();
|
|
831
|
+
wogaaRequest.open("POST", wogaaUrl, true);
|
|
832
|
+
wogaaRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
|
833
|
+
wogaaRequest.onerror = function(error) {
|
|
834
|
+
// do nothing
|
|
835
|
+
}
|
|
836
|
+
wogaaRequest.send(wogaaMessage);
|
|
837
|
+
}
|
|
838
|
+
} catch (error) {
|
|
839
|
+
// do nothing
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/*******************************************************************************
|
|
844
|
+
* WOGAA RELATED METHODS ENDS
|
|
845
|
+
******************************************************************************/
|
|
846
|
+
|
|
847
|
+
/*******************************************************************************
|
|
848
|
+
* COMMON LOGIN RELATED METHODS ENDS
|
|
849
|
+
******************************************************************************/
|