11package com.sakethh.linkora.presentation.routing
22
3+ import Colors
34import com.sakethh.linkora.Constants
5+ import com.sakethh.linkora.ServerConfiguration
46import com.sakethh.linkora.authenticate
57import com.sakethh.linkora.data.repository.*
68import com.sakethh.linkora.domain.Route
@@ -13,8 +15,25 @@ import io.ktor.http.*
1315import io.ktor.server.application.*
1416import io.ktor.server.response.*
1517import io.ktor.server.routing.*
18+ import kotlinx.html.ScriptType
19+ import kotlinx.html.html
20+ import kotlinx.html.script
21+ import kotlinx.html.stream.createHTML
22+ import kotlinx.html.unsafe
1623import org.jetbrains.exposed.sql.Database
24+ import sakethh.kapsule.*
25+ import sakethh.kapsule.utils.BoxSizing
26+ import sakethh.kapsule.utils.Cursor
27+ import sakethh.kapsule.utils.FontWeight
28+ import sakethh.kapsule.utils.px
29+ import java.io.File
30+ import java.io.FileInputStream
1731import java.net.InetAddress
32+ import java.util.zip.ZipEntry
33+ import java.util.zip.ZipOutputStream
34+ import kotlin.io.path.createFile
35+ import kotlin.io.path.exists
36+ import kotlin.io.path.pathString
1837
1938fun Application.configureRouting (serverConfig : ServerConfig , markdownManagerRepo : MarkdownManagerRepo ) {
2039 routing {
@@ -25,7 +44,207 @@ fun Application.configureRouting(serverConfig: ServerConfig, markdownManagerRepo
2544 get(Route .Sync .TEST_BEARER .name) {
2645 call.respond(message = HttpStatusCode .OK , status = HttpStatusCode .OK )
2746 }
47+ post(path = " /generate/certs-and-keystore" ) {
48+ val zipFile = ServerConfiguration .jarDir.resolve(" linkora-certs-and-keystore.zip" ).run {
49+ if (exists()) {
50+ this .toFile()
51+ } else {
52+ createFile().toFile()
53+ }
54+ }
55+
56+ ZipOutputStream (zipFile.outputStream()).use { zipOutputStream ->
57+ val files = listOf<File >(
58+ ServerConfiguration .jarDir.resolve(" linkoraServerCert.cer" ).toFile(),
59+ ServerConfiguration .jarDir.resolve(" linkoraServerCert.pem" ).toFile(),
60+ ServerConfiguration .jarDir.resolve(" linkoraServerCert.jks" ).toFile(),
61+ )
62+
63+ files.forEach {
64+ if (it.exists()) {
65+ it.delete()
66+ }
67+ }
68+
69+ ServerConfiguration .exportSignedCertificates(
70+ keyStore = ServerConfiguration .createOrLoadServerKeystore(
71+ serverConfig = serverConfig
72+ )
73+ )
74+ try {
75+ files.forEach { file ->
76+ zipOutputStream.putNextEntry(ZipEntry (file.name))
77+ FileInputStream (file).use {
78+ it.copyTo(zipOutputStream)
79+ }
80+ zipOutputStream.closeEntry()
81+ }
82+ } catch (e: Exception ) {
83+ call.respond(e.stackTraceToString())
84+ return @post
85+ } catch (e: Error ) {
86+ call.respond(e.stackTraceToString())
87+ return @post
88+ }
89+ }
90+ if (call.queryParameters[" download" ] == " true" ) {
91+ call.respondFile(file = zipFile)
92+ } else {
93+ call.respond(" Certificates and keystore have been successfully generated at ${ServerConfiguration .jarDir.pathString} . A ZIP file containing all these files is also saved in the same directory." )
94+ }
95+ }
2896 }
97+
98+ get(" /generate/certs-and-keystore" ) {
99+ call.respondText(text = createHTML().html {
100+ Surface (
101+ onTheHeadElement = {
102+ unsafe {
103+ + """
104+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
105+ """ .trimIndent()
106+ }
107+ },
108+ onTheBodyElement = {
109+ script(type = ScriptType .textJavaScript) {
110+ unsafe {
111+ + """
112+ document.addEventListener('DOMContentLoaded', function() {
113+ var generateBtn = document.getElementById('generate-btn');
114+ var downloadBtn = document.getElementById('download-btn');
115+ var authTokenInput = document.getElementById('authToken');
116+
117+ if (generateBtn) {
118+ generateBtn.addEventListener('click', function() {
119+ var token = authTokenInput.value.trim();
120+ if (!token) {
121+ alert('Authorization token required');
122+ return;
123+ }
124+
125+ fetch('/generate/certs-and-keystore', {
126+ method: 'POST',
127+ headers: { 'Authorization': 'Bearer ' + token }
128+ })
129+ .then(function(response) {
130+ if (response.ok) {
131+ return response.text();
132+ } else {
133+ return response.text().then(function(text) {
134+ throw new Error(text);
135+ });
136+ }
137+ })
138+ .then(function(text) {
139+ alert(text);
140+ })
141+ .catch(function(error) {
142+ alert('Error: ' + error.message);
143+ });
144+ });
145+ }
146+
147+ if (downloadBtn) {
148+ downloadBtn.addEventListener('click', function() {
149+ var token = authTokenInput.value.trim();
150+ if (!token) {
151+ alert('Authorization token required');
152+ return;
153+ }
154+
155+ fetch('/generate/certs-and-keystore?download=true', {
156+ method: 'POST',
157+ headers: { 'Authorization': 'Bearer ' + token }
158+ })
159+ .then(function(response) {
160+ if (response.ok) {
161+ return response.blob();
162+ } else {
163+ return response.text().then(function(text) {
164+ throw new Error(text);
165+ });
166+ }
167+ })
168+ .then(function(blob) {
169+ var url = window.URL.createObjectURL(blob);
170+ var a = document.createElement('a');
171+ a.href = url;
172+ a.download = 'linkora-certs-and-keystore.zip';
173+ document.body.appendChild(a);
174+ a.click();
175+ setTimeout(function() {
176+ document.body.removeChild(a);
177+ window.URL.revokeObjectURL(url);
178+ }, 100);
179+ })
180+ .catch(function(error) {
181+ alert('Error: ' + error.message);
182+ });
183+ });
184+ }
185+ });
186+ """ .trimIndent()
187+ }
188+ }
189+ },
190+ fonts = listOf (
191+ " https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap" ,
192+ ),
193+ modifier = Modifier .backgroundColor(color = Colors .surfaceDark).boxSizing(BoxSizing .BorderBox )
194+ .margin(0 .px).padding(0 .px).custom(" overflow: hidden;" )
195+ ) {
196+ Column (modifier = Modifier .margin(value = 50 .px)) {
197+ Text (
198+ text = " Enter the auth token" , fontFamily = " Inter" ,
199+ color = Colors .onSurfaceDark,
200+ )
201+ Spacer (modifier = Modifier .height(5 .px))
202+ TextInputField (
203+ id = " authToken" ,
204+ value = " " ,
205+ fontWeight = FontWeight .Predefined .Normal ,
206+ fontSize = 16 .px,
207+ fontFamily = " Inter" ,
208+ modifier = Modifier .height(25 .px).color(Colors .onSurfaceDark)
209+ .backgroundColor(Colors .ButtonContentColor )
210+ )
211+ Spacer (modifier = Modifier .height(10 .px))
212+ Button (
213+ id = " generate-btn" ,
214+ onClick = { " " },
215+ modifier = Modifier .height(25 .px).backgroundColor(Colors .ButtonContainerColor )
216+ .cursor(Cursor .Pointer )
217+ ) {
218+ Text (
219+ fontWeight = FontWeight .Predefined .Medium ,
220+ text = " Generate" ,
221+ fontFamily = " Inter" ,
222+ color = Colors .ButtonContentColor ,
223+ fontSize = 16 .px
224+ )
225+ }
226+ Spacer (modifier = Modifier .height(10 .px))
227+ Button (
228+ id = " download-btn" ,
229+ onClick = {
230+ " "
231+ },
232+ modifier = Modifier .height(25 .px).backgroundColor(Colors .ButtonContainerColor )
233+ .cursor(Cursor .Pointer )
234+ ) {
235+ Text (
236+ fontWeight = FontWeight .Predefined .Medium ,
237+ fontSize = 16 .px,
238+ text = " Download" ,
239+ fontFamily = " Inter" ,
240+ color = Colors .ButtonContentColor
241+ )
242+ }
243+ }
244+ }
245+ }, contentType = ContentType .Text .Html )
246+ }
247+
29248 get(Route .Sync .SERVER_IS_CONFIGURED .name) {
30249 val placeHolderValue =
31250 if ((useSysEnvValues().not () && serverConfig.hostAddress != InetAddress .getLocalHost().hostAddress) || (useSysEnvValues() && System .getenv(
0 commit comments