Skip to content

Commit

Permalink
Merge pull request #39 from archesproject/jtw/login-form
Browse files Browse the repository at this point in the history
Add login interface #13
  • Loading branch information
jacobtylerwalls authored Aug 14, 2024
2 parents 6d637ad + c11894d commit 035f9e8
Show file tree
Hide file tree
Showing 14 changed files with 965 additions and 39 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- Add login interface [#13](https://github.com/archesproject/arches-lingo/issues/13)

### Fixed

### Deprecated

### Removed

### Security
6 changes: 6 additions & 0 deletions arches_lingo/media/js/views/root.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import LingoApp from '@/arches_lingo/App.vue';
import createVueApplication from 'arches/arches/app/media/js/utils/create-vue-application';

createVueApplication(LingoApp).then(vueApp => {
vueApp.mount('#lingo-mounting-point');
});
17 changes: 17 additions & 0 deletions arches_lingo/src/arches_lingo/App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">
import Toast from "primevue/toast";
import ProgressSpinner from "primevue/progressspinner";
import PageSwitcher from "@/arches_lingo/pages/PageSwitcher.vue";
</script>

<template>
<Suspense>
<PageSwitcher />
<template #fallback>
<ProgressSpinner style="display: flex; margin-top: 8rem" />
</template>
</Suspense>
<Toast />
</template>
56 changes: 56 additions & 0 deletions arches_lingo/src/arches_lingo/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import arches from "arches";
import Cookies from "js-cookie";

function getToken() {
const token = Cookies.get("csrftoken");
if (!token) {
throw new Error("Missing csrftoken");
}
return token;
}

export const login = async (username: string, password: string) => {
const response = await fetch(arches.urls.api_login, {
method: "POST",
headers: { "X-CSRFTOKEN": getToken() },
body: JSON.stringify({ username, password }),
});
try {
const parsed = await response.json();
if (response.ok) {
return parsed;
}
throw new Error(parsed.message);
} catch (error) {
throw new Error((error as Error).message || response.statusText);
}
};

export const logout = async () => {
const response = await fetch(arches.urls.api_logout, {
method: "POST",
headers: { "X-CSRFTOKEN": getToken() },
});
if (response.ok) {
return true;
}
try {
const error = await response.json();
throw new Error(error.message);
} catch (error) {
throw new Error((error as Error).message || response.statusText);
}
};

export const fetchUser = async () => {
const response = await fetch(arches.urls.api_user);
try {
const parsed = await response.json();
if (response.ok) {
return parsed;
}
throw new Error(parsed.message);
} catch (error) {
throw new Error((error as Error).message || response.statusText);
}
};
7 changes: 7 additions & 0 deletions arches_lingo/src/arches_lingo/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { InjectionKey } from "vue";
import type { UserRefAndSetter } from "@/arches_lingo/types";

export const ERROR = "error";
export const DEFAULT_ERROR_TOAST_LIFE = 8000;

export const userKey = Symbol() as InjectionKey<UserRefAndSetter>;
55 changes: 55 additions & 0 deletions arches_lingo/src/arches_lingo/pages/HomePage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<script setup lang="ts">
import { computed, inject } from "vue";
import { useGettext } from "vue3-gettext";
import { useToast } from "primevue/usetoast";
import Button from "primevue/button";
import { DEFAULT_ERROR_TOAST_LIFE, ERROR } from "@/arches_lingo/constants.ts";
import { logout } from "@/arches_lingo/api.ts";
import { userKey } from "@/arches_lingo/constants.ts";
import type { UserRefAndSetter } from "@/arches_lingo/types";
const { user, setUser } = inject(userKey) as UserRefAndSetter;
const { $gettext } = useGettext();
const toast = useToast();
const issueLogout = async () => {
try {
await logout();
setUser(null);
} catch (error) {
toast.add({
severity: ERROR,
life: DEFAULT_ERROR_TOAST_LIFE,
summary: $gettext("Sign out failed."),
detail: error instanceof Error ? error.message : undefined,
});
}
};
const bestName = computed(() => {
if (!user.value) {
return "";
}
// TODO: determine appropriate i18n for this.
if (user.value.first_name && user.value.last_name) {
return user.value.first_name + " " + user.value.last_name;
}
return user.value.username;
});
</script>

<template>
<main>
<div style="display: flex; justify-content: space-between">
<h1>{{ $gettext("LINGO") }}</h1>
<span>{{ $gettext("Hello %{bestName}", { bestName }) }}</span>
<Button @click="issueLogout">
{{ $gettext("Sign out") }}
</Button>
</div>
</main>
</template>
44 changes: 44 additions & 0 deletions arches_lingo/src/arches_lingo/pages/PageSwitcher.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<script setup lang="ts">
import { provide, ref } from "vue";
import { useGettext } from "vue3-gettext";
import { useToast } from "primevue/usetoast";
import { fetchUser } from "@/arches_lingo/api.ts";
import {
DEFAULT_ERROR_TOAST_LIFE,
ERROR,
userKey,
} from "@/arches_lingo/constants.ts";
import HomePage from "@/arches_lingo/pages/HomePage.vue";
import LoginPage from "@/arches_lingo/pages/login/LoginPage.vue";
import type { User } from "@/arches_lingo/types";
const { $gettext } = useGettext();
const toast = useToast();
const user = ref<User | null>(null);
const setUser = (userToSet: User | null) => {
user.value = userToSet;
};
provide(userKey, { user, setUser });
try {
setUser(await fetchUser());
} catch (error) {
toast.add({
severity: ERROR,
life: DEFAULT_ERROR_TOAST_LIFE,
summary: $gettext("Login required"), // most likely case is inactive user
detail: error instanceof Error ? error.message : undefined,
});
}
</script>

<template>
<div style="font-family: sans-serif">
<HomePage v-if="user && user.username !== 'anonymous'" />
<LoginPage v-else />
</div>
</template>
15 changes: 15 additions & 0 deletions arches_lingo/src/arches_lingo/pages/login/LoginPage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup lang="ts">
import LoginForm from "@/arches_lingo/pages/login/components/LoginForm.vue";
import LoginLinks from "@/arches_lingo/pages/login/components/LoginLinks.vue";
</script>

<template>
<div style="margin: 5%">
<LoginForm />
<div
class="spacer"
style="height: 10rem"
></div>
<LoginLinks />
</div>
</template>
78 changes: 78 additions & 0 deletions arches_lingo/src/arches_lingo/pages/login/components/LoginForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<script setup lang="ts">
import { inject, ref } from "vue";
import { useGettext } from "vue3-gettext";
import { useToast } from "primevue/usetoast";
import InputText from "primevue/inputtext";
import Button from "primevue/button";
import { login } from "@/arches_lingo/api.ts";
import {
DEFAULT_ERROR_TOAST_LIFE,
ERROR,
userKey,
} from "@/arches_lingo/constants.ts";
import type { UserRefAndSetter } from "@/arches_lingo/types";
const { $gettext } = useGettext();
const toast = useToast();
const { setUser } = inject(userKey) as UserRefAndSetter;
const username = ref();
const password = ref();
const submit = async () => {
try {
const userToSet = await login(username.value, password.value);
setUser(userToSet);
} catch (error) {
toast.add({
severity: ERROR,
life: DEFAULT_ERROR_TOAST_LIFE,
summary: $gettext("Sign in failed."),
detail: error instanceof Error ? error.message : undefined,
});
}
};
</script>

<template>
<form>
<h1>{{ $gettext("LINGO") }}</h1>
<h2>{{ $gettext("Vocabulary management powered by Arches.") }}</h2>
<InputText
v-model="username"
:placeholder="$gettext('Username')"
:aria-label="$gettext('Username')"
autocomplete="username"
/>
<InputText
v-model="password"
:placeholder="$gettext('Password')"
:aria-label="$gettext('Password')"
type="password"
autocomplete="password"
@keyup.enter="submit"
/>

<Button
type="button"
:label="$gettext('Sign In')"
@click="submit"
/>
</form>
</template>

<style scoped>
form {
display: flex;
flex-direction: column;
gap: 1rem;
width: 30%;
}
input {
width: 100%;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script setup lang="ts">
import arches from "arches";
import { useGettext } from "vue3-gettext";
import Button from "primevue/button";
const { $gettext } = useGettext();
</script>

<template>
<div style="display: flex; justify-content: space-between; width: 30%">
<Button
as="a"
:label="$gettext('Register')"
:href="arches.urls.signup"
/>
<Button
as="a"
:label="$gettext('Multi-factor login')"
:href="arches.urls.auth + '?next=/'"
/>
</div>
</template>
13 changes: 13 additions & 0 deletions arches_lingo/src/arches_lingo/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Ref } from "vue";

export interface User {
first_name: string;
last_name: string;
username: string;
}

// Prop injection types
export interface UserRefAndSetter {
user: Ref<User | null>;
setUser: (userToSet: User | null) => void;
}
2 changes: 2 additions & 0 deletions arches_lingo/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.shortcuts import render
from django.utils.translation import gettext_lazy as _
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import View

from arches.app.models.models import (
Expand Down Expand Up @@ -200,6 +201,7 @@ def get(self, request):


class LingoRootView(BaseManagerView):
@method_decorator(ensure_csrf_cookie)
def get(self, request, graphid=None, resourceid=None):
context = self.get_context_data(main_script="views/root")
context["page_title"] = _("Lingo")
Expand Down
Loading

0 comments on commit 035f9e8

Please sign in to comment.