// SPDX-FileCopyrightText: 2026 Progmasoft // SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 using System.Security.Cryptography; using System.Text; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Antiforgery; namespace Progmasoft.OAuth.Pages; /// Renders the pages of Progmasoft OAuth: plain HTML forms, no script. /// /// The pages need no JavaScript, so the content security policy allows none. The one stylesheet is inline and /// allowed by its digest. The policy has no form-action: a form here posts to this host, and the /// redirection that follows a consent goes to the address the application registered, which browsers would check /// against form-action as well. /// internal static class HtmlPage { /// The stylesheet of every page. internal const string Stylesheet = "body{margin:0;font:16px/1.5 system-ui,-apple-system,'Segoe UI',sans-serif;color:#1d1d1f;background:#f6f7f9}" + "main{max-width:34rem;margin:3rem auto;padding:2rem;background:#fff;border:1px solid #dcdfe4;border-radius:12px}" + "h1{font-size:1.5rem;margin:0 0 1rem}h2{font-size:1.1rem;margin:1.5rem 0 .5rem}" + "label{display:block;margin:.75rem 0 .25rem;font-weight:600}" + "input[type=text],input[type=email],input[type=password],input[type=url],textarea{box-sizing:border-box;" + "width:100%;padding:.55rem .65rem;border:1px solid #b8bec8;border-radius:8px;font:inherit}" + "textarea{min-height:5rem}button{margin-top:1rem;padding:.55rem 1rem;border:0;border-radius:8px;" + "background:#1f4fd6;color:#fff;font:inherit;cursor:pointer}button.secondary{background:#e5e8ee;color:#1d1d1f}" + "button.danger{background:#b3261e}.error{color:#b3261e}.note{color:#5b6270;font-size:.9rem}" + "code{font:14px ui-monospace,SFMono-Regular,Consolas,monospace;background:#eef0f4;padding:.1rem .3rem;" + "border-radius:4px;word-break:break-all}ul{padding-left:1.2rem}a{color:#1f4fd6}" + ".row{display:flex;gap:.5rem;flex-wrap:wrap}.check{font-weight:400}" + "@media(prefers-color-scheme:dark){body{background:#121316;color:#e8e9ec}main{background:#1b1d22;" + "border-color:#30343c}input[type=text],input[type=email],input[type=password],input[type=url],textarea" + "{background:#121316;color:#e8e9ec;border-color:#454a54}code{background:#262a31}a{color:#8fb0ff}" + "button.secondary{background:#30343c;color:#e8e9ec}.note{color:#a3a9b5}}"; /// The content security policy source that allows . internal static readonly string StylesheetSource = "'sha256-" + Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(Stylesheet))) + "'"; /// Encodes text for an HTML element or attribute. /// The text. /// The encoded text. public static string Encode(string? text) => HtmlEncoder.Default.Encode(text ?? string.Empty); /// Answers with a page. /// The request. /// The title of the page. /// The content of the page, already encoded. /// The status of the response. /// The result. public static IResult Render(HttpContext context, string title, string body, int statusCode = 200) { IHeaderDictionary headers = context.Response.Headers; headers.ContentSecurityPolicy = $"default-src 'none'; style-src {StylesheetSource}; img-src 'self'; frame-ancestors 'none'; base-uri 'none'"; headers.CacheControl = "no-store"; string html = "" + "" + "" + $"{Encode(title)} ยท Progmasoft" + $"
{body}
"; return Results.Content(html, "text/html; charset=utf-8", Encoding.UTF8, statusCode); } /// A form that posts to this host, with the token that proves it was served here. /// The request that renders the form. /// The path the form posts to. /// The fields and buttons, already encoded. /// The form. public static string Form(HttpContext context, string action, string content) { IAntiforgery antiforgery = context.RequestServices.GetRequiredService(); AntiforgeryTokenSet tokens = antiforgery.GetAndStoreTokens(context); return $"
" + $"" + content + "
"; } /// A hidden field. /// The name of the field. /// The value of the field. /// The field. public static string Hidden(string name, string? value) => $""; /// A paragraph that reports an error, or nothing. /// The message, or . /// The paragraph. public static string Error(string? message) => string.IsNullOrEmpty(message) ? string.Empty : $"

{Encode(message)}

"; }