Skip to content

Commit c2b4515

Browse files
authored
Add login page instead of modal (#3366)
1 parent 8698318 commit c2b4515

13 files changed

Lines changed: 230 additions & 116 deletions

File tree

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<template>
2+
<form v-focustrap class="flex flex-col gap-4 relative max-w-md w-full text-sm rounded-md pt-9">
3+
<div class="flex justify-center gap-2">
4+
<a
5+
class="inline-block text-xl text-muted-color transition-all duration-300 hover:text-primary-400 hover:scale-150 cursor-pointer"
6+
@click="openWebAuthn"
7+
title="WebAuthn"
8+
>
9+
<i class="fa-solid fa-fingerprint" />
10+
</a>
11+
<template v-if="oauths !== undefined">
12+
<a
13+
v-for="oauth in oauths"
14+
:href="oauth.url"
15+
class="inline-block text-xl text-muted-color hover:scale-125 transition-all cursor-pointer hover:text-primary-400 mb-6"
16+
:title="oauth.provider"
17+
>
18+
<i class="items-center" :class="oauth.icon"></i>
19+
</a>
20+
</template>
21+
</div>
22+
<div class="inline-flex flex-col gap-2" :class="props.padding ?? 'px-9'">
23+
<FloatLabel variant="on">
24+
<InputText id="username" v-model="username" autocomplete="username" :autofocus="true" />
25+
<label for="username">{{ $t("dialogs.login.username") }}</label>
26+
</FloatLabel>
27+
</div>
28+
<div class="inline-flex flex-col gap-2" :class="props.padding ?? 'px-9'">
29+
<FloatLabel variant="on">
30+
<InputPassword id="password" v-model="password" @keydown.enter="login" autocomplete="current-password" />
31+
<label for="password">{{ $t("dialogs.login.password") }}</label>
32+
</FloatLabel>
33+
<Message v-if="invalidPassword" severity="error">{{ $t("dialogs.login.unknown_invalid") }}</Message>
34+
</div>
35+
<div class="text-muted-color text-right font-semibold" :class="props.padding ?? 'px-9'">
36+
Lychee <span class="text-primary-500" v-if="is_se_enabled">SE</span>
37+
</div>
38+
<div class="flex items-center mt-9">
39+
<Button
40+
v-if="closeCallback !== undefined"
41+
@click="props.closeCallback"
42+
severity="secondary"
43+
class="w-full font-bold border-none rounded-none rounded-bl-xl shrink"
44+
>
45+
{{ $t("dialogs.button.cancel") }}
46+
</Button>
47+
<Button
48+
@click="login"
49+
severity="contrast"
50+
:class="{
51+
'w-full font-bold border-none shrink': true,
52+
'rounded-none rounded-br-xl': closeCallback !== undefined,
53+
'rounded-xl': closeCallback === undefined,
54+
}"
55+
>
56+
{{ $t("dialogs.login.signin") }}
57+
</Button>
58+
</div>
59+
</form>
60+
</template>
61+
<script setup lang="ts">
62+
import { ref } from "vue";
63+
import FloatLabel from "primevue/floatlabel";
64+
import Button from "primevue/button";
65+
import Message from "primevue/message";
66+
import AuthService from "@/services/auth-service";
67+
import InputText from "@/components/forms/basic/InputText.vue";
68+
import InputPassword from "@/components/forms/basic/InputPassword.vue";
69+
import { useAuthStore } from "@/stores/Auth";
70+
import AlbumService from "@/services/album-service";
71+
import { useLycheeStateStore } from "@/stores/LycheeState";
72+
import { storeToRefs } from "pinia";
73+
import { useTogglablesStateStore } from "@/stores/ModalsState";
74+
import { onMounted } from "vue";
75+
76+
const emits = defineEmits<{
77+
"logged-in": [];
78+
}>();
79+
80+
type OauthProvider = {
81+
url: string;
82+
icon: string;
83+
provider: App.Enum.OauthProvidersType;
84+
};
85+
86+
const props = defineProps<{
87+
closeCallback?: () => void;
88+
padding?: string;
89+
}>();
90+
91+
const username = ref("");
92+
const password = ref("");
93+
const authStore = useAuthStore();
94+
const togglableStore = useTogglablesStateStore();
95+
const lycheeStore = useLycheeStateStore();
96+
const { is_se_enabled } = storeToRefs(lycheeStore);
97+
const { is_login_open, is_webauthn_open } = storeToRefs(togglableStore);
98+
const invalidPassword = ref(false);
99+
100+
const oauths = ref<OauthProvider[] | undefined>(undefined);
101+
102+
function login() {
103+
AuthService.login(username.value, password.value)
104+
.then(() => {
105+
is_login_open.value = false;
106+
authStore.setUser(null);
107+
invalidPassword.value = false;
108+
AlbumService.clearCache();
109+
emits("logged-in");
110+
})
111+
.catch((e: any) => {
112+
if (e.response && e.response.status === 401) {
113+
invalidPassword.value = true;
114+
}
115+
});
116+
}
117+
118+
function openWebAuthn() {
119+
is_login_open.value = false;
120+
is_webauthn_open.value = true;
121+
username.value = "";
122+
password.value = "";
123+
invalidPassword.value = false;
124+
}
125+
126+
onMounted(() => {
127+
authStore.getOauthData().then((data) => {
128+
oauths.value = data;
129+
});
130+
});
131+
</script>
Lines changed: 3 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,21 @@
11
<template>
22
<Dialog v-model:visible="is_login_open" modal pt:root:class="border-none" pt:mask:style="backdrop-filter: blur(2px)">
33
<template #container="{ closeCallback }">
4-
<form v-focustrap class="flex flex-col gap-4 relative max-w-md w-full text-sm rounded-md pt-9">
5-
<div class="flex justify-center gap-2">
6-
<a
7-
class="inline-block text-xl text-muted-color transition-all duration-300 hover:text-primary-400 hover:scale-150 cursor-pointer"
8-
@click="openWebAuthn"
9-
title="WebAuthn"
10-
>
11-
<i class="fa-solid fa-fingerprint" />
12-
</a>
13-
<template v-if="oauths !== undefined">
14-
<a
15-
v-for="oauth in oauths"
16-
:href="oauth.url"
17-
class="inline-block text-xl text-muted-color hover:scale-125 transition-all cursor-pointer hover:text-primary-400 mb-6"
18-
:title="oauth.provider"
19-
>
20-
<i class="items-center" :class="oauth.icon"></i>
21-
</a>
22-
</template>
23-
</div>
24-
<div class="inline-flex flex-col gap-2 px-9">
25-
<FloatLabel variant="on">
26-
<InputText id="username" v-model="username" autocomplete="username" :autofocus="true" />
27-
<label for="username">{{ $t("dialogs.login.username") }}</label>
28-
</FloatLabel>
29-
</div>
30-
<div class="inline-flex flex-col gap-2 px-9">
31-
<FloatLabel variant="on">
32-
<InputPassword id="password" v-model="password" @keydown.enter="login" autocomplete="current-password" />
33-
<label for="password">{{ $t("dialogs.login.password") }}</label>
34-
</FloatLabel>
35-
<Message v-if="invalidPassword" severity="error">{{ $t("dialogs.login.unknown_invalid") }}</Message>
36-
</div>
37-
<div class="px-9 text-muted-color text-right font-semibold">Lychee <span class="text-primary-500" v-if="is_se_enabled">SE</span></div>
38-
<div class="flex items-center mt-9">
39-
<Button @click="closeCallback" severity="secondary" class="w-full font-bold border-none rounded-none rounded-bl-xl shrink">
40-
{{ $t("dialogs.button.cancel") }}
41-
</Button>
42-
<Button @click="login" severity="contrast" class="w-full font-bold border-none rounded-none rounded-br-xl shrink">
43-
{{ $t("dialogs.login.signin") }}
44-
</Button>
45-
</div>
46-
</form>
4+
<LoginForm :close-callback="closeCallback" @logged-in="emits('logged-in')" />
475
</template>
486
</Dialog>
497
</template>
508

