(Add) complete prototype

This commit is contained in:
tiennm99 committed 2024-08-15 00:23:35 +07:00
1 parent 24ff5f8ebd
commit df014229c8
36 files changed
+778 -16933

No files matched your search

-1
View File
@@ -1 +0,0 @@
gradlew text eol=lf
+3 -5
View File
@@ -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
View File
@@ -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
+12
View File
@@ -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
View File
@@ -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
}
-16654
View File
File diff suppressed because it is too large. Load diff
-105
View File
@@ -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"
}
}
-23
View File
@@ -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,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";
}
}
}
@@ -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";
}
}
@@ -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";
}
}
@@ -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";
}
}
}
@@ -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";
}
}
@@ -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;
}
}
@@ -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"));
}
}
+1 -1
View File
@@ -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>
+43
View File
@@ -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>
+112
View File
@@ -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>
+48
View File
@@ -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>
-39
View File
@@ -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
View File
@@ -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;
}
}
-9
View File
@@ -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);