Compare commits
4 commits
1cf2619393
...
9d33093339
Author | SHA1 | Date | |
---|---|---|---|
9d33093339 | |||
26b32b40e2 | |||
5cdadc6158 | |||
39a3098a99 |
41 changed files with 834 additions and 456 deletions
|
@ -183,6 +183,63 @@ public class EmailAuthController(
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("forgot-password")]
|
||||||
|
public async Task<IActionResult> ForgotPasswordAsync([FromBody] EmailForgotPasswordRequest req)
|
||||||
|
{
|
||||||
|
CheckRequirements();
|
||||||
|
|
||||||
|
if (!req.Email.Contains('@'))
|
||||||
|
throw new ApiError.BadRequest("Email is invalid", "email", req.Email);
|
||||||
|
|
||||||
|
AuthMethod? authMethod = await db
|
||||||
|
.AuthMethods.Where(m => m.AuthType == AuthType.Email && m.RemoteId == req.Email)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (authMethod == null)
|
||||||
|
return NoContent();
|
||||||
|
|
||||||
|
string state = await keyCacheService.GenerateForgotPasswordStateAsync(
|
||||||
|
req.Email,
|
||||||
|
authMethod.UserId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (IsRateLimited())
|
||||||
|
return NoContent();
|
||||||
|
|
||||||
|
mailService.QueueResetPasswordEmail(req.Email, state);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("reset-password")]
|
||||||
|
public async Task<IActionResult> ResetPasswordAsync([FromBody] EmailResetPasswordRequest req)
|
||||||
|
{
|
||||||
|
ForgotPasswordState? state = await keyCacheService.GetForgotPasswordStateAsync(req.State);
|
||||||
|
if (state == null)
|
||||||
|
throw new ApiError.BadRequest("Unknown state", "state", req.State);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!await db
|
||||||
|
.AuthMethods.Where(m =>
|
||||||
|
m.AuthType == AuthType.Email
|
||||||
|
&& m.RemoteId == state.Email
|
||||||
|
&& m.UserId == state.UserId
|
||||||
|
)
|
||||||
|
.AnyAsync()
|
||||||
|
)
|
||||||
|
{
|
||||||
|
throw new ApiError.BadRequest("Invalid state");
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationUtils.Validate([("password", ValidationUtils.ValidatePassword(req.Password))]);
|
||||||
|
|
||||||
|
User user = await db.Users.FirstAsync(u => u.Id == state.UserId);
|
||||||
|
await authService.SetUserPasswordAsync(user, req.Password);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
mailService.QueuePasswordChangedEmail(state.Email);
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("add-account")]
|
[HttpPost("add-account")]
|
||||||
[Authorize("*")]
|
[Authorize("*")]
|
||||||
public async Task<IActionResult> AddEmailAddressAsync([FromBody] AddEmailAddressRequest req)
|
public async Task<IActionResult> AddEmailAddressAsync([FromBody] AddEmailAddressRequest req)
|
||||||
|
|
|
@ -59,4 +59,8 @@ public record EmailCallbackRequest(string State);
|
||||||
|
|
||||||
public record EmailChangePasswordRequest(string Current, string New);
|
public record EmailChangePasswordRequest(string Current, string New);
|
||||||
|
|
||||||
|
public record EmailForgotPasswordRequest(string Email);
|
||||||
|
|
||||||
|
public record EmailResetPasswordRequest(string State, string Password);
|
||||||
|
|
||||||
public record FediverseCallbackRequest(string Instance, string Code, string? State = null);
|
public record FediverseCallbackRequest(string Instance, string Code, string? State = null);
|
||||||
|
|
|
@ -28,7 +28,7 @@ public static class KeyCacheExtensions
|
||||||
CancellationToken ct = default
|
CancellationToken ct = default
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
string state = AuthUtils.RandomToken().Replace('+', '-').Replace('/', '_');
|
string state = AuthUtils.RandomToken();
|
||||||
await keyCacheService.SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10), ct);
|
await keyCacheService.SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10), ct);
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -51,8 +51,7 @@ public static class KeyCacheExtensions
|
||||||
CancellationToken ct = default
|
CancellationToken ct = default
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
// This state is used in links, not just as JSON values, so make it URL-safe
|
string state = AuthUtils.RandomToken();
|
||||||
string state = AuthUtils.RandomToken().Replace('+', '-').Replace('/', '_');
|
|
||||||
await keyCacheService.SetKeyAsync(
|
await keyCacheService.SetKeyAsync(
|
||||||
$"email_state:{state}",
|
$"email_state:{state}",
|
||||||
new RegisterEmailState(email, userId),
|
new RegisterEmailState(email, userId),
|
||||||
|
@ -112,11 +111,12 @@ public static class KeyCacheExtensions
|
||||||
public static async Task<ForgotPasswordState?> GetForgotPasswordStateAsync(
|
public static async Task<ForgotPasswordState?> GetForgotPasswordStateAsync(
|
||||||
this KeyCacheService keyCacheService,
|
this KeyCacheService keyCacheService,
|
||||||
string state,
|
string state,
|
||||||
|
bool delete = true,
|
||||||
CancellationToken ct = default
|
CancellationToken ct = default
|
||||||
) =>
|
) =>
|
||||||
await keyCacheService.GetKeyAsync<ForgotPasswordState>(
|
await keyCacheService.GetKeyAsync<ForgotPasswordState>(
|
||||||
$"forgot_password:{state}",
|
$"forgot_password:{state}",
|
||||||
true,
|
delete,
|
||||||
ct
|
ct
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,7 @@ public class CreateDataExportInvocable(
|
||||||
stream.Seek(0, SeekOrigin.Begin);
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
// Upload the file!
|
// Upload the file!
|
||||||
string filename = AuthUtils.RandomToken().Replace('+', '-').Replace('/', '_');
|
string filename = AuthUtils.RandomToken();
|
||||||
await objectStorageService.PutObjectAsync(
|
await objectStorageService.PutObjectAsync(
|
||||||
ExportPath(user.Id, filename),
|
ExportPath(user.Id, filename),
|
||||||
stream,
|
stream,
|
||||||
|
|
25
Foxnouns.Backend/Mailables/PasswordChangedMailable.cs
Normal file
25
Foxnouns.Backend/Mailables/PasswordChangedMailable.cs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
using Coravel.Mailer.Mail;
|
||||||
|
|
||||||
|
namespace Foxnouns.Backend.Mailables;
|
||||||
|
|
||||||
|
public class PasswordChangedMailable(Config config, PasswordChangedMailableView view)
|
||||||
|
: Mailable<PasswordChangedMailableView>
|
||||||
|
{
|
||||||
|
private string PlainText() =>
|
||||||
|
$"""
|
||||||
|
Your password has been changed using a "forgot password" link.
|
||||||
|
If this wasn't you, request a password reset immediately:
|
||||||
|
{view.BaseUrl}/auth/forgot-password
|
||||||
|
""";
|
||||||
|
|
||||||
|
public override void Build()
|
||||||
|
{
|
||||||
|
To(view.To)
|
||||||
|
.From(config.EmailAuth.From!)
|
||||||
|
.Subject("Your password has been changed")
|
||||||
|
.View("~/Views/Mail/PasswordChanged.cshtml", view)
|
||||||
|
.Text(PlainText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PasswordChangedMailableView : BaseView;
|
32
Foxnouns.Backend/Mailables/ResetPasswordMailable.cs
Normal file
32
Foxnouns.Backend/Mailables/ResetPasswordMailable.cs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
using Coravel.Mailer.Mail;
|
||||||
|
|
||||||
|
namespace Foxnouns.Backend.Mailables;
|
||||||
|
|
||||||
|
public class ResetPasswordMailable(Config config, ResetPasswordMailableView view)
|
||||||
|
: Mailable<ResetPasswordMailableView>
|
||||||
|
{
|
||||||
|
private string PlainText() =>
|
||||||
|
$"""
|
||||||
|
Somebody (hopefully you!) has requested a password reset.
|
||||||
|
You can use the following link to do this:
|
||||||
|
{view.BaseUrl}/auth/forgot-password/{view.Code}
|
||||||
|
Note that this link will expire in one hour.
|
||||||
|
|
||||||
|
If you weren't expecting this email, you don't have to do anything.
|
||||||
|
Your password can't be changed without the above link.
|
||||||
|
""";
|
||||||
|
|
||||||
|
public override void Build()
|
||||||
|
{
|
||||||
|
To(view.To)
|
||||||
|
.From(config.EmailAuth.From!)
|
||||||
|
.Subject("Reset your account's password")
|
||||||
|
.View("~/Views/Mail/ResetPassword.cshtml", view)
|
||||||
|
.Text(PlainText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ResetPasswordMailableView : BaseView
|
||||||
|
{
|
||||||
|
public required string Code { get; init; }
|
||||||
|
}
|
|
@ -63,6 +63,41 @@ public class MailService(ILogger logger, IMailer mailer, IQueue queue, Config co
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void QueueResetPasswordEmail(string to, string code)
|
||||||
|
{
|
||||||
|
_logger.Debug("Sending add email address email to {ToEmail}", to);
|
||||||
|
queue.QueueAsyncTask(async () =>
|
||||||
|
{
|
||||||
|
await SendEmailAsync(
|
||||||
|
to,
|
||||||
|
new ResetPasswordMailable(
|
||||||
|
config,
|
||||||
|
new ResetPasswordMailableView
|
||||||
|
{
|
||||||
|
BaseUrl = config.BaseUrl,
|
||||||
|
To = to,
|
||||||
|
Code = code,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void QueuePasswordChangedEmail(string to)
|
||||||
|
{
|
||||||
|
_logger.Debug("Sending add email address email to {ToEmail}", to);
|
||||||
|
queue.QueueAsyncTask(async () =>
|
||||||
|
{
|
||||||
|
await SendEmailAsync(
|
||||||
|
to,
|
||||||
|
new PasswordChangedMailable(
|
||||||
|
config,
|
||||||
|
new PasswordChangedMailableView { BaseUrl = config.BaseUrl, To = to }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async Task SendEmailAsync<T>(string to, Mailable<T> mailable)
|
private async Task SendEmailAsync<T>(string to, Mailable<T> mailable)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
@ -131,7 +131,12 @@ public static class AuthUtils
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string RandomToken(int bytes = 48) =>
|
public static string RandomToken(int bytes = 48) =>
|
||||||
Convert.ToBase64String(RandomNumberGenerator.GetBytes(bytes)).Trim('=');
|
Convert
|
||||||
|
.ToBase64String(RandomNumberGenerator.GetBytes(bytes))
|
||||||
|
.Trim('=')
|
||||||
|
// Make the token URL-safe
|
||||||
|
.Replace('+', '-')
|
||||||
|
.Replace('/', '_');
|
||||||
|
|
||||||
public const int MaxAuthMethodsPerType = 3; // Maximum of 3 Discord accounts, 3 emails, etc
|
public const int MaxAuthMethodsPerType = 3; // Maximum of 3 Discord accounts, 3 emails, etc
|
||||||
}
|
}
|
||||||
|
|
8
Foxnouns.Backend/Views/Mail/PasswordChanged.cshtml
Normal file
8
Foxnouns.Backend/Views/Mail/PasswordChanged.cshtml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
@model Foxnouns.Backend.Mailables.PasswordChangedMailableView
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Your password has been changed using a "forgot password" link.
|
||||||
|
If this wasn't you, please a password reset immediately:
|
||||||
|
<br />
|
||||||
|
<a href="@Model.BaseUrl/auth/forgot-password">@Model.BaseUrl/auth/forgot-password</a>
|
||||||
|
</p>
|
14
Foxnouns.Backend/Views/Mail/ResetPassword.cshtml
Normal file
14
Foxnouns.Backend/Views/Mail/ResetPassword.cshtml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
@model Foxnouns.Backend.Mailables.ResetPasswordMailableView
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Somebody (hopefully you!) has requested a password reset.
|
||||||
|
You can use the following link to do this:
|
||||||
|
<br />
|
||||||
|
<a href="@Model.BaseUrl/auth/forgot-password/@Model.Code">@Model.BaseUrl/auth/forgot-password/@Model.Code</a>
|
||||||
|
<br />
|
||||||
|
Note that this link will expire in one hour.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you weren't expecting this email, you don't have to do anything.
|
||||||
|
Your password can't be changed without the above link.
|
||||||
|
</p>
|
|
@ -30,4 +30,16 @@ export default ts.config(
|
||||||
{
|
{
|
||||||
ignores: ["build/", ".svelte-kit/", "dist/"],
|
ignores: ["build/", ".svelte-kit/", "dist/"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
varsIgnorePattern: "^_",
|
||||||
|
caughtErrorsIgnorePattern: "^_",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -12,29 +12,29 @@
|
||||||
"lint": "prettier --check . && eslint ."
|
"lint": "prettier --check . && eslint ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-node": "^5.2.9",
|
"@sveltejs/adapter-node": "^5.2.10",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.11.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/vite-plugin-svelte": "^4.0.3",
|
||||||
"@sveltestrap/sveltestrap": "^6.2.7",
|
"@sveltestrap/sveltestrap": "^6.2.7",
|
||||||
"@types/eslint": "^9.6.0",
|
"@types/eslint": "^9.6.1",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/sanitize-html": "^2.13.0",
|
"@types/sanitize-html": "^2.13.0",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"eslint": "^9.7.0",
|
"eslint": "^9.17.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-svelte": "^2.36.0",
|
"eslint-plugin-svelte": "^2.46.1",
|
||||||
"globals": "^15.0.0",
|
"globals": "^15.13.0",
|
||||||
"prettier": "^3.3.2",
|
"prettier": "^3.4.2",
|
||||||
"prettier-plugin-svelte": "^3.2.6",
|
"prettier-plugin-svelte": "^3.3.2",
|
||||||
"sass": "^1.81.0",
|
"sass": "^1.83.0",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.12.0",
|
||||||
"svelte-bootstrap-icons": "^3.1.1",
|
"svelte-bootstrap-icons": "^3.1.1",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.1.1",
|
||||||
"sveltekit-i18n": "^2.4.2",
|
"sveltekit-i18n": "^2.4.2",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.7.2",
|
||||||
"typescript-eslint": "^8.0.0",
|
"typescript-eslint": "^8.18.0",
|
||||||
"vite": "^5.0.3"
|
"vite": "^5.4.11"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee",
|
"packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -49,3 +49,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Give identicons a background distinguishable from the page
|
||||||
|
.identicon {
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
background-color: var(--bs-secondary-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
background-color: var(--bs-light-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-flag {
|
||||||
|
height: 1.5rem;
|
||||||
|
max-width: 200px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ export default function createCallbackLoader(
|
||||||
bodyFn?: (event: ServerLoadEvent) => Promise<unknown>,
|
bodyFn?: (event: ServerLoadEvent) => Promise<unknown>,
|
||||||
) {
|
) {
|
||||||
return async (event: ServerLoadEvent) => {
|
return async (event: ServerLoadEvent) => {
|
||||||
const { url, parent, fetch, cookies } = event;
|
const { parent, fetch, cookies } = event;
|
||||||
|
|
||||||
bodyFn ??= async ({ url }) => {
|
bodyFn ??= async ({ url }) => {
|
||||||
const code = url.searchParams.get("code") as string | null;
|
const code = url.searchParams.get("code") as string | null;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
export default class ApiError {
|
export default class ApiError {
|
||||||
raw?: RawApiError;
|
raw?: RawApiError;
|
||||||
code: ErrorCode;
|
code: ErrorCode;
|
||||||
|
|
|
@ -23,7 +23,7 @@ export type RequestArgs = {
|
||||||
/**
|
/**
|
||||||
* The body for this request, which will be serialized to JSON. Should be a plain JS object.
|
* The body for this request, which will be serialized to JSON. Should be a plain JS object.
|
||||||
*/
|
*/
|
||||||
body?: any;
|
body?: unknown;
|
||||||
/**
|
/**
|
||||||
* The fetch function to use. Should be passed in loader and action functions, but can be safely ignored for client-side requests.
|
* The fetch function to use. Should be passed in loader and action functions, but can be safely ignored for client-side requests.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -26,12 +26,4 @@
|
||||||
img {
|
img {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.identicon {
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
background-color: var(--bs-secondary-border-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
background-color: var(--bs-light-border-subtle);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -21,6 +21,8 @@
|
||||||
{#if value !== ""}
|
{#if value !== ""}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">{$t("edit-profile.preview")}</div>
|
<div class="card-header">{$t("edit-profile.preview")}</div>
|
||||||
|
<!-- bios are sanitized before being passed to @html and the allowed markdown is heavily restricted -->
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
<div class="card-body">{@html renderMarkdown(value)}</div>
|
<div class="card-body">{@html renderMarkdown(value)}</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -10,17 +10,9 @@
|
||||||
<span class="mx-2 my-1">
|
<span class="mx-2 my-1">
|
||||||
<img
|
<img
|
||||||
use:tippy={{ content: flag.description ?? flag.name }}
|
use:tippy={{ content: flag.description ?? flag.name }}
|
||||||
class="flag"
|
class="profile-flag"
|
||||||
src={flag.image_url ?? DEFAULT_FLAG}
|
src={flag.image_url ?? DEFAULT_FLAG}
|
||||||
alt={flag.description ?? flag.name}
|
alt={flag.description ?? flag.name}
|
||||||
/>
|
/>
|
||||||
{flag.name}
|
{flag.name}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<style>
|
|
||||||
.flag {
|
|
||||||
height: 1.5rem;
|
|
||||||
max-width: 200px;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -5,14 +5,16 @@
|
||||||
import ProfileLink from "./ProfileLink.svelte";
|
import ProfileLink from "./ProfileLink.svelte";
|
||||||
import ProfileFlag from "./ProfileFlag.svelte";
|
import ProfileFlag from "./ProfileFlag.svelte";
|
||||||
import Avatar from "$components/Avatar.svelte";
|
import Avatar from "$components/Avatar.svelte";
|
||||||
|
import TimeOffset from "./TimeOffset.svelte";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
name: string;
|
name: string;
|
||||||
profile: User | Member;
|
profile: User | Member;
|
||||||
lazyLoadAvatar?: boolean;
|
lazyLoadAvatar?: boolean;
|
||||||
|
offset?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
let { name, profile, lazyLoadAvatar }: Props = $props();
|
let { name, profile, lazyLoadAvatar, offset }: Props = $props();
|
||||||
|
|
||||||
// renderMarkdown sanitizes the output HTML for us
|
// renderMarkdown sanitizes the output HTML for us
|
||||||
let bio = $derived(renderMarkdown(profile.bio));
|
let bio = $derived(renderMarkdown(profile.bio));
|
||||||
|
@ -45,8 +47,11 @@
|
||||||
{:else}
|
{:else}
|
||||||
<h2>{name}</h2>
|
<h2>{name}</h2>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if offset}<TimeOffset {offset} />{/if}
|
||||||
{#if bio}
|
{#if bio}
|
||||||
<hr />
|
<hr />
|
||||||
|
<!-- bios are sanitized before being passed to @html and the allowed markdown is heavily restricted -->
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
<p>{@html bio}</p>
|
<p>{@html bio}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { DateTime, FixedOffsetZone } from "luxon";
|
||||||
|
import Clock from "svelte-bootstrap-icons/lib/Clock.svelte";
|
||||||
|
|
||||||
|
type Props = { offset: number };
|
||||||
|
let { offset }: Props = $props();
|
||||||
|
|
||||||
|
let { currentTime, timezone } = $derived.by(() => {
|
||||||
|
const zone = FixedOffsetZone.instance(offset / 60);
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentTime: DateTime.now().setZone(zone).toLocaleString(DateTime.TIME_SIMPLE),
|
||||||
|
timezone: zone.formatOffset(DateTime.now().toUnixInteger(), "narrow"),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Clock aria-hidden />
|
||||||
|
{currentTime} <span class="text-body-secondary">(UTC{timezone})</span>
|
|
@ -35,7 +35,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<FormStatusMarker {form} />
|
<FormStatusMarker {form} successMessage={$t("auth.password-changed-hint")} />
|
||||||
<h4>Change password</h4>
|
<h4>Change password</h4>
|
||||||
<form method="POST" action="?/password">
|
<form method="POST" action="?/password">
|
||||||
<div class="mb-1">
|
<div class="mb-1">
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { ErrorCode } from "$api/error";
|
import { ErrorCode } from "$api/error";
|
||||||
import type { Modifier } from "sveltekit-i18n";
|
import type { Modifier } from "sveltekit-i18n";
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
type TranslateFn = (key: string, payload?: any, props?: Modifier.Props<{}> | undefined) => any;
|
type TranslateFn = (key: string, payload?: any, props?: Modifier.Props<{}> | undefined) => any;
|
||||||
|
|
||||||
export default function errorDescription(t: TranslateFn, code: ErrorCode): string {
|
export default function errorDescription(t: TranslateFn, code: ErrorCode): string {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { PUBLIC_LANGUAGE } from "$env/static/public";
|
import { PUBLIC_LANGUAGE } from "$env/static/public";
|
||||||
import i18n, { type Config } from "sveltekit-i18n";
|
import i18n, { type Config } from "sveltekit-i18n";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const config: Config<any> = {
|
const config: Config<any> = {
|
||||||
initLocale: PUBLIC_LANGUAGE,
|
initLocale: PUBLIC_LANGUAGE,
|
||||||
fallbackLocale: "en",
|
fallbackLocale: "en",
|
||||||
|
|
|
@ -61,7 +61,13 @@
|
||||||
"add-email-address": "Add email address",
|
"add-email-address": "Add email address",
|
||||||
"no-email-addresses": "You haven't linked any email addresses yet.",
|
"no-email-addresses": "You haven't linked any email addresses yet.",
|
||||||
"check-inbox-for-link-hint": "Check your inbox for a link!",
|
"check-inbox-for-link-hint": "Check your inbox for a link!",
|
||||||
"successful-link-email": "Your account has successfully been linked to the following email address:"
|
"successful-link-email": "Your account has successfully been linked to the following email address:",
|
||||||
|
"reset-password-button": "Reset password",
|
||||||
|
"log-in-forgot-password-link": "Forgot your password?",
|
||||||
|
"log-in-sign-up-link": "Sign up with email",
|
||||||
|
"forgot-password-title": "Forgot password",
|
||||||
|
"reset-password-title": "Reset password",
|
||||||
|
"password-changed-hint": "Your password has been changed!"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"bad-request-header": "Something was wrong with your input",
|
"bad-request-header": "Something was wrong with your input",
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
<OwnProfileNotice editLink="/settings/profile" />
|
<OwnProfileNotice editLink="/settings/profile" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<ProfileHeader name="@{data.user.username}" profile={data.user} />
|
<ProfileHeader name="@{data.user.username}" profile={data.user} offset={data.user.utc_offset} />
|
||||||
<ProfileFields profile={data.user} {allPreferences} />
|
<ProfileFields profile={data.user} {allPreferences} />
|
||||||
|
|
||||||
{#if data.members.length > 0}
|
{#if data.members.length > 0}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { apiRequest, fastRequest } from "$api";
|
||||||
|
import ApiError from "$api/error.js";
|
||||||
|
import type { AuthUrls } from "$api/models/auth";
|
||||||
|
import log from "$lib/log.js";
|
||||||
|
import { redirect } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export const load = async ({ parent, fetch }) => {
|
||||||
|
const { meUser } = await parent();
|
||||||
|
if (meUser) redirect(303, `/@${meUser.username}`);
|
||||||
|
|
||||||
|
const urls = await apiRequest<AuthUrls>("POST", "/auth/urls", { fetch, isInternal: true });
|
||||||
|
if (!urls.email_enabled) redirect(303, "/");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
default: async ({ request, fetch }) => {
|
||||||
|
const data = await request.formData();
|
||||||
|
const email = data.get("email") as string;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fastRequest("POST", "/auth/email/forgot-password", {
|
||||||
|
body: { email },
|
||||||
|
isInternal: true,
|
||||||
|
fetch,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ok: true, error: null };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ApiError) return { ok: false, error: e.obj };
|
||||||
|
log.error("error sending forget password email:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import FormStatusMarker from "$components/editor/FormStatusMarker.svelte";
|
||||||
|
import { t } from "$lib/i18n";
|
||||||
|
import type { ActionData } from "./$types";
|
||||||
|
|
||||||
|
type Props = { form: ActionData };
|
||||||
|
let { form }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{$t("auth.forgot-password-title")} • pronouns.cc</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="mx-auto w-lg-50">
|
||||||
|
<h3>{$t("auth.forgot-password-title")}</h3>
|
||||||
|
|
||||||
|
<FormStatusMarker {form} successMessage={$t("auth.check-inbox-for-link-hint")} />
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<label for="email" class="form-label">{$t("auth.log-in-form-email-label")}</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
placeholder="me@example.com"
|
||||||
|
class="form-control mb-2"
|
||||||
|
/>
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary">{$t("auth.reset-password-button")}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { apiRequest, fastRequest } from "$api";
|
||||||
|
import ApiError, { ErrorCode, type RawApiError } from "$api/error";
|
||||||
|
import type { AuthUrls } from "$api/models";
|
||||||
|
import log from "$lib/log";
|
||||||
|
import { redirect } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export const load = async ({ params, parent, fetch }) => {
|
||||||
|
const { meUser } = await parent();
|
||||||
|
if (meUser) redirect(303, `/@${meUser.username}`);
|
||||||
|
|
||||||
|
const urls = await apiRequest<AuthUrls>("POST", "/auth/urls", { fetch, isInternal: true });
|
||||||
|
if (!urls.email_enabled) redirect(303, "/");
|
||||||
|
|
||||||
|
return { state: params.code };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
default: async ({ request, fetch }) => {
|
||||||
|
const data = await request.formData();
|
||||||
|
const state = data.get("state") as string;
|
||||||
|
const password = data.get("password") as string;
|
||||||
|
const password2 = data.get("confirm-password") as string;
|
||||||
|
if (password !== password2) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
status: 400,
|
||||||
|
message: "Passwords don't match",
|
||||||
|
code: ErrorCode.BadRequest,
|
||||||
|
} as RawApiError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fastRequest("POST", "/auth/email/reset-password", {
|
||||||
|
body: { state, password },
|
||||||
|
isInternal: true,
|
||||||
|
fetch,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ok: true, error: null };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ApiError) return { ok: false, error: e.obj };
|
||||||
|
log.error("error resetting password:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,41 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import FormStatusMarker from "$components/editor/FormStatusMarker.svelte";
|
||||||
|
import { t } from "$lib/i18n";
|
||||||
|
import type { ActionData, PageData } from "./$types";
|
||||||
|
|
||||||
|
type Props = { data: PageData; form: ActionData };
|
||||||
|
let { data, form }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{$t("auth.reset-password-title")} • pronouns.cc</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="mx-auto w-lg-50">
|
||||||
|
<h3>{$t("auth.reset-password-title")}</h3>
|
||||||
|
|
||||||
|
<FormStatusMarker {form} successMessage={$t("auth.password-changed-hint")} />
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="state" readonly value={data.state} />
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="password" class="form-label">{$t("auth.log-in-form-password-label")}</label>
|
||||||
|
<input required type="password" id="password" name="password" class="form-control" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="confirm-password" class="form-label">{$t("auth.confirm-password-label")}</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
id="confirm-password"
|
||||||
|
name="confirm-password"
|
||||||
|
class="form-control"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary">{$t("auth.reset-password-button")}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -2,7 +2,6 @@
|
||||||
import type { ActionData, PageData } from "./$types";
|
import type { ActionData, PageData } from "./$types";
|
||||||
import { t } from "$lib/i18n";
|
import { t } from "$lib/i18n";
|
||||||
import { enhance } from "$app/forms";
|
import { enhance } from "$app/forms";
|
||||||
import { Button, ButtonGroup, Input, InputGroup } from "@sveltestrap/sveltestrap";
|
|
||||||
import ErrorAlert from "$components/ErrorAlert.svelte";
|
import ErrorAlert from "$components/ErrorAlert.svelte";
|
||||||
|
|
||||||
type Props = { data: PageData; form: ActionData };
|
type Props = { data: PageData; form: ActionData };
|
||||||
|
@ -21,29 +20,34 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{#if data.urls.email_enabled}
|
{#if data.urls.email_enabled}
|
||||||
<div class="col col-md mb-4">
|
<div class="col-md mb-4">
|
||||||
<h2>{$t("auth.log-in-form-title")}</h2>
|
<h2>{$t("auth.log-in-form-title")}</h2>
|
||||||
<form method="POST" action="?/login" use:enhance>
|
<form method="POST" action="?/login" use:enhance>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label" for="email">{$t("auth.log-in-form-email-label")}</label>
|
<label class="form-label" for="email">{$t("auth.log-in-form-email-label")}</label>
|
||||||
<Input type="email" id="email" name="email" placeholder="me@example.com" />
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
placeholder="me@example.com"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label" for="password">{$t("auth.log-in-form-password-label")}</label>
|
<label class="form-label" for="password">{$t("auth.log-in-form-password-label")}</label>
|
||||||
<Input type="password" id="password" name="password" />
|
<input class="form-control" type="password" id="password" name="password" />
|
||||||
</div>
|
</div>
|
||||||
<ButtonGroup>
|
<button class="btn btn-primary" type="submit">{$t("auth.log-in-button")}</button>
|
||||||
<Button type="submit" color="primary">{$t("auth.log-in-button")}</Button>
|
|
||||||
<a class="btn btn-secondary" href="/auth/register">
|
|
||||||
{$t("auth.register-with-email-button")}
|
|
||||||
</a>
|
|
||||||
</ButtonGroup>
|
|
||||||
</form>
|
</form>
|
||||||
|
<p class="mt-2">
|
||||||
|
<a href="/auth/register">{$t("auth.log-in-sign-up-link")}</a> •
|
||||||
|
<a href="/auth/forgot-password">{$t("auth.log-in-forgot-password-link")}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="col-lg-3"></div>
|
<div class="col-lg-3"></div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="col col-md">
|
<div class="col-md">
|
||||||
<h3>{$t("auth.log-in-3rd-party-header")}</h3>
|
<h3>{$t("auth.log-in-3rd-party-header")}</h3>
|
||||||
<p>{$t("auth.log-in-3rd-party-desc")}</p>
|
<p>{$t("auth.log-in-3rd-party-desc")}</p>
|
||||||
<form method="POST" action="?/fediToggle" use:enhance>
|
<form method="POST" action="?/fediToggle" use:enhance>
|
||||||
|
@ -71,19 +75,20 @@
|
||||||
{#if form?.showFediBox}
|
{#if form?.showFediBox}
|
||||||
<h4 class="mt-4">{$t("auth.log-in-with-the-fediverse")}</h4>
|
<h4 class="mt-4">{$t("auth.log-in-with-the-fediverse")}</h4>
|
||||||
<form method="POST" action="?/fedi" use:enhance>
|
<form method="POST" action="?/fedi" use:enhance>
|
||||||
<InputGroup>
|
<div class="input-group">
|
||||||
<Input
|
<input
|
||||||
|
class="form-control"
|
||||||
name="instance"
|
name="instance"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$t("auth.log-in-with-fediverse-instance-placeholder")}
|
placeholder={$t("auth.log-in-with-fediverse-instance-placeholder")}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" color="secondary">{$t("auth.log-in-button")}</Button>
|
<button class="btn btn-secondary" type="submit">{$t("auth.log-in-button")}</button>
|
||||||
</InputGroup>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
{$t("auth.log-in-with-fediverse-error-blurb")}
|
{$t("auth.log-in-with-fediverse-error-blurb")}
|
||||||
<Button formaction="?/fediForceRefresh" type="submit" color="link">
|
<button class="btn btn-link" formaction="?/fediForceRefresh" type="submit">
|
||||||
{$t("auth.log-in-with-fediverse-force-refresh-button")}
|
{$t("auth.log-in-with-fediverse-force-refresh-button")}
|
||||||
</Button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ActionData, PageData } from "./$types";
|
import type { ActionData } from "./$types";
|
||||||
import { t } from "$lib/i18n";
|
import { t } from "$lib/i18n";
|
||||||
import { enhance } from "$app/forms";
|
import { enhance } from "$app/forms";
|
||||||
import { Button, Input, InputGroup } from "@sveltestrap/sveltestrap";
|
import { Button, Input, InputGroup } from "@sveltestrap/sveltestrap";
|
||||||
import FormStatusMarker from "$components/editor/FormStatusMarker.svelte";
|
import FormStatusMarker from "$components/editor/FormStatusMarker.svelte";
|
||||||
|
|
||||||
type Props = { data: PageData; form: ActionData };
|
type Props = { form: ActionData };
|
||||||
let { data, form }: Props = $props();
|
let { form }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ActionData, PageData } from "./$types";
|
import type { ActionData, PageData } from "./$types";
|
||||||
import { t } from "$lib/i18n";
|
import { t } from "$lib/i18n";
|
||||||
import { Button, FormGroup, Icon, Input, InputGroup, Label } from "@sveltestrap/sveltestrap";
|
import { Button, FormGroup, Icon, Input, InputGroup } from "@sveltestrap/sveltestrap";
|
||||||
import Avatar from "$components/Avatar.svelte";
|
import Avatar from "$components/Avatar.svelte";
|
||||||
import { firstErrorFor } from "$api/error";
|
import { firstErrorFor } from "$api/error";
|
||||||
import Error from "$components/Error.svelte";
|
import Error from "$components/Error.svelte";
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { fastRequest } from "$api";
|
import { fastRequest } from "$api";
|
||||||
import ApiError, { ErrorCode, type RawApiError } from "$api/error.js";
|
import ApiError, { ErrorCode, type RawApiError } from "$api/error.js";
|
||||||
import log from "$lib/log.js";
|
import log from "$lib/log.js";
|
||||||
import { redirect } from "@sveltejs/kit";
|
|
||||||
|
|
||||||
export const load = async ({ parent }) => {
|
export const load = async ({ parent }) => {
|
||||||
const { user } = await parent();
|
const { user } = await parent();
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { t } from "$lib/i18n";
|
import { t } from "$lib/i18n";
|
||||||
import { Button } from "@sveltestrap/sveltestrap";
|
|
||||||
import type { ActionData, PageData } from "./$types";
|
import type { ActionData, PageData } from "./$types";
|
||||||
import FormStatusMarker from "$components/editor/FormStatusMarker.svelte";
|
import FormStatusMarker from "$components/editor/FormStatusMarker.svelte";
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import type { ActionData, PageData } from "./$types";
|
import type { ActionData, PageData } from "./$types";
|
||||||
import ErrorAlert from "$components/ErrorAlert.svelte";
|
|
||||||
import { Icon } from "@sveltestrap/sveltestrap";
|
|
||||||
import { t } from "$lib/i18n";
|
import { t } from "$lib/i18n";
|
||||||
import { enhance } from "$app/forms";
|
import { enhance } from "$app/forms";
|
||||||
import FormStatusMarker from "$components/editor/FormStatusMarker.svelte";
|
import FormStatusMarker from "$components/editor/FormStatusMarker.svelte";
|
||||||
|
|
|
@ -4,7 +4,7 @@ import type { PrideFlag } from "$api/models/user";
|
||||||
import log from "$lib/log";
|
import log from "$lib/log";
|
||||||
import { encode } from "base64-arraybuffer";
|
import { encode } from "base64-arraybuffer";
|
||||||
|
|
||||||
export const load = async ({ url, fetch, cookies }) => {
|
export const load = async ({ fetch, cookies }) => {
|
||||||
const resp = await apiRequest<PrideFlag[]>("GET", "/users/@me/flags", { fetch, cookies });
|
const resp = await apiRequest<PrideFlag[]>("GET", "/users/@me/flags", { fetch, cookies });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -82,7 +82,7 @@ export const actions = {
|
||||||
},
|
},
|
||||||
options: async ({ params, request, fetch, cookies }) => {
|
options: async ({ params, request, fetch, cookies }) => {
|
||||||
const body = await request.formData();
|
const body = await request.formData();
|
||||||
let unlisted = !!body.get("unlisted");
|
const unlisted = !!body.get("unlisted");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fastRequest("PATCH", `/users/@me/members/${params.id}`, {
|
await fastRequest("PATCH", `/users/@me/members/${params.id}`, {
|
||||||
|
|
|
@ -5,12 +5,11 @@
|
||||||
import { apiRequest, fastRequest } from "$api";
|
import { apiRequest, fastRequest } from "$api";
|
||||||
import ApiError from "$api/error";
|
import ApiError from "$api/error";
|
||||||
import log from "$lib/log";
|
import log from "$lib/log";
|
||||||
import { Icon, InputGroup } from "@sveltestrap/sveltestrap";
|
import { InputGroup } from "@sveltestrap/sveltestrap";
|
||||||
import InfoCircleFill from "svelte-bootstrap-icons/lib/InfoCircleFill.svelte";
|
import InfoCircleFill from "svelte-bootstrap-icons/lib/InfoCircleFill.svelte";
|
||||||
import { t } from "$lib/i18n";
|
import { t } from "$lib/i18n";
|
||||||
import AvatarEditor from "$components/editor/AvatarEditor.svelte";
|
import AvatarEditor from "$components/editor/AvatarEditor.svelte";
|
||||||
import ErrorAlert from "$components/ErrorAlert.svelte";
|
import ErrorAlert from "$components/ErrorAlert.svelte";
|
||||||
import NoscriptWarning from "$components/editor/NoscriptWarning.svelte";
|
|
||||||
import FormStatusMarker from "$components/editor/FormStatusMarker.svelte";
|
import FormStatusMarker from "$components/editor/FormStatusMarker.svelte";
|
||||||
import SidEditor from "$components/editor/SidEditor.svelte";
|
import SidEditor from "$components/editor/SidEditor.svelte";
|
||||||
import BioEditor from "$components/editor/BioEditor.svelte";
|
import BioEditor from "$components/editor/BioEditor.svelte";
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const actions = {
|
||||||
let timezone = body.get("timezone") as string | null;
|
let timezone = body.get("timezone") as string | null;
|
||||||
if (!timezone || timezone === "") timezone = null;
|
if (!timezone || timezone === "") timezone = null;
|
||||||
|
|
||||||
let hideMemberList = !!body.get("hide-member-list");
|
const hideMemberList = !!body.get("hide-member-list");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fastRequest("PATCH", "/users/@me", {
|
await fastRequest("PATCH", "/users/@me", {
|
||||||
|
|
Loading…
Reference in a new issue