519
<script setup lang="ts">
52-
import { ref } from "vue";
53-
import FloatLabel from "primevue/floatlabel";
54-
import Button from "primevue/button";
5510
import Dialog from "primevue/dialog";
56-
import Message from "primevue/message";
57-
import AuthService from "@/services/auth-service";
58-
import InputText from "@/components/forms/basic/InputText.vue";
59-
import InputPassword from "@/components/forms/basic/InputPassword.vue";
60-
import { useAuthStore } from "@/stores/Auth";
61-
import AlbumService from "@/services/album-service";
62-
import { useLycheeStateStore } from "@/stores/LycheeState";
6311
import { storeToRefs } from "pinia";
6412
import { useTogglablesStateStore } from "@/stores/ModalsState";
13+
import LoginForm from "../forms/auth/LoginForm.vue";
6514
6615
const emits = defineEmits<{
6716
"logged-in": [];
6817
}>();
6918
70-
type OauthProvider = {
71-
url: string;
72-
icon: string;
73-
provider: App.Enum.OauthProvidersType;
74-
};
75-
76-
const username = ref("");
77-
const password = ref("");
78-
const authStore = useAuthStore();
7919
const togglableStore = useTogglablesStateStore();
80-
const lycheeStore = useLycheeStateStore();
81-
const { is_se_enabled } = storeToRefs(lycheeStore);
82-
const { is_login_open, is_webauthn_open } = storeToRefs(togglableStore);
83-
const invalidPassword = ref(false);
84-
85-
const oauths = ref<OauthProvider[] | undefined>(undefined);
86-
87-
function login() {
88-
AuthService.login(username.value, password.value)
89-
.then(() => {
90-
is_login_open.value = false;
91-
authStore.setUser(null);
92-
invalidPassword.value = false;
93-
AlbumService.clearCache();
94-
emits("logged-in");
95-
})
96-
.catch((e: any) => {
97-
if (e.response && e.response.status === 401) {
98-
invalidPassword.value = true;
99-
}
100-
});
101-
}
102-
103-
authStore.getOauthData().then((data) => {
104-
oauths.value = data;
105-
});
106-
107-
function openWebAuthn() {
108-
is_login_open.value = false;
109-
is_webauthn_open.value = true;
110-
username.value = "";
111-
password.value = "";
112-
invalidPassword.value = false;
113-
}
20+
const { is_login_open } = storeToRefs(togglableStore);
11421
</script>

