fhirsmith 0.3.0
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/CHANGELOG.md +42 -0
- package/FHIRsmith.png +0 -0
- package/README.md +277 -0
- package/config-template.json +144 -0
- package/library/folder-setup.js +58 -0
- package/library/html-server.js +166 -0
- package/library/html.js +835 -0
- package/library/i18nsupport.js +259 -0
- package/library/languages.js +779 -0
- package/library/logger-telnet.js +205 -0
- package/library/logger.js +279 -0
- package/library/package-manager.js +876 -0
- package/library/utilities.js +196 -0
- package/library/version-utilities.js +1056 -0
- package/npmprojector/config-example.json +13 -0
- package/npmprojector/indexer.js +394 -0
- package/npmprojector/npmprojector.js +395 -0
- package/npmprojector/readme.md +174 -0
- package/npmprojector/watcher.js +335 -0
- package/package.json +119 -0
- package/packages/package-crawler.js +846 -0
- package/packages/packages-template.html +126 -0
- package/packages/packages.js +2838 -0
- package/passwords.ini +2 -0
- package/publisher/publisher-template.html +208 -0
- package/publisher/publisher.js +2167 -0
- package/publisher/task-draft.js +458 -0
- package/registry/api.js +735 -0
- package/registry/crawler.js +637 -0
- package/registry/model.js +513 -0
- package/registry/readme.md +243 -0
- package/registry/registry-data.json +121015 -0
- package/registry/registry-template.html +126 -0
- package/registry/registry.js +1395 -0
- package/registry/test-runner.js +237 -0
- package/root-template.html +124 -0
- package/server.js +524 -0
- package/shl/private-key.pem +5 -0
- package/shl/public-key.pem +18 -0
- package/shl/shl.js +1125 -0
- package/shl/vhl.js +69 -0
- package/static/FHIRsmith128.png +0 -0
- package/static/FHIRsmith16.png +0 -0
- package/static/FHIRsmith32.png +0 -0
- package/static/FHIRsmith64.png +0 -0
- package/static/assets/css/bootstrap-fhir.css +5302 -0
- package/static/assets/css/bootstrap-glyphicons.css +2 -0
- package/static/assets/css/bootstrap.css +4097 -0
- package/static/assets/css/jquery-ui.css +523 -0
- package/static/assets/css/jquery-ui.structure.css +863 -0
- package/static/assets/css/jquery-ui.structure.min.css +5 -0
- package/static/assets/css/jquery-ui.theme.css +439 -0
- package/static/assets/css/jquery-ui.theme.min.css +5 -0
- package/static/assets/css/jquery.ui.all.css +7 -0
- package/static/assets/css/modules.css +18 -0
- package/static/assets/css/project.css +367 -0
- package/static/assets/css/pygments-manni.css +66 -0
- package/static/assets/css/tags.css +74 -0
- package/static/assets/css/xml.css +2 -0
- package/static/assets/fonts/glyphiconshalflings-regular.eot +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.otf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.svg +175 -0
- package/static/assets/fonts/glyphiconshalflings-regular.ttf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.woff +0 -0
- package/static/assets/ico/apple-touch-icon-114-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-144-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-57-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-72-precomposed.png +0 -0
- package/static/assets/ico/favicon.ico +0 -0
- package/static/assets/ico/favicon.png +0 -0
- package/static/assets/images/fhir-logo-www.png +0 -0
- package/static/assets/images/fhir-logo.png +0 -0
- package/static/assets/images/hl7-logo.png +0 -0
- package/static/assets/images/logo_ansinew.jpg +0 -0
- package/static/assets/images/search.png +0 -0
- package/static/assets/images/stripe.png +0 -0
- package/static/assets/images/target.png +0 -0
- package/static/assets/images/tx-registry-root.gif +0 -0
- package/static/assets/images/tx-registry.png +0 -0
- package/static/assets/images/tx-server.png +0 -0
- package/static/assets/images/tx-version.png +0 -0
- package/static/assets/js/bootstrap.min.js +6 -0
- package/static/assets/js/fhir-gw.js +259 -0
- package/static/assets/js/fhir.js +2 -0
- package/static/assets/js/html5shiv.js +8 -0
- package/static/assets/js/jcookie.js +96 -0
- package/static/assets/js/jquery-ui.min.js +6 -0
- package/static/assets/js/jquery.js +10716 -0
- package/static/assets/js/jquery.min.js +2 -0
- package/static/assets/js/jquery.ui.core.js +314 -0
- package/static/assets/js/jquery.ui.draggable.js +825 -0
- package/static/assets/js/jquery.ui.mouse.js +162 -0
- package/static/assets/js/jquery.ui.resizable.js +842 -0
- package/static/assets/js/jquery.ui.widget.js +268 -0
- package/static/assets/js/json2.js +487 -0
- package/static/assets/js/jtip.js +97 -0
- package/static/assets/js/respond.min.js +6 -0
- package/static/assets/js/statuspage.js +70 -0
- package/static/assets/js/xml.js +2 -0
- package/static/dist/js/bootstrap.js +1964 -0
- package/static/favicon.png +0 -0
- package/static/fhir.css +626 -0
- package/static/icon-fhir-16.png +0 -0
- package/static/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
- package/static/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
- package/static/images/ui-bg_flat_10_000000_40x100.png +0 -0
- package/static/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
- package/static/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
- package/static/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- package/static/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
- package/static/images/ui-icons_222222_256x240.png +0 -0
- package/static/images/ui-icons_228ef1_256x240.png +0 -0
- package/static/images/ui-icons_ef8c08_256x240.png +0 -0
- package/static/images/ui-icons_ffd27a_256x240.png +0 -0
- package/static/images/ui-icons_ffffff_256x240.png +0 -0
- package/static/js/jquery.effects.blind.js +49 -0
- package/static/js/jquery.effects.bounce.js +78 -0
- package/static/js/jquery.effects.clip.js +54 -0
- package/static/js/jquery.effects.core.js +763 -0
- package/static/js/jquery.effects.drop.js +50 -0
- package/static/js/jquery.effects.explode.js +79 -0
- package/static/js/jquery.effects.fade.js +32 -0
- package/static/js/jquery.effects.fold.js +56 -0
- package/static/js/jquery.effects.highlight.js +50 -0
- package/static/js/jquery.effects.pulsate.js +51 -0
- package/static/js/jquery.effects.scale.js +178 -0
- package/static/js/jquery.effects.shake.js +57 -0
- package/static/js/jquery.effects.slide.js +50 -0
- package/static/js/jquery.effects.transfer.js +45 -0
- package/static/js/jquery.ui.accordion.js +611 -0
- package/static/js/jquery.ui.autocomplete.js +612 -0
- package/static/js/jquery.ui.button.js +416 -0
- package/static/js/jquery.ui.datepicker.js +1823 -0
- package/static/js/jquery.ui.dialog.js +878 -0
- package/static/js/jquery.ui.droppable.js +296 -0
- package/static/js/jquery.ui.position.js +252 -0
- package/static/js/jquery.ui.progressbar.js +109 -0
- package/static/js/jquery.ui.selectable.js +266 -0
- package/static/js/jquery.ui.slider.js +666 -0
- package/static/js/jquery.ui.sortable.js +1077 -0
- package/static/js/jquery.ui.tabs.js +758 -0
- package/stats.js +80 -0
- package/test-cache/vsac/vsac-valuesets.db +0 -0
- package/token/nginx_passport_setup.md +383 -0
- package/token/security_guide.md +294 -0
- package/token/token-template.html +330 -0
- package/token/token.js +1300 -0
- package/translations/Messages.properties +1510 -0
- package/translations/Messages_ar.properties +1399 -0
- package/translations/Messages_de.properties +836 -0
- package/translations/Messages_es.properties +737 -0
- package/translations/Messages_fr.properties +1 -0
- package/translations/Messages_ja.properties +893 -0
- package/translations/Messages_nl.properties +1357 -0
- package/translations/Messages_pt.properties +1302 -0
- package/translations/Messages_ru.properties +1 -0
- package/translations/Messages_uz.properties +1 -0
- package/translations/Messages_zh.properties +1 -0
- package/translations/rendering-phrases.properties +1128 -0
- package/translations/rendering-phrases_ar.properties +1091 -0
- package/translations/rendering-phrases_de.properties +6 -0
- package/translations/rendering-phrases_es.properties +6 -0
- package/translations/rendering-phrases_fr.properties +624 -0
- package/translations/rendering-phrases_ja.properties +21 -0
- package/translations/rendering-phrases_nl.properties +970 -0
- package/translations/rendering-phrases_pt.properties +1020 -0
- package/translations/rendering-phrases_ru.properties +1094 -0
- package/translations/rendering-phrases_uz.properties +1 -0
- package/translations/rendering-phrases_zh.properties +1 -0
- package/tx/README.md +418 -0
- package/tx/cm/cm-api.js +110 -0
- package/tx/cm/cm-database.js +735 -0
- package/tx/cm/cm-package.js +325 -0
- package/tx/cs/cs-api.js +789 -0
- package/tx/cs/cs-areacode.js +615 -0
- package/tx/cs/cs-country.js +1110 -0
- package/tx/cs/cs-cpt.js +785 -0
- package/tx/cs/cs-cs.js +1579 -0
- package/tx/cs/cs-currency.js +539 -0
- package/tx/cs/cs-db.js +1321 -0
- package/tx/cs/cs-hgvs.js +329 -0
- package/tx/cs/cs-lang.js +465 -0
- package/tx/cs/cs-loinc.js +1485 -0
- package/tx/cs/cs-mimetypes.js +238 -0
- package/tx/cs/cs-ndc.js +704 -0
- package/tx/cs/cs-omop.js +1025 -0
- package/tx/cs/cs-provider-api.js +43 -0
- package/tx/cs/cs-provider-list.js +37 -0
- package/tx/cs/cs-rxnorm.js +808 -0
- package/tx/cs/cs-snomed.js +1102 -0
- package/tx/cs/cs-ucum.js +514 -0
- package/tx/cs/cs-unii.js +271 -0
- package/tx/cs/cs-uri.js +218 -0
- package/tx/cs/cs-usstates.js +305 -0
- package/tx/dev.fhir.org.yml +14 -0
- package/tx/fixtures/test-cases-setup.json +18 -0
- package/tx/fixtures/test-cases.yml +16 -0
- package/tx/html/codesystem-operations.liquid +25 -0
- package/tx/html/home-metrics.liquid +247 -0
- package/tx/html/operations-form.liquid +148 -0
- package/tx/html/search-form.liquid +62 -0
- package/tx/html/tx-template.html +133 -0
- package/tx/html/valueset-operations.liquid +54 -0
- package/tx/importers/atc-to-fhir.js +316 -0
- package/tx/importers/import-loinc.module.js +1536 -0
- package/tx/importers/import-ndc.module.js +1088 -0
- package/tx/importers/import-rxnorm.module.js +898 -0
- package/tx/importers/import-sct.module.js +2457 -0
- package/tx/importers/import-unii.module.js +601 -0
- package/tx/importers/readme.md +453 -0
- package/tx/importers/subset-loinc.module.js +1081 -0
- package/tx/importers/subset-rxnorm.module.js +938 -0
- package/tx/importers/tx-import-base.js +351 -0
- package/tx/importers/tx-import-settings.js +310 -0
- package/tx/importers/tx-import.js +357 -0
- package/tx/library/canonical-resource.js +88 -0
- package/tx/library/capabilitystatement.js +292 -0
- package/tx/library/codesystem.js +774 -0
- package/tx/library/conceptmap.js +568 -0
- package/tx/library/designations.js +932 -0
- package/tx/library/errors.js +77 -0
- package/tx/library/extensions.js +117 -0
- package/tx/library/namingsystem.js +322 -0
- package/tx/library/operation-outcome.js +127 -0
- package/tx/library/parameters.js +105 -0
- package/tx/library/renderer.js +1559 -0
- package/tx/library/terminologycapabilities.js +418 -0
- package/tx/library/ucum-parsers.js +1029 -0
- package/tx/library/ucum-service.js +370 -0
- package/tx/library/ucum-types.js +1099 -0
- package/tx/library/valueset.js +543 -0
- package/tx/library.js +676 -0
- package/tx/ocl/cm-ocl.js +106 -0
- package/tx/ocl/cs-ocl.js +39 -0
- package/tx/ocl/vs-ocl.js +105 -0
- package/tx/operation-context.js +568 -0
- package/tx/params.js +613 -0
- package/tx/provider.js +403 -0
- package/tx/sct/ecl.js +1560 -0
- package/tx/sct/expressions.js +2077 -0
- package/tx/sct/structures.js +1396 -0
- package/tx/tx-html.js +1063 -0
- package/tx/tx.fhir.org.yml +39 -0
- package/tx/tx.js +927 -0
- package/tx/vs/vs-api.js +112 -0
- package/tx/vs/vs-database.js +786 -0
- package/tx/vs/vs-package.js +358 -0
- package/tx/vs/vs-vsac.js +366 -0
- package/tx/workers/batch-validate.js +129 -0
- package/tx/workers/batch.js +361 -0
- package/tx/workers/closure.js +32 -0
- package/tx/workers/expand.js +1845 -0
- package/tx/workers/lookup.js +407 -0
- package/tx/workers/metadata.js +467 -0
- package/tx/workers/operations.js +34 -0
- package/tx/workers/read.js +164 -0
- package/tx/workers/search.js +384 -0
- package/tx/workers/subsumes.js +334 -0
- package/tx/workers/translate.js +492 -0
- package/tx/workers/validate.js +2504 -0
- package/tx/workers/worker.js +904 -0
- package/tx/xml/capabilitystatement-xml.js +63 -0
- package/tx/xml/codesystem-xml.js +62 -0
- package/tx/xml/conceptmap-xml.js +65 -0
- package/tx/xml/namingsystem-xml.js +65 -0
- package/tx/xml/operationoutcome-xml.js +127 -0
- package/tx/xml/parameters-xml.js +312 -0
- package/tx/xml/terminologycapabilities-xml.js +64 -0
- package/tx/xml/valueset-xml.js +64 -0
- package/tx/xml/xml-base.js +603 -0
- package/vcl/vcl-parser.js +1098 -0
- package/vcl/vcl.js +253 -0
- package/windows-install.js +19 -0
- package/xig/xig-template.html +124 -0
- package/xig/xig.js +3049 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# Passport.js Security Implementation Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This guide covers the security considerations and proper implementation of Passport.js OAuth authentication in the FHIR Token Server.
|
|
6
|
+
|
|
7
|
+
## Key Security Features Implemented
|
|
8
|
+
|
|
9
|
+
### 1. **Session Security**
|
|
10
|
+
```javascript
|
|
11
|
+
// Secure session configuration
|
|
12
|
+
{
|
|
13
|
+
store: SQLiteStore, // Persistent session storage
|
|
14
|
+
secret: crypto.randomBytes(64), // Strong session secret
|
|
15
|
+
name: 'fhir.token.sid', // Custom session name (security through obscurity)
|
|
16
|
+
resave: false, // Don't save unchanged sessions
|
|
17
|
+
saveUninitialized: false, // Don't create sessions until something stored
|
|
18
|
+
rolling: true, // Reset expiration on each request
|
|
19
|
+
cookie: {
|
|
20
|
+
secure: NODE_ENV === 'production', // HTTPS only in production
|
|
21
|
+
httpOnly: true, // Prevent XSS access to cookies
|
|
22
|
+
maxAge: 24 * 60 * 60 * 1000, // 24 hour expiration
|
|
23
|
+
sameSite: 'lax' // CSRF protection
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 2. **CSRF Protection**
|
|
29
|
+
```javascript
|
|
30
|
+
// Using Lusca for comprehensive protection
|
|
31
|
+
lusca({
|
|
32
|
+
csrf: true, // CSRF token validation
|
|
33
|
+
csp: { /* Content Security Policy */ },
|
|
34
|
+
xframe: 'SAMEORIGIN', // Clickjacking protection
|
|
35
|
+
hsts: { maxAge: 31536000 }, // Force HTTPS
|
|
36
|
+
xssProtection: true, // XSS header
|
|
37
|
+
nosniff: true // MIME sniffing protection
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 3. **Rate Limiting**
|
|
42
|
+
```javascript
|
|
43
|
+
// OAuth authentication rate limiting
|
|
44
|
+
const authLimiter = rateLimit({
|
|
45
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
46
|
+
max: 5, // 5 attempts per window
|
|
47
|
+
message: 'Too many authentication attempts'
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 4. **API Key Security**
|
|
52
|
+
```javascript
|
|
53
|
+
// Secure key generation and storage
|
|
54
|
+
const keyData = crypto.randomBytes(32); // Cryptographically secure
|
|
55
|
+
const keyHash = await bcrypt.hash(fullKey, 12); // Strong bcrypt rounds
|
|
56
|
+
const keyPrefix = 'tk_' + crypto.randomBytes(4).toString('hex'); // Identifiable prefix
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## OAuth Provider Security
|
|
60
|
+
|
|
61
|
+
### 1. **Google OAuth Configuration**
|
|
62
|
+
```javascript
|
|
63
|
+
// Recommended Google OAuth settings
|
|
64
|
+
{
|
|
65
|
+
clientID: 'your-google-client-id',
|
|
66
|
+
clientSecret: 'your-google-client-secret',
|
|
67
|
+
callbackURL: 'https://yourdomain.com/token/callback/google', // HTTPS required
|
|
68
|
+
scope: ['openid', 'profile', 'email'], // Minimal required scopes
|
|
69
|
+
prompt: 'select_account' // Force account selection
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 2. **Facebook OAuth Configuration**
|
|
74
|
+
```javascript
|
|
75
|
+
{
|
|
76
|
+
clientID: 'your-facebook-app-id',
|
|
77
|
+
clientSecret: 'your-facebook-app-secret',
|
|
78
|
+
callbackURL: 'https://yourdomain.com/token/callback/facebook',
|
|
79
|
+
profileFields: ['id', 'emails', 'name'], // Limit profile data
|
|
80
|
+
enableProof: true // App secret proof for added security
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3. **GitHub OAuth Configuration**
|
|
85
|
+
```javascript
|
|
86
|
+
{
|
|
87
|
+
clientID: 'your-github-client-id',
|
|
88
|
+
clientSecret: 'your-github-client-secret',
|
|
89
|
+
callbackURL: 'https://yourdomain.com/token/callback/github',
|
|
90
|
+
scope: ['user:email'], // Minimal scope for email access
|
|
91
|
+
userAgent: 'YourApp/1.0' // Identify your application
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Security Audit Trail
|
|
96
|
+
|
|
97
|
+
### 1. **Security Event Logging**
|
|
98
|
+
All security-relevant events are logged:
|
|
99
|
+
- OAuth login attempts
|
|
100
|
+
- API key creation/deletion
|
|
101
|
+
- Failed authentication attempts
|
|
102
|
+
- Suspicious activity patterns
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
await this.logSecurityEvent(userId, 'oauth_login', req.ip, req.get('User-Agent'), {
|
|
106
|
+
provider: 'google',
|
|
107
|
+
provider_id: profile.id
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 2. **Usage Tracking**
|
|
112
|
+
- Daily request counts per API key
|
|
113
|
+
- IP address tracking for usage patterns
|
|
114
|
+
- Anomaly detection capabilities
|
|
115
|
+
|
|
116
|
+
## Production Deployment Checklist
|
|
117
|
+
|
|
118
|
+
### 1. **Environment Variables**
|
|
119
|
+
```bash
|
|
120
|
+
# Required environment variables
|
|
121
|
+
NODE_ENV=production
|
|
122
|
+
SESSION_SECRET=your-super-secret-session-key-64-chars-minimum
|
|
123
|
+
CSRF_SECRET=your-csrf-secret-key
|
|
124
|
+
|
|
125
|
+
# OAuth Provider Credentials
|
|
126
|
+
GOOGLE_CLIENT_ID=your-google-client-id
|
|
127
|
+
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
|
128
|
+
FACEBOOK_CLIENT_ID=your-facebook-app-id
|
|
129
|
+
FACEBOOK_CLIENT_SECRET=your-facebook-app-secret
|
|
130
|
+
GITHUB_CLIENT_ID=your-github-client-id
|
|
131
|
+
GITHUB_CLIENT_SECRET=your-github-client-secret
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 2. **OAuth Provider Setup**
|
|
135
|
+
|
|
136
|
+
#### Google Cloud Console:
|
|
137
|
+
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
|
138
|
+
2. Create a new project or select existing
|
|
139
|
+
3. Enable Google+ API and Google OAuth2 API
|
|
140
|
+
4. Go to Credentials → Create OAuth 2.0 Client ID
|
|
141
|
+
5. Set authorized redirect URIs: `https://yourdomain.com/token/auth/google/callback`
|
|
142
|
+
6. Configure OAuth consent screen with minimal scopes
|
|
143
|
+
|
|
144
|
+
#### Facebook for Developers:
|
|
145
|
+
1. Go to [Facebook for Developers](https://developers.facebook.com/)
|
|
146
|
+
2. Create a new app or select existing
|
|
147
|
+
3. Add Facebook Login product
|
|
148
|
+
4. Configure Valid OAuth Redirect URIs: `https://yourdomain.com/token/auth/facebook/callback`
|
|
149
|
+
5. Set app domain and privacy policy URL
|
|
150
|
+
6. Request only `email` permission
|
|
151
|
+
|
|
152
|
+
#### GitHub OAuth Apps:
|
|
153
|
+
1. Go to GitHub Settings → Developer settings → OAuth Apps
|
|
154
|
+
2. Create new OAuth App
|
|
155
|
+
3. Set Authorization callback URL: `https://yourdomain.com/token/auth/github/callback`
|
|
156
|
+
4. Configure application name and homepage URL
|
|
157
|
+
|
|
158
|
+
### 3. **Database Security**
|
|
159
|
+
```javascript
|
|
160
|
+
// Database connection with security settings
|
|
161
|
+
const db = new sqlite3.Database(dbPath, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, (err) => {
|
|
162
|
+
if (err) throw err;
|
|
163
|
+
|
|
164
|
+
// Enable foreign key constraints
|
|
165
|
+
db.run('PRAGMA foreign_keys = ON');
|
|
166
|
+
|
|
167
|
+
// Set secure database settings
|
|
168
|
+
db.run('PRAGMA journal_mode = WAL'); // Write-Ahead Logging
|
|
169
|
+
db.run('PRAGMA synchronous = NORMAL'); // Balance performance/safety
|
|
170
|
+
db.run('PRAGMA temp_store = MEMORY'); // Store temp data in memory
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 4. **HTTPS Configuration**
|
|
175
|
+
```javascript
|
|
176
|
+
// Production server setup with HTTPS
|
|
177
|
+
const https = require('https');
|
|
178
|
+
const fs = require('fs');
|
|
179
|
+
|
|
180
|
+
const options = {
|
|
181
|
+
key: fs.readFileSync('path/to/private-key.pem'),
|
|
182
|
+
cert: fs.readFileSync('path/to/certificate.pem'),
|
|
183
|
+
// Optional: intermediate certificates
|
|
184
|
+
ca: fs.readFileSync('path/to/ca-bundle.pem')
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
https.createServer(options, app).listen(443, () => {
|
|
188
|
+
console.log('HTTPS server running on port 443');
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Security Best Practices
|
|
193
|
+
|
|
194
|
+
### 1. **API Key Management**
|
|
195
|
+
- Keys are shown only once during creation
|
|
196
|
+
- Keys are hashed with bcrypt (12 rounds)
|
|
197
|
+
- Key prefixes allow for efficient database lookup
|
|
198
|
+
- Usage tracking for anomaly detection
|
|
199
|
+
- Configurable expiration dates
|
|
200
|
+
- Scope-based permissions
|
|
201
|
+
|
|
202
|
+
### 2. **User Session Management**
|
|
203
|
+
- Sessions stored in database, not memory
|
|
204
|
+
- Session rotation on privilege change
|
|
205
|
+
- Automatic cleanup of expired sessions
|
|
206
|
+
- Logout destroys both session and cookies
|
|
207
|
+
|
|
208
|
+
### 3. **OAuth State Management**
|
|
209
|
+
- CSRF protection via state parameter
|
|
210
|
+
- State values are cryptographically random
|
|
211
|
+
- State is validated on callback
|
|
212
|
+
- Short-lived state tokens
|
|
213
|
+
|
|
214
|
+
### 4. **Input Validation**
|
|
215
|
+
- All inputs sanitized and validated
|
|
216
|
+
- SQL injection prevention via parameterized queries
|
|
217
|
+
- XSS prevention via output encoding
|
|
218
|
+
- Rate limiting on all endpoints
|
|
219
|
+
|
|
220
|
+
### 5. **Error Handling**
|
|
221
|
+
- Generic error messages to prevent information leakage
|
|
222
|
+
- Detailed logging for administrators
|
|
223
|
+
- Graceful degradation on service failures
|
|
224
|
+
- No sensitive data in error responses
|
|
225
|
+
|
|
226
|
+
## Monitoring and Alerting
|
|
227
|
+
|
|
228
|
+
### 1. **Security Events to Monitor**
|
|
229
|
+
- Multiple failed authentication attempts
|
|
230
|
+
- API key creation from new IP addresses
|
|
231
|
+
- Unusual usage patterns
|
|
232
|
+
- Session hijacking attempts
|
|
233
|
+
- OAuth callback failures
|
|
234
|
+
|
|
235
|
+
### 2. **Log Analysis**
|
|
236
|
+
```javascript
|
|
237
|
+
// Example log entries to monitor
|
|
238
|
+
{
|
|
239
|
+
"level": "warn",
|
|
240
|
+
"message": "Failed authentication attempt",
|
|
241
|
+
"ip": "192.168.1.100",
|
|
242
|
+
"userAgent": "Mozilla/5.0...",
|
|
243
|
+
"provider": "google",
|
|
244
|
+
"timestamp": "2025-09-14T10:30:00Z"
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Compliance Considerations
|
|
249
|
+
|
|
250
|
+
### 1. **GDPR Compliance**
|
|
251
|
+
- User consent for data processing
|
|
252
|
+
- Right to data portability
|
|
253
|
+
- Right to erasure (delete account)
|
|
254
|
+
- Data minimization (only collect necessary data)
|
|
255
|
+
- Privacy by design
|
|
256
|
+
|
|
257
|
+
### 2. **HIPAA Considerations** (if handling health data)
|
|
258
|
+
- Encryption at rest and in transit
|
|
259
|
+
- Access logging and audit trails
|
|
260
|
+
- User authentication and authorization
|
|
261
|
+
- Business Associate Agreements with OAuth providers
|
|
262
|
+
|
|
263
|
+
## Testing Security
|
|
264
|
+
|
|
265
|
+
### 1. **Automated Security Testing**
|
|
266
|
+
```javascript
|
|
267
|
+
// Example security tests
|
|
268
|
+
describe('OAuth Security', () => {
|
|
269
|
+
test('should reject requests without CSRF token', async () => {
|
|
270
|
+
const response = await request(app)
|
|
271
|
+
.post('/token/keys')
|
|
272
|
+
.send({ name: 'test' });
|
|
273
|
+
expect(response.status).toBe(403);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('should rate limit authentication attempts', async () => {
|
|
277
|
+
// Make 6 failed requests
|
|
278
|
+
for (let i = 0; i < 6; i++) {
|
|
279
|
+
await request(app).get('/token/auth/google');
|
|
280
|
+
}
|
|
281
|
+
const response = await request(app).get('/token/auth/google');
|
|
282
|
+
expect(response.status).toBe(429);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### 2. **Manual Security Testing**
|
|
288
|
+
- Penetration testing of OAuth flows
|
|
289
|
+
- Session management testing
|
|
290
|
+
- CSRF attack simulation
|
|
291
|
+
- XSS vulnerability testing
|
|
292
|
+
- SQL injection testing
|
|
293
|
+
|
|
294
|
+
This comprehensive security implementation ensures that your Passport.js OAuth integration follows security best practices and protects both user data and API access.
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
|
|
3
|
+
<html xml:lang="en" lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<title>FHIRsmith: [%title%]</title>
|
|
6
|
+
|
|
7
|
+
<meta charset="utf-8"/>
|
|
8
|
+
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
|
9
|
+
<meta content="http://hl7.org/fhir" name="author"/>
|
|
10
|
+
<meta charset="utf-8" http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
11
|
+
|
|
12
|
+
<link rel="stylesheet" href="/fhir.css"/>
|
|
13
|
+
|
|
14
|
+
<!-- Bootstrap core CSS -->
|
|
15
|
+
<link rel="stylesheet" href="/assets/css/bootstrap.css"/>
|
|
16
|
+
<link rel="stylesheet" href="/assets/css/bootstrap-fhir.css"/>
|
|
17
|
+
|
|
18
|
+
<!-- Project extras -->
|
|
19
|
+
<link rel="stylesheet" href="/assets/css/project.css"/>
|
|
20
|
+
<link rel="stylesheet" href="/assets/css/pygments-manni.css"/>
|
|
21
|
+
|
|
22
|
+
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
|
23
|
+
<!-- [if lt IE 9]>
|
|
24
|
+
<script src="/assets/js/html5shiv.js"></script>
|
|
25
|
+
<script src="/assets/js/respond.min.js"></script>
|
|
26
|
+
<![endif] -->
|
|
27
|
+
|
|
28
|
+
<!-- Favicons -->
|
|
29
|
+
<link sizes="144x144" rel="apple-touch-icon-precomposed" href="/assets/ico/apple-touch-icon-144-precomposed.png"/>
|
|
30
|
+
<link sizes="114x114" rel="apple-touch-icon-precomposed" href="/assets/ico/apple-touch-icon-114-precomposed.png"/>
|
|
31
|
+
<link sizes="72x72" rel="apple-touch-icon-precomposed" href="/assets/ico/apple-touch-icon-72-precomposed.png"/>
|
|
32
|
+
<link rel="apple-touch-icon-precomposed" href="/assets/ico/apple-touch-icon-57-precomposed.png"/>
|
|
33
|
+
<link rel="shortcut icon" href="/assets/ico/favicon.png"/>
|
|
34
|
+
<script type="text/javascript" src="/assets/js/json2.js"></script>
|
|
35
|
+
<script type="text/javascript" src="/assets/js/statuspage.js"></script>
|
|
36
|
+
<script type="text/javascript" src="/assets/js/jquery.min.js"></script>
|
|
37
|
+
<script type="text/javascript" src="/assets/js/jquery-ui.min.js"></script>
|
|
38
|
+
<link rel="stylesheet" href="/assets/css/jquery.ui.all.css">
|
|
39
|
+
<script type="text/javascript" src="/assets/js/jquery.ui.core.js"></script>
|
|
40
|
+
<script type="text/javascript" src="/assets/js/jquery.ui.widget.js"></script>
|
|
41
|
+
<script type="text/javascript" src="/assets/js/jquery.ui.mouse.js"></script>
|
|
42
|
+
<script type="text/javascript" src="/assets/js/jquery.ui.resizable.js"></script>
|
|
43
|
+
<script type="text/javascript" src="/assets/js/jquery.ui.draggable.js"></script>
|
|
44
|
+
<script type="text/javascript" src="/assets/js/jtip.js"></script>
|
|
45
|
+
<script type="text/javascript" src="/assets/js/jcookie.js"></script>
|
|
46
|
+
<script type="text/javascript" src="/assets/js/fhir-gw.js"></script>
|
|
47
|
+
|
|
48
|
+
<style>
|
|
49
|
+
.api-key-display {
|
|
50
|
+
background: #f8f9fa;
|
|
51
|
+
border: 1px solid #dee2e6;
|
|
52
|
+
border-radius: 5px;
|
|
53
|
+
padding: 15px;
|
|
54
|
+
font-family: monospace;
|
|
55
|
+
word-break: break-all;
|
|
56
|
+
margin-bottom: 15px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.usage-stats {
|
|
60
|
+
background: #e9ecef;
|
|
61
|
+
padding: 10px;
|
|
62
|
+
border-radius: 5px;
|
|
63
|
+
margin-bottom: 10px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.oauth-provider {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
padding: 10px;
|
|
70
|
+
border: 1px solid #dee2e6;
|
|
71
|
+
border-radius: 5px;
|
|
72
|
+
margin-bottom: 10px;
|
|
73
|
+
text-decoration: none;
|
|
74
|
+
color: inherit;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.oauth-provider:hover {
|
|
78
|
+
background-color: #f8f9fa;
|
|
79
|
+
text-decoration: none;
|
|
80
|
+
color: inherit;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.oauth-icon {
|
|
84
|
+
width: 24px;
|
|
85
|
+
height: 24px;
|
|
86
|
+
margin-right: 10px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.status-active { color: #198754; }
|
|
90
|
+
.status-inactive { color: #6c757d; }
|
|
91
|
+
|
|
92
|
+
.table-actions {
|
|
93
|
+
display: flex;
|
|
94
|
+
gap: 5px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.key-warning {
|
|
98
|
+
background: #fff3cd;
|
|
99
|
+
border: 1px solid #ffeaa7;
|
|
100
|
+
border-radius: 5px;
|
|
101
|
+
padding: 15px;
|
|
102
|
+
margin-bottom: 20px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.api-docs {
|
|
106
|
+
background: #f8f9fa;
|
|
107
|
+
border: 1px solid #dee2e6;
|
|
108
|
+
border-radius: 5px;
|
|
109
|
+
padding: 20px;
|
|
110
|
+
margin-top: 20px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.api-endpoint {
|
|
114
|
+
background: #e9ecef;
|
|
115
|
+
padding: 8px 12px;
|
|
116
|
+
border-radius: 3px;
|
|
117
|
+
font-family: monospace;
|
|
118
|
+
font-size: 0.9em;
|
|
119
|
+
margin: 5px 0;
|
|
120
|
+
}
|
|
121
|
+
</style>
|
|
122
|
+
|
|
123
|
+
</head>
|
|
124
|
+
|
|
125
|
+
<body>
|
|
126
|
+
|
|
127
|
+
<div id="segment-navbar" class="segment"> <!-- segment-breadcrumb -->
|
|
128
|
+
<div id="stripe"> </div>
|
|
129
|
+
<div class="container"> <!-- container -->
|
|
130
|
+
<div style="background-color: #ad1f2f; padding: 6px; color: white;"> <!-- container -->
|
|
131
|
+
<a href="http://www.hl7.org/fhir" style="color: gold" title="Fast Healthcare Interoperability Resources - Home Page"><img border="0" src="/icon-fhir-16.png" style="vertical-align: text-bottom"/> <b>FHIR</b></a> © HL7.org |
|
|
132
|
+
<a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] |
|
|
133
|
+
<a href="/" style="color: gold"> Server Home</a> |
|
|
134
|
+
<a href="/token" style="color: gold">Token Home</a>
|
|
135
|
+
</div> <!-- /container -->
|
|
136
|
+
</div> <!-- /container -->
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<!-- /segment-breadcrumb -->
|
|
140
|
+
|
|
141
|
+
<div id="segment-content" class="segment"> <!-- segment-content -->
|
|
142
|
+
<div class="container"> <!-- container -->
|
|
143
|
+
<div class="row">
|
|
144
|
+
<div class="inner-wrapper">
|
|
145
|
+
<div class="col-9">
|
|
146
|
+
|
|
147
|
+
<h2><img border="0" src="../FHIRsmith32.png" style="vertical-align: text-bottom"/> Token Server: [%title%] </h2>
|
|
148
|
+
|
|
149
|
+
[%content%]
|
|
150
|
+
|
|
151
|
+
<div class="api-docs">
|
|
152
|
+
<h4>API Documentation</h4>
|
|
153
|
+
<p>Use these endpoints to validate and track API key usage:</p>
|
|
154
|
+
|
|
155
|
+
<div class="mb-3">
|
|
156
|
+
<strong>Validate API Key:</strong>
|
|
157
|
+
<div class="api-endpoint">GET /token/api/validate/{api_key}</div>
|
|
158
|
+
<p>Returns user information and allowed request count.</p>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div class="mb-3">
|
|
162
|
+
<strong>Record Usage:</strong>
|
|
163
|
+
<div class="api-endpoint">POST /token/api/usage/{api_key}</div>
|
|
164
|
+
<p>Records API usage. Send <code>{"count": 1}</code> in request body.</p>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<div class="mb-3">
|
|
168
|
+
<strong>Get Usage Statistics:</strong>
|
|
169
|
+
<div class="api-endpoint">GET /token/api/stats/{api_key}?days=30</div>
|
|
170
|
+
<p>Returns usage statistics for the specified number of days.</p>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
</div> <!-- /inner-wrapper -->
|
|
177
|
+
</div> <!-- /row -->
|
|
178
|
+
</div> <!-- /container -->
|
|
179
|
+
</div> <!-- /segment-content -->
|
|
180
|
+
|
|
181
|
+
<div id="segment-footer" class="segment"> <!-- segment-footer -->
|
|
182
|
+
<div class="container"> <!-- container -->
|
|
183
|
+
<div class="inner-wrapper">
|
|
184
|
+
<p>
|
|
185
|
+
<a href="http://www.hl7.org/fhir" style="color: gold" title="Fast Healthcare Interoperability Resources - Home Page"><img border="0" src="/icon-fhir-16.png" style="vertical-align: text-bottom"/> <b>FHIR</b></a> © HL7.org 2011+. |
|
|
186
|
+
<a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] © HealthIntersections.com.au 2023+ |
|
|
187
|
+
([%ms%] ms)
|
|
188
|
+
|
|
189
|
+
</p>
|
|
190
|
+
</div> <!-- /inner-wrapper -->
|
|
191
|
+
</div> <!-- /container -->
|
|
192
|
+
</div> <!-- /segment-footer -->
|
|
193
|
+
|
|
194
|
+
<div id="segment-post-footer" class="segment hidden"> <!-- segment-post-footer -->
|
|
195
|
+
<div class="container"> <!-- container -->
|
|
196
|
+
</div> <!-- /container -->
|
|
197
|
+
</div> <!-- /segment-post-footer -->
|
|
198
|
+
|
|
199
|
+
<!-- JS and analytics only. -->
|
|
200
|
+
<!-- Bootstrap core JavaScript
|
|
201
|
+
================================================== -->
|
|
202
|
+
<!-- Placed at the end of the document so the pages load faster -->
|
|
203
|
+
<script src="/assets/js/bootstrap.min.js"></script>
|
|
204
|
+
<script src="/assets/js/respond.min.js"></script>
|
|
205
|
+
<script src="/assets/js/fhir.js"></script>
|
|
206
|
+
|
|
207
|
+
<script>
|
|
208
|
+
// Handle API key creation
|
|
209
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
210
|
+
const createForm = document.getElementById('create-key-form');
|
|
211
|
+
if (createForm) {
|
|
212
|
+
createForm.addEventListener('submit', async function(e) {
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
const formData = new FormData(e.target);
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const response = await fetch('/token/keys', {
|
|
218
|
+
method: 'POST',
|
|
219
|
+
headers: { 'Content-Type': 'application/json' },
|
|
220
|
+
body: JSON.stringify({ name: formData.get('name') })
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const result = await response.json();
|
|
224
|
+
|
|
225
|
+
if (response.ok) {
|
|
226
|
+
// Show the API key in a modal or alert
|
|
227
|
+
showApiKeyModal(result.apiKey);
|
|
228
|
+
|
|
229
|
+
// Reset form and optionally reload
|
|
230
|
+
e.target.reset();
|
|
231
|
+
setTimeout(() => location.reload(), 2000);
|
|
232
|
+
} else {
|
|
233
|
+
alert('Error: ' + result.error);
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
alert('Error creating key: ' + error.message);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Show API key in a modal (you might want to use Bootstrap modal instead)
|
|
243
|
+
function showApiKeyModal(apiKey) {
|
|
244
|
+
const modal = document.createElement('div');
|
|
245
|
+
modal.innerHTML = `
|
|
246
|
+
<div style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; display: flex; justify-content: center; align-items: center;">
|
|
247
|
+
<div style="background: white; padding: 30px; border-radius: 10px; max-width: 600px; width: 90%;">
|
|
248
|
+
<h4>API Key Created Successfully</h4>
|
|
249
|
+
<div class="key-warning">
|
|
250
|
+
<strong>Important:</strong> Please save this API key now. You won't be able to see it again!
|
|
251
|
+
</div>
|
|
252
|
+
<div class="api-key-display">
|
|
253
|
+
<strong>Your API Key:</strong><br>
|
|
254
|
+
${apiKey}
|
|
255
|
+
</div>
|
|
256
|
+
<button onclick="copyToClipboard('${apiKey}')" class="btn btn-secondary me-2">Copy Key</button>
|
|
257
|
+
<button onclick="closeModal()" class="btn btn-primary">Close</button>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
`;
|
|
261
|
+
|
|
262
|
+
document.body.appendChild(modal);
|
|
263
|
+
|
|
264
|
+
// Auto-close after 30 seconds
|
|
265
|
+
setTimeout(() => {
|
|
266
|
+
if (document.body.contains(modal)) {
|
|
267
|
+
document.body.removeChild(modal);
|
|
268
|
+
}
|
|
269
|
+
}, 30000);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function closeModal() {
|
|
273
|
+
const modal = document.querySelector('[style*="position: fixed"]');
|
|
274
|
+
if (modal) {
|
|
275
|
+
document.body.removeChild(modal);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function copyToClipboard(text) {
|
|
280
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
281
|
+
alert('API key copied to clipboard!');
|
|
282
|
+
}).catch(function(err) {
|
|
283
|
+
console.error('Could not copy text: ', err);
|
|
284
|
+
// Fallback for older browsers
|
|
285
|
+
const textArea = document.createElement('textarea');
|
|
286
|
+
textArea.value = text;
|
|
287
|
+
document.body.appendChild(textArea);
|
|
288
|
+
textArea.select();
|
|
289
|
+
document.execCommand('copy');
|
|
290
|
+
document.body.removeChild(textArea);
|
|
291
|
+
alert('API key copied to clipboard!');
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Handle key deletion
|
|
296
|
+
async function deleteKey(keyId) {
|
|
297
|
+
if (!confirm('Are you sure you want to delete this API key? This action cannot be undone.')) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
const response = await fetch('/token/keys/' + keyId, {
|
|
303
|
+
method: 'DELETE'
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (response.ok) {
|
|
307
|
+
location.reload();
|
|
308
|
+
} else {
|
|
309
|
+
const result = await response.json();
|
|
310
|
+
alert('Error: ' + result.error);
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
alert('Error deleting key: ' + error.message);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Form validation
|
|
318
|
+
function validateKeyForm(form) {
|
|
319
|
+
const nameInput = form.querySelector('input[name="name"]');
|
|
320
|
+
if (!nameInput.value.trim()) {
|
|
321
|
+
alert('Please enter a name for your API key');
|
|
322
|
+
nameInput.focus();
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
</script>
|
|
328
|
+
|
|
329
|
+
</body>
|
|
330
|
+
</html>
|