mirror of
https://github.com/jlengrand/pluckr.git
synced 2026-03-10 08:41:17 +00:00
@@ -2,6 +2,8 @@ val ktor_version: String by project
|
||||
val kotlin_version: String by project
|
||||
val logback_version: String by project
|
||||
val exposedVersion: String by project
|
||||
val postgresqlVersion: String by project
|
||||
val postgisVersion: String by project
|
||||
|
||||
plugins {
|
||||
application
|
||||
@@ -25,20 +27,26 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
|
||||
implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
|
||||
implementation("io.ktor:ktor-server-netty-jvm:$ktor_version")
|
||||
implementation("ch.qos.logback:logback-classic:$logback_version")
|
||||
implementation("io.ktor:ktor-server-call-logging:$ktor_version")
|
||||
implementation("io.ktor:ktor-server-metrics-micrometer:$ktor_version")
|
||||
implementation("io.ktor:ktor-server-cors:$ktor_version")
|
||||
implementation("io.ktor:ktor-server-sessions:$ktor_version")
|
||||
implementation("io.ktor:ktor-server-auth:$ktor_version")
|
||||
|
||||
implementation("org.mindrot:jbcrypt:0.4")
|
||||
|
||||
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
|
||||
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
|
||||
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
|
||||
implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion")
|
||||
implementation("io.ktor:ktor-server-content-negotiation:$ktor_version")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")
|
||||
implementation("org.postgresql:postgresql:42.3.6")
|
||||
implementation("net.postgis:postgis-jdbc:2021.1.0")
|
||||
implementation("org.postgresql:postgresql:$postgresqlVersion")
|
||||
implementation("net.postgis:postgis-jdbc:$postgisVersion")
|
||||
|
||||
testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
|
||||
|
||||
@@ -3,3 +3,5 @@ kotlin_version=1.7.0
|
||||
logback_version=1.2.11
|
||||
kotlin.code.style=official
|
||||
exposedVersion=0.38.2
|
||||
postgresqlVersion=42.3.6
|
||||
postgisVersion=2021.1.0
|
||||
13
src/main/js/pluckr-app/.eslintrc.js
Normal file
13
src/main/js/pluckr-app/.eslintrc.js
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
parserOptions: {
|
||||
ecmaVersion: 13,
|
||||
},
|
||||
extends: ['@open-wc', 'prettier'],
|
||||
rules: {
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{ devDependencies: ['rollup.mapbox.js', 'rollup.config.js'] },
|
||||
],
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
},
|
||||
};
|
||||
1675
src/main/js/pluckr-app/package-lock.json
generated
1675
src/main/js/pluckr-app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,9 +15,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@mapbox/mapbox-gl-geocoder": "^5.0.1",
|
||||
"@vaadin/app-layout": "^23.1.3",
|
||||
"@vaadin/button": "^23.1.3",
|
||||
"@vaadin/dialog": "^23.1.4",
|
||||
"@vaadin/form-layout": "^23.1.4",
|
||||
"@vaadin/horizontal-layout": "^23.1.3",
|
||||
"@vaadin/icon": "^23.1.3",
|
||||
"@vaadin/icons": "^23.1.3",
|
||||
"@vaadin/text-field": "^23.1.3",
|
||||
"@vaadin/password-field": "^23.1.4",
|
||||
"@vaadin/progress-bar": "^23.1.4",
|
||||
"@vaadin/tabs": "^23.1.3",
|
||||
"@vaadin/text-field": "^23.1.4",
|
||||
"leaflet": "^1.8.0",
|
||||
"leaflet-geosearch": "jlengrand/leaflet-geosearch#update",
|
||||
"lit": "^2.0.2",
|
||||
@@ -27,7 +35,7 @@
|
||||
"@babel/preset-env": "^7.16.4",
|
||||
"@custom-elements-manifest/analyzer": "^0.4.17",
|
||||
"@open-wc/building-rollup": "^2.0.1",
|
||||
"@open-wc/eslint-config": "^4.3.0",
|
||||
"@open-wc/eslint-config": "^8.0.2",
|
||||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@rollup/plugin-commonjs": "^22.0.1",
|
||||
"@rollup/plugin-node-resolve": "^13.0.6",
|
||||
@@ -36,23 +44,17 @@
|
||||
"@web/rollup-plugin-import-meta-assets": "^1.0.7",
|
||||
"babel-plugin-template-html-minifier": "^4.1.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"husky": "^4.3.8",
|
||||
"koa-proxies": "^0.12.2",
|
||||
"lint-staged": "^10.5.4",
|
||||
"prettier": "^2.4.1",
|
||||
"prettier": "^2.7.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup": "^2.60.0",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-workbox": "^6.2.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"@open-wc",
|
||||
"prettier"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
"singleQuote": true,
|
||||
"arrowParens": "avoid"
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import './pluckr-login.js';
|
||||
|
||||
import '@vaadin/text-field';
|
||||
import '@vaadin/icons';
|
||||
import mapbox from '../dist/mapbox-gl.esm.js'
|
||||
import MapboxGeocoder from '../dist/mapbox-gl-geocoder.esm.min.js'
|
||||
import '@vaadin/app-layout';
|
||||
import '@vaadin/app-layout/vaadin-drawer-toggle.js';
|
||||
import '@vaadin/tabs';
|
||||
import '@vaadin/button';
|
||||
|
||||
import mapboxgl from '../dist/mapbox-gl.esm.js';
|
||||
import MapboxGeocoder from '../dist/mapbox-gl-geocoder.esm.min.js';
|
||||
|
||||
export class PluckrApp extends LitElement {
|
||||
map = null;
|
||||
@@ -10,7 +17,7 @@ export class PluckrApp extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
title: { type: String },
|
||||
location: {type: Object},
|
||||
location: { type: Object },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,30 +45,36 @@ export class PluckrApp extends LitElement {
|
||||
height: 1096px;
|
||||
width: 1096px;
|
||||
}
|
||||
|
||||
pluckr-login {
|
||||
margin-left: auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.title = 'My app';
|
||||
this.location = { x: 52.0474828687443, y: 5.080036739440433};
|
||||
this.title = 'Pluckr';
|
||||
this.location = { x: 52.0474828687443, y: 5.080036739440433 };
|
||||
}
|
||||
|
||||
firstUpdated(_changedProperties) {
|
||||
super.firstUpdated(_changedProperties);
|
||||
|
||||
this.map = new mapbox.Map({
|
||||
accessToken: 'pk.eyJ1IjoiamxlbmdyYW5kIiwiYSI6ImNsNWM3YTl3YjBla3ozYm8yMHo3NTRtbHkifQ.mhHRpOn0v-v59tXbvEYnlQ',
|
||||
this.map = new mapboxgl.Map({
|
||||
accessToken:
|
||||
'pk.eyJ1IjoiamxlbmdyYW5kIiwiYSI6ImNsNWM3YTl3YjBla3ozYm8yMHo3NTRtbHkifQ.mhHRpOn0v-v59tXbvEYnlQ',
|
||||
container: this.renderRoot.querySelector('#map'),
|
||||
style: 'mapbox://styles/mapbox/streets-v11',
|
||||
center: [this.location.y, this.location.x],
|
||||
zoom: 13
|
||||
zoom: 13,
|
||||
});
|
||||
|
||||
this.map.addControl(
|
||||
new MapboxGeocoder({
|
||||
accessToken: 'pk.eyJ1IjoiamxlbmdyYW5kIiwiYSI6ImNsNWM3YTl3YjBla3ozYm8yMHo3NTRtbHkifQ.mhHRpOn0v-v59tXbvEYnlQ',
|
||||
mapboxgl: mapbox
|
||||
accessToken:
|
||||
'pk.eyJ1IjoiamxlbmdyYW5kIiwiYSI6ImNsNWM3YTl3YjBla3ozYm8yMHo3NTRtbHkifQ.mhHRpOn0v-v59tXbvEYnlQ',
|
||||
mapboxgl,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -70,19 +83,19 @@ export class PluckrApp extends LitElement {
|
||||
this.loadMarkers(this.map.getBounds());
|
||||
}
|
||||
|
||||
loadMarkers(bounds){
|
||||
fetch(`/api/trees?bbox=${bounds._ne.lat},${bounds._ne.lng},${bounds._sw.lat},${bounds._sw.lng}`)
|
||||
loadMarkers(bounds) {
|
||||
fetch(
|
||||
`/api/trees?bbox=${bounds._ne.lat},${bounds._ne.lng},${bounds._sw.lat},${bounds._sw.lng}`
|
||||
)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Loaded POIs', data);
|
||||
data.map(p =>
|
||||
new mapbox.Marker()
|
||||
new mapboxgl.Marker()
|
||||
.setLngLat([p.location.y, p.location.x])
|
||||
.addTo(this.map)
|
||||
);
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
console.error('Impossible to log Points of Interests:', error);
|
||||
});
|
||||
}
|
||||
@@ -99,15 +112,27 @@ export class PluckrApp extends LitElement {
|
||||
integrity="sha512-hoalWLoI8r4UszCkZ5kL8vayOGVae1oxXe/2A4AO6J9+580uKHDO3JdHb7NzwwzK5xr/Fs0W40kiNHxM9vyTtQ=="
|
||||
crossorigin=""
|
||||
/>
|
||||
<link href='https://api.mapbox.com/mapbox-gl-js/v2.9.1/mapbox-gl.css' rel='stylesheet' />
|
||||
<link rel="stylesheet" href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.css" type="text/css">
|
||||
<main>
|
||||
<h1>${this.title}</h1>
|
||||
<vaadin-text-field placeholder="Search">
|
||||
<vaadin-icon slot="prefix" icon="vaadin:search"></vaadin-icon>
|
||||
</vaadin-text-field>
|
||||
<div id="map"></div>
|
||||
</main>
|
||||
<link
|
||||
href="https://api.mapbox.com/mapbox-gl-js/v2.9.1/mapbox-gl.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.css"
|
||||
type="text/css"
|
||||
/>
|
||||
<vaadin-app-layout>
|
||||
<vaadin-drawer-toggle slot="navbar touch-optimized">
|
||||
</vaadin-drawer-toggle>
|
||||
<h3 slot="navbar touch-optimized">${this.title}</h3>
|
||||
<pluckr-login slot="navbar"></pluckr-login>
|
||||
<div>
|
||||
<vaadin-text-field placeholder="Search">
|
||||
<vaadin-icon slot="prefix" icon="vaadin:search"></vaadin-icon>
|
||||
</vaadin-text-field>
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</vaadin-app-layout>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
272
src/main/js/pluckr-app/src/PluckrLogin.js
Normal file
272
src/main/js/pluckr-app/src/PluckrLogin.js
Normal file
@@ -0,0 +1,272 @@
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import '@vaadin/horizontal-layout';
|
||||
import '@vaadin/button';
|
||||
import '@vaadin/dialog';
|
||||
import '@vaadin/form-layout';
|
||||
import '@vaadin/text-field';
|
||||
import '@vaadin/password-field';
|
||||
import { dialogRenderer } from '@vaadin/dialog/lit';
|
||||
import '@vaadin/progress-bar';
|
||||
|
||||
export class PluckrLogin extends LitElement {
|
||||
responsiveSteps = [{ minWidth: 0, columns: 1 }];
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
signUpOpened: { type: Boolean },
|
||||
logInOpened: { type: Boolean },
|
||||
username: { type: String },
|
||||
password: { type: String },
|
||||
passwordConfirm: { type: String },
|
||||
loadingBarActive: { type: Boolean },
|
||||
errorMessage: { type: String },
|
||||
};
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css``;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.signUpOpened = false;
|
||||
this.logInOpened = false;
|
||||
this.loadingBarActive = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<vaadin-horizontal-layout
|
||||
slot="navbar touch-optimized"
|
||||
theme="spacing padding"
|
||||
>
|
||||
<vaadin-button theme="primary" @click="${this.signUpClicked}"
|
||||
>Sign up</vaadin-button
|
||||
>
|
||||
<vaadin-button theme="secondary" @click="${this.logInClicked}"
|
||||
>Login</vaadin-button
|
||||
>
|
||||
</vaadin-horizontal-layout>
|
||||
|
||||
<vaadin-dialog
|
||||
header-title="SignUp"
|
||||
.opened="${this.signUpOpened}"
|
||||
@opened-changed="${e => {
|
||||
this.signUpOpened = e.detail.value;
|
||||
}}"
|
||||
${dialogRenderer(this.signUpRenderer, [
|
||||
this.username,
|
||||
this.signUpOpened,
|
||||
this.password,
|
||||
this.loadingBarActive,
|
||||
this.errorMessage,
|
||||
])}
|
||||
id="signUp"
|
||||
>
|
||||
</vaadin-dialog>
|
||||
|
||||
<vaadin-dialog
|
||||
header-title="LogIn"
|
||||
.opened="${this.logInOpened}"
|
||||
@opened-changed="${e => {
|
||||
this.logInOpened = e.detail.value;
|
||||
}}"
|
||||
${dialogRenderer(this.logInRenderer, [
|
||||
this.username,
|
||||
this.logInOpened,
|
||||
this.password,
|
||||
this.loadingBarActive,
|
||||
this.errorMessage,
|
||||
])}
|
||||
id="logIn"
|
||||
>
|
||||
</vaadin-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
signUpClicked() {
|
||||
this.signUpOpened = true;
|
||||
}
|
||||
|
||||
logInClicked() {
|
||||
this.logInOpened = true;
|
||||
}
|
||||
|
||||
logInRenderer() {
|
||||
return html`
|
||||
<vaadin-form-layout .responsiveSteps="${this.responsiveSteps}">
|
||||
<vaadin-text-field
|
||||
value="${this.username}"
|
||||
@change="${e => {
|
||||
this.username = e.target.value;
|
||||
}}"
|
||||
label="email"
|
||||
></vaadin-text-field>
|
||||
<vaadin-password-field
|
||||
value="${this.password}"
|
||||
@change="${e => {
|
||||
this.password = e.target.value;
|
||||
}}"
|
||||
label="Password"
|
||||
></vaadin-password-field>
|
||||
|
||||
${this.loadingBarActive
|
||||
? html`<vaadin-progress-bar indeterminate></vaadin-progress-bar>`
|
||||
: undefined}
|
||||
${this.errorMessage
|
||||
? html`<p class="error" style="color:red">${this.errorMessage}</p>`
|
||||
: undefined}
|
||||
|
||||
<vaadin-horizontal-layout
|
||||
theme="spacing padding"
|
||||
style="justify-content: end"
|
||||
>
|
||||
<vaadin-button
|
||||
theme="secondary"
|
||||
@click="${() => {
|
||||
this.logInOpened = false;
|
||||
}}"
|
||||
>Cancel</vaadin-button
|
||||
>
|
||||
<vaadin-button theme="primary" @click="${this.logIn}"
|
||||
>LogIn</vaadin-button
|
||||
>
|
||||
</vaadin-horizontal-layout>
|
||||
</vaadin-form-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
signUpRenderer() {
|
||||
return html`
|
||||
<vaadin-form-layout .responsiveSteps="${this.responsiveSteps}">
|
||||
<vaadin-text-field
|
||||
value="${this.username}"
|
||||
@change="${e => {
|
||||
this.username = e.target.value;
|
||||
}}"
|
||||
label="email"
|
||||
></vaadin-text-field>
|
||||
<vaadin-password-field
|
||||
value="${this.password}"
|
||||
@change="${e => {
|
||||
this.password = e.target.value;
|
||||
}}"
|
||||
label="Password"
|
||||
></vaadin-password-field>
|
||||
<vaadin-password-field
|
||||
value="${this.passwordConfirm}"
|
||||
@change="${e => {
|
||||
this.passwordConfirm = e.target.value;
|
||||
}}"
|
||||
label="Confirm password"
|
||||
></vaadin-password-field>
|
||||
|
||||
${this.loadingBarActive
|
||||
? html`<vaadin-progress-bar indeterminate></vaadin-progress-bar>`
|
||||
: undefined}
|
||||
${this.errorMessage
|
||||
? html`<p class="error" style="color:red">${this.errorMessage}</p>`
|
||||
: undefined}
|
||||
|
||||
<vaadin-horizontal-layout
|
||||
theme="spacing padding"
|
||||
style="justify-content: end"
|
||||
>
|
||||
<vaadin-button
|
||||
theme="secondary"
|
||||
@click="${() => {
|
||||
this.signUpOpened = false;
|
||||
}}"
|
||||
>Cancel
|
||||
</vaadin-button>
|
||||
<vaadin-button theme="primary" @click="${this.signUp}"
|
||||
>Sign up</vaadin-button
|
||||
>
|
||||
</vaadin-horizontal-layout>
|
||||
</vaadin-form-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
logIn() {
|
||||
this.loadingBarActive = true;
|
||||
|
||||
fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
}),
|
||||
})
|
||||
.then(response => {
|
||||
this.loadingBarActive = false;
|
||||
if (!response.ok) {
|
||||
this.errorMessage = response.statusText;
|
||||
console.error(
|
||||
'There has been a problem logging in the user:',
|
||||
response.statusText
|
||||
);
|
||||
} else {
|
||||
this.username = null;
|
||||
this.password = null;
|
||||
this.passwordConfirm = null;
|
||||
this.logInOpened = false;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.errorMessage =
|
||||
'There has been an issue contacting the server. Please try again later';
|
||||
console.error(
|
||||
'There has been a problem with your fetch operation:',
|
||||
error
|
||||
);
|
||||
this.username = null;
|
||||
this.password = null;
|
||||
this.passwordConfirm = null;
|
||||
});
|
||||
}
|
||||
|
||||
signUp() {
|
||||
this.loadingBarActive = true;
|
||||
|
||||
fetch('/api/signup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
}),
|
||||
})
|
||||
.then(response => {
|
||||
this.loadingBarActive = false;
|
||||
if (!response.ok) {
|
||||
this.errorMessage = response.statusText;
|
||||
console.error(
|
||||
'There has been a problem saving the user:',
|
||||
response.statusText
|
||||
);
|
||||
} else {
|
||||
this.username = null;
|
||||
this.password = null;
|
||||
this.passwordConfirm = null;
|
||||
this.signUpOpened = false;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.errorMessage =
|
||||
'There has been an issue contacting the server. Please try again later';
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
'There has been a problem with your fetch operation:',
|
||||
error
|
||||
);
|
||||
this.username = null;
|
||||
this.password = null;
|
||||
this.passwordConfirm = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
3
src/main/js/pluckr-app/src/pluckr-login.js
Normal file
3
src/main/js/pluckr-app/src/pluckr-login.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import {PluckrLogin} from "./PluckrLogin.js";
|
||||
|
||||
customElements.define('pluckr-login', PluckrLogin);
|
||||
@@ -1,24 +1,58 @@
|
||||
package nl.lengrand.pluckr
|
||||
|
||||
import UserSession
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.engine.*
|
||||
import io.ktor.server.metrics.micrometer.*
|
||||
import io.ktor.server.netty.*
|
||||
import io.ktor.server.plugins.callloging.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
import io.ktor.server.plugins.cors.routing.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.sessions.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import net.postgis.jdbc.geometry.Point
|
||||
import nl.lengrand.pluckr.plugins.*
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import nl.lengrand.pluckr.plugins.configureRouting
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.sql.SchemaUtils
|
||||
import org.jetbrains.exposed.sql.StdOutSqlLogger
|
||||
import org.jetbrains.exposed.sql.addLogger
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
fun Application.myapp(){
|
||||
|
||||
val database = initDb()
|
||||
|
||||
install(CORS)
|
||||
install(Sessions) {
|
||||
cookie<UserSession>("user_session", SessionStorageMemory()) {
|
||||
cookie.path = "/"
|
||||
cookie.maxAgeInSeconds = 6000
|
||||
}
|
||||
}
|
||||
|
||||
install(Authentication) {
|
||||
session<UserSession>("user_session") {
|
||||
println("validating session!")
|
||||
|
||||
|
||||
validate { session ->
|
||||
if(session.name.isNotEmpty()) {
|
||||
println("Valid!")
|
||||
println(session)
|
||||
session
|
||||
} else {
|
||||
println("Not valid!")
|
||||
println(session)
|
||||
null
|
||||
}
|
||||
}
|
||||
challenge {
|
||||
println("redirecting")
|
||||
call.respondRedirect("/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// install(CORS)
|
||||
install(ContentNegotiation){
|
||||
json(Json {
|
||||
prettyPrint = true
|
||||
@@ -27,7 +61,6 @@ fun Application.myapp(){
|
||||
}
|
||||
install(CallLogging)
|
||||
// install(MicrometerMetrics)
|
||||
|
||||
configureRouting(database)
|
||||
}
|
||||
|
||||
@@ -37,24 +70,7 @@ fun initDb(): Database {
|
||||
|
||||
transaction {
|
||||
addLogger(StdOutSqlLogger)
|
||||
|
||||
SchemaUtils.create(Trees)
|
||||
|
||||
// val first = Tree.new {
|
||||
// name = "Laurier"
|
||||
// description = "un laurier accessible à tous"
|
||||
// location = Point(52.04681865145196, 5.079779509938945)
|
||||
// }
|
||||
|
||||
// Trees.insert {
|
||||
// it[name] = "Laurier 2"
|
||||
// it[description] = "un laurier accessible à tous"
|
||||
// it[location] = Point(52.04681865145196, 5.079779509938945)
|
||||
// }
|
||||
|
||||
|
||||
// println("Trees: ${Tree.all().joinToString {it.location.value}}")
|
||||
|
||||
SchemaUtils.create(Trees, Users)
|
||||
}
|
||||
return database
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import net.postgis.jdbc.geometry.Point
|
||||
import nl.lengrand.pluckr.Trees
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
@Serializable
|
||||
data class Tree(
|
||||
val id: Int? = null,
|
||||
val name: String,
|
||||
val description: String?,
|
||||
@Serializable(with = PointSerializer::class)
|
||||
val location : Point
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@SerialName("Point")
|
||||
private class PointSurrogate(val srid: Int, val x: Double, val y: Double)
|
||||
|
||||
object PointSerializer : KSerializer<Point> {
|
||||
override val descriptor: SerialDescriptor = PointSurrogate.serializer().descriptor
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Point) {
|
||||
val surrogate = PointSurrogate(value.srid, value.x, value.y)
|
||||
encoder.encodeSerializableValue(PointSurrogate.serializer(), surrogate)
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): Point {
|
||||
val surrogate = decoder.decodeSerializableValue(PointSurrogate.serializer())
|
||||
return Point(surrogate.x, surrogate.y)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fromRow(it: ResultRow): Tree {
|
||||
return Tree(it[Trees.id], it[Trees.name], it[Trees.description], it[Trees.location])
|
||||
}
|
||||
|
||||
class Controller(private val database: Database) {
|
||||
|
||||
fun getTrees() : ArrayList<Tree> {
|
||||
val trees : ArrayList<Tree> = arrayListOf()
|
||||
transaction(database){
|
||||
Trees.selectAll().map { trees.add(fromRow(it)) }
|
||||
}
|
||||
return trees
|
||||
}
|
||||
|
||||
fun getTrees(bbox: List<Double>?) : ArrayList<Tree> {
|
||||
println(bbox)
|
||||
return getTrees()
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import net.postgis.jdbc.geometry.Point
|
||||
import org.jetbrains.exposed.sql.Column
|
||||
import org.jetbrains.exposed.sql.ColumnType
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import org.jetbrains.exposed.sql.javatime.datetime
|
||||
import java.time.LocalDateTime
|
||||
|
||||
object Trees : Table() {
|
||||
val id = integer("id").autoIncrement()
|
||||
@@ -15,6 +17,14 @@ object Trees : Table() {
|
||||
override val primaryKey = PrimaryKey(id) // name is optional here
|
||||
}
|
||||
|
||||
object Users: Table() {
|
||||
val id = integer("id").autoIncrement()
|
||||
val username = varchar("username", 100).uniqueIndex()
|
||||
val password = varchar("password", 100)
|
||||
val createdAt = datetime("created_at").clientDefault{ LocalDateTime.now() }
|
||||
val updatedAt = datetime("updatedAt").clientDefault{ LocalDateTime.now() }
|
||||
}
|
||||
|
||||
fun Table.point(name: String, srid: Int = 4326): Column<Point>
|
||||
= registerColumn(name, PointColumnType())
|
||||
|
||||
|
||||
106
src/main/kotlin/nl/lengrand/pluckr/TreeController.kt
Normal file
106
src/main/kotlin/nl/lengrand/pluckr/TreeController.kt
Normal file
@@ -0,0 +1,106 @@
|
||||
import io.ktor.server.auth.*
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import net.postgis.jdbc.geometry.Point
|
||||
import nl.lengrand.pluckr.Trees
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.toKotlinLocalDateTime
|
||||
import nl.lengrand.pluckr.Users
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.mindrot.jbcrypt.BCrypt.*
|
||||
|
||||
@Serializable
|
||||
data class UserSession(val name: String) : Principal
|
||||
|
||||
@Serializable
|
||||
data class User(
|
||||
val id: Int? = null,
|
||||
val username: String,
|
||||
val password: String,
|
||||
val createdAt: LocalDateTime,
|
||||
val updatedAt: LocalDateTime,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Tree(
|
||||
val id: Int? = null,
|
||||
val name: String,
|
||||
val description: String?,
|
||||
@Serializable(with = PointSerializer::class)
|
||||
val location : Point
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@SerialName("Point")
|
||||
private class PointSurrogate(val srid: Int, val x: Double, val y: Double)
|
||||
|
||||
object PointSerializer : KSerializer<Point> {
|
||||
override val descriptor: SerialDescriptor = PointSurrogate.serializer().descriptor
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Point) {
|
||||
val surrogate = PointSurrogate(value.srid, value.x, value.y)
|
||||
encoder.encodeSerializableValue(PointSurrogate.serializer(), surrogate)
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): Point {
|
||||
val surrogate = decoder.decodeSerializableValue(PointSurrogate.serializer())
|
||||
return Point(surrogate.x, surrogate.y)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ResultRow.toTree(): Tree {
|
||||
return Tree(this[Trees.id], this[Trees.name], this[Trees.description], this[Trees.location])
|
||||
}
|
||||
|
||||
private fun ResultRow.toUser(): User {
|
||||
return User(this[Users.id], this[Users.username], this[Users.password], this[Users.createdAt].toKotlinLocalDateTime(), this[Users.updatedAt].toKotlinLocalDateTime())
|
||||
}
|
||||
|
||||
class UserController(private val database: Database){
|
||||
fun createUser(email: String, zepassword: String) {
|
||||
val salt = gensalt()
|
||||
transaction(database) {
|
||||
Users.insert {
|
||||
it[username] = email
|
||||
it[password] = hashpw(zepassword, salt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Will throw NoSuchElementException if there are no results, or IllegalArgumentException if there are more than one
|
||||
*/
|
||||
private fun getUser(email: String): User {
|
||||
val user = transaction(database) {
|
||||
Users.select{ Users.username eq email}.single().toUser()
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
fun getUser(email: String, zepassword: String): User {
|
||||
val user = getUser(email)
|
||||
if (!checkpw(zepassword, user.password)) throw AuthenticationException("Incorrect password detected")
|
||||
return user
|
||||
}
|
||||
}
|
||||
|
||||
class TreeController(private val database: Database) {
|
||||
fun getTrees() : ArrayList<Tree> {
|
||||
val trees : ArrayList<Tree> = arrayListOf()
|
||||
transaction(database){
|
||||
Trees.selectAll().map { trees.add(it.toTree()) }
|
||||
}
|
||||
return trees
|
||||
}
|
||||
|
||||
fun getTrees(bbox: List<Double>?) : ArrayList<Tree> {
|
||||
return getTrees()
|
||||
}
|
||||
}
|
||||
|
||||
class AuthenticationException(message:String): Exception(message)
|
||||
@@ -1,32 +1,78 @@
|
||||
package nl.lengrand.pluckr.plugins
|
||||
|
||||
import Controller
|
||||
import TreeController
|
||||
import UserController
|
||||
import UserSession
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.http.content.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.sessions.*
|
||||
import org.jetbrains.exposed.exceptions.ExposedSQLException
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
|
||||
fun Application.configureRouting(database: Database) {
|
||||
|
||||
val controller = Controller(database)
|
||||
val treeController = TreeController(database)
|
||||
val userController = UserController(database)
|
||||
|
||||
routing {
|
||||
|
||||
get("/api/trees") {
|
||||
println("IN HERE FIRST")
|
||||
if(call.request.queryParameters["bbox"] != null){
|
||||
println("IN HERE")
|
||||
val bbox = call.request.queryParameters["bbox"]?.split(",")?.map { it.toDouble() }
|
||||
call.respond(controller.getTrees(bbox))
|
||||
}
|
||||
else{
|
||||
call.respond(controller.getTrees())
|
||||
authenticate("user_session") {
|
||||
get("/api/authenticated"){
|
||||
call.respondText("Hello, ${call.principal<UserSession>()?.name}!")
|
||||
}
|
||||
}
|
||||
|
||||
post("/api/login") {
|
||||
val formParameters = call.receiveParameters()
|
||||
|
||||
try{
|
||||
val user = userController.getUser(formParameters["username"].toString(), formParameters["password"].toString())
|
||||
call.sessions.set(UserSession(user.username))
|
||||
call.respondRedirect("/")
|
||||
}
|
||||
catch(e: ExposedSQLException){
|
||||
call.response.status(HttpStatusCode(500, e.message!!))
|
||||
}
|
||||
}
|
||||
|
||||
get("/hello") {
|
||||
post("/api/logout") {
|
||||
call.sessions.clear<UserSession>()
|
||||
call.respondRedirect("/")
|
||||
}
|
||||
|
||||
post("/api/signup"){
|
||||
val formParameters = call.receiveParameters()
|
||||
try{
|
||||
userController.createUser(formParameters["username"].toString(), formParameters["password"].toString())
|
||||
call.response.status(HttpStatusCode.OK)
|
||||
}
|
||||
catch(e: ExposedSQLException){ // TODO: Should I leak exceptions here?
|
||||
val message = when (e.sqlState) {
|
||||
"23505" ->
|
||||
"User already exists"
|
||||
else ->
|
||||
"Unknown error, please retry later"
|
||||
}
|
||||
call.response.status(HttpStatusCode(500, message))
|
||||
}
|
||||
}
|
||||
|
||||
get("/api/trees") {
|
||||
if(call.request.queryParameters["bbox"] != null){
|
||||
val bbox = call.request.queryParameters["bbox"]?.split(",")?.map { it.toDouble() }
|
||||
call.respond(treeController.getTrees(bbox))
|
||||
}
|
||||
else{
|
||||
call.respond(treeController.getTrees())
|
||||
}
|
||||
}
|
||||
|
||||
get("/api/hello") {
|
||||
call.respondText("Hello the World!")
|
||||
}
|
||||
|
||||
|
||||
28
src/main/resources/dist/2a2586f8.js
vendored
28
src/main/resources/dist/2a2586f8.js
vendored
File diff suppressed because one or more lines are too long
3156
src/main/resources/dist/c3e72859.js
vendored
Normal file
3156
src/main/resources/dist/c3e72859.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
src/main/resources/dist/sw.js.map
vendored
2
src/main/resources/dist/sw.js.map
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"sw.js","sources":["../../../../../../../../private/var/folders/4b/jjn7wslx5fl1r5npk147_9b80000gn/T/a8724be573e0d97f416c1dabfa1c598c/sw.js"],"sourcesContent":["import {clientsClaim as workbox_core_clientsClaim} from '/Users/julienlengrand-lambert/Developer/pluckr/src/main/js/pluckr-app/node_modules/workbox-core/clientsClaim.mjs';\nimport {precacheAndRoute as workbox_precaching_precacheAndRoute} from '/Users/julienlengrand-lambert/Developer/pluckr/src/main/js/pluckr-app/node_modules/workbox-precaching/precacheAndRoute.mjs';\nimport {registerRoute as workbox_routing_registerRoute} from '/Users/julienlengrand-lambert/Developer/pluckr/src/main/js/pluckr-app/node_modules/workbox-routing/registerRoute.mjs';\nimport {NavigationRoute as workbox_routing_NavigationRoute} from '/Users/julienlengrand-lambert/Developer/pluckr/src/main/js/pluckr-app/node_modules/workbox-routing/NavigationRoute.mjs';\nimport {createHandlerBoundToURL as workbox_precaching_createHandlerBoundToURL} from '/Users/julienlengrand-lambert/Developer/pluckr/src/main/js/pluckr-app/node_modules/workbox-precaching/createHandlerBoundToURL.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\n\n\n\n\n\n\nself.skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n/**\n * The precacheAndRoute() method efficiently caches and responds to\n * requests for URLs in the manifest.\n * See https://goo.gl/S9QRab\n */\nworkbox_precaching_precacheAndRoute([\n {\n \"url\": \"bd0a6475.js\",\n \"revision\": \"b4aea298ab049d9d93db98f2f89dc5f4\"\n },\n {\n \"url\": \"index.html\",\n \"revision\": \"86949185ebc1ce841945a76fa5f21673\"\n },\n {\n \"url\": \"mapbox-gl-geocoder.esm.min.js\",\n \"revision\": \"4d4a445462472b4e53d5a0da309c261e\"\n },\n {\n \"url\": \"mapbox-gl.esm.js\",\n \"revision\": \"9c3ae05af3ab19e62b91b371aae8efbe\"\n },\n {\n \"url\": \"mapbox-gl.esm.min.js\",\n \"revision\": \"289a426b3421209ddd1c11c35e258de0\"\n }\n], {});\n\nworkbox_routing_registerRoute(new workbox_routing_NavigationRoute(workbox_precaching_createHandlerBoundToURL(\"/index.html\")));\n\n\n\n\n\n\n"],"names":["self","skipWaiting","workbox_core_clientsClaim","workbox_precaching_precacheAndRoute","url","revision","workbox","registerRoute","workbox_routing_NavigationRoute","NavigationRoute","workbox_precaching_createHandlerBoundToURL"],"mappings":"0nBAuBAA,KAAKC,cAELC,EAAAA,eAQAC,EAAAA,iBAAoC,CAClC,CACEC,IAAO,cACPC,SAAY,oCAEd,CACED,IAAO,aACPC,SAAY,oCAEd,CACED,IAAO,gCACPC,SAAY,oCAEd,CACED,IAAO,mBACPC,SAAY,oCAEd,CACED,IAAO,uBACPC,SAAY,qCAEb,IAE0BC,EAAAC,cAAC,IAAIC,EAAJC,gBAAoCC,0BAA2C"}
|
||||
{"version":3,"file":"sw.js","sources":["../../../../../../../../private/var/folders/4b/jjn7wslx5fl1r5npk147_9b80000gn/T/bec2369c6dd1d05ca69900335130d7e5/sw.js"],"sourcesContent":["import {clientsClaim as workbox_core_clientsClaim} from '/Users/julienlengrand-lambert/Developer/pluckr/src/main/js/pluckr-app/node_modules/workbox-core/clientsClaim.mjs';\nimport {precacheAndRoute as workbox_precaching_precacheAndRoute} from '/Users/julienlengrand-lambert/Developer/pluckr/src/main/js/pluckr-app/node_modules/workbox-precaching/precacheAndRoute.mjs';\nimport {registerRoute as workbox_routing_registerRoute} from '/Users/julienlengrand-lambert/Developer/pluckr/src/main/js/pluckr-app/node_modules/workbox-routing/registerRoute.mjs';\nimport {NavigationRoute as workbox_routing_NavigationRoute} from '/Users/julienlengrand-lambert/Developer/pluckr/src/main/js/pluckr-app/node_modules/workbox-routing/NavigationRoute.mjs';\nimport {createHandlerBoundToURL as workbox_precaching_createHandlerBoundToURL} from '/Users/julienlengrand-lambert/Developer/pluckr/src/main/js/pluckr-app/node_modules/workbox-precaching/createHandlerBoundToURL.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\n\n\n\n\n\n\nself.skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n/**\n * The precacheAndRoute() method efficiently caches and responds to\n * requests for URLs in the manifest.\n * See https://goo.gl/S9QRab\n */\nworkbox_precaching_precacheAndRoute([\n {\n \"url\": \"bd0a6475.js\",\n \"revision\": \"b4aea298ab049d9d93db98f2f89dc5f4\"\n },\n {\n \"url\": \"index.html\",\n \"revision\": \"86949185ebc1ce841945a76fa5f21673\"\n },\n {\n \"url\": \"mapbox-gl-geocoder.esm.min.js\",\n \"revision\": \"4d4a445462472b4e53d5a0da309c261e\"\n },\n {\n \"url\": \"mapbox-gl.esm.js\",\n \"revision\": \"9c3ae05af3ab19e62b91b371aae8efbe\"\n },\n {\n \"url\": \"mapbox-gl.esm.min.js\",\n \"revision\": \"289a426b3421209ddd1c11c35e258de0\"\n }\n], {});\n\nworkbox_routing_registerRoute(new workbox_routing_NavigationRoute(workbox_precaching_createHandlerBoundToURL(\"/index.html\")));\n\n\n\n\n\n\n"],"names":["self","skipWaiting","workbox_core_clientsClaim","workbox_precaching_precacheAndRoute","url","revision","workbox","registerRoute","workbox_routing_NavigationRoute","NavigationRoute","workbox_precaching_createHandlerBoundToURL"],"mappings":"0nBAuBAA,KAAKC,cAELC,EAAAA,eAQAC,EAAAA,iBAAoC,CAClC,CACEC,IAAO,cACPC,SAAY,oCAEd,CACED,IAAO,aACPC,SAAY,oCAEd,CACED,IAAO,gCACPC,SAAY,oCAEd,CACED,IAAO,mBACPC,SAAY,oCAEd,CACED,IAAO,uBACPC,SAAY,qCAEb,IAE0BC,EAAAC,cAAC,IAAIC,EAAJC,gBAAoCC,0BAA2C"}
|
||||
Reference in New Issue
Block a user