Skip to content

Commit e2f3d08

Browse files
authored
is787/invitation-registration (#790)
Implements #787 and #791 Allows registration only by invitation. Change in the server side: - option configurable at app.login - invitations are generated with confirmation-codes (utils to generate codes in login.registration) - confirmation codes have one use and have a lifetime of 5 days (currently hard-coded) - rest API: - updated schema request body to include invitation code - added entrypoint to retrieve front-end config Changes in the front-end side: - retrieves runtime configuration - enables/disables registration link - captures invitation token from fragments - refactor fragment parsing (see notes in #791)
1 parent 72a13c0 commit e2f3d08

32 files changed

+437
-157
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
ConfigEnveloped:
2+
type: object
3+
required:
4+
- data
5+
properties:
6+
data:
7+
$ref: '#/ConfigSchema'
8+
error:
9+
nullable: true
10+
default: null
11+
12+
ConfigSchema:
13+
type: object
14+
properties:
15+
invitation_required:
16+
type: bool
17+
example:
18+
invitation_required: true

api/specs/webserver/v0/components/schemas/registration.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,16 @@ RegistrationType:
2626
confirm:
2727
type: string
2828
#format: password
29+
invitation:
30+
type: string
31+
description: "Invitation code"
32+
required:
33+
- email
34+
- password
35+
36+
2937
example:
3038
3139
password: 'my secret'
32-
confim: 'my secret'
40+
confirm: 'my secret'
41+
invitation: 33c451d4-17b7-4e65-9880-694559b8ffc2

api/specs/webserver/v0/openapi-diagnostics.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,16 @@ paths:
5454
application/json:
5555
schema:
5656
$ref: './components/schemas/fake.yaml#/FakeEnveloped'
57+
/config:
58+
get:
59+
summary: Front end runtime configuration
60+
operationId: get_config
61+
responses:
62+
'200':
63+
description: configuration details
64+
content:
65+
application/json:
66+
schema:
67+
$ref: './components/schemas/config.yaml#/ConfigEnveloped'
68+
default:
69+
$ref: './openapi.yaml#/components/responses/DefaultErrorResponse'

api/specs/webserver/v0/openapi.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ paths:
3232
/check/{action}:
3333
$ref: './openapi-diagnostics.yaml#/paths/~1check~1{action}'
3434

35+
/config:
36+
$ref: './openapi-diagnostics.yaml#/paths/~1config'
37+
3538
# AUTHENTICATION & AUTHORIZATION --------------------------------------
3639

3740
/auth/register:

services/web/client/source/class/qxapp/Application.js

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,25 @@ qx.Class.define("qxapp.Application", {
7171
},
7272

7373
__initRouting: function() {
74-
// Route: /#/study/{id}
7574
// TODO: PC -> IP consider regex for uuid, i.e. /[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}/ ???
76-
let result = /#\/study\/([0-9a-zA-Z\-]+)/.exec(window.location.hash);
77-
if (result) {
78-
qxapp.utils.Utils.cookie.deleteCookie("user");
79-
qxapp.auth.Manager.getInstance().validateToken(() => this.__loadMainPage(result[1]), this.__loadLoginPage, this);
75+
const urlFragment = qxapp.utils.Utils.parseURLFragment();
76+
if (urlFragment.nav && urlFragment.nav.length) {
77+
if (urlFragment.nav[0] === "study" && urlFragment.nav.length > 1) {
78+
// Route: /#/study/{id}
79+
qxapp.utils.Utils.cookie.deleteCookie("user");
80+
qxapp.auth.Manager.getInstance().validateToken(() => this.__loadMainPage(urlFragment.nav[1]), this.__loadLoginPage, this);
81+
} else if (urlFragment.nav[0] === "registration" && urlFragment.params && urlFragment.params.invitation) {
82+
// Route: /#/registration/?invitation={token}
83+
qxapp.utils.Utils.cookie.deleteCookie("user");
84+
this.__restart();
85+
} else if (urlFragment.nav[0] === "reset-password" && urlFragment.params && urlFragment.params.code) {
86+
// Route: /#/reset-password/?code={resetCode}
87+
qxapp.utils.Utils.cookie.deleteCookie("user");
88+
this.__restart();
89+
} else {
90+
// this.load404();
91+
console.error("URL fragment format not recognized.");
92+
}
8093
} else {
8194
this.__restart();
8295
}
@@ -131,6 +144,8 @@ qx.Class.define("qxapp.Application", {
131144
}
132145
doc.add(view, options);
133146
this.__current = view;
147+
// Clear URL
148+
window.history.replaceState(null, "", "/");
134149
},
135150

136151
/**

services/web/client/source/class/qxapp/auth/LoginPage.js

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,15 @@ qx.Class.define("qxapp.auth.LoginPage", {
6565
pages.setSelection([reset]);
6666
}
6767

68+
const urlFragment = qxapp.utils.Utils.parseURLFragment();
69+
if (urlFragment.nav && urlFragment.nav.length) {
70+
if (urlFragment.nav[0] === "registration") {
71+
pages.setSelection([register]);
72+
} else if (urlFragment.nav[0] === "reset-password") {
73+
pages.setSelection([reset]);
74+
}
75+
}
76+
6877
// Transitions between pages
6978
login.addListener("done", function(msg) {
7079
login.resetValues();
@@ -103,19 +112,18 @@ qx.Class.define("qxapp.auth.LoginPage", {
103112

104113
members: {
105114
__addVersionLink: function() {
106-
const versionLinkLayout = new qx.ui.container.Composite(new qx.ui.layout.HBox(10));
107-
108-
versionLinkLayout.add(new qx.ui.core.Spacer(), {
109-
flex: 1
115+
const versionLinkLayout = new qx.ui.container.Composite(new qx.ui.layout.HBox(10).set({
116+
alignX: "center"
117+
})).set({
118+
margin: [10, 0]
110119
});
111120

112121
const platformVersion = qxapp.utils.LibVersions.getPlatformVersion();
113122
if (platformVersion) {
114123
const text = platformVersion.name + " v" + platformVersion.version;
115124
const versionLink = new qxapp.ui.basic.LinkLabel(text, platformVersion.url).set({
116-
alignX: "right",
117-
allowGrowX: true,
118-
font: "text-12"
125+
font: "text-12",
126+
textColor: "text-darker"
119127
});
120128
versionLinkLayout.add(versionLink);
121129

@@ -124,16 +132,11 @@ qx.Class.define("qxapp.auth.LoginPage", {
124132
}
125133

126134
const organizationLink = new qxapp.ui.basic.LinkLabel("© 2019 IT'IS Foundation", "https://itis.swiss").set({
127-
alignX: "left",
128-
allowGrowX: true,
129-
font: "text-12"
135+
font: "text-12",
136+
textColor: "text-darker"
130137
});
131138
versionLinkLayout.add(organizationLink);
132139

133-
versionLinkLayout.add(new qx.ui.core.Spacer(), {
134-
flex: 1
135-
});
136-
137140
this._add(versionLinkLayout, {
138141
row: 1,
139142
column: 0

services/web/client/source/class/qxapp/auth/core/Utils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ qx.Class.define("qxapp.auth.core.Utils", {
3939
*
4040
* Expected fragment format as https://osparc.io#page=reset-password;code=123546
4141
* where fragment is #page=reset-password;code=123546
42-
*/
42+
*/
4343
findParameterInFragment: function(parameterName) {
4444
let result = null;
4545
const params = window.location.hash.substr(1).split(";");

services/web/client/source/class/qxapp/auth/ui/LoginView.js

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ qx.Class.define("qxapp.auth.ui.LoginView", {
5353
_buildPage: function() {
5454
this.__form = new qx.ui.form.Form();
5555

56-
let atm = new qx.ui.basic.Atom().set({
56+
const atm = new qx.ui.basic.Atom().set({
5757
icon: "qxapp/osparc-white.svg",
5858
iconPosition: "top"
5959
});
@@ -64,7 +64,7 @@ qx.Class.define("qxapp.auth.ui.LoginView", {
6464
});
6565
this.add(atm);
6666

67-
let email = new qx.ui.form.TextField().set({
67+
const email = new qx.ui.form.TextField().set({
6868
placeholder: this.tr("Your email address"),
6969
required: true
7070
});
@@ -75,15 +75,15 @@ qx.Class.define("qxapp.auth.ui.LoginView", {
7575
email.focus();
7676
email.activate();
7777
});
78-
let pass = new qx.ui.form.PasswordField().set({
78+
const pass = new qx.ui.form.PasswordField().set({
7979
placeholder: this.tr("Your password"),
8080
required: true
8181
});
8282
pass.getContentElement().setAttribute("autocomplete", "current-password");
8383
this.add(pass);
8484
this.__form.add(pass, "", null, "password", null);
8585

86-
let loginBtn = new qx.ui.form.Button(this.tr("Log In"));
86+
const loginBtn = new qx.ui.form.Button(this.tr("Log In"));
8787
loginBtn.addListener("execute", function() {
8888
this.__login();
8989
}, this);
@@ -97,13 +97,31 @@ qx.Class.define("qxapp.auth.ui.LoginView", {
9797

9898

9999
// create account | forgot password? links
100-
let grp = new qx.ui.container.Composite(new qx.ui.layout.HBox());
101-
102-
let registerBtn = this.createLinkButton(this.tr("Create Account"), function() {
103-
this.fireEvent("toRegister");
100+
const grp = new qx.ui.container.Composite(new qx.ui.layout.HBox());
101+
102+
const registerBtn = this.createLinkButton(this.tr("Create Account"), () => {
103+
const interval = 1000;
104+
const configTimer = new qx.event.Timer(interval);
105+
const resource = qxapp.io.rest.ResourceFactory.getInstance();
106+
let registerWithInvitation = resource.registerWithInvitation();
107+
configTimer.addListener("interval", () => {
108+
registerWithInvitation = resource.registerWithInvitation();
109+
if (registerWithInvitation !== null) {
110+
configTimer.stop();
111+
if (registerWithInvitation) {
112+
let text = this.tr("Registration is currently only available with an invitation.");
113+
text += "<br>";
114+
text += this.tr("Please contact [email protected]");
115+
qxapp.component.widget.FlashMessenger.getInstance().logAs(text, "INFO");
116+
} else {
117+
this.fireEvent("toRegister");
118+
}
119+
}
120+
}, this);
121+
configTimer.start();
104122
}, this);
105123

106-
let forgotBtn = this.createLinkButton(this.tr("Forgot Password?"), function() {
124+
const forgotBtn = this.createLinkButton(this.tr("Forgot Password?"), () => {
107125
this.fireEvent("toReset");
108126
}, this);
109127

@@ -122,10 +140,10 @@ qx.Class.define("qxapp.auth.ui.LoginView", {
122140
},
123141

124142
__buildExternals: function() {
125-
let grp = new qx.ui.container.Composite(new qx.ui.layout.HBox());
143+
const grp = new qx.ui.container.Composite(new qx.ui.layout.HBox());
126144

127145
[this.tr("Login with NIH"), this.tr("Login with OpenID")].forEach(txt => {
128-
let btn = this.createLinkButton(txt, function() {
146+
const btn = this.createLinkButton(txt, function() {
129147
// TODO add here callback
130148
console.error("Login with external services are still not implemented");
131149
}, this);
@@ -148,17 +166,17 @@ qx.Class.define("qxapp.auth.ui.LoginView", {
148166
const email = this.__form.getItems().email;
149167
const pass = this.__form.getItems().password;
150168

151-
let manager = qxapp.auth.Manager.getInstance();
169+
const manager = qxapp.auth.Manager.getInstance();
152170

153-
let successFun = function(log) {
171+
const successFun = function(log) {
154172
this.fireDataEvent("done", log.message);
155173
// we don't need the form any more, so remove it and mock-navigate-away
156174
// and thus tell the password manager to save the content
157175
this._formElement.dispose();
158176
window.history.replaceState(null, window.document.title, window.location.pathname);
159177
};
160178

161-
let failFun = function(msg) {
179+
const failFun = function(msg) {
162180
// TODO: can get field info from response here
163181
msg = String(msg) || this.tr("Introduced an invalid email or password");
164182
[email, pass].forEach(item => {
@@ -175,8 +193,8 @@ qx.Class.define("qxapp.auth.ui.LoginView", {
175193
},
176194

177195
resetValues: function() {
178-
let fieldItems = this.__form.getItems();
179-
for (var key in fieldItems) {
196+
const fieldItems = this.__form.getItems();
197+
for (const key in fieldItems) {
180198
fieldItems[key].resetValue();
181199
}
182200
}

services/web/client/source/class/qxapp/auth/ui/RegistrationView.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ qx.Class.define("qxapp.auth.ui.RegistrationView", {
6565
});
6666
this.add(pass2);
6767

68+
const urlFragment = qxapp.utils.Utils.parseURLFragment();
69+
const token = urlFragment.params ? urlFragment.params.invitation || null : null;
70+
const invitation = new qx.ui.form.TextField().set({
71+
visibility: "excluded",
72+
value: token
73+
});
74+
this.add(invitation);
75+
6876
// interaction
6977
pass1.addListener("changeValue", e => {
7078
qxapp.auth.Manager.getInstance().evalPasswordStrength(e.getData(), (strength, rating, improvement) => {
@@ -123,7 +131,8 @@ qx.Class.define("qxapp.auth.ui.RegistrationView", {
123131
this.__submit({
124132
email: email.getValue(),
125133
password: pass1.getValue(),
126-
confirm: pass2.getValue()
134+
confirm: pass2.getValue(),
135+
invitation: invitation.getValue()
127136
});
128137
}
129138
}, this);

services/web/client/source/class/qxapp/auth/ui/ResetPassView.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ qx.Class.define("qxapp.auth.ui.ResetPassView", {
4848
});
4949
this.add(confirm);
5050

51+
const urlFragment = qxapp.utils.Utils.parseURLFragment();
52+
const resetCode = urlFragment.params ? urlFragment.params.code || null : null;
53+
const code = new qx.ui.form.TextField().set({
54+
visibility: "excluded",
55+
value: resetCode
56+
});
57+
this.add(code);
58+
5159
validator.setValidator(function(_itemForms) {
5260
return qxapp.auth.core.Utils.checkSamePasswords(password, confirm);
5361
});
@@ -69,10 +77,7 @@ qx.Class.define("qxapp.auth.ui.ResetPassView", {
6977
submitBtn.addListener("execute", function(e) {
7078
const valid = validator.validate();
7179
if (valid) {
72-
const code = qxapp.auth.core.Utils.findParameterInFragment("code");
73-
qxapp.auth.core.Utils.removeParameterInFragment("page");
74-
qxapp.auth.core.Utils.removeParameterInFragment("code");
75-
this.__submit(password.getValue(), confirm.getValue(), code);
80+
this.__submit(password.getValue(), confirm.getValue(), code.getValue());
7681
}
7782
}, this);
7883

@@ -88,7 +93,6 @@ qx.Class.define("qxapp.auth.ui.ResetPassView", {
8893

8994
let successFun = function(log) {
9095
this.fireDataEvent("done", log.message);
91-
// TODO: See #465: clean all query from url: e.g. /?page=reset-password&code=qwewqefgfg
9296
qxapp.component.widget.FlashMessenger.getInstance().log(log);
9397
};
9498

0 commit comments

Comments
 (0)