Skip to content

Commit

Permalink
Fixed #211, finishing COPPA compliance with non-email logins, trackin…
Browse files Browse the repository at this point in the history
…g disclosures.
  • Loading branch information
amyjko committed Oct 22, 2023
1 parent f977035 commit 1822f0c
Show file tree
Hide file tree
Showing 12 changed files with 344 additions and 66 deletions.
2 changes: 1 addition & 1 deletion src/components/app/CreatorView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
>{/if}{creator
? creator.email === null
? ''
: anonymize
: anonymize || creator.email.endsWith('@wordplay.dev')
? creator.email.split('@')[0].substring(0, 4)
: creator.email
: $locales.get((l) => l.ui.page.login.anonymous)}</div
Expand Down
2 changes: 1 addition & 1 deletion src/components/widgets/Note.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<style>
div {
font-style: italic;
font-size: 14pt;
font-size: calc(var(--wordplay-font-size) - 2pt);
color: var(--wordplay-inactive-color);
}
Expand Down
11 changes: 8 additions & 3 deletions src/components/widgets/TextField.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
export let right = false;
export let defaultFocus = false;
export let editable = true;
export let email = false;
export let kind: 'email' | 'password' | undefined = undefined;
let width = 0;
Expand All @@ -23,7 +23,8 @@
}
onMount(() => {
if (email && view) view.type = 'email';
if (kind === 'email' && view) view.type = 'email';
else if (kind === 'password' && view) view.type = 'password';
});
</script>

Expand All @@ -47,7 +48,11 @@
on:blur={() => (done ? done(text) : undefined)}
/>
<span class="measurer" bind:clientWidth={width}
>{text.length === 0 ? placeholder : text}</span
>{text.length === 0
? placeholder
: kind === 'password'
? ''.repeat(text.length)
: text}</span
>
</div>

