Skip to content

Commit

Permalink
Multiple accounts
Browse files Browse the repository at this point in the history
This implements a simple account switcher and a way to change/add accounts. To remove one, you have to logout from all of them.

It makes sharing family accounts easier, while keeping separate accounts/tracking per person.
  • Loading branch information
BrunoBernardino committed Jan 21, 2024
1 parent d6bb84c commit 90004d3
Show file tree
Hide file tree
Showing 12 changed files with 189 additions and 19 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.PHONY: start
start:
deno run --watch --allow-net --allow-read --allow-env main.ts
deno run --watch --allow-net --allow-read --allow-env --allow-write main.ts

.PHONY: format
format:
Expand Down
5 changes: 5 additions & 0 deletions components/header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ export default function header(currentPath: string) {
<li class="${currentPath.includes('/pricing') ? 'active' : ''}">
<a href="/pricing">Pricing</a>
</li>
<li data-has-valid-session class="hidden">
<select id="swap-accounts-select">
<option value="">Swap account...</option>
</select>
</li>
<li data-has-valid-session class="hidden">
<a onclick="window.app.doLogout();" style="cursor: pointer;">Logout</a>
</li>
Expand Down
11 changes: 0 additions & 11 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,6 @@ services:
soft: -1
hard: -1

# NOTE: This would be nice to develop with https:// locally, but it doesn't work, for whatever reason, so we need a system caddy instead
# caddy:
# image: caddy:2-alpine
# restart: unless-stopped
# command: caddy reverse-proxy --from https://localhost:443 --to http://localhost:8000
# network_mode: "host"
# volumes:
# - caddy:/data

volumes:
pgdata:
driver: local
# caddy:
# driver: local
6 changes: 3 additions & 3 deletions lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'std/dotenv/load.ts';
import { emit } from 'https://deno.land/x/emit@0.15.0/mod.ts';
import { transpile } from 'https://deno.land/x/emit@0.33.0/mod.ts';
import sass from 'https://deno.land/x/[email protected]/mod.ts';
import { serveFile } from 'std/http/file_server.ts';

Expand Down Expand Up @@ -106,15 +106,15 @@ export function escapeHtml(unsafe: string) {

async function transpileTs(content: string, specifier: URL) {
const urlStr = specifier.toString();
const result = await emit(specifier, {
const result = await transpile(specifier, {
load(specifier: string) {
if (specifier !== urlStr) {
return Promise.resolve({ kind: 'module', specifier, content: '' });
}
return Promise.resolve({ kind: 'module', specifier, content });
},
});
return result[urlStr];
return result.get(urlStr) || '';
}

export async function serveFileWithTs(request: Request, filePath: string, extraHeaders?: ResponseInit['headers']) {
Expand Down
4 changes: 2 additions & 2 deletions public/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ a.button.secondary:focus {
.input-wrapper input[type="url"],
.input-wrapper input[type="password"],
.input-wrapper textarea,
.input-wrapper select
.input-wrapper select
{
box-sizing: border-box;
width: 100%;
Expand All @@ -394,7 +394,7 @@ a.button.secondary:focus {
.input-wrapper input[type="url"]:focus,
.input-wrapper input[type="password"]:focus,
.input-wrapper textarea:focus,
.input-wrapper select:focus
.input-wrapper select:focus
{
border-color: var(--color-link-hover);
}
Expand Down
17 changes: 17 additions & 0 deletions public/scss/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,20 @@
}
}
}

#swap-accounts-select {
box-sizing: border-box;
width: auto;
max-width: 120px;
display: block;
outline: none;
border: none;
font-size: 0.9rem;
padding: 0.25rem 0.25rem;
border: 1px solid #fff;
background: #fff;
border-radius: 3px;
transition: all 80ms ease-in-out;
text-overflow: ellipsis;
margin: 0 0.5rem;
}
9 changes: 8 additions & 1 deletion public/ts/billing.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { checkForValidSession, commonRequestHeaders, dateDiffInDays, showNotification } from './utils.ts';
import {
checkForValidSession,
commonInitializer,
commonRequestHeaders,
dateDiffInDays,
showNotification,
} from './utils.ts';
import LocalData from './local-data.ts';

document.addEventListener('app-loaded', async () => {
Expand Down Expand Up @@ -191,6 +197,7 @@ document.addEventListener('app-loaded', async () => {

function initializePage() {
updateUI();
commonInitializer();
}

if (window.app.isLoggedIn) {
Expand Down
2 changes: 2 additions & 0 deletions public/ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Budget, Expense } from '/lib/types.ts';
import {
BudgetToShow,
checkForValidSession,
commonInitializer,
copyBudgetsAndExpenses,
createAccount,
debounce,
Expand Down Expand Up @@ -362,6 +363,7 @@ document.addEventListener('app-loaded', async () => {

function initializePage() {
showData();
commonInitializer();
}

let isAddingExpense = false;
Expand Down
1 change: 1 addition & 0 deletions public/ts/local-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface StoredSession {
userId: string;
email: string;
keyPair: KeyPair;
otherSessions?: Omit<StoredSession, 'otherSessions'>[];
}

export default class LocalData {
Expand Down
9 changes: 8 additions & 1 deletion public/ts/pricing.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { checkForValidSession, commonRequestHeaders, dateDiffInDays, showNotification } from './utils.ts';
import {
checkForValidSession,
commonInitializer,
commonRequestHeaders,
dateDiffInDays,
showNotification,
} from './utils.ts';
import LocalData from './local-data.ts';

document.addEventListener('app-loaded', async () => {
Expand Down Expand Up @@ -127,6 +133,7 @@ document.addEventListener('app-loaded', async () => {

function initializePage() {
updateUI();
commonInitializer();
}

if (window.app.isLoggedIn) {
Expand Down
2 changes: 2 additions & 0 deletions public/ts/settings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Budget, Expense } from '/lib/types.ts';
import {
checkForValidSession,
commonInitializer,
commonRequestHeaders,
exportAllData,
importData,
Expand Down Expand Up @@ -370,6 +371,7 @@ document.addEventListener('app-loaded', async () => {

function initializePage() {
newCurrencySelect.value = user?.extra.currency || '$';
commonInitializer();
}

if (window.app.isLoggedIn) {
Expand Down
140 changes: 140 additions & 0 deletions public/ts/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,60 @@ async function getUser() {
return null;
}

export function getOtherAccounts() {
try {
const session = LocalData.get('session')!;

return session.otherSessions?.map((otherSession) => ({
email: otherSession.email,
})) || [];
} catch (_error) {
// Do nothing
}

return [];
}

export function swapAccount(newEmail: string) {
try {
const session = LocalData.get('session')!;

const foundSession = session.otherSessions?.find((otherSession) => otherSession.email === newEmail);

if (foundSession) {
const otherSessions = [...(session.otherSessions || [])].filter((otherSession) =>
otherSession.email !== foundSession.email
);

otherSessions.unshift(session);

const newSession: StoredSession = {
...foundSession,
otherSessions,
};

LocalData.set('session', newSession);

window.location.reload();
}
} catch (_error) {
// Do nothing
}

return [];
}

export async function validateLogin(email: string, password: string) {
const { Swal } = window;

let existingSession: StoredSession | null = null;

try {
existingSession = LocalData.get('session');
} catch (_error) {
// Do nothing
}

try {
const headers = commonRequestHeaders;

Expand Down Expand Up @@ -230,11 +281,18 @@ export async function validateLogin(email: string, password: string) {

await fetch('/api/session', { method: 'PATCH', headers, body: JSON.stringify(verificationBody) });

const otherSessions = [...(existingSession?.otherSessions || [])];

if (existingSession && existingSession.email !== lowercaseEmail) {
otherSessions.unshift(existingSession);
}

const session: StoredSession = {
sessionId,
userId: user.id,
email: lowercaseEmail,
keyPair,
otherSessions,
};

LocalData.set('session', session);
Expand All @@ -252,6 +310,14 @@ export async function validateLogin(email: string, password: string) {
}

export async function createAccount(email: string, password: string) {
let existingSession: StoredSession | null = null;

try {
existingSession = LocalData.get('session');
} catch (_error) {
// Do nothing
}

try {
const headers = commonRequestHeaders;

Expand All @@ -273,11 +339,18 @@ export async function createAccount(email: string, password: string) {
throw new Error('Failed to create user. Try logging in instead.');
}

const otherSessions = [...(existingSession?.otherSessions || [])];

if (existingSession && existingSession.email !== lowercaseEmail) {
otherSessions.unshift(existingSession);
}

const session: StoredSession = {
sessionId,
userId: user.id,
email: lowercaseEmail,
keyPair,
otherSessions,
};

LocalData.set('session', session);
Expand Down Expand Up @@ -867,6 +940,57 @@ export async function importData(replaceData: boolean, budgets: Budget[], expens
return false;
}

export async function commonInitializer() {
const user = await checkForValidSession();
const swapAccountsSelect = document.getElementById('swap-accounts-select') as HTMLSelectElement;

function populateSwapAccountsSelect() {
if (user) {
const otherSessions = getOtherAccounts();
otherSessions.sort(sortByEmail);

const currentUserOptionHtml = `<option>${user.email}</option>`;
const newLoginOptionHtml = `<option value="new">Login to another account</option>`;
const fullSelectHtmlStrings: string[] = [currentUserOptionHtml];

for (const otherSession of otherSessions) {
const optionHtml = `<option>${otherSession.email}</option>`;
fullSelectHtmlStrings.push(optionHtml);
}

fullSelectHtmlStrings.push(newLoginOptionHtml);

swapAccountsSelect.innerHTML = fullSelectHtmlStrings.join('\n');
}
}

function chooseAnotherAccount() {
const currentEmail = user?.email;
const chosenEmail = swapAccountsSelect.value;

if (!chosenEmail) {
return;
}

if (chosenEmail === currentEmail) {
return;
}

if (chosenEmail === 'new') {
// Show login form again
hideValidSessionElements();
return;
}

swapAccount(chosenEmail);
}

populateSwapAccountsSelect();

swapAccountsSelect.removeEventListener('change', chooseAnotherAccount);
swapAccountsSelect.addEventListener('change', chooseAnotherAccount);
}

const months = [
'January',
'February',
Expand Down Expand Up @@ -935,6 +1059,22 @@ export function sortByName(
return 0;
}

type SortableByEmail = { email: string };
export function sortByEmail(
objectA: SortableByEmail,
objectB: SortableByEmail,
) {
const emailA = objectA.email.toLowerCase();
const emailB = objectB.email.toLowerCase();
if (emailA < emailB) {
return -1;
}
if (emailA > emailB) {
return 1;
}
return 0;
}

export interface BudgetToShow extends Omit<Budget, 'user_id' | 'extra'> {
expensesCost: number;
}
Expand Down

0 comments on commit 90004d3

Please sign in to comment.