Skip to content

Commit 8ef7561

Browse files
committed
Setter opp første api-kall fra frontend for å opprette url og vise respons i klienten
1 parent 56683bd commit 8ef7561

File tree

19 files changed

+285
-29
lines changed

19 files changed

+285
-29
lines changed

backend/src/main/kotlin/no/nais/cloud/testnais/sandbox/bachelorurlforkorter/UrlForkorterApi.kt

+16-8
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,36 @@ import io.javalin.http.staticfiles.Location
1212
import io.javalin.security.RouteRole
1313
import mu.KotlinLogging
1414
import no.nais.cloud.testnais.sandbox.bachelorurlforkorter.common.config.*
15-
import no.nais.cloud.testnais.sandbox.bachelorurlforkorter.common.db.DatabaseInitializer
15+
import no.nais.cloud.testnais.sandbox.bachelorurlforkorter.common.db.DatabaseInit
1616
import org.slf4j.MDC
1717

1818
private val logger = KotlinLogging.logger {}
1919

2020
fun main() {
2121
val config = createApplicationConfig()
22-
DatabaseInitializer.init(config)
22+
DatabaseInit.start(config)
2323
startAppServer(config);
2424
}
2525

2626
fun startAppServer(config: Config) {
2727
val app = Javalin.create { javalinConfig ->
2828
javalinConfig.router.apiBuilder {
2929
path("api") {
30+
post("sjekk", UrlForkorterController::sjekk, Rolle.Alle)
3031
get("test", UrlForkorterController::test, Rolle.Alle)
31-
get("sjekk/{korturl}", UrlForkorterController::sjekk, Rolle.Alle)
3232

33-
post("test", UrlForkorterController::test, Rolle.NavInnloggetBruker)
34-
post("forkort/{langurl}", UrlForkorterController::forkort, Rolle.Alle)
33+
post("test", UrlForkorterController::test, Rolle.InternNavInnlogget)
34+
post("forkort", UrlForkorterController::forkort, Rolle.Alle)
3535
}
3636
}
37+
javalinConfig.router.apiBuilder {
38+
get("{korturl}", UrlForkorterController::redirect, Rolle.Alle)
39+
}
3740
javalinConfig.staticFiles.add("/public", Location.CLASSPATH)
41+
// TODO: Kun for lokal utvikling med hot reload
42+
javalinConfig.bundledPlugins.enableCors {cors ->
43+
cors.addRule {it.allowHost("http://localhost:5173")}
44+
}
3845
}
3946

4047
app.before { ctx ->
@@ -104,8 +111,9 @@ private fun validateAcceptHeader(ctx: Context) {
104111
}
105112

106113
enum class Rolle : RouteRole {
107-
NavInnloggetBruker,
108-
Alle
114+
Alle,
115+
InternNavInnlogget,
116+
AdminNavInnlogget
109117
}
110118

111119
private fun checkAccessToEndpoint(ctx: Context, config: Config) {
@@ -115,7 +123,7 @@ private fun checkAccessToEndpoint(ctx: Context, config: Config) {
115123
throw UnauthorizedResponse()
116124
}
117125

118-
ctx.routeRoles().contains(Rolle.NavInnloggetBruker) -> {
126+
ctx.routeRoles().contains(Rolle.InternNavInnlogget) -> {
119127
val isValidUsername = config.authConfig.basicAuthUsername == ctx.basicAuthCredentials()?.username
120128
val isValidPassword = config.authConfig.basicAuthPassword.value == ctx.basicAuthCredentials()?.password
121129

Original file line numberDiff line numberDiff line change
@@ -1,23 +1,65 @@
11
package no.nais.cloud.testnais.sandbox.bachelorurlforkorter
22

33
import io.javalin.http.Context
4+
import mu.KotlinLogging
45
import no.nais.cloud.testnais.sandbox.bachelorurlforkorter.common.db.ShortUrlDataAccessObject
6+
import no.nais.cloud.testnais.sandbox.bachelorurlforkorter.forkorter.Forkorter
7+
8+
private val logger = KotlinLogging.logger {}
59

610
object UrlForkorterController {
711

812
fun test(ctx: Context) {
913
ctx.result("Hello world!")
1014
}
1115

12-
fun forkort(ctx: Context) {
13-
val langurl = ctx.pathParam("langurl")
14-
ShortUrlDataAccessObject.storeShortUrl("123", langurl, "Sigurd")
15-
ctx.status(201)
16+
fun redirect(ctx: Context) {
17+
val korturl = ctx.pathParam("korturl")
18+
try {
19+
val langurl = ShortUrlDataAccessObject.getLongUrl(korturl)
20+
if (langurl.isNullOrBlank()) {
21+
ctx.status(404).json(mapOf("message" to "Finner ingen URL"))
22+
return
23+
}
24+
ctx.status(307).redirect(langurl)
25+
} catch (e: Exception) {
26+
logger.error("Feil ved redirect: {}", korturl, e)
27+
ctx.status(500)
28+
}
1629
}
1730

1831
fun sjekk(ctx: Context) {
19-
val korturl = ctx.pathParam("korturl")
20-
val langurl = ShortUrlDataAccessObject.getLongUrl(korturl)
21-
ctx.result(langurl.toString())
32+
val korturl = ctx.queryParam("korturl")
33+
if (korturl.isNullOrBlank()) {
34+
ctx.status(400).json(mapOf("message" to "Mangler URL"))
35+
return
36+
}
37+
try {
38+
val langurl = ShortUrlDataAccessObject.getLongUrl(korturl.toString())
39+
if (langurl == null) {
40+
ctx.status(404)
41+
return
42+
}
43+
ctx.status(200).json(mapOf("langurl" to langurl))
44+
} catch (e: Exception) {
45+
logger.error("Feil ved sjekk av url: {}", korturl, e)
46+
ctx.status(500)
47+
}
48+
}
49+
50+
fun forkort(ctx: Context) {
51+
val originalUrl = ctx.queryParam("langurl")
52+
if (originalUrl.isNullOrBlank()) {
53+
ctx.status(400).json(mapOf("message" to "Mangler URL"))
54+
return
55+
}
56+
try {
57+
val forkortetUrl = Forkorter.lagUnikKortUrl()
58+
ShortUrlDataAccessObject.storeShortUrl(forkortetUrl, originalUrl.toString(), "Sigurd")
59+
ctx.status(201).json(mapOf("forkortetUrl" to forkortetUrl))
60+
} catch (e: Exception) {
61+
logger.error("Feil ved forkorting av url: {}", originalUrl, e)
62+
ctx.status(500)
63+
}
2264
}
2365
}

backend/src/main/kotlin/no/nais/cloud/testnais/sandbox/bachelorurlforkorter/common/db/DbInitializer.kt backend/src/main/kotlin/no/nais/cloud/testnais/sandbox/bachelorurlforkorter/common/db/DatabaseInit.kt

+7-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package no.nais.cloud.testnais.sandbox.bachelorurlforkorter.common.db
22

33
import mu.KotlinLogging
44
import no.nais.cloud.testnais.sandbox.bachelorurlforkorter.common.config.Config
5-
import no.nais.cloud.testnais.sandbox.bachelorurlforkorter.common.config.Env
5+
import no.nais.cloud.testnais.sandbox.bachelorurlforkorter.common.config.Env.Local
66
import org.jetbrains.exposed.sql.*
77
import org.jetbrains.exposed.sql.javatime.CurrentDateTime
88
import org.jetbrains.exposed.sql.transactions.transaction
@@ -20,10 +20,11 @@ object ShortUrls : Table("short_urls") {
2020
override val primaryKey = PrimaryKey(id)
2121
}
2222

23-
object DatabaseInitializer {
24-
fun init(config: Config) {
23+
object DatabaseInit {
24+
fun start(config: Config) {
25+
logger.info { "Oppretter database connection ..." }
2526
try {
26-
val db = if (config.environment == Env.Local) {
27+
val db = if (config.environment == Local) {
2728
logger.info("Kjører lokalt med H2 in-memory database")
2829
Database.connect(config.dbConfig.jdbcUrl, driver = "org.h2.Driver")
2930
} else {
@@ -33,9 +34,8 @@ object DatabaseInitializer {
3334
SchemaUtils.create(ShortUrls)
3435
}
3536
} catch (e: Exception) {
36-
throw RuntimeException("Kunne ikke initialisere database connection", e)
37+
throw RuntimeException("Kunne ikke opprette database connection", e)
3738
}
38-
39-
logger.info("Database connection initialisert!")
39+
logger.info("Database connection opprettet!")
4040
}
4141
}

backend/src/main/kotlin/no/nais/cloud/testnais/sandbox/bachelorurlforkorter/common/db/ShortUrlDataAccessObject.kt

+6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ object ShortUrlDataAccessObject {
1414
}
1515
}
1616

17+
fun getAllShortUrls(): List<String> {
18+
return transaction {
19+
ShortUrls.selectAll().map { it[ShortUrls.shortUrl] }
20+
}
21+
}
22+
1723
fun getLongUrl(shortUrl: String): String? {
1824
return transaction {
1925
ShortUrls.select { ShortUrls.shortUrl eq shortUrl }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package no.nais.cloud.testnais.sandbox.bachelorurlforkorter.forkorter
2+
3+
import no.nais.cloud.testnais.sandbox.bachelorurlforkorter.common.db.ShortUrlDataAccessObject
4+
5+
object Forkorter {
6+
7+
private fun lagKortUrl(lengde: Int): String {
8+
val chars = "abcdefghijklmnopqrstuvwxyz0123456789"
9+
return (1..lengde)
10+
.map { chars.random() }
11+
.joinToString("")
12+
}
13+
14+
fun lagUnikKortUrl(lengde: Int = 6): String {
15+
val existingShortUrls = ShortUrlDataAccessObject.getAllShortUrls()
16+
var newShortUrl: String
17+
do {
18+
newShortUrl = lagKortUrl(lengde)
19+
} while (newShortUrl in existingShortUrls)
20+
21+
return newShortUrl
22+
}
23+
24+
}

backend/src/test/resources/test.http

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1-
### Forkort URL
1+
### Forkort URL lokal
22
POST localhost:8080/api/forkort/asd
33
Content-Type: application/json
44
Accept: application/json
55

6-
### Sjekk URL
6+
### Sjekk URL lokal
77
GET localhost:8080/api/sjekk/123
88
Accept: application/json
9+
10+
### Forkort URL cluster
11+
POST https://bachelor-url-forkorter.sandbox.test-nais.cloud.nais.io/api/forkort/asd
12+
Content-Type: application/json
13+
Accept: application/json
14+
15+
### Sjekk URL cluster
16+
GET https://bachelor-url-forkorter.sandbox.test-nais.cloud.nais.io/api/sjekk/123
17+
Accept: application/json

frontend/.env

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
VITE_BASE_URL=http://localhost:8080/
2+
VITE_API_BASE_URL=/api/

frontend/.env.development

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
VITE_BASE_URL=http://localhost:8080/
2+
VITE_API_BASE_URL=http://localhost:8080/api/
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {isValidUrl} from "../util/urlUtil.ts";
2+
import {apiRequest} from "../util/api/apiRequest.ts";
3+
import {useState} from "react";
4+
import Input from "./shared/Input/Input.tsx";
5+
6+
export default function CreateShortUrl() {
7+
const [inputValue, setInputValue] = useState("");
8+
const [result, setResult] = useState<string | null>(null);
9+
10+
async function postShortUrlSearch(longUrl: string) {
11+
try {
12+
return await apiRequest<{ forkortetUrl: string }>(`forkort?langurl=${longUrl}`, "POST");
13+
} catch (error) {
14+
console.error("API error:", error);
15+
}
16+
}
17+
18+
function handleCreateClick() {
19+
if (!isValidUrl(inputValue)) return;
20+
postShortUrlSearch(inputValue).then((res) => {
21+
if (res) setResult(res.forkortetUrl);
22+
}).catch(error => {
23+
setResult(null);
24+
console.error(error);
25+
});
26+
}
27+
28+
return (
29+
<>
30+
<Input placeholder="Skriv inn din lenke.."
31+
onClick={handleCreateClick}
32+
onChange={setInputValue}>
33+
</Input>
34+
<p>{result}</p>
35+
</>
36+
)
37+
}
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import Search from "./shared/Search/Search.tsx";
2+
import {extractShortUrl} from "../util/urlUtil.ts";
3+
import {apiRequest} from "../util/api/apiRequest.ts";
4+
import {useState} from "react";
5+
6+
export default function SearchShortUrl() {
7+
const [inputValue, setInputValue] = useState("");
8+
const [searchResult, setSearchResult] = useState<string | null>(null);
9+
10+
async function postShortUrlSearch(shortUrl: string) {
11+
try {
12+
return await apiRequest<{langurl: string }>(`sjekk?korturl=${shortUrl}`, "POST");
13+
} catch (error) {
14+
console.error("API error:", error);
15+
}
16+
}
17+
18+
function handleSearchClick() {
19+
const shortUrl = extractShortUrl(inputValue)
20+
if (shortUrl === null || shortUrl.length !== 6) {
21+
return;
22+
}
23+
postShortUrlSearch(shortUrl).then((res) => {
24+
if (res) setSearchResult(res.langurl);
25+
else setSearchResult(null);
26+
}).catch(error => {
27+
setSearchResult(null);
28+
console.error(error);
29+
});
30+
}
31+
32+
return (
33+
<>
34+
<Search placeholder="Kontroller din lenke.."
35+
onClick={handleSearchClick}
36+
onChange={setInputValue}>
37+
</Search>
38+
<div>{searchResult}</div>
39+
</>
40+
)
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {InputContainer, StyledButton, StyledInput} from "./input.style.ts";
2+
3+
interface Props {
4+
placeholder?: string;
5+
buttonText?: string;
6+
onClick?: () => void;
7+
onChange?: (value: string) => void;
8+
}
9+
10+
export default function Input({placeholder, buttonText = "Opprett", onClick, onChange}: Props) {
11+
return (
12+
<InputContainer>
13+
<StyledInput type="search" placeholder={placeholder} onChange={(e) => onChange?.(e.target.value)}/>
14+
<StyledButton text={buttonText} onClick={onClick}/>
15+
</InputContainer>
16+
)
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import styled from "styled-components";
2+
import Button from "../Button/Button.tsx";
3+
4+
export const InputContainer = styled.div`
5+
display: flex;
6+
align-items: center;
7+
position: relative;
8+
width: 40vw;
9+
`
10+
11+
export const StyledInput = styled.input`
12+
width: 100%;
13+
padding: 10px;
14+
border-radius: 5px;
15+
outline: none;
16+
border: 2px solid var(--theme-color);
17+
18+
&:focus {
19+
border: 2px solid var(--theme-color-focus);
20+
}
21+
`
22+
23+
export const StyledButton = styled(Button)`
24+
position: absolute;
25+
right: 0;
26+
height: 100%;
27+
width: 5em;
28+
border-top-left-radius: 0;
29+
border-bottom-left-radius: 0;
30+
`

frontend/src/components/Search/Search.tsx frontend/src/components/shared/Search/Search.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import {SearchContainer, StyledButton, StyledInput} from "./search.style.ts";
33
interface Props {
44
placeholder?: string;
55
onClick?: () => void;
6+
onChange?: (value: string) => void;
67
}
78

8-
export default function Search({placeholder, onClick}: Props) {
9+
export default function Search({placeholder, onClick, onChange}: Props) {
910
return (
1011
<SearchContainer>
11-
<StyledInput type="search" placeholder={placeholder}/>
12+
<StyledInput type="search" placeholder={placeholder} onChange={(e) => onChange?.(e.target.value)}/>
1213
<StyledButton text="Søk" onClick={onClick}/>
1314
</SearchContainer>
1415
)

0 commit comments

Comments
 (0)