More work on post registration form

This commit is contained in:
Melody Becker 2024-10-28 16:33:17 +01:00
parent 952949b609
commit 8b03454d6f
27 changed files with 247 additions and 51 deletions

View file

@ -45,7 +45,9 @@ Make use of the many generators for code, try `ember help generate` for more det
### Deploying ### Deploying
Specify what it takes to deploy your app. 1. Build it (see point above)
2. Build the Go server
3. Now you have a binary with everything needed embedded into it ready to use
## Further Reading / Useful Links ## Further Reading / Useful Links

View file

@ -0,0 +1,18 @@
<div class="auth-wrapper">
<div class="auth-username-wrapper">
<label>
Username
<Input
@type="text"
@value={{this.username}}
placeholder="Username"
/>
</label>
</div>
<div type="button" class="auth-button-login" {{on "click" this.startLogin}}>
Login
</div>
<div type="button" class="auth-button-register" {{on "click" this.startRegistration}}>
Register
</div>
</div>

View file

@ -9,7 +9,7 @@ import {
} from '@simplewebauthn/browser'; } from '@simplewebauthn/browser';
import type AuthService from 'frontend-reactive/services/auth'; import type AuthService from 'frontend-reactive/services/auth';
export interface PasskeySignature { export interface AuthSignature {
// The arguments accepted by the component // The arguments accepted by the component
Args: {}; Args: {};
// Any blocks yielded by the component // Any blocks yielded by the component
@ -20,14 +20,15 @@ export interface PasskeySignature {
Element: null; Element: null;
} }
export default class Auth extends Component<PasskeySignature> { export default class Auth extends Component<AuthSignature> {
@tracked username: string = ''; @tracked username: string = '';
@tracked error: string | undefined; @tracked error: string | undefined;
@tracked isLogin = true; //@tracked isLogin = true;
@service declare auth: AuthService; @service declare auth: AuthService;
@action async startLogin() { @action async startLogin() {
try { try {
// TODO: Check if account exists and is alowed to login
this.auth.startLogin(this.username); this.auth.startLogin(this.username);
} catch (error: any) { } catch (error: any) {
this.error = 'Error: ' + error.message; this.error = 'Error: ' + error.message;
@ -36,7 +37,12 @@ export default class Auth extends Component<PasskeySignature> {
@action async startRegistration() { @action async startRegistration() {
try { try {
this.auth.startRegistration(this.username); // TODO: Check if handle is already taken
await this.auth.startRegistration(this.username);
// After registration, log in immediately to obtain a valid session token
// for the "post" registration data, such as email
await this.auth.startLogin(this.username);
// And after login,
} catch (error: any) { } catch (error: any) {
this.error = 'Error: ' + error.message; this.error = 'Error: ' + error.message;
} }

View file

@ -3,7 +3,7 @@
<Input <Input
@type="text" @type="text"
@value={{this.username}} @value={{this.username}}
@placeholder="Username" placeholder="Username"
/> />
</label> </label>
<div type="button" class="login-start-button" {{on "click" this.onLoginStart}}> <div type="button" class="login-start-button" {{on "click" this.onLoginStart}}>

View file

@ -1,23 +1,23 @@
import { action } from '@ember/object' import { action } from '@ember/object';
import Component from '@glimmer/component' import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking' import { tracked } from '@glimmer/tracking';
export interface AuthLoginSignature { export interface AuthLoginSignature {
// The arguments accepted by the component // The arguments accepted by the component
Args: {} Args: {};
// Any blocks yielded by the component // Any blocks yielded by the component
Blocks: { Blocks: {
default: [] default: [];
} };
// The element to which `...attributes` is applied in the component template // The element to which `...attributes` is applied in the component template
Element: null Element: null;
} }
export default class AuthLogin extends Component<AuthLoginSignature> { export default class AuthLogin extends Component<AuthLoginSignature> {
@tracked username = '' @tracked username = '';
@action onLoginStart() { @action onLoginStart() {
console.log('Starting login for username ' + this.username) console.log('Starting login for username ' + this.username);
// Check if username is approved for login // Check if username is approved for login
// If it is, continue with login // If it is, continue with login
} }

View file

@ -10,6 +10,7 @@
/> />
</label> </label>
</div> </div>
<Util::MailEntry @data={{this.mail}}/>
<div class="registration-form-description-wrapper"> <div class="registration-form-description-wrapper">
<label> <label>
Description Description
@ -20,24 +21,20 @@
/> />
</label> </label>
</div> </div>
{{!--<div class="registration-form-mail-wrapper">--}}
{{!-- <label>--}}
{{!-- Email--}}
{{!-- <Input @type="text" @value={{this.email}} placeholder="Email address" />--}}
{{!-- </label>--}}
{{!--</div>--}}
<div class="registration-form-gender-wrapper"> <div class="registration-form-gender-wrapper">
<p class="registration-form-gender-info">Add your preferred pronouns</p>
<Util::StringArray <Util::StringArray
@list={{this.gender}} @list={{this.gender}}
@onNewElement={{this.genderAddedHandler}} @onNewElement={{this.genderAddedHandler}}
@onDeleteElement={{this.genderRemovedHandler}} @onDeleteElement={{this.genderRemovedHandler}}
/> />
</div> </div>
<p>{{this.extracted}}</p>
<div class="register-form-being-wrapper"> <div class="register-form-being-wrapper">
<p class="registration-form-being-info">Select the type of being you are. Multiselect is possible</p>
<Util::Multiselect @elements={{this.beingTypes}} /> <Util::Multiselect @elements={{this.beingTypes}} />
</div> </div>
<div class="register-form-default-post-mode-wrapper"> <div class="register-form-default-post-mode-wrapper">
<p class="registration-form-default-post-mode-info">Select the default mode for your posts</p>
<Util::OneOfArray <Util::OneOfArray
@elements={{array "Public" "Local" "Followers" "Direct"}} @elements={{array "Public" "Local" "Followers" "Direct"}}
@selected={{this.defaultpostmode}} @selected={{this.defaultpostmode}}

View file

@ -1,27 +1,29 @@
import Component from '@glimmer/component'; import { action } from '@ember/object'
import { tracked } from '@glimmer/tracking'; import Component from '@glimmer/component'
import { tracked } from '@glimmer/tracking'
import isValidMail from 'frontend-reactive/helpers/is-valid-mail'
export interface AuthPostRegistrationFormSignature { export interface AuthPostRegistrationFormSignature {
// The arguments accepted by the component // The arguments accepted by the component
Args: { Args: {
username: string; username: string
}; }
// Any blocks yielded by the component // Any blocks yielded by the component
Blocks: { Blocks: {
default: []; default: []
}; }
// The element to which `...attributes` is applied in the component template // The element to which `...attributes` is applied in the component template
Element: null; Element: null
} }
export default class AuthPostRegistrationForm extends Component<AuthPostRegistrationFormSignature> { export default class AuthPostRegistrationForm extends Component<AuthPostRegistrationFormSignature> {
@tracked displayname: string = this.args.username; @tracked displayname: string = this.args.username
@tracked description: string = ''; @tracked description: string = ''
@tracked gender: Array<{ value: string }> = []; @tracked gender: Array<{ value: string }> = []
@tracked beingTypes: Array<{ @tracked beingTypes: Array<{
name: string; name: string
checked: boolean; checked: boolean
description: string; description: string
}> = [ }> = [
{ {
name: 'Human', name: 'Human',
@ -53,17 +55,22 @@ export default class AuthPostRegistrationForm extends Component<AuthPostRegistra
description: 'Doll', description: 'Doll',
checked: false, checked: false,
}, },
]; ]
@tracked defaultpostmode: string = 'public'; @tracked defaultpostmode: string = 'Public'
@tracked followapproval: boolean = false; @tracked followapproval: boolean = false
// Actual custom properties stored in here // Actual custom properties stored in here
@tracked customProperties: Array<{ key: string; value: string }> = []; @tracked customProperties: Array<{ key: string; value: string }> = []
@tracked indexable: boolean = true; @tracked indexable: boolean = true
@tracked mail = { mail: '', valid: false }
genderAddedHandler(newIndex: number) { genderAddedHandler(newIndex: number) {
console.log('gender added'); console.log('gender added')
} }
genderRemovedHandler(removedIndex: number) { genderRemovedHandler(removedIndex: number) {
console.log('gender removed'); console.log('gender removed')
}
@action test() {
console.log(this.mail)
} }
} }

View file

@ -0,0 +1,11 @@
<div class="mail-entry {{@wrapper-classes}}">
<label>
Email
<Input @type="text" @value={{this.args.data.mail}} placeholder="Email address" {{on "change" (fn this.checkMail)}}/>
</label>
{{#if this.mailOk}}
<p class="mail-ok">O</p>
{{else}}
<p class="mail-error">X</p>
{{/if}}
</div>

View file

@ -0,0 +1,27 @@
import { action } from '@ember/object'
import { map } from '@ember/object/computed'
import Component from '@glimmer/component'
import { tracked } from '@glimmer/tracking'
const re = /.+@\S+\.\S+/
export interface UtilMailEntrySignature {
// The arguments accepted by the component
Args: {
data: { mail: string; valid: boolean }
}
// Any blocks yielded by the component
Blocks: {
default: []
}
// The element to which `...attributes` is applied in the component template
Element: null
}
export default class UtilMailEntry extends Component<UtilMailEntrySignature> {
@tracked mailOk = this.args.data.valid
@action checkMail() {
this.args.data.valid = re.test(this.args.data.mail)
this.mailOk = this.args.data.valid
}
}

View file

@ -1,5 +1,5 @@
<div class="{{@wrapper-class}}"> <div class="{{@wrapper-class}}">
{{#each @elements as |element|}} {{#each @elements as |element index|}}
<RadioButton <RadioButton
@value="{{element}}" @value="{{element}}"
@groupValue={{@selected}} @groupValue={{@selected}}

View file

@ -1,6 +1,7 @@
import { helper } from '@ember/component/helper'; import { helper } from '@ember/component/helper'
export default helper(function equals(args) { export default helper(function equals(args) {
if (args.length != 2) return false; if (args.length != 2) return false
return args[0] == args[1]; console.log(args[0], args[1])
}); return args[0] == args[1]
})

View file

@ -0,0 +1,14 @@
import { helper } from '@ember/component/helper';
const re = /.+@\S+\.\S+/;
// Helper to check if a given email is *probably* valid
// Ofc, the only surefire way to check if an email exists is to send a test mail to it.
// This sending is expensive however, and thus some mostly sane defaults can be checked for
// beforehand. "Bananentürkis" for example is obviously not a valid address
export default helper(function isValidMail(positional: string[] /*, named*/) {
for (const mail of positional) {
if (!re.test(mail)) return false;
}
return true;
});

View file

@ -9,4 +9,6 @@ export default class Router extends EmberRouter {
Router.map(function () { Router.map(function () {
this.route('about'); this.route('about');
this.route('registerform'); this.route('registerform');
this.route('auth');
this.route('testing');
}); });

View file

@ -0,0 +1,4 @@
import Route from '@ember/routing/route';
import { tracked } from '@glimmer/tracking';
export default class AuthRoute extends Route {}

View file

@ -0,0 +1,3 @@
import Route from '@ember/routing/route';
export default class TestingRoute extends Route {}

View file

@ -109,6 +109,27 @@ export default class AuthService extends Service {
return false; return false;
} }
} }
// Check if a given username exist on the server
// Note: The server enforces this check itself during both registration and login
// but provides the information too so that frontends can provide a better UX
async doesUsernameExist(username: string): Promise<boolean> {
// TODO: Make API call to check if username/handle is already in use
return true;
}
// Check if a given username is allowed to log in.
// This includes a check for the existence of the username in the first place
// A username may not log in for various reasons, two of which are the account not being approved yet
// or the account being barred login from an admin
// Note: The server enforces this check itself during login. However, it also provides an API endpoint
// for performing this check to allow frontends to have a better UX
async canUsernameLogin(username: string): Promise<boolean> {
// Can't login into a non-existing account
if (!(await this.doesUsernameExist(username))) return false;
// TODO: Make API call to check if username is allowed to login
return true;
}
} }
// Don't remove this declaration: this is what enables TypeScript to resolve // Don't remove this declaration: this is what enables TypeScript to resolve

View file

@ -1,11 +1,16 @@
/* Ember supports plain CSS out of the box. More info: https://cli.emberjs.com/release/advanced-use/stylesheets/ */ /* Ember supports plain CSS out of the box. More info: https://cli.emberjs.com/release/advanced-use/stylesheets/ */
/* Note: CSS is fucking stupid. It applies styles not in the order classes are set on an element,
* but in the order they appear in the css files */
/* @import url("debug.css"); */ /* @import url("debug.css"); */
@import url("fonts.css"); @import url("fonts.css");
@import url("colors.css"); @import url("colors.css");
@import url("notes.css");
@import url("util.css"); @import url("util.css");
@import url("util/stringArray.css");
@import url("util/mailEntry.css");
@import url("svgs.css"); @import url("svgs.css");
@import url("notes.css");
@import url("timeline.css"); @import url("timeline.css");
@import url("auth.css"); @import url("auth.css");
@import url("stringArray.css"); @import url("auth/registerForm.css");

View file

@ -0,0 +1,3 @@
.registration-form {
background: var(--accent);
}

View file

@ -0,0 +1,6 @@
.mail-entry {
}
.mail-ok {
}
.mail-ok {
}

View file

@ -0,0 +1,2 @@
{{page-title "Auth"}}
{{outlet}}

View file

@ -1,4 +1,4 @@
{{page-title "RegisterForm"}} {{page-title "RegisterForm"}}
<Auth::Login /> <Auth::Login />
<br> <br>
<!--<Auth::PostRegistrationForm @username="bob" />--> {{!--<Auth::PostRegistrationForm @username="bob" />--}}

View file

@ -0,0 +1,2 @@
{{page-title "Testing"}}
<Auth::PostRegistrationForm @username="bob" />

View file

@ -0,0 +1,26 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'frontend-reactive/tests/helpers';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | util/mail-entry', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.set('myAction', function(val) { ... });
await render(hbs`<Util::MailEntry />`);
assert.dom().hasText('');
// Template block usage:
await render(hbs`
<Util::MailEntry>
template block text
</Util::MailEntry>
`);
assert.dom().hasText('template block text');
});
});

View file

@ -0,0 +1,17 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'frontend-reactive/tests/helpers';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Helper | isValidMail', function (hooks) {
setupRenderingTest(hooks);
// TODO: Replace this with your real tests.
test('it renders', async function (assert) {
this.set('inputValue', '1234');
await render(hbs`{{is-valid-mail this.inputValue}}`);
assert.dom().hasText('1234');
});
});

View file

@ -0,0 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from 'frontend-reactive/tests/helpers';
module('Unit | Route | auth', function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
const route = this.owner.lookup('route:auth');
assert.ok(route);
});
});

View file

@ -0,0 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from 'frontend-reactive/tests/helpers';
module('Unit | Route | testing', function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
const route = this.owner.lookup('route:testing');
assert.ok(route);
});
});