resources/js/composables/album/albumRefresher.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ALL } from "@/config/constants";
22
import AlbumService from "@/services/album-service";
3-
import { AuthStore } from "@/stores/Auth";
4-
import { computed, Ref, ref } from "vue";
3+
import { type AuthStore } from "@/stores/Auth";
4+
import { computed, type Ref, ref } from "vue";
55
import { type SplitData, useSplitter } from "./splitter";
66

77
const { spliter, merge } = useSplitter();

resources/js/composables/album/albumsRefresher.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import AlbumService from "@/services/album-service";
2-
import { AuthStore } from "@/stores/Auth";
3-
import { LycheeStateStore } from "@/stores/LycheeState";
4-
import { computed, ref, Ref } from "vue";
2+
import { type AuthStore } from "@/stores/Auth";
3+
import { type LycheeStateStore } from "@/stores/LycheeState";
4+
import { computed, ref, type Ref } from "vue";
55
import { type SplitData, useSplitter } from "./splitter";
6+
import { type Router } from "vue-router";
67

7-
export function useAlbumsRefresher(auth: AuthStore, lycheeStore: LycheeStateStore, isLoginOpen: Ref<boolean>) {
8+
export function useAlbumsRefresher(auth: AuthStore, lycheeStore: LycheeStateStore, isLoginOpen: Ref<boolean>, router: Router) {
89
const { spliter } = useSplitter();
910
const user = ref<App.Http.Resources.Models.UserResource | undefined>(undefined);
1011
const isKeybindingsHelpOpen = ref(false);
@@ -45,12 +46,20 @@ export function useAlbumsRefresher(auth: AuthStore, lycheeStore: LycheeStateStor
4546
rootConfig.value = data.data.config;
4647
rootRights.value = data.data.rights;
4748

48-
if (albums.value.length === 0 && smartAlbums.value.length === 0 && sharedAlbums.value.length === 0) {
49-
isLoginOpen.value = true;
49+
// If we are not logged in and there are no albums, we redirect to the login page.
50+
if (
51+
(auth.user?.id === undefined || auth.user?.id === null) &&
52+
albums.value.length === 0 &&
53+
smartAlbums.value.length === 0 &&
54+
sharedAlbums.value.length === 0
55+
) {
56+
router.push({ name: "login" });
5057
}
5158
})
5259
.catch((error) => {
5360
// We are required to login :)
61+
// We use the modal instead of the login page to avoid the redirect back.
62+
// Once logged in, we just refresh the page.
5463
if (error.response && error.response.status === 401) {
5564
isLoginOpen.value = true;
5665
console.error("require login");

resources/js/composables/album/photoActions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import AlbumService from "@/services/album-service";
22
import PhotoService from "@/services/photo-service";
3-
import { LycheeStateStore } from "@/stores/LycheeState";
3+
import { type LycheeStateStore } from "@/stores/LycheeState";
44
import { trans } from "laravel-vue-i18n";
5-
import { ToastServiceMethods } from "primevue/toastservice";
6-
import { Ref } from "vue";
5+
import { type ToastServiceMethods } from "primevue/toastservice";
6+
import { type Ref } from "vue";
77

88
export function usePhotoActions(
99
photo: Ref<App.Http.Resources.Models.PhotoResource | undefined>,

resources/js/composables/album/scrollable.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { TogglablesStateStore } from "@/stores/ModalsState";
2-
import { Ref } from "vue";
1+
import { type TogglablesStateStore } from "@/stores/ModalsState";
2+
import { type Ref } from "vue";
33

44
export function useScrollable(toggleableStore: TogglablesStateStore, path: Ref<string>) {
55
function onScroll() {

resources/js/composables/album/treeOperations.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { set } from "@vueuse/core";
21
import { trans } from "laravel-vue-i18n";
3-
import { ToastServiceMethods } from "primevue/toastservice";
2+
import { type ToastServiceMethods } from "primevue/toastservice";
43
import { sprintf } from "sprintf-js";
5-
import { computed, ref, Ref } from "vue";
4+
import { ref, type Ref } from "vue";
65

76
export type Augmented = {
87
prefix: string;

resources/js/composables/album/uploadEvents.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { shouldIgnoreKeystroke } from "@/utils/keybindings-utils";
2-
import { Ref } from "vue";
2+
import { type Ref } from "vue";
33

44
export type Uploadable = {
55
file: File;

resources/js/menus/LeftMenu.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
<Drawer v-model:visible="left_menu_open" :pt:content:class="'flex flex-col justify-start gap-10'">
33
<template #header>
44
<div v-if="user?.id === null && isGallery" class="flex items-center gap-2 text-muted-color hover:text-primary-400 w-full">
5-
<Button class="border-none w-full text-left justify-start" severity="secondary" text @click="togglableStore.toggleLogin()">
5+
<RouterLink :to="{ name: 'login' }">
66
<i class="pi pi-sign-in text-lg mr-2" />
77
{{ $t("left-menu.login") }}
8-
</Button>
8+
</RouterLink>
99
</div>
1010
<div v-else class="flex items-center gap-2 text-muted-color hover:text-primary-400">
1111
<router-link v-if="!isGallery" v-slot="{ href, navigate }" :to="{ name: 'gallery' }" custom>

resources/js/router/routes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const Jobs = () => import("@/views/Jobs.vue");
1818
const FixTree = () => import("@/views/FixTree.vue");
1919
const DuplicatesFinder = () => import("@/views/DuplicatesFinder.vue");
2020
const Changelogs = () => import("@/views/ChangeLogs.vue");
21+
const LoginPage = () => import("@/views/LoginPage.vue");
2122

2223
const routes_ = [
2324
{
@@ -120,6 +121,11 @@ const routes_ = [
120121
path: "/changelogs",
121122
component: Changelogs,
122123
},
124+
{
125+
name: "login",
126+
path: "/login",
127+
component: LoginPage,
128+
},
123129
];
124130

125131
if (import.meta.env.MODE === "development" && import.meta.env.VITE_LOCAL_DEV === "true") {

0 commit comments

Comments
 (0)