Reimplement apps and events endpoints from Ktor app in Spring Boot
Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
parent
10a5bf9e7b
commit
e6b7efa95f
99 changed files with 452 additions and 11 deletions
|
@ -11,6 +11,7 @@ repositories {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':shared')
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-quartz'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
package com.wbrawner.flayre.server.apps;
|
||||
|
||||
import com.wbrawner.flayre.App;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/apps")
|
||||
public class AppController {
|
||||
|
||||
private final AppRepository appRepository;
|
||||
|
||||
@Autowired
|
||||
public AppController(AppRepository appRepository) {
|
||||
this.appRepository = appRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<App>> getApps() {
|
||||
return ResponseEntity.ok(appRepository.getApps());
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<App> getApp(@PathVariable String id) {
|
||||
App app;
|
||||
if ((app = appRepository.getApp(id)) != null) {
|
||||
return ResponseEntity.ok(app);
|
||||
}
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<App> createApp(@RequestBody String name) {
|
||||
App app = new App(name);
|
||||
if (appRepository.saveApp(app)) {
|
||||
return ResponseEntity.ok(app);
|
||||
}
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}")
|
||||
public ResponseEntity<Void> updateApp(@PathVariable String id, @RequestBody String name) {
|
||||
if (appRepository.updateApp(id, name)) {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteApp(@PathVariable String id) {
|
||||
if (appRepository.deleteApp(id)) {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package com.wbrawner.flayre.server.apps;
|
||||
|
||||
import com.wbrawner.flayre.App;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class AppRepository {
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
public AppRepository(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS apps (\n" +
|
||||
" id VARCHAR(32) PRIMARY KEY,\n" +
|
||||
" name VARCHAR(256) UNIQUE NOT NULL\n" +
|
||||
")");
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<App> getApps() {
|
||||
return jdbcTemplate.query("SELECT * from apps", (rs, rowNum) -> new App(
|
||||
rs.getString(rs.findColumn("id")),
|
||||
rs.getString(rs.findColumn("name"))
|
||||
));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public App getApp(String appId) {
|
||||
var results = jdbcTemplate.query(
|
||||
"SELECT * from apps WHERE id = ?",
|
||||
new Object[]{appId},
|
||||
(rs, rowNum) -> new App(
|
||||
rs.getString(rs.findColumn("id")),
|
||||
rs.getString(rs.findColumn("name"))
|
||||
)
|
||||
);
|
||||
|
||||
if (results.size() == 1) {
|
||||
return results.get(0);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean saveApp(App app) {
|
||||
return jdbcTemplate.update(
|
||||
"INSERT INTO apps (id, name) VALUES (?, ?)",
|
||||
app.getId(),
|
||||
app.getName()
|
||||
) == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the name of a given app.
|
||||
*
|
||||
* @param appId the id of the app to update
|
||||
* @param name the new name for the app
|
||||
* @return true on success or false on failure
|
||||
*/
|
||||
public boolean updateApp(String appId, String name) {
|
||||
return jdbcTemplate.update("UPDATE apps SET name = ? WHERE id = ?", name, appId) == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an app by its id.
|
||||
*
|
||||
* @param appId the id of the app to delete
|
||||
* @return true if the app was deleted or false if not
|
||||
*/
|
||||
public boolean deleteApp(String appId) {
|
||||
return jdbcTemplate.update("DELETE FROM apps WHERE id = ?", appId) == 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package com.wbrawner.flayre.server.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
public SecurityConfigurerAdapter(Environment environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
|
||||
auth.inMemoryAuthentication()
|
||||
.withUser(environment.getProperty("flayre.admin.user")).password(passwordEncoder().encode(environment.getProperty("flayre.admin.password")))
|
||||
.authorities("ROLE_USER");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http.authorizeRequests()
|
||||
.antMatchers(HttpMethod.GET, "/flayre.js")
|
||||
.permitAll()
|
||||
.antMatchers(HttpMethod.POST, "/api/events")
|
||||
.permitAll()
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
.anyRequest()
|
||||
.authenticated()
|
||||
.and()
|
||||
.httpBasic()
|
||||
.and()
|
||||
.cors()
|
||||
.configurationSource(request -> {
|
||||
var corsConfig = new CorsConfiguration();
|
||||
corsConfig.applyPermitDefaultValues();
|
||||
corsConfig.setAllowedOrigins(Collections.singletonList("*"));
|
||||
corsConfig.setAllowedMethods(Arrays.asList(HttpMethod.POST.name(), HttpMethod.OPTIONS.name()));
|
||||
corsConfig.setAllowCredentials(true);
|
||||
return corsConfig;
|
||||
})
|
||||
.and()
|
||||
.csrf()
|
||||
.disable();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package com.wbrawner.flayre.server.events;
|
||||
|
||||
import com.wbrawner.flayre.Event;
|
||||
import com.wbrawner.flayre.EventRequest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/events")
|
||||
public class EventController {
|
||||
|
||||
private final EventRepository eventRepository;
|
||||
|
||||
@Autowired
|
||||
public EventController(EventRepository eventRepository) {
|
||||
this.eventRepository = eventRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<Event>> getEvents() {
|
||||
return ResponseEntity.ok(eventRepository.getEvents());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Event> createEvent(
|
||||
@RequestHeader("User-Agent") String userAgent,
|
||||
@RequestBody EventRequest eventRequest
|
||||
) {
|
||||
Event event = new Event(
|
||||
eventRequest.appId,
|
||||
eventRequest.date,
|
||||
eventRequest.type,
|
||||
userAgent,
|
||||
eventRequest.platform,
|
||||
eventRequest.manufacturer,
|
||||
eventRequest.model,
|
||||
eventRequest.version,
|
||||
eventRequest.locale,
|
||||
eventRequest.sessionId,
|
||||
eventRequest.data
|
||||
);
|
||||
if (eventRepository.saveEvent(event)) {
|
||||
return ResponseEntity.ok(event);
|
||||
}
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package com.wbrawner.flayre.server.events;
|
||||
|
||||
import com.wbrawner.flayre.Event;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class EventRepository {
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
public EventRepository(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS events (\n" +
|
||||
" id VARCHAR(32) PRIMARY KEY,\n" +
|
||||
" app_id VARCHAR(32) NOT NULL,\n" +
|
||||
" date DATETIME NOT NULL,\n" +
|
||||
" user_agent VARCHAR(256),\n" +
|
||||
" platform VARCHAR(32),\n" +
|
||||
" manufacturer VARCHAR(256),\n" +
|
||||
" model VARCHAR(256),\n" +
|
||||
" version VARCHAR(32),\n" +
|
||||
" locale VARCHAR(8),\n" +
|
||||
" session_id VARCHAR(32),\n" +
|
||||
" data TEXT DEFAULT NULL,\n" +
|
||||
" type VARCHAR(256) DEFAULT NULL,\n" +
|
||||
" FOREIGN KEY (app_id)\n" +
|
||||
" REFERENCES apps(id)\n" +
|
||||
" ON DELETE CASCADE\n" +
|
||||
")");
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<Event> getEvents() {
|
||||
return jdbcTemplate.query("SELECT * from events", (rs, rowNum) -> Event.fromResultSet(rs));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Event getEvent(String eventId) {
|
||||
var results = jdbcTemplate.query(
|
||||
"SELECT * from events WHERE id = ?",
|
||||
new Object[]{eventId},
|
||||
(rs, rowNum) -> Event.fromResultSet(rs)
|
||||
);
|
||||
|
||||
if (results.size() == 1) {
|
||||
return results.get(0);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean saveEvent(Event event) {
|
||||
return jdbcTemplate.update(
|
||||
"INSERT INTO events (" +
|
||||
"id, " +
|
||||
"app_id, " +
|
||||
"date, " +
|
||||
"user_agent, " +
|
||||
"platform, " +
|
||||
"manufacturer, " +
|
||||
"model, " +
|
||||
"version, " +
|
||||
"locale, " +
|
||||
"session_id, " +
|
||||
"data, " +
|
||||
"type" +
|
||||
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
event.getId(),
|
||||
event.getAppId(),
|
||||
event.getDate(),
|
||||
event.getUserAgent(),
|
||||
event.getPlatform(),
|
||||
event.getManufacturer(),
|
||||
event.getModel(),
|
||||
event.getVersion(),
|
||||
event.getLocale(),
|
||||
event.getSessionId(),
|
||||
event.getData(),
|
||||
event.getType().name()
|
||||
) == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all events before a given date.
|
||||
*
|
||||
* @param before the date before which all events will be deleted
|
||||
* @return the number of events that were deleted
|
||||
*/
|
||||
public int deleteEvents(Date before) {
|
||||
return jdbcTemplate.update("DELETE FROM events WHERE date <= ?", before);
|
||||
}
|
||||
}
|
|
@ -1 +1,9 @@
|
|||
|
||||
spring.datasource.url=jdbc:mysql://localhost:3306/flayre
|
||||
spring.datasource.username=flayre
|
||||
spring.datasource.password=flayre
|
||||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
spring.datasource.testWhileIdle=true
|
||||
spring.datasource.timeBetweenEvictionRunsMillis=60000
|
||||
spring.datasource.validationQuery=SELECT 1
|
||||
flayre.admin.user=admin
|
||||
flayre.admin.password=flayre
|
||||
|
|
25
server/src/main/resources/static/flayre.js
Normal file
25
server/src/main/resources/static/flayre.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
(function () {
|
||||
if (window.navigator.doNotTrack === '1') {
|
||||
console.log('Flayre respects DNT');
|
||||
return;
|
||||
}
|
||||
const flayreDomain = document.currentScript.src.split('/').slice(0, 3).join('/');
|
||||
const app = document.currentScript.dataset.app;
|
||||
|
||||
fetch(`${flayreDomain}/api/events`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
appId: app,
|
||||
date: new Date().toISOString(),
|
||||
platform: window.navigator.platform,
|
||||
locale: window.navigator.language,
|
||||
data: window.location.pathname,
|
||||
type: 'VIEW'
|
||||
})
|
||||
}).catch((err) => {
|
||||
console.error(err)
|
||||
})
|
||||
})();
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,2 +0,0 @@
|
|||
5
|
||||
2
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1 +0,0 @@
|
|||
/home/wbrawner/Projects/flayre-android/shared/build/classes/kotlin/main/com/wbrawner/flayre/App.class:/home/wbrawner/Projects/flayre-android/shared/build/classes/kotlin/main/com/wbrawner/flayre/Event$InteractionType.class:/home/wbrawner/Projects/flayre-android/shared/build/classes/kotlin/main/com/wbrawner/flayre/Event.class:/home/wbrawner/Projects/flayre-android/shared/build/classes/kotlin/main/com/wbrawner/flayre/UtilsKt.class
|
Binary file not shown.
|
@ -1,2 +0,0 @@
|
|||
Manifest-Version: 1.0
|
||||
|
|
@ -2,15 +2,15 @@ package com.wbrawner.flayre;
|
|||
|
||||
import static com.wbrawner.flayre.Utils.randomId;
|
||||
|
||||
class App {
|
||||
public class App {
|
||||
private final String id;
|
||||
private final String name;
|
||||
|
||||
App(String name) {
|
||||
public App(String name) {
|
||||
this(randomId(32), name);
|
||||
}
|
||||
|
||||
App(String id, String name) {
|
||||
public App(String id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package com.wbrawner.flayre;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Date;
|
||||
|
||||
import static com.wbrawner.flayre.Utils.randomId;
|
||||
|
||||
class Event {
|
||||
public class Event {
|
||||
private final String id;
|
||||
private final String appId;
|
||||
private final Date date;
|
||||
|
@ -119,11 +121,28 @@ class Event {
|
|||
return data;
|
||||
}
|
||||
|
||||
enum InteractionType {
|
||||
public enum InteractionType {
|
||||
VIEW,
|
||||
CLICK,
|
||||
ERROR,
|
||||
CRASH
|
||||
}
|
||||
|
||||
public static Event fromResultSet(ResultSet rs) throws SQLException {
|
||||
return new Event(
|
||||
rs.getString(rs.findColumn("id")),
|
||||
rs.getString(rs.findColumn("name")),
|
||||
rs.getDate(rs.findColumn("date")),
|
||||
Event.InteractionType.valueOf(rs.getString(rs.findColumn("type"))),
|
||||
rs.getString(rs.findColumn("user_agent")),
|
||||
rs.getString(rs.findColumn("platform")),
|
||||
rs.getString(rs.findColumn("manufacturer")),
|
||||
rs.getString(rs.findColumn("model")),
|
||||
rs.getString(rs.findColumn("version")),
|
||||
rs.getString(rs.findColumn("locale")),
|
||||
rs.getString(rs.findColumn("session_id")),
|
||||
rs.getString(rs.findColumn("data"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
38
shared/src/main/java/com/wbrawner/flayre/EventRequest.java
Normal file
38
shared/src/main/java/com/wbrawner/flayre/EventRequest.java
Normal file
|
@ -0,0 +1,38 @@
|
|||
package com.wbrawner.flayre;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class EventRequest {
|
||||
public final String appId;
|
||||
public final Date date;
|
||||
public final Event.InteractionType type;
|
||||
public final String platform;
|
||||
public final String manufacturer;
|
||||
public final String model;
|
||||
public final String version;
|
||||
public final String locale;
|
||||
public final String sessionId;
|
||||
public final String data;
|
||||
|
||||
public EventRequest(String appId,
|
||||
Date date,
|
||||
Event.InteractionType type,
|
||||
String platform,
|
||||
String manufacturer,
|
||||
String model,
|
||||
String version,
|
||||
String locale,
|
||||
String sessionId,
|
||||
String data) {
|
||||
this.appId = appId;
|
||||
this.date = date;
|
||||
this.type = type;
|
||||
this.platform = platform;
|
||||
this.manufacturer = manufacturer;
|
||||
this.model = model;
|
||||
this.version = version;
|
||||
this.locale = locale;
|
||||
this.sessionId = sessionId;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue