Forge
csharp3407750f
1using AgentForge.Abstractions;
2using AgentForge.Data.Entities;
3using AgentForge.Plugin.View;
4using AgentForge.Services;
5
6namespace AgentForge.Plugin.OAuth.Web.Pages;
7
8internal static class ForgeAuthPages
9{
10 internal static string RenderLogin(
11 ForgeOAuthProviderRegistry providers,
12 IForgeOAuthLoginContributorCatalog oauthUi,
13 ForgeViewViewerContext viewer,
14 string? error)
15 {
16 var errorHtml = string.IsNullOrWhiteSpace(error)
17 ? ""
18 : ForgeHtml.P("error", ForgeHtml.Text(error switch
19 {
20 "invalid_token" => "Token invalid or revoked.",
21 "missing_token" => "Token required.",
22 "access_denied" => "Sign-in not allowed on this forge host. Ask an org admin for access.",
23 _ => "Sign-in failed.",
24 }));
25
26 var providerLines = new List<string>();
27 foreach (var provider in providers.ListProviders())
28 {
29 providerLines.Add(ForgeHtml.P("meta",
30 ForgeHtml.Text($"{provider.DisplayName}: run "),
31 ForgeHtml.Code($"forge auth login --oauth {provider.Id}")));
32 }
33
34 if (providers.ListProviders().Count == 0)
35 {
36 providerLines.Add(ForgeHtml.P("meta",
37 ForgeHtml.Text("No OAuth providers configured. Set FORGE_GITHUB_CLIENT_ID/SECRET or use device login.")));
38 }
39
40 var signOut = viewer.IsGuest
41 ? ""
42 : ForgeHtml.Form(
43 "post",
44 "/view/auth/logout",
45 "field field-top",
46 ForgeHtml.Button("submit", "Sign out", "btn-reject"));
47
48 return ForgeHtml.Fragment(
49 ForgeHtml.H1("Sign in to Forge"),
50 ForgeHtml.P("meta",
51 ForgeHtml.Text("Paste an API token from "),
52 ForgeHtml.Code("forge auth login"),
53 ForgeHtml.Text(" to use the web view as yourself.")),
54 errorHtml,
55 RenderOAuthButtons(providers, oauthUi),
56 ForgeHtml.Fragment([.. providerLines]),
57 RenderTokenForm(),
58 ForgeHtml.P("meta", ForgeHtml.A("/view/auth/device", "Device login (bootstrap)")),
59 signOut);
60 }
61
62 private static string RenderTokenForm() =>
63 ForgeHtml.Form(
64 "post",
65 "/view/auth/session",
66 "field",
67 ForgeHtml.Label("token", "API token"),
68 ForgeHtml.Input(new ForgeInputSpec("token", Type: "password", Autocomplete: "off", Required: true)),
69 ForgeHtml.FieldSubmit(null, ForgeHtml.Button("submit", "Sign in")));
70
71 private static string RenderOAuthButtons(
72 ForgeOAuthProviderRegistry providers,
73 IForgeOAuthLoginContributorCatalog oauthUi)
74 {
75 var configured = providers.ListProviders()
76 .Select(p => p.Id)
77 .ToHashSet(StringComparer.OrdinalIgnoreCase);
78
79 var controls = oauthUi.Contributors
80 .Where(c => configured.Contains(c.ProviderId))
81 .Select(c => c.RenderSignInControl(Uri.EscapeDataString("/view")))
82 .Where(html => !string.IsNullOrWhiteSpace(html))
83 .ToList();
84
85 if (controls.Count == 0)
86 return "";
87
88 return ForgeHtml.Fragment(
89 ForgeHtml.Div("forge-oauth-row", string.Concat(controls)),
90 ForgeHtml.P("meta", ForgeHtml.Text("Browser OAuth sets a session cookie — no token paste.")));
91 }
92
93 internal static string RenderDeviceLogin(string userCode, ForgeDeviceLoginEntity? pending)
94 {
95 return ForgeHtml.Fragment(
96 ForgeHtml.H1("Device login"),
97 RenderDeviceSteps(),
98 RenderDeviceLookupForm(userCode),
99 RenderDeviceNotFound(userCode, pending),
100 pending is null ? "" : RenderDeviceApprove(pending));
101 }
102
103 private static string RenderDeviceSteps() =>
104 ForgeHtml.Ol("steps meta",
105 ForgeHtml.Li(null,
106 ForgeHtml.Text("Run "),
107 ForgeHtml.Code("forge auth login"),
108 ForgeHtml.Text(" (or CIDE "),
109 ForgeHtml.Code("forge_lens.connect"),
110 ForgeHtml.Text(") in a terminal.")),
111 ForgeHtml.Li(null,
112 ForgeHtml.Text("Copy the "),
113 ForgeHtml.Strong("user code"),
114 ForgeHtml.Text(" it prints (e.g. "),
115 ForgeHtml.Code("ABCD-EFGH"),
116 ForgeHtml.Text(") or click the verification link.")),
117 ForgeHtml.Li(null, ForgeHtml.Text("Paste the code below and approve with your bootstrap token.")));
118
119 private static string RenderDeviceLookupForm(string userCode) =>
120 ForgeHtml.Form(
121 "get",
122 "/view/auth/device",
123 "field",
124 ForgeHtml.Label("code", "User code"),
125 ForgeHtml.Input(new ForgeInputSpec(
126 "code",
127 Id: "code",
128 Placeholder: "ABCD-EFGH",
129 Autocomplete: "one-time-code",
130 InputMode: "text",
131 Required: true,
132 Value: userCode.Length > 0 ? userCode : null)),
133 ForgeHtml.FieldSubmit(null, ForgeHtml.Button("submit", "Look up request")));
134
135 private static string RenderDeviceNotFound(string userCode, ForgeDeviceLoginEntity? pending) =>
136 userCode.Length > 0 && pending is null
137 ? ForgeHtml.P("error",
138 ForgeHtml.Text("No pending login for "),
139 ForgeHtml.Code(userCode),
140 ForgeHtml.Text(". Run "),
141 ForgeHtml.Code("forge auth login"),
142 ForgeHtml.Text(" again — codes expire."))
143 : "";
144
145 private static string RenderDeviceApprove(ForgeDeviceLoginEntity pending) =>
146 ForgeHtml.Fragment(
147 ForgeHtml.H2("Approve this request"),
148 ForgeHtml.P("meta",
149 ForgeHtml.Text("Client "),
150 ForgeHtml.Strong(pending.ClientName),
151 ForgeHtml.Text(" wants API access.")),
152 ForgeHtml.FormWithId(
153 "approve-form",
154 "post",
155 "#",
156 "field",
157 ForgeHtml.Label("bootstrap", "Bootstrap or write-scoped API token"),
158 ForgeHtml.Input(new ForgeInputSpec("bootstrap", Type: "password", Autocomplete: "off", Required: true)),
159 ForgeHtml.FieldSubmit(null, ForgeHtml.Button("submit", "Approve login"))),
160 ForgeHtml.PWithId("status", "meta", ""),
161 ForgeAuthDeviceApproveScript.Render(ForgeViewPresenter.JsonEncode(pending.UserCode)),
162 ForgeHtml.P("meta",
163 ForgeHtml.Text("Terminal alternative: "),
164 ForgeHtml.Code($"forge auth approve {pending.UserCode}")));
165}
166
View only · write via MCP/CIDE