Quarkus : User login, Logout dan Register dengan Qute Template Engine

 


Dalam dunia pengembangan aplikasi web, hampir setiap sistem membutuhkan mekanisme autentikasi seperti login, registrasi, hingga logout. Dengan framework modern seperti Quarkus, proses ini bisa dibuat lebih sederhana, cepat, dan efisien. Ditambah lagi, integrasi dengan Qute templating engine bawaan Quarkus — memungkinkan kita membangun antarmuka web yang ringan namun tetap powerful.

Pada tulisan ini, kita akan membahas langkah demi langkah bagaimana membangun sistem User Registration dan Login menggunakan Quarkus, Hibernate Panache untuk manajemen database, serta Qute untuk rendering halaman seperti /login, /registration, hingga /dashboard.

Dengan pendekatan ini, kamu akan melihat betapa mudahnya menggabungkan kekuatan Quarkus + Qute untuk membuat aplikasi web full-stack dengan fitur autentikasi dasar.

 1) Tambah dependencies (Gradle)

Di build.gradle, pastikan ini ada (yang penting: bridge Qute ↔ REST dan BCrypt untuk hashing):

dependencies {
implementation 'io.quarkus:quarkus-jdbc-postgresql'
implementation 'io.quarkus:quarkus-hibernate-orm-panache'
implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")
implementation 'io.quarkus:quarkus-arc'
implementation 'io.quarkus:quarkus-rest'
implementation 'io.quarkus:quarkus-rest-jackson'
implementation 'io.quarkus:quarkus-qute'
implementation 'io.quarkus:quarkus-rest-qute'
implementation "io.quarkus:quarkus-elytron-security-common" // BcryptUtil
testImplementation 'io.quarkus:quarkus-junit5'
testImplementation 'io.rest-assured:rest-assured'
}

Tanpa quarkus-rest-qute, return TemplateInstance tidak akan otomatis dirender.

2) Konfigurasi DB (dev)

Di src/main/resources/application.properties (contoh dev):

quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/qyboilerplate_db
quarkus.datasource.username=postgres
quarkus.datasource.password=postgres
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true
Table app_users (auto generated dari entity User)
Table user_sessions (auto generated dari entity Session)

 

3) Entity & Enum

3.1 Status enum

package com.yoesoff.plate.enums;

public enum Status {
Active,
Pending,
Inactive,
Banned,
}

3.2 User entity 

package com.yoesoff.plate.entity;

import com.yoesoff.plate.enums.Status;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import jakarta.persistence.*;

import java.util.UUID;

@Entity
@Table(name = "app_users", uniqueConstraints = {
@UniqueConstraint(columnNames = {"username"}),
@UniqueConstraint(columnNames = {"email"})
})
public class User extends PanacheEntityBase {
@Id
@GeneratedValue
public UUID id;

@Column(nullable = false)
public String username;

@Column(nullable = false)
public String passwordHash; // simpan HASH, bukan plaintext

@Column(nullable = false)
public String email;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
public Status status = Status.Active;

public User() {
}

public User(String username, String aDefault, String email) {
this.username = username;
this.passwordHash = aDefault;
this.email = email;
}
}

3.3 Session entity (untuk cookie session sederhana) 

package com.yoesoff.plate.entity;

import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import jakarta.persistence.*;

import java.time.Instant;
import java.util.UUID;

@Entity
@Table(name = "user_sessions", indexes = {
@Index(columnList = "token", unique = true)
})
public class Session extends PanacheEntityBase {
@Id
@GeneratedValue
public UUID id;

@ManyToOne(optional = false, fetch = FetchType.LAZY)
public User user;

@Column(nullable = false, unique = true, length = 64)
public String token;

@Column(nullable = false)
public Instant createdAt = Instant.now();

@Column(nullable = false)
public Instant expiresAt; // mis. now + 7 hari
}

4) Service sederhana (hash, auth, session) 

package com.yoesoff.plate.service;

import com.yoesoff.plate.entity.Session;
import com.yoesoff.plate.entity.User;
import com.yoesoff.plate.enums.Status;
import io.quarkus.hibernate.orm.panache.Panache;
import io.quarkus.runtime.util.StringUtil;
import io.quarkus.elytron.security.common.BcryptUtil;
import jakarta.enterprise.context.ApplicationScoped;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Optional;
import java.util.UUID;

