feat: Basic password reset form
This commit is contained in:
32
src/hooks.ts
32
src/hooks.ts
@@ -16,22 +16,24 @@ export const handle = handleSession(
|
|||||||
async function ({ event, resolve }) {
|
async function ({ event, resolve }) {
|
||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
let gitlabToken = event.locals.cookies.gitlabToken;
|
if (event.locals.cookies) {
|
||||||
let ghToken = event.locals.cookies.ghToken;
|
let gitlabToken = event.locals.cookies.gitlabToken || null;
|
||||||
if (event.locals.cookies['kit.session']) {
|
let ghToken = event.locals.cookies.ghToken;
|
||||||
const { permission, teamId, userId } = await getUserDetails(event, false);
|
if (event.locals.cookies['kit.session']) {
|
||||||
const newSession = {
|
const { permission, teamId, userId } = await getUserDetails(event, false);
|
||||||
userId,
|
const newSession = {
|
||||||
teamId,
|
userId,
|
||||||
permission,
|
teamId,
|
||||||
isAdmin: permission === 'admin' || permission === 'owner',
|
permission,
|
||||||
expires: event.locals.session.data.expires,
|
isAdmin: permission === 'admin' || permission === 'owner',
|
||||||
gitlabToken,
|
expires: event.locals.session.data.expires,
|
||||||
ghToken
|
gitlabToken,
|
||||||
};
|
ghToken
|
||||||
|
};
|
||||||
|
|
||||||
if (JSON.stringify(event.locals.session.data) !== JSON.stringify(newSession)) {
|
if (JSON.stringify(event.locals.session.data) !== JSON.stringify(newSession)) {
|
||||||
event.locals.session.data = { ...newSession };
|
event.locals.session.data = { ...newSession };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -29,7 +29,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span
|
<div
|
||||||
class="relative"
|
class="relative"
|
||||||
on:mouseenter={() => showActions(true)}
|
on:mouseenter={() => showActions(true)}
|
||||||
on:mouseleave={() => showActions(false)}
|
on:mouseleave={() => showActions(false)}
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if actionsShow}
|
{#if actionsShow}
|
||||||
<div class="absolute top-0 right-0 mx-2 cursor-pointer text-warmGray-600 hover:text-white">
|
<div class="absolute top-0 right-0 m-3 cursor-pointer text-warmGray-600 hover:text-white">
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
{#if isPasswordField}
|
{#if isPasswordField}
|
||||||
<div on:click={() => (showPassword = !showPassword)}>
|
<div on:click={() => (showPassword = !showPassword)}>
|
||||||
@@ -142,4 +142,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</div>
|
||||||
|
@@ -11,7 +11,7 @@
|
|||||||
<span class="loader" />
|
<span class="loader" />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class=" main h-64 py-24 left-0 top-0 flex flex-wrap content-center mx-auto">
|
<div class="main h-64 py-24 left-0 top-0 flex flex-wrap content-center mx-auto">
|
||||||
<span class="loader" />
|
<span class="loader" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@@ -6,16 +6,17 @@ import { asyncExecShell, uniqueName } from '$lib/common';
|
|||||||
|
|
||||||
import * as db from '$lib/database';
|
import * as db from '$lib/database';
|
||||||
import { startCoolifyProxy } from '$lib/haproxy';
|
import { startCoolifyProxy } from '$lib/haproxy';
|
||||||
|
export async function hashPassword(password: string) {
|
||||||
export async function login({ email, password }) {
|
|
||||||
const saltRounds = 15;
|
const saltRounds = 15;
|
||||||
|
return bcrypt.hash(password, saltRounds);
|
||||||
|
}
|
||||||
|
export async function login({ email, password }) {
|
||||||
const users = await prisma.user.count();
|
const users = await prisma.user.count();
|
||||||
const userFound = await prisma.user.findUnique({
|
const userFound = await prisma.user.findUnique({
|
||||||
where: { email },
|
where: { email },
|
||||||
include: { teams: true, permission: true },
|
include: { teams: true, permission: true },
|
||||||
rejectOnNotFound: false
|
rejectOnNotFound: false
|
||||||
});
|
});
|
||||||
console.log(userFound);
|
|
||||||
// Registration disabled if database is not seeded properly
|
// Registration disabled if database is not seeded properly
|
||||||
const { isRegistrationEnabled, id } = await db.listSettings();
|
const { isRegistrationEnabled, id } = await db.listSettings();
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ export async function login({ email, password }) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
const hashedPassword = await hashPassword(password);
|
||||||
if (users === 0) {
|
if (users === 0) {
|
||||||
permission = 'owner';
|
permission = 'owner';
|
||||||
isAdmin = true;
|
isAdmin = true;
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
export const publicPaths = [
|
export const publicPaths = [
|
||||||
'/login',
|
'/login',
|
||||||
|
'/reset',
|
||||||
|
'/reset/password',
|
||||||
'/webhooks/success',
|
'/webhooks/success',
|
||||||
'/webhooks/github',
|
'/webhooks/github',
|
||||||
'/webhooks/github/install',
|
'/webhooks/github/install',
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
import type { Load } from '@sveltejs/kit';
|
import type { Load } from '@sveltejs/kit';
|
||||||
import { publicPaths } from '$lib/settings';
|
import { publicPaths } from '$lib/settings';
|
||||||
|
|
||||||
export const load: Load = async ({ fetch, url, params, session }) => {
|
export const load: Load = async ({ fetch, url, session }) => {
|
||||||
if (!session.userId && !publicPaths.includes(url.pathname)) {
|
if (!session.userId && !publicPaths.includes(url.pathname)) {
|
||||||
return {
|
return {
|
||||||
status: 302,
|
status: 302,
|
||||||
|
@@ -67,6 +67,7 @@
|
|||||||
class:text-stone-600={loading}
|
class:text-stone-600={loading}
|
||||||
class:bg-coollabs={!loading}>{loading ? 'Authenticating...' : 'Login'}</button
|
class:bg-coollabs={!loading}>{loading ? 'Authenticating...' : 'Login'}</button
|
||||||
>
|
>
|
||||||
|
<button on:click|preventDefault={() => goto('/reset')}>Reset password</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
26
src/routes/reset/index.json.ts
Normal file
26
src/routes/reset/index.json.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { RequestHandler } from '@sveltejs/kit';
|
||||||
|
import * as db from '$lib/database';
|
||||||
|
|
||||||
|
export const get: RequestHandler = async () => {
|
||||||
|
const users = await db.prisma.user.findMany({});
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
users
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export const post: RequestHandler = async (event) => {
|
||||||
|
const { secretKey } = await event.request.json();
|
||||||
|
if (secretKey !== process.env.COOLIFY_SECRET_KEY) {
|
||||||
|
return {
|
||||||
|
status: 500,
|
||||||
|
body: {
|
||||||
|
error: 'Invalid secret key.'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: 200
|
||||||
|
};
|
||||||
|
};
|
96
src/routes/reset/index.svelte
Normal file
96
src/routes/reset/index.svelte
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { get, post } from '$lib/api';
|
||||||
|
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||||
|
import { errorNotification } from '$lib/form';
|
||||||
|
import { toast } from '@zerodevx/svelte-toast';
|
||||||
|
|
||||||
|
let secretKey;
|
||||||
|
let password = false;
|
||||||
|
let users = [];
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
try {
|
||||||
|
await post(`/reset.json`, { secretKey });
|
||||||
|
password = true;
|
||||||
|
const data = await get('/reset.json');
|
||||||
|
users = data.users;
|
||||||
|
return;
|
||||||
|
} catch ({ error }) {
|
||||||
|
return errorNotification(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function resetPassword(user) {
|
||||||
|
try {
|
||||||
|
await post(`/reset/password.json`, { secretKey, user });
|
||||||
|
toast.push('Password reset done.');
|
||||||
|
return;
|
||||||
|
} catch ({ error }) {
|
||||||
|
return errorNotification(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="icons fixed top-0 left-0 m-3 cursor-pointer" on:click={() => goto('/')}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12" />
|
||||||
|
<line x1="5" y1="12" x2="11" y2="18" />
|
||||||
|
<line x1="5" y1="12" x2="11" y2="6" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="pb-10 pt-24 text-center text-4xl font-bold">Reset Password</div>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
{#if password}
|
||||||
|
<table class="mx-2 text-left">
|
||||||
|
<thead class="mb-2">
|
||||||
|
<tr>
|
||||||
|
<th class="px-2">Email</th>
|
||||||
|
<th>New password</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each users as user}
|
||||||
|
<tr>
|
||||||
|
<td class="px-2">{user.email}</td>
|
||||||
|
<td class="flex space-x-2">
|
||||||
|
<input
|
||||||
|
id="newPassword"
|
||||||
|
name="newPassword"
|
||||||
|
bind:value={user.newPassword}
|
||||||
|
placeholder="Super secure new password"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="mx-auto my-4 w-32 bg-coollabs hover:bg-coollabs-100"
|
||||||
|
on:click={() => resetPassword(user)}>Reset</button
|
||||||
|
></td
|
||||||
|
>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{:else}
|
||||||
|
<form class="flex flex-col" on:submit|preventDefault={handleSubmit}>
|
||||||
|
<div class="text-center text-2xl py-2 font-bold">Secret Key</div>
|
||||||
|
<CopyPasswordField
|
||||||
|
isPasswordField={true}
|
||||||
|
id="secretKey"
|
||||||
|
name="secretKey"
|
||||||
|
bind:value={secretKey}
|
||||||
|
placeholder="You can find it in ~/coolify/.env (COOLIFY_SECRET_KEY)"
|
||||||
|
/>
|
||||||
|
<button type="submit" class="bg-coollabs hover:bg-coollabs-100 mx-auto w-32 my-4"
|
||||||
|
>Submit</button
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
</div>
|
27
src/routes/reset/password.json.ts
Normal file
27
src/routes/reset/password.json.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type { RequestHandler } from '@sveltejs/kit';
|
||||||
|
import * as db from '$lib/database';
|
||||||
|
import { ErrorHandler, hashPassword } from '$lib/database';
|
||||||
|
|
||||||
|
export const post: RequestHandler = async (event) => {
|
||||||
|
const { secretKey, user } = await event.request.json();
|
||||||
|
if (secretKey !== process.env.COOLIFY_SECRET_KEY) {
|
||||||
|
return {
|
||||||
|
status: 500,
|
||||||
|
body: {
|
||||||
|
error: 'Invalid secret key.'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const hashedPassword = await hashPassword(user.newPassword);
|
||||||
|
await db.prisma.user.update({
|
||||||
|
where: { email: user.email },
|
||||||
|
data: { password: hashedPassword }
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
status: 200
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorHandler(error);
|
||||||
|
}
|
||||||
|
};
|
Reference in New Issue
Block a user