mirror of
https://github.com/tiennm99/qr-attendance-demo.git
synced 2026-10-11 03:13:47 +00:00
(Add) complete prototype
This commit is contained in:
1 parent
24ff5f8ebd
commit
df014229c8
36 files changed
+778
-16933
No files matched your search
@@ -1 +0,0 @@
|
|||||||
gradlew text eol=lf
|
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
name: Build and Release Vaadin Project
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
tags:
|
||||||
pull_request:
|
- 'v*'
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-release:
|
build-and-release:
|
||||||
@@ -29,6 +26,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
files: build/libs/*
|
files: build/libs/*
|
||||||
env:
|
env:
|
||||||
|
|||||||
+1
-21
@@ -1,25 +1,5 @@
|
|||||||
/target/
|
submissions.xlsx
|
||||||
.idea/
|
|
||||||
.settings
|
|
||||||
.project
|
|
||||||
.classpath
|
|
||||||
|
|
||||||
*.iml
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# The following files are generated/updated by vaadin-maven-plugin
|
|
||||||
node_modules/
|
|
||||||
src/main/frontend/generated/
|
|
||||||
pnpmfile.js
|
|
||||||
vite.generated.ts
|
|
||||||
|
|
||||||
# Browser drivers for local integration tests
|
|
||||||
drivers/
|
|
||||||
# Error screenshots generated by TestBench for failed integration tests
|
|
||||||
error-screenshots/
|
|
||||||
webpack.generated.js
|
|
||||||
|
|
||||||
src/main/dev-bundle
|
|
||||||
|
|
||||||
|
|
||||||
HELP.md
|
HELP.md
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
# java-qr-attendance
|
# java-qr-attendance
|
||||||
|
|
||||||
Just a QR code attendance demo app for my sister's representation, but written in java
|
Just a QR code attendance demo app for my sister's representation, but written in java
|
||||||
|
|
||||||
|
## How to run
|
||||||
|
|
||||||
|
1. Download java 21 from [here](https://www.oracle.com/java/technologies/downloads/#jdk21-windows)
|
||||||
|
2. Download the .jar file from the releases page
|
||||||
|
3. Run the .jar file with `java -jar <filename>.jar`
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
**These code for educational purposes only. It is not secure and not optimized for production use.**
|
||||||
|
|
||||||
|
_Good luck, sis!_
|
||||||
+10
-24
@@ -1,15 +1,12 @@
|
|||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
idea
|
idea
|
||||||
`maven-publish`
|
|
||||||
id("com.google.osdetector") version "1.7.3"
|
id("com.google.osdetector") version "1.7.3"
|
||||||
id("io.spring.dependency-management") version "1.1.6"
|
id("io.spring.dependency-management") version "1.1.6"
|
||||||
id("org.springframework.boot") version "3.3.2"
|
id("org.springframework.boot") version "3.3.2"
|
||||||
id("com.vaadin") version "24.4.8"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val ngrokVersion = "1.1.0"
|
val ngrokVersion = "1.1.0"
|
||||||
val vaadinVersion by extra("24.4.8")
|
|
||||||
val springCloudVersion by extra("2023.0.3")
|
val springCloudVersion by extra("2023.0.3")
|
||||||
|
|
||||||
group = "io.github.tiennm99"
|
group = "io.github.tiennm99"
|
||||||
@@ -39,7 +36,6 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation("io.projectreactor:reactor-test")
|
|
||||||
annotationProcessor("org.projectlombok:lombok")
|
annotationProcessor("org.projectlombok:lombok")
|
||||||
compileOnly("org.projectlombok:lombok")
|
compileOnly("org.projectlombok:lombok")
|
||||||
|
|
||||||
@@ -47,9 +43,10 @@ dependencies {
|
|||||||
implementation("com.google.zxing:javase:3.5.3")
|
implementation("com.google.zxing:javase:3.5.3")
|
||||||
implementation("com.ngrok:ngrok-java:${ngrokVersion}")
|
implementation("com.ngrok:ngrok-java:${ngrokVersion}")
|
||||||
implementation("com.ngrok:ngrok-java-native:${ngrokVersion}:${osdetector.classifier}")
|
implementation("com.ngrok:ngrok-java-native:${ngrokVersion}:${osdetector.classifier}")
|
||||||
implementation("com.vaadin:vaadin-spring-boot-starter")
|
|
||||||
implementation("org.apache.poi:poi-ooxml:5.3.0")
|
implementation("org.apache.poi:poi-ooxml:5.3.0")
|
||||||
implementation("org.springframework.boot:spring-boot-starter")
|
implementation("org.springframework.boot:spring-boot-starter")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
|
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
@@ -57,7 +54,6 @@ dependencies {
|
|||||||
|
|
||||||
dependencyManagement {
|
dependencyManagement {
|
||||||
imports {
|
imports {
|
||||||
mavenBom("com.vaadin:vaadin-bom:$vaadinVersion")
|
|
||||||
mavenBom("org.springframework.cloud:spring-cloud-dependencies:$springCloudVersion")
|
mavenBom("org.springframework.cloud:spring-cloud-dependencies:$springCloudVersion")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,25 +62,15 @@ tasks.withType<Test> {
|
|||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
publishing {
|
|
||||||
repositories {
|
|
||||||
maven {
|
|
||||||
name = "GitHubPackages"
|
|
||||||
url = uri("https://maven.pkg.github.com/${System.getenv("GITHUB_REPOSITORY")}")
|
|
||||||
credentials {
|
|
||||||
username = System.getenv("GITHUB_ACTOR")
|
|
||||||
password = System.getenv("GITHUB_TOKEN")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
publications {
|
|
||||||
create<MavenPublication>("gpr") {
|
|
||||||
from(components["java"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType<JavaCompile> {
|
tasks.withType<JavaCompile> {
|
||||||
sourceCompatibility = JavaVersion.VERSION_21.toString()
|
sourceCompatibility = JavaVersion.VERSION_21.toString()
|
||||||
targetCompatibility = JavaVersion.VERSION_21.toString()
|
targetCompatibility = JavaVersion.VERSION_21.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.jar {
|
||||||
|
manifest {
|
||||||
|
attributes["Main-Class"] = "io.github.tiennm99.qrattendance.QrAttendanceApplication"
|
||||||
|
}
|
||||||
|
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
|
}
|
||||||
Generated
-16654
File diff suppressed because it is too large.
Load diff
-105
@@ -1,105 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "no-name",
|
|
||||||
"license": "UNLICENSED",
|
|
||||||
"type": "module",
|
|
||||||
"dependencies": {
|
|
||||||
"@polymer/polymer": "3.5.1",
|
|
||||||
"@vaadin/bundles": "24.4.5",
|
|
||||||
"@vaadin/common-frontend": "0.0.19",
|
|
||||||
"@vaadin/polymer-legacy-adapter": "24.4.5",
|
|
||||||
"@vaadin/react-components": "24.4.5",
|
|
||||||
"@vaadin/router": "1.7.5",
|
|
||||||
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
|
||||||
"@vaadin/vaadin-lumo-styles": "24.4.5",
|
|
||||||
"@vaadin/vaadin-material-styles": "24.4.5",
|
|
||||||
"@vaadin/vaadin-themable-mixin": "24.4.5",
|
|
||||||
"@vaadin/vaadin-usage-statistics": "2.1.2",
|
|
||||||
"construct-style-sheets-polyfill": "3.1.0",
|
|
||||||
"date-fns": "2.29.3",
|
|
||||||
"lit": "3.1.4",
|
|
||||||
"react": "18.3.1",
|
|
||||||
"react-dom": "18.3.1",
|
|
||||||
"react-router-dom": "6.23.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/preset-react": "7.24.7",
|
|
||||||
"@rollup/plugin-replace": "5.0.7",
|
|
||||||
"@rollup/pluginutils": "5.1.0",
|
|
||||||
"@types/react": "18.3.3",
|
|
||||||
"@types/react-dom": "18.3.0",
|
|
||||||
"@vitejs/plugin-react": "4.3.1",
|
|
||||||
"async": "3.2.5",
|
|
||||||
"glob": "10.4.1",
|
|
||||||
"rollup-plugin-brotli": "3.1.0",
|
|
||||||
"rollup-plugin-visualizer": "5.12.0",
|
|
||||||
"strip-css-comments": "5.0.0",
|
|
||||||
"transform-ast": "2.4.4",
|
|
||||||
"typescript": "5.4.5",
|
|
||||||
"vite": "5.3.3",
|
|
||||||
"vite-plugin-checker": "0.6.4",
|
|
||||||
"workbox-build": "7.1.1",
|
|
||||||
"workbox-core": "7.1.0",
|
|
||||||
"workbox-precaching": "7.1.0"
|
|
||||||
},
|
|
||||||
"vaadin": {
|
|
||||||
"dependencies": {
|
|
||||||
"@polymer/polymer": "3.5.1",
|
|
||||||
"@vaadin/bundles": "24.4.5",
|
|
||||||
"@vaadin/common-frontend": "0.0.19",
|
|
||||||
"@vaadin/polymer-legacy-adapter": "24.4.5",
|
|
||||||
"@vaadin/react-components": "24.4.5",
|
|
||||||
"@vaadin/router": "1.7.5",
|
|
||||||
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
|
||||||
"@vaadin/vaadin-lumo-styles": "24.4.5",
|
|
||||||
"@vaadin/vaadin-material-styles": "24.4.5",
|
|
||||||
"@vaadin/vaadin-themable-mixin": "24.4.5",
|
|
||||||
"@vaadin/vaadin-usage-statistics": "2.1.2",
|
|
||||||
"construct-style-sheets-polyfill": "3.1.0",
|
|
||||||
"date-fns": "2.29.3",
|
|
||||||
"lit": "3.1.4",
|
|
||||||
"react": "18.3.1",
|
|
||||||
"react-dom": "18.3.1",
|
|
||||||
"react-router-dom": "6.23.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/preset-react": "7.24.7",
|
|
||||||
"@rollup/plugin-replace": "5.0.7",
|
|
||||||
"@rollup/pluginutils": "5.1.0",
|
|
||||||
"@types/react": "18.3.3",
|
|
||||||
"@types/react-dom": "18.3.0",
|
|
||||||
"@vitejs/plugin-react": "4.3.1",
|
|
||||||
"async": "3.2.5",
|
|
||||||
"glob": "10.4.1",
|
|
||||||
"rollup-plugin-brotli": "3.1.0",
|
|
||||||
"rollup-plugin-visualizer": "5.12.0",
|
|
||||||
"strip-css-comments": "5.0.0",
|
|
||||||
"transform-ast": "2.4.4",
|
|
||||||
"typescript": "5.4.5",
|
|
||||||
"vite": "5.3.3",
|
|
||||||
"vite-plugin-checker": "0.6.4",
|
|
||||||
"workbox-build": "7.1.1",
|
|
||||||
"workbox-core": "7.1.0",
|
|
||||||
"workbox-precaching": "7.1.0"
|
|
||||||
},
|
|
||||||
"hash": "2a4c77083ab51f28e5e318219da09835683db988de08b49ff99c8f3532628271"
|
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"@vaadin/bundles": "$@vaadin/bundles",
|
|
||||||
"@vaadin/polymer-legacy-adapter": "$@vaadin/polymer-legacy-adapter",
|
|
||||||
"@vaadin/vaadin-development-mode-detector": "$@vaadin/vaadin-development-mode-detector",
|
|
||||||
"@vaadin/router": "$@vaadin/router",
|
|
||||||
"@vaadin/vaadin-usage-statistics": "$@vaadin/vaadin-usage-statistics",
|
|
||||||
"@vaadin/react-components": "$@vaadin/react-components",
|
|
||||||
"@vaadin/common-frontend": "$@vaadin/common-frontend",
|
|
||||||
"react-dom": "$react-dom",
|
|
||||||
"construct-style-sheets-polyfill": "$construct-style-sheets-polyfill",
|
|
||||||
"react-router-dom": "$react-router-dom",
|
|
||||||
"lit": "$lit",
|
|
||||||
"@polymer/polymer": "$@polymer/polymer",
|
|
||||||
"react": "$react",
|
|
||||||
"date-fns": "$date-fns",
|
|
||||||
"@vaadin/vaadin-themable-mixin": "$@vaadin/vaadin-themable-mixin",
|
|
||||||
"@vaadin/vaadin-lumo-styles": "$@vaadin/vaadin-lumo-styles",
|
|
||||||
"@vaadin/vaadin-material-styles": "$@vaadin/vaadin-material-styles"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<!--
|
|
||||||
This file is auto-generated by Vaadin.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<style>
|
|
||||||
body, #outlet {
|
|
||||||
height: 100vh;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- This outlet div is where the views are rendered -->
|
|
||||||
<div id="outlet"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
package io.github.tiennm99.qrattendance;
|
package io.github.tiennm99.qrattendance;
|
||||||
|
|
||||||
import com.vaadin.flow.spring.annotation.EnableVaadin;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import com.vaadin.flow.theme.Theme;
|
|
||||||
import com.vaadin.flow.component.page.AppShellConfigurator;
|
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@EnableVaadin
|
@Slf4j
|
||||||
public class QRAttendanceApplication implements AppShellConfigurator {
|
public class QRAttendanceApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(QRAttendanceApplication.class, args);
|
SpringApplication.run(QRAttendanceApplication.class, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package io.github.tiennm99.qrattendance.config;
|
package io.github.tiennm99.qrattendance.configuration;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package io.github.tiennm99.qrattendance.configuration;
|
||||||
|
|
||||||
|
import io.github.tiennm99.qrattendance.ngrok.NgrokInterceptor;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
||||||
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
private final NgrokInterceptor ngrokInterceptor;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
registry
|
||||||
|
.addInterceptor(ngrokInterceptor)
|
||||||
|
.addPathPatterns(
|
||||||
|
"/",
|
||||||
|
"/ask-server-location",
|
||||||
|
"/set-server-location",
|
||||||
|
"/manage",
|
||||||
|
"/submissions",
|
||||||
|
"/download-submissions");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package io.github.tiennm99.qrattendance.controller.student;
|
||||||
|
|
||||||
|
import io.github.tiennm99.qrattendance.data.ApplicationData;
|
||||||
|
import io.github.tiennm99.qrattendance.util.DistanceUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
||||||
|
public class GateController {
|
||||||
|
|
||||||
|
private final ApplicationData data;
|
||||||
|
|
||||||
|
@GetMapping("/gate")
|
||||||
|
public String gate() {
|
||||||
|
return "gate";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/check-location")
|
||||||
|
@ResponseBody
|
||||||
|
public String checkLocation(@RequestParam double latitude, @RequestParam double longitude) {
|
||||||
|
double serverLatitude = data.getServerLocation().getLeft();
|
||||||
|
double serverLongitude = data.getServerLocation().getRight();
|
||||||
|
double distance =
|
||||||
|
DistanceUtil.calculateDistance(latitude, longitude, serverLatitude, serverLongitude);
|
||||||
|
|
||||||
|
if (distance > 1.0) {
|
||||||
|
return "rejected";
|
||||||
|
} else {
|
||||||
|
return "accepted";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package io.github.tiennm99.qrattendance.controller.student;
|
||||||
|
|
||||||
|
import io.github.tiennm99.qrattendance.service.SubmissionService;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
||||||
|
public class SubmitController {
|
||||||
|
|
||||||
|
private final SubmissionService submissionService;
|
||||||
|
|
||||||
|
@PostMapping("/submit")
|
||||||
|
@ResponseBody
|
||||||
|
public String submit(
|
||||||
|
@RequestParam String studentId,
|
||||||
|
@RequestParam double latitude,
|
||||||
|
@RequestParam double longitude,
|
||||||
|
@RequestHeader("X-Forwarded-For") String ip,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
String userAgent = request.getHeader("User-Agent");
|
||||||
|
submissionService.addSubmission(studentId, latitude, longitude, ip, userAgent);
|
||||||
|
return "Submission received";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package io.github.tiennm99.qrattendance.controller.student;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
||||||
|
public class ViewController {
|
||||||
|
|
||||||
|
@GetMapping("/view")
|
||||||
|
public String view() {
|
||||||
|
return "view";
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
package io.github.tiennm99.qrattendance.controller.teacher;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class AccessDeniedController {
|
||||||
|
|
||||||
|
@GetMapping("/access-denied")
|
||||||
|
public String accessDenied() {
|
||||||
|
return "access-denied";
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package io.github.tiennm99.qrattendance.controller.teacher;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
||||||
|
public class AskServerLocationController {
|
||||||
|
@GetMapping("/ask-server-location")
|
||||||
|
public String askServerLocation() {
|
||||||
|
return "ask-server-location";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package io.github.tiennm99.qrattendance.controller.teacher;
|
||||||
|
|
||||||
|
import io.github.tiennm99.qrattendance.data.ApplicationData;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
||||||
|
public class HomeController {
|
||||||
|
|
||||||
|
private final ApplicationData data;
|
||||||
|
|
||||||
|
@GetMapping("/")
|
||||||
|
public String home() {
|
||||||
|
if (data.getServerLocation() == null) {
|
||||||
|
return "redirect:/ask-server-location";
|
||||||
|
} else {
|
||||||
|
return "redirect:/manage";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
package io.github.tiennm99.qrattendance.controller.teacher;
|
||||||
|
|
||||||
|
import io.github.tiennm99.qrattendance.data.ApplicationData;
|
||||||
|
import io.github.tiennm99.qrattendance.data.Submission;
|
||||||
|
import io.github.tiennm99.qrattendance.service.QRCodeService;
|
||||||
|
import io.github.tiennm99.qrattendance.service.SubmissionService;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
||||||
|
public class ManageController {
|
||||||
|
private final ApplicationData data;
|
||||||
|
private final QRCodeService qrCodeService;
|
||||||
|
private final SubmissionService submissionService;
|
||||||
|
|
||||||
|
@GetMapping("/manage")
|
||||||
|
public String manage(Model model) {
|
||||||
|
var qrCode = qrCodeService.generateQRCode(data.getNgrokUrl() + "/gate");
|
||||||
|
var submissions = data.getSubmissions();
|
||||||
|
model.addAttribute("qrCode", qrCode);
|
||||||
|
model.addAttribute("submissions", submissions);
|
||||||
|
return "manage";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/submissions")
|
||||||
|
@ResponseBody
|
||||||
|
public List<Submission> getSubmissions() {
|
||||||
|
return data.getSubmissions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/export-submissions")
|
||||||
|
@ResponseBody
|
||||||
|
public String exportSubmissions() {
|
||||||
|
submissionService.exportSubmissions();
|
||||||
|
return "Submissions exported";
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package io.github.tiennm99.qrattendance.controller.teacher;
|
||||||
|
|
||||||
|
import io.github.tiennm99.qrattendance.data.ApplicationData;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
||||||
|
@Slf4j
|
||||||
|
public class SetServerLocationController {
|
||||||
|
|
||||||
|
private final ApplicationData data;
|
||||||
|
|
||||||
|
@PostMapping("/set-server-location")
|
||||||
|
public String confirmLocation(@RequestParam("lat") double lat, @RequestParam("lon") double lon) {
|
||||||
|
log.info("Location confirmed: {}, {}", lat, lon);
|
||||||
|
data.setServerLocation(new ImmutablePair<>(lat, lon));
|
||||||
|
return "redirect:/manage";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package io.github.tiennm99.qrattendance.data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.context.annotation.ApplicationScope;
|
||||||
|
|
||||||
|
@ApplicationScope
|
||||||
|
@Component
|
||||||
|
@Data
|
||||||
|
public class ApplicationData {
|
||||||
|
String ngrokUrl;
|
||||||
|
Pair<Double, Double> serverLocation;
|
||||||
|
List<Submission> submissions = new ArrayList<>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package io.github.tiennm99.qrattendance.data;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Data
|
||||||
|
public final class Submission {
|
||||||
|
private final String studentId;
|
||||||
|
private final double latitude;
|
||||||
|
private final double longitude;
|
||||||
|
private final String ip;
|
||||||
|
private final String userAgent;
|
||||||
|
private final String duplicatedWith;
|
||||||
|
private final boolean tooFarFromServer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package io.github.tiennm99.qrattendance.ngrok;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
||||||
|
public class NgrokInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||||
|
throws Exception {
|
||||||
|
var requestUrl = request.getRequestURL().toString();
|
||||||
|
if (requestUrl.contains("ngrok")) {
|
||||||
|
response.sendRedirect("/access-denied");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-6
@@ -1,13 +1,15 @@
|
|||||||
package io.github.tiennm99.qrattendance.ngrok;
|
package io.github.tiennm99.qrattendance.ngrok;
|
||||||
|
|
||||||
import com.ngrok.Session;
|
import com.ngrok.Session;
|
||||||
import io.github.tiennm99.qrattendance.config.NgrokConfiguration;
|
import io.github.tiennm99.qrattendance.configuration.NgrokConfiguration;
|
||||||
|
import io.github.tiennm99.qrattendance.data.ApplicationData;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.web.context.WebServerInitializedEvent;
|
import org.springframework.boot.web.context.WebServerInitializedEvent;
|
||||||
import org.springframework.context.event.EventListener;
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -19,7 +21,10 @@ import org.springframework.stereotype.Component;
|
|||||||
public class NgrokWebServerEventListener {
|
public class NgrokWebServerEventListener {
|
||||||
|
|
||||||
private final NgrokConfiguration ngrokConfiguration;
|
private final NgrokConfiguration ngrokConfiguration;
|
||||||
private String url;
|
private final ApplicationData data;
|
||||||
|
|
||||||
|
@Value("${server.port}")
|
||||||
|
int port;
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
@EventListener
|
@EventListener
|
||||||
@@ -27,10 +32,12 @@ public class NgrokWebServerEventListener {
|
|||||||
if (ngrokConfiguration.isEnabled()) {
|
if (ngrokConfiguration.isEnabled()) {
|
||||||
var sessionBuilder = Session.withAuthtoken(ngrokConfiguration.getAuthToken());
|
var sessionBuilder = Session.withAuthtoken(ngrokConfiguration.getAuthToken());
|
||||||
try (var session = sessionBuilder.connect()) {
|
try (var session = sessionBuilder.connect()) {
|
||||||
var endpoint =
|
var url = "http://localhost:" + port;
|
||||||
session.forwardHttp(session.httpEndpoint(), new URI("http://localhost:8080").toURL());
|
var endpoint = session.forwardHttp(session.httpEndpoint(), new URI(url).toURL());
|
||||||
url = endpoint.getUrl();
|
var ngrokUrl = endpoint.getUrl();
|
||||||
log.info("ngrok url: {}", url);
|
log.info("url: {}", url);
|
||||||
|
log.info("ngrok url: {}", ngrokUrl);
|
||||||
|
data.setNgrokUrl(ngrokUrl);
|
||||||
endpoint.join();
|
endpoint.join();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package io.github.tiennm99.qrattendance.service;
|
||||||
|
|
||||||
|
import com.google.zxing.BarcodeFormat;
|
||||||
|
import com.google.zxing.client.j2se.MatrixToImageWriter;
|
||||||
|
import com.google.zxing.qrcode.QRCodeWriter;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.util.Base64;
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class QRCodeService {
|
||||||
|
|
||||||
|
public String generateQRCode(String text) {
|
||||||
|
try {
|
||||||
|
QRCodeWriter qrCodeWriter = new QRCodeWriter();
|
||||||
|
var bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, 256, 256);
|
||||||
|
BufferedImage qrImage = MatrixToImageWriter.toBufferedImage(bitMatrix);
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
ImageIO.write(qrImage, "png", baos);
|
||||||
|
byte[] imageBytes = baos.toByteArray();
|
||||||
|
return Base64.getEncoder().encodeToString(imageBytes);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to generate QR code", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package io.github.tiennm99.qrattendance.service;
|
||||||
|
|
||||||
|
import io.github.tiennm99.qrattendance.data.ApplicationData;
|
||||||
|
import io.github.tiennm99.qrattendance.data.Submission;
|
||||||
|
import io.github.tiennm99.qrattendance.util.DistanceUtil;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.poi.ss.usermodel.Cell;
|
||||||
|
import org.apache.poi.ss.usermodel.CellStyle;
|
||||||
|
import org.apache.poi.ss.usermodel.FillPatternType;
|
||||||
|
import org.apache.poi.ss.usermodel.IndexedColors;
|
||||||
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
|
import org.apache.poi.ss.usermodel.Sheet;
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class SubmissionService {
|
||||||
|
private final ApplicationData data;
|
||||||
|
|
||||||
|
public void addSubmission(
|
||||||
|
String studentId, double latitude, double longitude, String ip, String userAgent) {
|
||||||
|
var duplicatedWith = "";
|
||||||
|
for (Submission submission : data.getSubmissions()) {
|
||||||
|
if (submission.getUserAgent().equals(userAgent)) {
|
||||||
|
duplicatedWith = submission.getStudentId();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double serverLatitude = data.getServerLocation().getLeft();
|
||||||
|
double serverLongitude = data.getServerLocation().getRight();
|
||||||
|
double distance =
|
||||||
|
DistanceUtil.calculateDistance(latitude, longitude, serverLatitude, serverLongitude);
|
||||||
|
|
||||||
|
boolean tooFarFromServer = distance > 1.0;
|
||||||
|
|
||||||
|
var newSubmission =
|
||||||
|
new Submission(
|
||||||
|
studentId, latitude, longitude, ip, userAgent, duplicatedWith, tooFarFromServer);
|
||||||
|
|
||||||
|
data.getSubmissions().add(newSubmission);
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"New submission: {}, {}, {}, {}, {}, {}, {}",
|
||||||
|
studentId,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
ip,
|
||||||
|
userAgent,
|
||||||
|
duplicatedWith,
|
||||||
|
tooFarFromServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void exportSubmissions() {
|
||||||
|
List<Submission> submissions = data.getSubmissions();
|
||||||
|
try (Workbook workbook = new XSSFWorkbook()) {
|
||||||
|
Sheet sheet = workbook.createSheet("Submissions");
|
||||||
|
Row headerRow = sheet.createRow(0);
|
||||||
|
String[] headers = {
|
||||||
|
"Student ID",
|
||||||
|
"Latitude",
|
||||||
|
"Longitude",
|
||||||
|
"IP",
|
||||||
|
"User Agent",
|
||||||
|
"Duplicated With",
|
||||||
|
"Too Far From Server"
|
||||||
|
};
|
||||||
|
for (int i = 0; i < headers.length; i++) {
|
||||||
|
Cell cell = headerRow.createCell(i);
|
||||||
|
cell.setCellValue(headers[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
CellStyle redStyle = workbook.createCellStyle();
|
||||||
|
redStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
|
||||||
|
redStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||||
|
|
||||||
|
int rowNum = 1;
|
||||||
|
for (Submission submission : submissions) {
|
||||||
|
Row row = sheet.createRow(rowNum++);
|
||||||
|
row.createCell(0).setCellValue(submission.getStudentId());
|
||||||
|
row.createCell(1).setCellValue(submission.getLatitude());
|
||||||
|
row.createCell(2).setCellValue(submission.getLongitude());
|
||||||
|
row.createCell(3).setCellValue(submission.getIp());
|
||||||
|
row.createCell(4).setCellValue(submission.getUserAgent());
|
||||||
|
row.createCell(5).setCellValue(submission.getDuplicatedWith());
|
||||||
|
row.createCell(6).setCellValue(submission.isTooFarFromServer());
|
||||||
|
|
||||||
|
if (!submission.getDuplicatedWith().isEmpty() || submission.isTooFarFromServer()) {
|
||||||
|
for (int i = 0; i < headers.length; i++) {
|
||||||
|
row.getCell(i).setCellStyle(redStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
File exportFile = new File("submissions.xlsx");
|
||||||
|
try (FileOutputStream fileOut = new FileOutputStream(exportFile)) {
|
||||||
|
workbook.write(fileOut);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error exporting submissions", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package io.github.tiennm99.qrattendance.util;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.apache.commons.math3.util.FastMath;
|
||||||
|
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public class DistanceUtil {
|
||||||
|
public static double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
|
||||||
|
final int R = 6371; // Radius of the earth in km
|
||||||
|
double latDistance = FastMath.toRadians(lat2 - lat1);
|
||||||
|
double lonDistance = FastMath.toRadians(lon2 - lon1);
|
||||||
|
double a =
|
||||||
|
FastMath.sin(latDistance / 2) * FastMath.sin(latDistance / 2)
|
||||||
|
+ FastMath.cos(FastMath.toRadians(lat1))
|
||||||
|
* FastMath.cos(FastMath.toRadians(lat2))
|
||||||
|
* FastMath.sin(lonDistance / 2)
|
||||||
|
* FastMath.sin(lonDistance / 2);
|
||||||
|
double c = 2 * FastMath.atan2(FastMath.sqrt(a), FastMath.sqrt(1 - a));
|
||||||
|
return R * c; // Distance in km
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package io.github.tiennm99.qrattendance.views;
|
|
||||||
|
|
||||||
import com.vaadin.flow.component.html.H1;
|
|
||||||
import com.vaadin.flow.component.html.Paragraph;
|
|
||||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
|
||||||
import com.vaadin.flow.router.Route;
|
|
||||||
|
|
||||||
@Route("")
|
|
||||||
public class HomeView extends VerticalLayout {
|
|
||||||
|
|
||||||
public HomeView() {
|
|
||||||
|
|
||||||
add(new H1("Welcome to your new application"));
|
|
||||||
add(new Paragraph("This is the home view"));
|
|
||||||
|
|
||||||
add(new Paragraph("You can edit this view in src\\main\\java\\io\\github\\tiennm99\\qrattendance\\views\\HomeView.java"));
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ ngrok:
|
|||||||
enabled: true
|
enabled: true
|
||||||
authToken: CHANGE_ME
|
authToken: CHANGE_ME
|
||||||
server:
|
server:
|
||||||
port: 8080
|
port: 31009
|
||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: java-qr-attendance
|
name: java-qr-attendance
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<title>Access Denied</title>
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container mt-5 text-center">
|
||||||
|
<h1>Access Denied</h1>
|
||||||
|
<p>You do not have permission to access this page.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<title>Map</title>
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"/>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"/>
|
||||||
|
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
|
||||||
|
<style>
|
||||||
|
#map {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container mt-5">
|
||||||
|
<div id="map" class="mb-3"></div>
|
||||||
|
<form id="locationForm" th:action="@{/set-server-location}" method="post" class="text-center">
|
||||||
|
<input type="hidden" id="lat" name="lat"/>
|
||||||
|
<input type="hidden" id="lon" name="lon"/>
|
||||||
|
<button type="submit" id="confirmBtn" class="btn btn-primary" disabled>Confirm Location</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script th:inline="javascript">
|
||||||
|
var map = L.map('map').setView([0, 0], 2);
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap contributors'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
var marker;
|
||||||
|
|
||||||
|
function onLocationFound(e) {
|
||||||
|
var radius = e.accuracy / 2;
|
||||||
|
if (marker) {
|
||||||
|
map.removeLayer(marker);
|
||||||
|
}
|
||||||
|
marker = L.marker(e.latlng).addTo(map);
|
||||||
|
map.setView(e.latlng, 13);
|
||||||
|
document.getElementById('confirmBtn').disabled = false;
|
||||||
|
updateForm(e.latlng);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLocationError(e) {
|
||||||
|
alert(e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateForm(latlng) {
|
||||||
|
document.getElementById('lat').value = latlng.lat;
|
||||||
|
document.getElementById('lon').value = latlng.lng;
|
||||||
|
}
|
||||||
|
|
||||||
|
map.on('locationfound', onLocationFound);
|
||||||
|
map.on('locationerror', onLocationError);
|
||||||
|
map.on('click', function (e) {
|
||||||
|
onLocationFound(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
map.locate({setView: true, maxZoom: 16});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<title>Gate</title>
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"/>
|
||||||
|
<script>
|
||||||
|
function getLocation() {
|
||||||
|
if (navigator.geolocation) {
|
||||||
|
navigator.geolocation.getCurrentPosition(sendLocation);
|
||||||
|
} else {
|
||||||
|
alert("Geolocation is not supported by this browser.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendLocation(position) {
|
||||||
|
const latitude = position.coords.latitude;
|
||||||
|
const longitude = position.coords.longitude;
|
||||||
|
|
||||||
|
fetch('/check-location', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: `latitude=${latitude}&longitude=${longitude}`
|
||||||
|
})
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(result => {
|
||||||
|
if (result === 'rejected') {
|
||||||
|
alert('You are rejected due to being more than 1km away from the server location.');
|
||||||
|
} else {
|
||||||
|
window.location.href = '/view';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body onload="getLocation()">
|
||||||
|
<div class="container text-center mt-5">
|
||||||
|
<h1>Gate</h1>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<title>Manage</title>
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"/>
|
||||||
|
<style>
|
||||||
|
#submissions {
|
||||||
|
max-height: 50vh;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code {
|
||||||
|
width: 256px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-red {
|
||||||
|
color: red !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-striped tbody tr:nth-of-type(odd) {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-striped tbody tr:nth-of-type(even) {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
function displaySubmission(submission) {
|
||||||
|
var submissionsTable = document.getElementById('submissionsTable');
|
||||||
|
var newRow = submissionsTable.insertRow();
|
||||||
|
var highlightClass = (submission.duplicatedWith != '' || submission.tooFarFromServer)
|
||||||
|
? 'highlight-red' : '';
|
||||||
|
newRow.innerHTML = `
|
||||||
|
<td class="${highlightClass}">${submission.studentId}</td>
|
||||||
|
<td class="${highlightClass}">${submission.latitude}</td>
|
||||||
|
<td class="${highlightClass}">${submission.longitude}</td>
|
||||||
|
<td class="${highlightClass}">${submission.ip}</td>
|
||||||
|
<td class="${highlightClass}">${submission.userAgent}</td>
|
||||||
|
<td class="${highlightClass}">${submission.duplicatedWith}</td>
|
||||||
|
<td class="${highlightClass}">${submission.tooFarFromServer}</td>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshSubmissions() {
|
||||||
|
fetch('/submissions')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
var submissionsTable = document.getElementById('submissionsTable');
|
||||||
|
submissionsTable.innerHTML = '';
|
||||||
|
data.forEach(submission => displaySubmission(submission));
|
||||||
|
var submissionsDiv = document.getElementById('submissions');
|
||||||
|
submissionsDiv.scrollTop = submissionsDiv.scrollHeight;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(refreshSubmissions, 5000);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container mt-5">
|
||||||
|
<h1 class="text-center">Manage</h1>
|
||||||
|
<div class="text-center mb-3">
|
||||||
|
<img th:src="'data:image/png;base64,' + ${qrCode}" alt="QR Code" class="qr-code"/>
|
||||||
|
</div>
|
||||||
|
<div id="submissions" class="mb-3">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Student ID</th>
|
||||||
|
<th>Latitude</th>
|
||||||
|
<th>Longitude</th>
|
||||||
|
<th>IP</th>
|
||||||
|
<th>User Agent</th>
|
||||||
|
<th>Duplicated With</th>
|
||||||
|
<th>Too Far From Server</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="submissionsTable">
|
||||||
|
<tr th:each="submission : ${submissions}"
|
||||||
|
th:class="${(submission.duplicatedWith != '' or submission.tooFarFromServer) ? 'highlight-red' : ''}">
|
||||||
|
<td th:text="${submission.studentId}"></td>
|
||||||
|
<td th:text="${submission.latitude}"></td>
|
||||||
|
<td th:text="${submission.longitude}"></td>
|
||||||
|
<td th:text="${submission.ip}"></td>
|
||||||
|
<td th:text="${submission.userAgent}"></td>
|
||||||
|
<td th:text="${submission.duplicatedWith}"></td>
|
||||||
|
<td th:text="${submission.tooFarFromServer}"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<button class="btn btn-primary" onclick="exportSubmissions()">Export Submissions</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function exportSubmissions() {
|
||||||
|
fetch('/export-submissions', {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(message => alert(message))
|
||||||
|
.catch(error => console.error('Error:', error));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<title>View</title>
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"/>
|
||||||
|
<script>
|
||||||
|
function getLocationAndSubmit() {
|
||||||
|
if (navigator.geolocation) {
|
||||||
|
navigator.geolocation.getCurrentPosition(sendLocation);
|
||||||
|
} else {
|
||||||
|
alert("Geolocation is not supported by this browser.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendLocation(position) {
|
||||||
|
const latitude = position.coords.latitude;
|
||||||
|
const longitude = position.coords.longitude;
|
||||||
|
const studentId = document.getElementById("studentId").value;
|
||||||
|
const userAgent = navigator.userAgent;
|
||||||
|
|
||||||
|
fetch('/submit', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: `studentId=${studentId}&latitude=${latitude}&longitude=${longitude}`
|
||||||
|
})
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(result => {
|
||||||
|
alert(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container mt-5">
|
||||||
|
<h1 class="text-center">Submit Your Information</h1>
|
||||||
|
<form onsubmit="event.preventDefault(); getLocationAndSubmit();" class="text-center">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="studentId">Student ID:</label>
|
||||||
|
<input type="text" id="studentId" name="studentId" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
// This TypeScript configuration file is generated by vaadin-maven-plugin.
|
|
||||||
// This is needed for TypeScript compiler to compile your TypeScript code in the project.
|
|
||||||
// It is recommended to commit this file to the VCS.
|
|
||||||
// You might want to change the configurations to fit your preferences
|
|
||||||
// For more information about the configurations, please refer to http://www.typescriptlang.org/docs/handbook/tsconfig-json.html
|
|
||||||
{
|
|
||||||
"_version": "9.1",
|
|
||||||
"compilerOptions": {
|
|
||||||
"sourceMap": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"inlineSources": true,
|
|
||||||
"module": "esNext",
|
|
||||||
"target": "es2020",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"strict": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"noImplicitAny": true,
|
|
||||||
"noImplicitThis": true,
|
|
||||||
"noUnusedLocals": false,
|
|
||||||
"noUnusedParameters": false,
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"useDefineForClassFields": false,
|
|
||||||
"baseUrl": "src/main/frontend",
|
|
||||||
"paths": {
|
|
||||||
"@vaadin/flow-frontend": ["generated/jar-resources"],
|
|
||||||
"@vaadin/flow-frontend/*": ["generated/jar-resources/*"],
|
|
||||||
"Frontend/*": ["*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src/main/frontend/**/*",
|
|
||||||
"types.d.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"src/main/frontend/generated/jar-resources/**"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Vendored
-17
@@ -1,17 +0,0 @@
|
|||||||
// This TypeScript modules definition file is generated by vaadin-maven-plugin.
|
|
||||||
// You can not directly import your different static files into TypeScript,
|
|
||||||
// This is needed for TypeScript compiler to declare and export as a TypeScript module.
|
|
||||||
// It is recommended to commit this file to the VCS.
|
|
||||||
// You might want to change the configurations to fit your preferences
|
|
||||||
declare module '*.css?inline' {
|
|
||||||
import type { CSSResultGroup } from 'lit';
|
|
||||||
const content: CSSResultGroup;
|
|
||||||
export default content;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow any CSS Custom Properties
|
|
||||||
declare module 'csstype' {
|
|
||||||
interface Properties {
|
|
||||||
[index: `--${string}`]: any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { UserConfigFn } from 'vite';
|
|
||||||
import { overrideVaadinConfig } from './vite.generated';
|
|
||||||
|
|
||||||
const customConfig: UserConfigFn = (env) => ({
|
|
||||||
// Here you can add custom Vite parameters
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
});
|
|
||||||
|
|
||||||
export default overrideVaadinConfig(customConfig);
|
|
||||||
Reference in new issue
Block a user