Skip to content

Commit 1072b57

Browse files
committed
feat: add route /generate/certs-and-keystore for generating signed certificates and keystore files
1 parent 64ab182 commit 1072b57

2 files changed

Lines changed: 220 additions & 1 deletion

File tree

src/main/kotlin/com/sakethh/linkora/Application.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ object ServerConfiguration {
6868
prettyPrint = true
6969
encodeDefaults = true
7070
}
71-
private val jarDir = Paths.get(this::class.java.protectionDomain.codeSource.location.toURI()).parent
71+
val jarDir = Paths.get(this::class.java.protectionDomain.codeSource.location.toURI()).parent
7272
private val configFilePath = jarDir.resolve("linkoraConfig.json")
7373

7474
private fun doesConfigFileExists(): Boolean {

src/main/kotlin/com/sakethh/linkora/presentation/routing/Routing.kt

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.sakethh.linkora.presentation.routing
22

3+
import Colors
34
import com.sakethh.linkora.Constants
5+
import com.sakethh.linkora.ServerConfiguration
46
import com.sakethh.linkora.authenticate
57
import com.sakethh.linkora.data.repository.*
68
import com.sakethh.linkora.domain.Route
@@ -13,8 +15,25 @@ import io.ktor.http.*
1315
import io.ktor.server.application.*
1416
import io.ktor.server.response.*
1517
import 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
1623
import 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
1731
import 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

1938
fun 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

Comments
 (0)