@ApplicationScoped
public class AuthService {

public Optional<User> findByUsername(String username) {
return Optional.ofNullable(User.find("username", username).firstResult());
}

public boolean usernameExists(String username) {
return User.count("username", username) > 0;
}

public boolean emailExists(String email) {
return User.count("email", email) > 0;
}

public User register(String username, String email, String plainPassword) {
String hash = BcryptUtil.bcryptHash(plainPassword);
User u = new User();
u.username = username;
u.email = email;
u.passwordHash = hash;
u.status = Status.Active;
u.persist();
return u;
}

public Optional<User> authenticate(String username, String plainPassword) {
User u = User.find("username", username).firstResult();
if (u == null) return Optional.empty();
if (u.status != Status.Active) return Optional.empty();
return BcryptUtil.matches(plainPassword, u.passwordHash) ? Optional.of(u) : Optional.empty();
}

public Session createSession(User user, int daysValid) {
Session s = new Session();
s.user = user;
s.token = UUID.randomUUID().toString().replace("-", "");
s.expiresAt = Instant.now().plus(daysValid, ChronoUnit.DAYS);
s.persist();
return s;
}

public Optional<User> findUserByToken(String token) {
Session s = Session.find("token = ?1 and expiresAt > ?2", token, Instant.now()).firstResult();
return Optional.ofNullable(s != null ? s.user : null);
}

public void deleteSession(String token) {
Panache.getEntityManager().createQuery("delete from Session s where s.token = :t")
.setParameter("t", token)
.executeUpdate();
}
}

5) Resource (endpoint + Qute)

Kita bikin satu resource AuthResource untuk semua route.

package com.yoesoff.plate.resource;

import com.yoesoff.plate.entity.User;
import com.yoesoff.plate.service.AuthService;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;

import org.jboss.resteasy.reactive.RestForm;

import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@Path("/")
public class AuthResource {

private static final String SESSION_COOKIE = "SESSION";

@Inject Template login; // templates/login.html
@Inject Template registration; // templates/registration.html
@Inject Template dashboard; // templates/dashboard.html

@Inject AuthService auth;

// Utility: ambil user dari cookie token
private Optional<User> currentUser(@CookieParam(SESSION_COOKIE) String token) {
if (token == null || token.isBlank()) return Optional.empty();
return auth.findUserByToken(token);
}

// --- LOGIN ---
@GET
@Path("login")
@Produces(MediaType.TEXT_HTML)
public TemplateInstance loginPage(@QueryParam("msg") String msg) {
return login.data("msg", msg);
}

@POST
@Path("login")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Transactional
public Response doLogin(@RestForm String username, @RestForm String password, @Context UriInfo uriInfo) {
if (username == null || password == null || username.isBlank() || password.isBlank()) {
URI uri = uriInfo.getBaseUriBuilder().path("login").queryParam("msg", "Username/password wajib").build();
return Response.seeOther(uri).build();
}

Optional<User> userOpt = auth.authenticate(username, password);
if (userOpt.isEmpty()) {
URI uri = uriInfo.getBaseUriBuilder().path("login").queryParam("msg", "Login gagal").build();
return Response.seeOther(uri).build();
}

var session = auth.createSession(userOpt.get(), 7);
NewCookie cookie = new NewCookie(
SESSION_COOKIE, session.token, "/", null,
"login session", 7 * 24 * 3600, // maxAge 7 hari
true, // secure? set true kalau sudah HTTPS
true // httpOnly
);

return Response.seeOther(uriInfo.getBaseUriBuilder().path("dashboard").build())
.cookie(cookie)
.build();
}

// --- REGISTRATION ---
@GET
@Path("registration")
@Produces(MediaType.TEXT_HTML)
public TemplateInstance registrationPage(@QueryParam("msg") String msg) {
return registration.data("msg", msg);
}

@POST
@Path("registration")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Transactional
    public Response doRegister(@RestForm String username, @RestForm String email, @RestForm String password, @Context UriInfo uriInfo) {
if (username == null || email == null || password == null ||
username.isBlank() || email.isBlank() || password.isBlank()) {
URI uri = uriInfo.getBaseUriBuilder().path("registration").queryParam("msg", "Semua field wajib diisi").build();
return Response.seeOther(uri).build();
}
if (auth.usernameExists(username)) {
URI uri = uriInfo.getBaseUriBuilder().path("registration").queryParam("msg", "Username sudah dipakai").build();
return Response.seeOther(uri).build();
}
if (auth.emailExists(email)) {
URI uri = uriInfo.getBaseUriBuilder().path("registration").queryParam("msg", "Email sudah dipakai").build();
return Response.seeOther(uri).build();
}

var user = auth.register(username, email, password);
URI uri = uriInfo.getBaseUriBuilder().path("login").queryParam("msg", "Registrasi berhasil, silakan login").build();
return Response.seeOther(uri).build();
}

// --- DASHBOARD (perlu login) ---
@GET
@Path("dashboard")
@Produces(MediaType.TEXT_HTML)
public Response dashboardPage(@CookieParam(SESSION_COOKIE) String token, @Context UriInfo uriInfo) {
var userOpt = currentUser(token);
if (userOpt.isEmpty()) {
URI uri = uriInfo.getBaseUriBuilder().path("login").queryParam("msg", "Silakan login dulu").build();
return Response.seeOther(uri).build();
}
Map<String, Object> data = new HashMap<>();
data.put("user", userOpt.get());
return Response.ok(dashboard.data(data)).build();
}

// --- LOGOUT ---
@GET
@Path("logout")
    @Transactional
public Response logout(@CookieParam(SESSION_COOKIE) String token, @Context UriInfo uriInfo) {
if (token != null && !token.isBlank()) {
auth.deleteSession(token);
}
// hapus cookie
NewCookie expired = new NewCookie(SESSION_COOKIE, "", "/", null, "logout", 0, true, true);
return Response.seeOther(uriInfo.getBaseUriBuilder().path("login").queryParam("msg", "Anda telah logout").build())
.cookie(expired)
.build();
}
}

