mirror of
https://github.com/wireadmin/wireadmin
synced 2025-06-26 18:28:06 +00:00
fix and complete auth ui
This commit is contained in:
parent
ece632f7bb
commit
cfaa98fea9
@ -31,13 +31,13 @@ function remove_duplicate_env() {
|
|||||||
mv "$temp_file" "$file"
|
mv "$temp_file" "$file"
|
||||||
}
|
}
|
||||||
|
|
||||||
touch /app/.env.local
|
touch /app/.env
|
||||||
chmod 400 /app/.env.local
|
chmod 400 /app/.env
|
||||||
|
|
||||||
|
|
||||||
if ! grep -q "NEXTAUTH_SECRET" /app/.env.local; then
|
if ! grep -q "AUTH_SECRET" /app/.env; then
|
||||||
cat <<EOF >>/app/.env.local
|
cat <<EOF >>/app/.env
|
||||||
NEXTAUTH_SECRET=$(openssl rand -base64 32)
|
AUTH_SECRET=$(openssl rand -base64 32)
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -46,8 +46,8 @@ fi
|
|||||||
# the .env.local
|
# the .env.local
|
||||||
if [ -n "$UI_PASSWORD" ]; then
|
if [ -n "$UI_PASSWORD" ]; then
|
||||||
ui_password_hex=$(echo -n "$UI_PASSWORD" | xxd -ps -u)
|
ui_password_hex=$(echo -n "$UI_PASSWORD" | xxd -ps -u)
|
||||||
sed -e '/^HASHED_PASSWORD=/d' /app/.env.local
|
sed -e '/^HASHED_PASSWORD=/d' /app/.env
|
||||||
cat <<EOF >>/app/.env.local
|
cat <<EOF >>/app/.env
|
||||||
HASHED_PASSWORD=$ui_password_hex
|
HASHED_PASSWORD=$ui_password_hex
|
||||||
EOF
|
EOF
|
||||||
unset UI_PASSWORD
|
unset UI_PASSWORD
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"printWidth": 100,
|
"printWidth": 120,
|
||||||
"jsxBracketSameLine": true,
|
"jsxBracketSameLine": true,
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
|
|||||||
BIN
web/bun.lockb
BIN
web/bun.lockb
Binary file not shown.
@ -1,73 +1,74 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 210 20% 98%;
|
||||||
--foreground: 224 71.4% 4.1%;
|
--foreground: 224 71.4% 4.1%;
|
||||||
|
|
||||||
--muted: 220 14.3% 95.9%;
|
--muted: 220 14.3% 95.9%;
|
||||||
--muted-foreground: 220 8.9% 46.1%;
|
--muted-foreground: 220 8.9% 46.1%;
|
||||||
|
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 224 71.4% 4.1%;
|
--popover-foreground: 224 71.4% 4.1%;
|
||||||
|
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 224 71.4% 4.1%;
|
--card-foreground: 224 71.4% 4.1%;
|
||||||
|
|
||||||
--border: 220 13% 91%;
|
--border: 220 13% 91%;
|
||||||
--input: 220 13% 91%;
|
--input: 220 13% 91%;
|
||||||
|
|
||||||
--primary: 220.9 39.3% 11%;
|
--primary: 358 72% 31%;
|
||||||
--primary-foreground: 210 20% 98%;
|
--primary-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--secondary: 220 14.3% 95.9%;
|
--secondary: 220 14.3% 95.9%;
|
||||||
--secondary-foreground: 220.9 39.3% 11%;
|
--secondary-foreground: 220.9 39.3% 11%;
|
||||||
|
|
||||||
--accent: 220 14.3% 95.9%;
|
--accent: 220 14.3% 95.9%;
|
||||||
--accent-foreground: 220.9 39.3% 11%;
|
--accent-foreground: 220.9 39.3% 11%;
|
||||||
|
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 210 20% 98%;
|
--destructive-foreground: 210 20% 98%;
|
||||||
|
|
||||||
--ring: 224 71.4% 4.1%;
|
/*--ring: 224 71.4% 4.1%;*/
|
||||||
|
--ring: var(--primary);
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 224 71.4% 4.1%;
|
--background: 224 71.4% 4.1%;
|
||||||
--foreground: 210 20% 98%;
|
--foreground: 210 20% 98%;
|
||||||
|
|
||||||
--muted: 215 27.9% 16.9%;
|
--muted: 215 27.9% 16.9%;
|
||||||
--muted-foreground: 217.9 10.6% 64.9%;
|
--muted-foreground: 217.9 10.6% 64.9%;
|
||||||
|
|
||||||
--popover: 224 71.4% 4.1%;
|
--popover: 224 71.4% 4.1%;
|
||||||
--popover-foreground: 210 20% 98%;
|
--popover-foreground: 210 20% 98%;
|
||||||
|
|
||||||
--card: 224 71.4% 4.1%;
|
--card: 224 71.4% 4.1%;
|
||||||
--card-foreground: 210 20% 98%;
|
--card-foreground: 210 20% 98%;
|
||||||
|
|
||||||
--border: 215 27.9% 16.9%;
|
--border: 215 27.9% 16.9%;
|
||||||
--input: 215 27.9% 16.9%;
|
--input: 215 27.9% 16.9%;
|
||||||
|
|
||||||
--primary: 210 20% 98%;
|
--primary: 210 20% 98%;
|
||||||
--primary-foreground: 220.9 39.3% 11%;
|
--primary-foreground: 220.9 39.3% 11%;
|
||||||
|
|
||||||
--secondary: 215 27.9% 16.9%;
|
--secondary: 215 27.9% 16.9%;
|
||||||
--secondary-foreground: 210 20% 98%;
|
--secondary-foreground: 210 20% 98%;
|
||||||
|
|
||||||
--accent: 215 27.9% 16.9%;
|
--accent: 215 27.9% 16.9%;
|
||||||
--accent-foreground: 210 20% 98%;
|
--accent-foreground: 210 20% 98%;
|
||||||
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 210 20% 98%;
|
--destructive-foreground: 210 20% 98%;
|
||||||
|
|
||||||
--ring: 216 12.2% 83.9%;
|
--ring: 216 12.2% 83.9%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
@ -75,4 +76,37 @@
|
|||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
@apply text-4xl font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
@apply text-3xl font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
@apply text-2xl font-semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
@apply text-xl font-semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
@apply text-lg font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
@apply text-base font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,16 +1,10 @@
|
|||||||
import type { Handle } from '@sveltejs/kit';
|
import type { Handle } from '@sveltejs/kit';
|
||||||
import { verifyToken } from '$lib/auth';
|
import { verifyToken } from '$lib/auth';
|
||||||
|
import { HASHED_PASSWORD } from '$env/static/private';
|
||||||
|
|
||||||
export const handle: Handle = async ({ event, resolve }) => {
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
if (event.url.pathname.startsWith('/custom')) {
|
|
||||||
const resp = new Response('custom response');
|
|
||||||
resp.headers.set('content-type', 'text/plain');
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auth_exception = ['/api/health', '/login'];
|
if (!!HASHED_PASSWORD && !AUTH_EXCEPTION.includes(event.url.pathname)) {
|
||||||
|
|
||||||
if (!auth_exception.includes(event.url.pathname)) {
|
|
||||||
const token = event.cookies.get('authorization');
|
const token = event.cookies.get('authorization');
|
||||||
const redirect = new Response(null, { status: 302, headers: { location: '/login' } });
|
const redirect = new Response(null, { status: 302, headers: { location: '/login' } });
|
||||||
|
|
||||||
@ -26,9 +20,17 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.url.pathname === '/login') {
|
||||||
|
console.log('handle', 'already logged in');
|
||||||
|
return new Response(null, { status: 302, headers: { location: '/' } });
|
||||||
|
}
|
||||||
|
|
||||||
const resp = await resolve(event);
|
const resp = await resolve(event);
|
||||||
|
|
||||||
console.log('handle', event.url.pathname, resp.status);
|
console.log('handle', event.url.pathname, resp.status);
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const AUTH_EXCEPTION = ['/api/health', '/login'];
|
||||||
|
|||||||
@ -2,13 +2,22 @@ import jwt from 'jsonwebtoken';
|
|||||||
import { AUTH_SECRET } from '$env/static/private';
|
import { AUTH_SECRET } from '$env/static/private';
|
||||||
|
|
||||||
export async function generateToken(): Promise<string> {
|
export async function generateToken(): Promise<string> {
|
||||||
return jwt.sign('OK', AUTH_SECRET, { expiresIn: '1d' });
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
return jwt.sign(
|
||||||
|
{
|
||||||
|
ok: true,
|
||||||
|
iat: now,
|
||||||
|
exp: now + 60 * 60,
|
||||||
|
},
|
||||||
|
AUTH_SECRET,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyToken(token: string): Promise<boolean> {
|
export async function verifyToken(token: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const decode = jwt.verify(token, AUTH_SECRET);
|
const decode = jwt.verify(token, AUTH_SECRET);
|
||||||
return !!(decode && decode === 'OK');
|
console.log('decode', decode);
|
||||||
|
return !!decode;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
5
web/src/lib/components/DotDivider.svelte
Normal file
5
web/src/lib/components/DotDivider.svelte
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let className: string | undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class:className> · </span>
|
||||||
21
web/src/lib/components/page/PageFooter.svelte
Normal file
21
web/src/lib/components/page/PageFooter.svelte
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script>
|
||||||
|
import DotDivider from '$lib/components/DotDivider.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<footer class={'flex items-center justify-center'}>
|
||||||
|
<a
|
||||||
|
href={'https://github.com/shahradelahi'}
|
||||||
|
title={'Find me on Github'}
|
||||||
|
class={'px-2 font-medium text-gray-400/80 hover:text-gray-500 text-xs'}
|
||||||
|
>
|
||||||
|
Made by <span class={'font-medium'}> Shahrad Elahi </span>
|
||||||
|
</a>
|
||||||
|
<DotDivider className="font-bold text-gray-400" />
|
||||||
|
<a
|
||||||
|
href={'https://github.com/shahradelahi/wireadmin'}
|
||||||
|
title={'Github'}
|
||||||
|
class={'px-2 font-medium text-gray-400/80 hover:text-gray-500 text-xs'}
|
||||||
|
>
|
||||||
|
Github
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
30
web/src/routes/login/+page.server.ts
Normal file
30
web/src/routes/login/+page.server.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { type Actions, fail } from '@sveltejs/kit';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
import { superValidate } from 'sveltekit-superforms/server';
|
||||||
|
import { formSchema } from './schema';
|
||||||
|
import { HASHED_PASSWORD } from '$env/static/private';
|
||||||
|
import { generateToken } from '$lib/auth';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = () => {
|
||||||
|
return {
|
||||||
|
form: superValidate(formSchema),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions: Actions = {
|
||||||
|
default: async ({ request, cookies }) => {
|
||||||
|
const data = await request.formData();
|
||||||
|
const password = data.get('password') ?? '';
|
||||||
|
|
||||||
|
if (HASHED_PASSWORD.toLowerCase() !== Buffer.from(password.toString()).toString('hex').toLowerCase()) {
|
||||||
|
console.warn('auth failed');
|
||||||
|
return fail(401, { message: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await generateToken();
|
||||||
|
cookies.set('authorization', token);
|
||||||
|
|
||||||
|
console.info('logged in.');
|
||||||
|
return { message: 'Success!' };
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -1 +1,40 @@
|
|||||||
<h1>Hello World!</h1>
|
<script lang="ts">
|
||||||
|
import PageFooter from '$lib/components/page/PageFooter.svelte';
|
||||||
|
import Logo from '$lib/assets/logo.png';
|
||||||
|
import * as Form from '$lib/components/ui/form';
|
||||||
|
import { formSchema, type FormSchema } from './schema';
|
||||||
|
import type { SuperValidated } from 'sveltekit-superforms';
|
||||||
|
|
||||||
|
export let form: SuperValidated<FormSchema>;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={'w-full min-h-screen flex justify-center px-2 md:px-6 py-2'}>
|
||||||
|
<div class={'w-full mx-auto max-w-3xl flex flex-col items-center gap-y-3.5'}>
|
||||||
|
<header class={'flex items-center gap-x-2 text-3xl font-medium py-4'}>
|
||||||
|
<img src={Logo} alt="WireAdmin" width="40" height="40" />
|
||||||
|
<h1>WireAdmin</h1>
|
||||||
|
</header>
|
||||||
|
<main class={'py-4'}>
|
||||||
|
<div class="w-full bg-white rounded-lg shadow-sm">
|
||||||
|
<Form.Root method="POST" {form} schema={formSchema} let:config class="p-4 space-y-8">
|
||||||
|
<div class="w-full flex items-center justify-center">
|
||||||
|
<div class="w-16 aspect-square flex items-center justify-center rounded-full bg-gray-200">
|
||||||
|
<i class="fas fa-user text-primary text-2xl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form.Field {config} name="password">
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>Password</Form.Label>
|
||||||
|
<Form.Input type="password" autocomplete="off" />
|
||||||
|
<Form.Validation />
|
||||||
|
</Form.Item>
|
||||||
|
</Form.Field>
|
||||||
|
|
||||||
|
<Form.Button class="w-full">Sign In</Form.Button>
|
||||||
|
</Form.Root>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<PageFooter />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
6
web/src/routes/login/schema.ts
Normal file
6
web/src/routes/login/schema.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const formSchema = z.object({
|
||||||
|
password: z.string(),
|
||||||
|
});
|
||||||
|
export type FormSchema = typeof formSchema;
|
||||||
Loading…
Reference in New Issue
Block a user