Implement password change flow
This commit is contained in:
@ -0,0 +1,67 @@
|
|||||||
|
{% extends "/dashboard/_base.html" %} {% block content %}
|
||||||
|
<div class="min-h-screen bg-gray-100 flex items-center justify-center p-4">
|
||||||
|
<div class="w-full max-w-xl p-6 space-y-8 bg-white rounded-lg shadow sm:p-8 dark:bg-gray-800">
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Change Password
|
||||||
|
</h2>
|
||||||
|
{% if errors | list %}
|
||||||
|
<div class="flex p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
|
||||||
|
<svg class="shrink-0 inline w-4 h-4 me-3 mt-[2px]" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">Danger</span>
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">Error changing password:</span>
|
||||||
|
<ul class="mt-1.5 list-disc list-inside">
|
||||||
|
{% for error in errors %}
|
||||||
|
<li> {{ error }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form class="mt-8 space-y-6" action="/password" method="POST" id="password-change-form">
|
||||||
|
<input hidden type="text" name="username" value="{{ user.display_name }}" autocomplete="username">
|
||||||
|
<div>
|
||||||
|
<label for="current_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Current password</label>
|
||||||
|
<input type="password" name="current_password" id="current_password" placeholder="••••••••" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required autocomplete="current-password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div hx-target="this" hx-swap="outerHTML">
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">New password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
id="password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
|
||||||
|
required
|
||||||
|
autocomplete="new-password"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="confirm_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Confirm New Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="confirm_password"
|
||||||
|
id="confirm-password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
|
||||||
|
required
|
||||||
|
autocomplete="new-password"
|
||||||
|
hx-post="/password/validate-confirm"
|
||||||
|
hx-include="[name='password']"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<button type="submit" class="w-full px-5 py-3 text-base font-medium text-center text-white rounded-lg bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Change password</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
<div hx-target="this" hx-swap="outerHTML">
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block mb-2 text-sm font-medium text-red-900 dark:text-white">New password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
id="password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
value="{{ password }}"
|
||||||
|
class="bg-red-50 border border-red-500 text-red-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-red-700 dark:border-red-600 dark:placeholder-red-400 dark:text-white dark:focus:ring-red-500 dark:focus:border-red-500"
|
||||||
|
required
|
||||||
|
autocomplete="new-password"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="confirm_password" class="block mb-2 text-sm font-medium text-red-900 dark:text-white">Confirm New Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="confirm_password"
|
||||||
|
id="confirm-password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
class="bg-red-50 border border-red-500 text-red-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-red-700 dark:border-red-600 dark:placeholder-red-400 dark:text-white dark:focus:ring-red-500 dark:focus:border-red-500"
|
||||||
|
required
|
||||||
|
value="{{ confirm_password }}"
|
||||||
|
autocomplete="new-password"
|
||||||
|
hx-post="/password/validate-confirm"
|
||||||
|
hx-include="[name='password']"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-2 text-sm text-red-600 dark:text-red-500"><span class="font-medium">Oops!</span> Passwords do not match!</p>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<button type="submit" class="w-full px-5 py-3 text-base font-medium text-center text-white rounded-lg bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800 cursor-not-allowed" disabled>Change password</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
{% extends "/dashboard/_base.html" %} {% block content %}
|
||||||
|
<div class="min-h-screen bg-gray-100 flex items-center justify-center p-4">
|
||||||
|
<div class="text-center xl:max-w-4xl">
|
||||||
|
<h1 class="mb-3 text-2xl font-bold leading-tight text-gray-900 sm:text-4xl lg:text-5xl dark:text-white">Password Changed</h1>
|
||||||
|
<p class="mb-5 text-base font-normal text-gray-500 md:text-lg dark:text-gray-400">Your password was changed sucessfully. Next time you log in, use your new password.</p>
|
||||||
|
<a href="/dashboard" class="text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center mr-3 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
|
||||||
|
<svg class="mr-2 -ml-1 w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"></path></svg>
|
||||||
|
Go back to the dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
<div hx-target="this" hx-swap="outerHTML">
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block mb-2 text-sm font-medium text-green-900 dark:text-white">New password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
id="password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
value="{{ password }}"
|
||||||
|
class="bg-green-50 border border-green-500 text-green-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-green-700 dark:border-green-600 dark:placeholder-green-400 dark:text-white dark:focus:ring-green-500 dark:focus:border-green-500"
|
||||||
|
required
|
||||||
|
autocomplete="new-password"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="confirm_password" class="block mb-2 text-sm font-medium text-green-900 dark:text-white">Confirm New Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="confirm_password"
|
||||||
|
id="confirm-password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
class="bg-green-50 border border-green-500 text-green-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-green-700 dark:border-green-600 dark:placeholder-green-400 dark:text-white dark:focus:ring-green-500 dark:focus:border-green-500"
|
||||||
|
required
|
||||||
|
value="{{ confirm_password }}"
|
||||||
|
autocomplete="new-password"
|
||||||
|
hx-post="/password/validate-confirm"
|
||||||
|
hx-include="[name='password']"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<button type="submit" class="w-full px-5 py-3 text-base font-medium text-center text-white rounded-lg bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Change password</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -94,7 +94,7 @@
|
|||||||
{% if user.local %}
|
{% if user.local %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="/password"
|
||||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
>Change Password</a
|
>Change Password</a
|
||||||
|
|||||||
@ -3,11 +3,17 @@
|
|||||||
# pyright: reportUnusedFunction=false
|
# pyright: reportUnusedFunction=false
|
||||||
import logging
|
import logging
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, Form, Request
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sshecret_admin.auth import LocalUserInfo
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sshecret_admin.auth import LocalUserInfo, authenticate_user_async
|
||||||
|
from sshecret_admin.auth.authentication import hash_password
|
||||||
|
from sshecret_admin.frontend.exceptions import RedirectException
|
||||||
from sshecret_admin.services import AdminBackend
|
from sshecret_admin.services import AdminBackend
|
||||||
|
from starlette.datastructures import URL
|
||||||
|
|
||||||
|
from sshecret.backend.models import Operation
|
||||||
|
|
||||||
from ..dependencies import FrontendDependencies
|
from ..dependencies import FrontendDependencies
|
||||||
|
|
||||||
@ -25,6 +31,14 @@ class StatsView(BaseModel):
|
|||||||
audit_events: int = 0
|
audit_events: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordChangeForm(BaseModel):
|
||||||
|
"""Password change form data."""
|
||||||
|
|
||||||
|
current_password: str
|
||||||
|
password: str
|
||||||
|
confirm_password: str
|
||||||
|
|
||||||
|
|
||||||
async def get_stats(admin: AdminBackend) -> StatsView:
|
async def get_stats(admin: AdminBackend) -> StatsView:
|
||||||
"""Get stats for the frontpage."""
|
"""Get stats for the frontpage."""
|
||||||
clients = await admin.get_clients()
|
clients = await admin.get_clients()
|
||||||
@ -75,4 +89,102 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app.get("/password")
|
||||||
|
async def get_change_password(
|
||||||
|
request: Request,
|
||||||
|
current_user: Annotated[LocalUserInfo, Depends(dependencies.get_user_info)],
|
||||||
|
):
|
||||||
|
"""Render Change password site."""
|
||||||
|
if not current_user.local:
|
||||||
|
LOG.debug("User tried to change password, but is not a local user.")
|
||||||
|
return RedirectException(to=URL("/"))
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request,
|
||||||
|
"change_password/index.html.j2",
|
||||||
|
{
|
||||||
|
"page_title": "Change Password",
|
||||||
|
"user": current_user,
|
||||||
|
"errors": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post("/password")
|
||||||
|
async def change_password(
|
||||||
|
request: Request,
|
||||||
|
current_user: Annotated[LocalUserInfo, Depends(dependencies.get_user_info)],
|
||||||
|
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||||
|
session: Annotated[AsyncSession, Depends(dependencies.get_async_session)],
|
||||||
|
passwd_form: Annotated[PasswordChangeForm, Form()],
|
||||||
|
):
|
||||||
|
"""Change password."""
|
||||||
|
errors: list[str] = []
|
||||||
|
user = await authenticate_user_async(
|
||||||
|
session, current_user.display_name, passwd_form.current_password
|
||||||
|
)
|
||||||
|
new_password_matches = passwd_form.password == passwd_form.confirm_password
|
||||||
|
if not user:
|
||||||
|
errors.append("Invalid current password entered")
|
||||||
|
if not new_password_matches:
|
||||||
|
errors.append("Passwords do not match")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request,
|
||||||
|
"change_password/index.html.j2",
|
||||||
|
{
|
||||||
|
"page_title": "Change Password",
|
||||||
|
"user": current_user,
|
||||||
|
"errors": errors,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert user is not None
|
||||||
|
new_password_hash = hash_password(passwd_form.password)
|
||||||
|
user.hashed_password = new_password_hash
|
||||||
|
session.add(user)
|
||||||
|
await session.commit()
|
||||||
|
origin = "UNKNOWN"
|
||||||
|
if request.client:
|
||||||
|
origin = request.client.host
|
||||||
|
await admin.write_audit_message(
|
||||||
|
Operation.UPDATE,
|
||||||
|
"User changed their password",
|
||||||
|
origin,
|
||||||
|
username=user.username,
|
||||||
|
)
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request,
|
||||||
|
"change_password/success.html.j2",
|
||||||
|
{
|
||||||
|
"page_title": "Change Password success",
|
||||||
|
"user": current_user,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post("/password/validate-confirm")
|
||||||
|
async def validate_password_match(
|
||||||
|
request: Request,
|
||||||
|
password: Annotated[str, Form()],
|
||||||
|
confirm_password: Annotated[str, Form()],
|
||||||
|
):
|
||||||
|
"""Validate password matches."""
|
||||||
|
valid = "/change_password/valid_password.html.j2"
|
||||||
|
invalid = "/change_password/invalid_password.html.j2"
|
||||||
|
template = valid
|
||||||
|
if password != confirm_password:
|
||||||
|
template = invalid
|
||||||
|
|
||||||
|
LOG.info("Password matches: %r", (password == confirm_password))
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request,
|
||||||
|
template,
|
||||||
|
{
|
||||||
|
"password": password,
|
||||||
|
"confirm_password": confirm_password,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
@ -25,11 +25,13 @@
|
|||||||
--color-orange-800: oklch(47% 0.157 37.304);
|
--color-orange-800: oklch(47% 0.157 37.304);
|
||||||
--color-lime-400: oklch(84.1% 0.238 128.85);
|
--color-lime-400: oklch(84.1% 0.238 128.85);
|
||||||
--color-lime-500: oklch(76.8% 0.233 130.85);
|
--color-lime-500: oklch(76.8% 0.233 130.85);
|
||||||
|
--color-green-50: oklch(98.2% 0.018 155.826);
|
||||||
--color-green-100: oklch(96.2% 0.044 156.743);
|
--color-green-100: oklch(96.2% 0.044 156.743);
|
||||||
--color-green-200: oklch(92.5% 0.084 155.995);
|
--color-green-200: oklch(92.5% 0.084 155.995);
|
||||||
--color-green-400: oklch(79.2% 0.209 151.711);
|
--color-green-400: oklch(79.2% 0.209 151.711);
|
||||||
--color-green-500: oklch(72.3% 0.219 149.579);
|
--color-green-500: oklch(72.3% 0.219 149.579);
|
||||||
--color-green-600: oklch(62.7% 0.194 149.214);
|
--color-green-600: oklch(62.7% 0.194 149.214);
|
||||||
|
--color-green-700: oklch(52.7% 0.154 150.069);
|
||||||
--color-green-800: oklch(44.8% 0.119 151.328);
|
--color-green-800: oklch(44.8% 0.119 151.328);
|
||||||
--color-green-900: oklch(39.3% 0.095 152.535);
|
--color-green-900: oklch(39.3% 0.095 152.535);
|
||||||
--color-emerald-500: oklch(69.6% 0.17 162.48);
|
--color-emerald-500: oklch(69.6% 0.17 162.48);
|
||||||
@ -413,9 +415,6 @@
|
|||||||
.m-361 {
|
.m-361 {
|
||||||
margin: calc(var(--spacing) * 361);
|
margin: calc(var(--spacing) * 361);
|
||||||
}
|
}
|
||||||
.mx-2 {
|
|
||||||
margin-inline: calc(var(--spacing) * 2);
|
|
||||||
}
|
|
||||||
.mx-3 {
|
.mx-3 {
|
||||||
margin-inline: calc(var(--spacing) * 3);
|
margin-inline: calc(var(--spacing) * 3);
|
||||||
}
|
}
|
||||||
@ -443,12 +442,6 @@
|
|||||||
.my-10 {
|
.my-10 {
|
||||||
margin-block: calc(var(--spacing) * 10);
|
margin-block: calc(var(--spacing) * 10);
|
||||||
}
|
}
|
||||||
.my-\[0\.5rem\] {
|
|
||||||
margin-block: 0.5rem;
|
|
||||||
}
|
|
||||||
.my-\[1rem\] {
|
|
||||||
margin-block: 1rem;
|
|
||||||
}
|
|
||||||
.my-auto {
|
.my-auto {
|
||||||
margin-block: auto;
|
margin-block: auto;
|
||||||
}
|
}
|
||||||
@ -500,6 +493,9 @@
|
|||||||
.mt-8 {
|
.mt-8 {
|
||||||
margin-top: calc(var(--spacing) * 8);
|
margin-top: calc(var(--spacing) * 8);
|
||||||
}
|
}
|
||||||
|
.mt-\[2px\] {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
.-mr-1 {
|
.-mr-1 {
|
||||||
margin-right: calc(var(--spacing) * -1);
|
margin-right: calc(var(--spacing) * -1);
|
||||||
}
|
}
|
||||||
@ -593,9 +589,6 @@
|
|||||||
.box-border {
|
.box-border {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.box-content {
|
|
||||||
box-sizing: content-box;
|
|
||||||
}
|
|
||||||
.block {
|
.block {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@ -779,9 +772,6 @@
|
|||||||
.w-full {
|
.w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.w-max {
|
|
||||||
width: max-content;
|
|
||||||
}
|
|
||||||
.max-w-2xl {
|
.max-w-2xl {
|
||||||
max-width: var(--container-2xl);
|
max-width: var(--container-2xl);
|
||||||
}
|
}
|
||||||
@ -794,9 +784,6 @@
|
|||||||
.max-w-lg {
|
.max-w-lg {
|
||||||
max-width: var(--container-lg);
|
max-width: var(--container-lg);
|
||||||
}
|
}
|
||||||
.max-w-max {
|
|
||||||
max-width: max-content;
|
|
||||||
}
|
|
||||||
.max-w-md {
|
.max-w-md {
|
||||||
max-width: var(--container-md);
|
max-width: var(--container-md);
|
||||||
}
|
}
|
||||||
@ -879,12 +866,18 @@
|
|||||||
.animate-spin {
|
.animate-spin {
|
||||||
animation: var(--animate-spin);
|
animation: var(--animate-spin);
|
||||||
}
|
}
|
||||||
|
.cursor-not-allowed {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.resize {
|
.resize {
|
||||||
resize: both;
|
resize: both;
|
||||||
}
|
}
|
||||||
|
.list-inside {
|
||||||
|
list-style-position: inside;
|
||||||
|
}
|
||||||
.list-disc {
|
.list-disc {
|
||||||
list-style-type: disc;
|
list-style-type: disc;
|
||||||
}
|
}
|
||||||
@ -1218,6 +1211,9 @@
|
|||||||
.border-green-100 {
|
.border-green-100 {
|
||||||
border-color: var(--color-green-100);
|
border-color: var(--color-green-100);
|
||||||
}
|
}
|
||||||
|
.border-green-500 {
|
||||||
|
border-color: var(--color-green-500);
|
||||||
|
}
|
||||||
.border-orange-100 {
|
.border-orange-100 {
|
||||||
border-color: var(--color-orange-100);
|
border-color: var(--color-orange-100);
|
||||||
}
|
}
|
||||||
@ -1230,6 +1226,9 @@
|
|||||||
.border-red-300 {
|
.border-red-300 {
|
||||||
border-color: var(--color-red-300);
|
border-color: var(--color-red-300);
|
||||||
}
|
}
|
||||||
|
.border-red-500 {
|
||||||
|
border-color: var(--color-red-500);
|
||||||
|
}
|
||||||
.border-red-600 {
|
.border-red-600 {
|
||||||
border-color: var(--color-red-600);
|
border-color: var(--color-red-600);
|
||||||
}
|
}
|
||||||
@ -1296,9 +1295,6 @@
|
|||||||
.bg-gray-200 {
|
.bg-gray-200 {
|
||||||
background-color: var(--color-gray-200);
|
background-color: var(--color-gray-200);
|
||||||
}
|
}
|
||||||
.bg-gray-700 {
|
|
||||||
background-color: var(--color-gray-700);
|
|
||||||
}
|
|
||||||
.bg-gray-800 {
|
.bg-gray-800 {
|
||||||
background-color: var(--color-gray-800);
|
background-color: var(--color-gray-800);
|
||||||
}
|
}
|
||||||
@ -1311,6 +1307,9 @@
|
|||||||
background-color: color-mix(in oklab, var(--color-gray-900) 50%, transparent);
|
background-color: color-mix(in oklab, var(--color-gray-900) 50%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.bg-green-50 {
|
||||||
|
background-color: var(--color-green-50);
|
||||||
|
}
|
||||||
.bg-green-100 {
|
.bg-green-100 {
|
||||||
background-color: var(--color-green-100);
|
background-color: var(--color-green-100);
|
||||||
}
|
}
|
||||||
@ -1320,9 +1319,6 @@
|
|||||||
.bg-green-400 {
|
.bg-green-400 {
|
||||||
background-color: var(--color-green-400);
|
background-color: var(--color-green-400);
|
||||||
}
|
}
|
||||||
.bg-indigo-200 {
|
|
||||||
background-color: var(--color-indigo-200);
|
|
||||||
}
|
|
||||||
.bg-indigo-600 {
|
.bg-indigo-600 {
|
||||||
background-color: var(--color-indigo-600);
|
background-color: var(--color-indigo-600);
|
||||||
}
|
}
|
||||||
@ -1389,9 +1385,6 @@
|
|||||||
.bg-teal-100 {
|
.bg-teal-100 {
|
||||||
background-color: var(--color-teal-100);
|
background-color: var(--color-teal-100);
|
||||||
}
|
}
|
||||||
.bg-teal-700 {
|
|
||||||
background-color: var(--color-teal-700);
|
|
||||||
}
|
|
||||||
.bg-transparent {
|
.bg-transparent {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
@ -1455,9 +1448,6 @@
|
|||||||
.px-6 {
|
.px-6 {
|
||||||
padding-inline: calc(var(--spacing) * 6);
|
padding-inline: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
.px-\[1\.125rem\] {
|
|
||||||
padding-inline: 1.125rem;
|
|
||||||
}
|
|
||||||
.py-0\.5 {
|
.py-0\.5 {
|
||||||
padding-block: calc(var(--spacing) * 0.5);
|
padding-block: calc(var(--spacing) * 0.5);
|
||||||
}
|
}
|
||||||
@ -1747,6 +1737,9 @@
|
|||||||
.text-green-800 {
|
.text-green-800 {
|
||||||
color: var(--color-green-800);
|
color: var(--color-green-800);
|
||||||
}
|
}
|
||||||
|
.text-green-900 {
|
||||||
|
color: var(--color-green-900);
|
||||||
|
}
|
||||||
.text-orange-800 {
|
.text-orange-800 {
|
||||||
color: var(--color-orange-800);
|
color: var(--color-orange-800);
|
||||||
}
|
}
|
||||||
@ -1771,6 +1764,9 @@
|
|||||||
.text-red-800 {
|
.text-red-800 {
|
||||||
color: var(--color-red-800);
|
color: var(--color-red-800);
|
||||||
}
|
}
|
||||||
|
.text-red-900 {
|
||||||
|
color: var(--color-red-900);
|
||||||
|
}
|
||||||
.text-rose-500 {
|
.text-rose-500 {
|
||||||
color: var(--color-rose-500);
|
color: var(--color-rose-500);
|
||||||
}
|
}
|
||||||
@ -3260,6 +3256,11 @@
|
|||||||
border-color: var(--color-green-500);
|
border-color: var(--color-green-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dark\:border-green-600 {
|
||||||
|
&:where(.dark, .dark *) {
|
||||||
|
border-color: var(--color-green-600);
|
||||||
|
}
|
||||||
|
}
|
||||||
.dark\:border-orange-300 {
|
.dark\:border-orange-300 {
|
||||||
&:where(.dark, .dark *) {
|
&:where(.dark, .dark *) {
|
||||||
border-color: var(--color-orange-300);
|
border-color: var(--color-orange-300);
|
||||||
@ -3280,6 +3281,11 @@
|
|||||||
border-color: var(--color-red-500);
|
border-color: var(--color-red-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dark\:border-red-600 {
|
||||||
|
&:where(.dark, .dark *) {
|
||||||
|
border-color: var(--color-red-600);
|
||||||
|
}
|
||||||
|
}
|
||||||
.dark\:border-red-800 {
|
.dark\:border-red-800 {
|
||||||
&:where(.dark, .dark *) {
|
&:where(.dark, .dark *) {
|
||||||
border-color: var(--color-red-800);
|
border-color: var(--color-red-800);
|
||||||
@ -3313,6 +3319,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dark\:bg-green-700 {
|
||||||
|
&:where(.dark, .dark *) {
|
||||||
|
background-color: var(--color-green-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
.dark\:bg-orange-400 {
|
.dark\:bg-orange-400 {
|
||||||
&:where(.dark, .dark *) {
|
&:where(.dark, .dark *) {
|
||||||
background-color: var(--color-orange-400);
|
background-color: var(--color-orange-400);
|
||||||
@ -3333,6 +3344,11 @@
|
|||||||
background-color: var(--color-primary-900);
|
background-color: var(--color-primary-900);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dark\:bg-red-700 {
|
||||||
|
&:where(.dark, .dark *) {
|
||||||
|
background-color: var(--color-red-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
.dark\:bg-red-900 {
|
.dark\:bg-red-900 {
|
||||||
&:where(.dark, .dark *) {
|
&:where(.dark, .dark *) {
|
||||||
background-color: var(--color-red-900);
|
background-color: var(--color-red-900);
|
||||||
@ -3455,6 +3471,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dark\:placeholder-green-400 {
|
||||||
|
&:where(.dark, .dark *) {
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--color-green-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:placeholder-red-400 {
|
||||||
|
&:where(.dark, .dark *) {
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--color-red-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.dark\:ring-offset-gray-700 {
|
.dark\:ring-offset-gray-700 {
|
||||||
&:where(.dark, .dark *) {
|
&:where(.dark, .dark *) {
|
||||||
--tw-ring-offset-color: var(--color-gray-700);
|
--tw-ring-offset-color: var(--color-gray-700);
|
||||||
@ -3603,6 +3633,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dark\:focus\:border-green-500 {
|
||||||
|
&:where(.dark, .dark *) {
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--color-green-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.dark\:focus\:border-primary-500 {
|
.dark\:focus\:border-primary-500 {
|
||||||
&:where(.dark, .dark *) {
|
&:where(.dark, .dark *) {
|
||||||
&:focus {
|
&:focus {
|
||||||
@ -3610,6 +3647,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dark\:focus\:border-red-500 {
|
||||||
|
&:where(.dark, .dark *) {
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--color-red-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.dark\:focus\:bg-gray-700 {
|
.dark\:focus\:bg-gray-700 {
|
||||||
&:where(.dark, .dark *) {
|
&:where(.dark, .dark *) {
|
||||||
&:focus {
|
&:focus {
|
||||||
@ -3645,6 +3689,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dark\:focus\:ring-green-500 {
|
||||||
|
&:where(.dark, .dark *) {
|
||||||
|
&:focus {
|
||||||
|
--tw-ring-color: var(--color-green-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.dark\:focus\:ring-primary-500 {
|
.dark\:focus\:ring-primary-500 {
|
||||||
&:where(.dark, .dark *) {
|
&:where(.dark, .dark *) {
|
||||||
&:focus {
|
&:focus {
|
||||||
@ -3673,6 +3724,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dark\:focus\:ring-red-500 {
|
||||||
|
&:where(.dark, .dark *) {
|
||||||
|
&:focus {
|
||||||
|
--tw-ring-color: var(--color-red-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.dark\:focus\:ring-red-800 {
|
.dark\:focus\:ring-red-800 {
|
||||||
&:where(.dark, .dark *) {
|
&:where(.dark, .dark *) {
|
||||||
&:focus {
|
&:focus {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: [
|
||||||
"./src/sshecret_admin/templates/**/*.html",
|
"./src/sshecret_admin/templates/**/*.html",
|
||||||
|
"./src/sshecret_admin/templates/**/*.html.j2",
|
||||||
"./src/sshecret_admin/static/**/*.js",
|
"./src/sshecret_admin/static/**/*.js",
|
||||||
],
|
],
|
||||||
safelist: [
|
safelist: [
|
||||||
|
|||||||
Reference in New Issue
Block a user