Expand Down
20 changes: 19 additions & 1 deletion src/locale/UITexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,12 @@ type UITexts = {
login: {
/** Header for the login page when not logged in */
header: string;
subheader: {
/** Header for logging in via email */
email: string;
/** Header for logging in via username and password */
username: string;
};
prompt: {
/** Prompts creator to login to save their work */
login: string;
Expand All @@ -658,6 +664,10 @@ type UITexts = {
enter: string;
/** Encouragement to go create after logging in. */
play: string;
/** Gives rules for emails above the login form */
emailrules: string;
/** Gives rules for usernames and passwords above the login form */
usernamerules: string;
/** Offers to log out the creator. */
logout: string;
/** Shown briefly before page redirects to projects */
Expand All @@ -670,12 +680,18 @@ type UITexts = {
reallyDelete: string;
/** Pick an emoji as a name */
name: string;
/** Text for age prompt */
age: ModeText<[string, string]>;
};
/** Shown in the footer a creator is not logged in. */
anonymous: string;
field: {
/** The login email */
email: FieldText;
/** The login username */
username: FieldText;
/** The login password */
password: FieldText;
};
feedback: {
/** Change email pending */
Expand All @@ -694,10 +710,12 @@ type UITexts = {
failure: string;
/** When there's no connection to Firebase */
offline: string;
/** When the email addres couldn't be changed for unknown reasons. */
/** When the email address couldn't be changed for unknown reasons. */
unchanged: string;
/** When account deletion failed */
delete: string;
/** When a password is wrong */
wrongPassword: string;
};
button: {
/** Log out of the account */
Expand Down
29 changes: 24 additions & 5 deletions src/locale/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -4168,11 +4168,21 @@
},
"login": {
"header": "Login",
"subheader": {
"email": "email link",
"username": "username + password"
},
"anonymous": "login",
"prompt": {
"login": "Your performances are saving on this device, but not online. If this is your device, that's okay! If it's not, others may see or delete your projects. To save them online and keep them private, log in.",
"age": {
"label": "What's your age?",
"modes": ["12 or younger", "13 or older"]
},
"enter": "It looks like your login link came from a different browser or device. Can you enter your email again, just so we're sure it's you?",
"play": "You're logged in, we can save your projects online now! Want to create something?",
"emailrules": "Don't provide your email if you are 12 or younger.",
"usernamerules": "Usernames should not contain identiable information. Passwords must be at least 10 characters long.",
"change": "Want to change your email? Submit a new one and we'll send a confirmation to the old one.",
"sent": "Check your email for a login link.",
"logout": "Leaving a shared device and want to keep your projects private? Logout and we'll remove your projects from this device. They will still be stored online.",
Expand All @@ -4189,7 +4199,8 @@
"failure": "Unable to login :(",
"offline": "You appear to be offline.",
"unchanged": "We couldn't change your email address, but we don't know why.",
"delete": "We couldn't delete your account, but we don't know why."
"delete": "We couldn't delete your account, but we don't know why.",
"wrongPassword": "Not a valid username and password"
},
"feedback": {
"changing": "Submitting new email...",
Expand All @@ -4199,6 +4210,14 @@
"email": {
"description": "edit login email",
"placeholder": "email"
},
"username": {
"description": "login username, don't use personally identifiable information",
"placeholder": "username"
},
"password": {
"description": "login password, at least 10 character",
"placeholder": "password"
}
},
"button": {
Expand Down Expand Up @@ -4226,11 +4245,11 @@
"The first thing to know is that we are not a commercial entity. We are a community-based research project housed at a not-for-profit university. Our goal is to create a platform that brings you joy and helps us make discoveries about a more equitable and just world of computing. We have no interest in making money on this platform; any money we gather (usually through public funding) is used to sustain the platform, not to enrich anyone who works on it (or contributes to it).",
"Because we are not seeking profit, this also means that we can't make any promises about the reliability, availability, or longevity of this platform. That said, <Amy@https://amyjko.phd> is committed long term to sustaining it, and as a tenured professor, she's got a pretty stable gig.",
"That brings is to *data*. Here's what we gather and store in the cloud:",
"Your *email address*. We use this to ensure that only you and anyone you share with can access your projects and settings.",
"• Your *projects*. We store any projects you contribute. This includes any personally identifiable information you put in your projects (which could be anything, since it's all text!)",
"If you are 13 or older, your *email address*. We use this to ensure that only you and anyone you share with can access your projects and settings. If you are younger than 13, then we only store the username you choose, which you should ensure doesn't contain any personally identiable information.",
"• Your *projects*. We store any projects you contribute.",
"• Your *settings*. This includes the locales you choose, your animation preferences, and your tutorial progress. Everything else is stored on your device.",
"*Traffic*. We use basic <Google Analytics@https://en.wikipedia.org/wiki/Google_Analytics> to gather aggregate, anonymous data about which pages people and projects people are visiting and how many times. We use this to help raise funding to sustain the platform.",
"We don't store anything else. No 'cookies' other than those used by Google Analytics, no tracking identifiers, no recordings of camera or microphone input. Our <source@https://github.com/amyjko/wordplay/tree/main/src> is public, so you can verify this if you wish at any time.",
"Your anonymized *activity*. We track the projects you view, the size of your screen, when you leave the site, when you read to the end of a page, and when you login. We use this to help prioritize engineering work, and to help raise funding by reporting how much the platform is being used in the aggregate. None of these events are in linked to you, and we do not track you across websites.",
"We don't store anything else. No 'cookies', no IP tracking, no recordings of any camera or microphone input. Our <source@https://github.com/amyjko/wordplay/tree/main/src> is public, so anyone can verify this, and report any unintended tracking.",
"*You* own your data, not us. That means:",
"• You control who can access your projects. They are private by default, but you can share them with individuals, groups, or make them entirely public.",
"• You can fully delete any project or your own account at any time.",
Expand Down
146 changes: 130 additions & 16 deletions src/routes/login/Login.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<script lang="ts">
import {
createUserWithEmailAndPassword,
isSignInWithEmailLink,
sendSignInLinkToEmail,
signInWithEmailAndPassword,
} from 'firebase/auth';
import Header from '../../components/app/Header.svelte';
import TextField from '../../components/widgets/TextField.svelte';
Expand All @@ -17,6 +19,10 @@
import { FirebaseError } from 'firebase/app';
import { getLoginErrorDescription } from './login';
import { goto } from '$app/navigation';
import Mode from '../../components/widgets/Mode.svelte';
import Subheader from '../../components/app/Subheader.svelte';
import MarkupHtmlView from '../../components/concepts/MarkupHTMLView.svelte';
import Note from '../../components/widgets/Note.svelte';
let user = getUser();
let success: boolean | undefined = undefined;
Expand All @@ -25,11 +31,17 @@
let email: string;
let sent = false;
let loginFeedback = '';
let younger = true;
let username = '';
let password = '';
$: submittable = !sent && validEmail(email);
$: emailSubmittable = !sent && validEmail(email);
async function startLogin() {
if (auth && submittable) {
$: usernameSubmittable =
!sent && username.length > 4 && password.length >= 10;
async function startEmailLogin() {
if (auth && emailSubmittable) {
try {
if (isSignInWithEmailLink(auth, window.location.href))
finishLogin();
Expand All @@ -45,7 +57,7 @@
sent = true;
}
} catch (err) {
communicateLoginFailure(err);
communicateError(err);
}
}
}
Expand Down Expand Up @@ -80,16 +92,47 @@
// Remove the query on the URL so there's no attempt to login on refresh.
goto('/login');
})
.catch((err) => communicateLoginFailure(err));
.catch((err) => communicateError(err));
}
} catch (err) {
communicateLoginFailure(err);
communicateError(err);
}
}
return undefined;
}
function communicateLoginFailure(err: unknown): string | undefined {
async function startUsernameLogin() {
const emailUsername = `${username}@wordplay.dev`;
if (auth && usernameSubmittable) {
try {
await signInWithEmailAndPassword(
auth,
`${username}@wordplay.dev`,
password
);
} catch (error) {
// If not found, then we create the user.
if (
error instanceof FirebaseError &&
error.code === 'auth/user-not-found'
) {
try {
await createUserWithEmailAndPassword(
auth,
emailUsername,
password
);
} catch (error) {
communicateError(error);
}
}
// Otherwise, communicate the error.
else communicateError(error);
}
}
}
function communicateError(err: unknown): string | undefined {
if (err instanceof FirebaseError) {
console.error(err.code);
console.error(err.message);
Expand Down Expand Up @@ -118,27 +161,98 @@
{$locales.get((l) => l.ui.page.login.prompt.login)}
{/if}
</p>
<form on:submit={startLogin}>

<Mode
modes={$locales.get((l) => l.ui.page.login.prompt.age.modes)}
choice={0}
select={(choice) => (younger = choice === 0)}
descriptions={$locales.get((l) => l.ui.page.login.prompt.age)}
/>

{#if missingEmail || !younger}
<Subheader>{$locales.get((l) => l.ui.page.login.subheader.email)}</Subheader
>
<form class="login-form" on:submit={startEmailLogin}>
<Note
><MarkupHtmlView
inline
markup={$locales.get((l) => l.ui.page.login.prompt.emailrules)}
/></Note
>
<div>
<TextField
kind="email"
description={$locales.get(
(l) => l.ui.page.login.field.email.description
)}
placeholder={$locales.get(
(l) => l.ui.page.login.field.email.placeholder
)}
bind:text={email}
editable={!sent}
/>
<Button
submit
tip={$locales.get((l) => l.ui.page.login.button.login)}
active={emailSubmittable}
action={() => undefined}>&gt;</Button
>
</div>
</form>
{/if}
<Subheader>{$locales.get((l) => l.ui.page.login.subheader.username)}</Subheader>
<form class="login-form" on:submit={startUsernameLogin}>
<Note
><MarkupHtmlView
markup={$locales.get((l) => l.ui.page.login.prompt.usernamerules)}
/></Note
>
<TextField
description={$locales.get(
(l) => l.ui.page.login.field.email.description
(l) => l.ui.page.login.field.username.description
)}
placeholder={$locales.get(
(l) => l.ui.page.login.field.email.placeholder
(l) => l.ui.page.login.field.username.placeholder
)}
bind:text={email}
bind:text={username}
editable={!sent}
/><Button
submit
tip={$locales.get((l) => l.ui.page.login.button.login)}
active={submittable}
action={() => undefined}>&gt;</Button
/>
<div>
<TextField
kind="password"
description={$locales.get(
(l) => l.ui.page.login.field.password.description
)}
placeholder={$locales.get(
(l) => l.ui.page.login.field.password.placeholder
)}
bind:text={password}
editable={!sent}
/>
<Button
submit
tip={$locales.get((l) => l.ui.page.login.button.login)}
active={usernameSubmittable}
action={() => undefined}>&gt;</Button
></div
>
</form>

{#if sent === true}
<Feedback>{$locales.get((l) => l.ui.page.login.prompt.sent)}</Feedback>
{:else if success === true}
<Feedback>{$locales.get((l) => l.ui.page.login.prompt.success)}</Feedback>
{:else if success === false}
<Feedback>{loginFeedback}</Feedback>
{/if}

<style>
.login-form {
display: flex;
flex-direction: column;
gap: var(--wordplay-spacing);
padding: var(--wordplay-spacing);
border: var(--wordplay-border-width) solid var(--wordplay-border-color);
border-radius: var(--wordplay-border-radius);
}
</style>
Loading

0 comments on commit 1822f0c

Please sign in to comment.