6) Qute Templates

Buat file-file ini di src/main/resources/templates/:

6.1 login.html

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Login</title></head>
<body>
{#if msg}<p style="color:red">{msg}</p>{/if}
<h1>Login</h1>
<form action="/login" method="post">
<div><label>Username: <input type="text" name="username" required></label></div>
<div><label>Password: <input type="password" name="password" required></label></div>
<button type="submit">Login</button>
</form>
<p>Belum punya akun? <a href="/registration">Daftar</a></p>
</body>
</html>

6.2 registration.html 

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Registration</title></head>
<body>
{#if msg}<p style="color:red">{msg}</p>{/if}
<h1>Registrasi</h1>
<form action="/registration" method="post">
<div><label>Username: <input type="text" name="username" required></label></div>
<div><label>Email: <input type="email" name="email" required></label></div>
<div><label>Password: <input type="password" name="password" required></label></div>
<button type="submit">Daftar</button>
</form>
<p>Sudah punya akun? <a href="/login">Login</a></p>
</body>
</html>

6.3 dashboard.html 

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Dashboard</title></head>
<body>
<h1>Hi, {user.username} 👋</h1>
<p>Email: {user.email}</p>
<p>Status: {user.status}</p>
<p><a href="/logout">Logout</a></p>
</body>
</html> 

7) Jalankan & Tes

  1. Buka http://localhost:8080/registration → daftar.

    Register

     

  2. http://localhost:8080/login → login.

    Login Page

     

  3. http://localhost:8080/dashboard → harusnya masuk.

    Dashboard

     

  4. http://localhost:8080/logout → sesi berakhir.

    Logged Out

     

8) Catatan Keamanan (penting tapi singkat)

  • Hash password  (pakai BcryptUtil).

  • CSRF: untuk form POST di produksi, tambahkan CSRF token.

  • Cookie: di produksi pastikan secure=true (HTTPS), pertimbangkan SameSite=Lax/Strict.

  • Validasi input: perketat panjang & format.

  • Migrations: untuk prod pakai Flyway (hindari drop-and-create).

 

Comments

Popular posts from this blog

Numpang Kerja Remote dari Bandung Creative Hub

Numpang Kerja Remote dari Bandung Digital Valley

Cara Decompile berkas Dex dan Apk Android