🗃️ Move from file system to database

This commit is contained in:
andreas.dinauer 2025-10-26 18:57:09 +01:00
parent 61c62738b5
commit c07d177a24
8 changed files with 171 additions and 116 deletions

View File

@ -105,8 +105,6 @@ COPY target/quarkus-app/quarkus/ /deployments/quarkus/
EXPOSE 8080 EXPOSE 8080
RUN sudo chmod -R 777 /var/lib/kubooboo
USER quarkus USER quarkus
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0" ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0"

View File

@ -1,18 +1,20 @@
package dev.dinauer; package dev.dinauer;
import dev.dinauer.login.User; import dev.dinauer.login.User;
import dev.dinauer.login.UserEntity;
import dev.dinauer.login.UserRepo; import dev.dinauer.login.UserRepo;
import io.quarkus.elytron.security.common.BcryptUtil; import io.quarkus.elytron.security.common.BcryptUtil;
import io.quarkus.security.Authenticated; import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.SecurityIdentity;
import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.*; import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.Optional;
import java.util.Set;
@Path("/users") @Path("/users")
@ApplicationScoped @ApplicationScoped
@ -28,52 +30,50 @@ public class UserResource
@GET @GET
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Path("/{username}") @Path("/{username}")
public User getUser(@PathParam("username") String username) throws IOException public User getUser(@PathParam("username") String id)
{ {
User persistentUser = userRepo.findByUsername(username); Optional<UserEntity> userOptional = userRepo.findByIdOptional(id);
return new User(persistentUser.username(), persistentUser.email(), persistentUser.roles(), null, null); if (userOptional.isPresent())
{
UserEntity user = userOptional.get();
return new User(user.getUsername(), user.getEmail(), user.getRoles(), null);
}
throw new NotFoundException();
} }
@POST @POST
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
public User createUser(User user) throws IOException @Transactional
public void createUser(User user)
{ {
userRepo.persist(new User(user.username(), user.email(), user.roles(), BcryptUtil.bcryptHash(user.password()), false)); UserEntity userEntity = UserEntity.init();
return new User(user.username(), user.email(), user.roles(), null, false); userEntity.setUsername(user.username());
userEntity.setPassword(BcryptUtil.bcryptHash(user.password()));
userEntity.setRoles(Set.of("user"));
userEntity.setEmail(user.email());
userRepo.persist(userEntity);
} }
@PUT @PUT
@Path("/{username}/password") @Path("/{username}/password")
@Produces @Produces
@Consumes(MediaType.TEXT_PLAIN) @Consumes(MediaType.TEXT_PLAIN)
@Transactional
public void changePassword(@PathParam("username") String username, String password) throws IOException public void changePassword(@PathParam("username") String username, String password) throws IOException
{ {
User persistentUser = userRepo.findByUsername(username); Optional<UserEntity> persistentUserOptional = userRepo.findOptionalByUsername(username);
if(password != null && !password.isBlank()) if(persistentUserOptional.isPresent() && password != null && !password.isBlank())
{ {
if(securityIdentity.getPrincipal().getName().equals(persistentUser.username())) UserEntity persistentUser = persistentUserOptional.get();
if(securityIdentity.getPrincipal().getName().equals(persistentUser.getUsername()))
{ {
try persistentUser.setPassword(BcryptUtil.bcryptHash(password));
{ userRepo.persist(persistentUser);
userRepo.persist(new User(persistentUser.username(), persistentUser.email(), persistentUser.roles(), BcryptUtil.bcryptHash(password), persistentUser.initial()));
return; return;
} }
catch (IOException e)
{
throw new WebApplicationException("failed_to_write_to_file", 500);
}
}
throw new ForbiddenException(); throw new ForbiddenException();
} }
throw new BadRequestException("no_password_provided"); throw new BadRequestException("no_password_provided");
} }
@GET
@Produces(MediaType.APPLICATION_JSON)
@RolesAllowed("admin")
public List<User> getUsers() throws IOException
{
return userRepo.findAll().stream().map(user -> new User(user.username(), user.email(), user.roles(), null, user.initial())).toList();
}
} }

View File

@ -25,15 +25,15 @@ public class LoginResource
@POST @POST
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN) @Produces(MediaType.TEXT_PLAIN)
public String login(Login login) throws IOException public String login(Login login)
{ {
Optional<User> userOptional = userRepo.findOptionalByUsername(login.username()); Optional<UserEntity> userOptional = userRepo.findOptionalByUsername(login.username());
if(userOptional.isPresent()) if(userOptional.isPresent())
{ {
User user = userOptional.get(); UserEntity user = userOptional.get();
if(BcryptUtil.matches(login.password(), user.password())) if(BcryptUtil.matches(login.password(), user.getPassword()))
{ {
return Jwt.upn(user.username()).expiresAt(ZonedDateTime.now().plusDays(15).toInstant()).groups(user.roles()).sign(); return Jwt.upn(user.getId()).expiresAt(ZonedDateTime.now().plusDays(15).toInstant()).groups(user.getRoles()).sign();
} }
LOG.info("Cannot access user. Forbidden"); LOG.info("Cannot access user. Forbidden");
throw new ForbiddenException(); throw new ForbiddenException();

View File

@ -2,6 +2,6 @@ package dev.dinauer.login;
import java.util.Set; import java.util.Set;
public record User(String username, String email, Set<String> roles, String password, Boolean initial) public record User(String username, String email, Set<String> roles, String password)
{ {
} }

View File

@ -0,0 +1,119 @@
package dev.dinauer.login;
import jakarta.persistence.*;
import java.util.List;
import java.util.Set;
import java.util.UUID;
@Entity
@Table(name = "kubooboo_user")
public class UserEntity
{
@Id
private String id;
private String username;
private String firstname;
private String lastname;
@Column(name = "user_password")
private String password;
private String email;
private String roles;
public static UserEntity init()
{
UserEntity user = new UserEntity();
user.setId(UUID.randomUUID().toString());
return user;
}
public String getId()
{
return id;
}
public UserEntity setId(String id)
{
this.id = id;
return this;
}
public String getUsername()
{
return username;
}
public UserEntity setUsername(String username)
{
this.username = username;
return this;
}
public String getFirstname()
{
return firstname;
}
public UserEntity setFirstname(String firstname)
{
this.firstname = firstname;
return this;
}
public String getLastname()
{
return lastname;
}
public UserEntity setLastname(String lastname)
{
this.lastname = lastname;
return this;
}
public String getPassword()
{
return password;
}
public UserEntity setPassword(String password)
{
this.password = password;
return this;
}
public String getEmail()
{
return email;
}
public UserEntity setEmail(String email)
{
this.email = email;
return this;
}
public Set<String> getRoles()
{
if (this.roles != null)
{
return Set.of(this.roles.split(","));
}
return null;
}
public UserEntity setRoles(Set<String> roles)
{
if (roles != null)
{
this.roles = String.join(",", roles);
}
return this;
}
}

View File

@ -1,88 +1,17 @@
package dev.dinauer.login; package dev.dinauer.login;
import com.fasterxml.jackson.databind.DeserializationFeature; import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.panache.common.Parameters;
import dev.dinauer.WorkdirProvider;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Stream;
@ApplicationScoped @ApplicationScoped
public class UserRepo public class UserRepo implements PanacheRepositoryBase<UserEntity, String>
{ {
private static final Logger LOG = LoggerFactory.getLogger(UserRepo.class);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@Inject public Optional<UserEntity> findOptionalByUsername(String username)
WorkdirProvider workdirProvider;
public User findByUsername(String username) throws IOException
{ {
File file = new File(getFilePathForUsername(username)); return find("username = :username", Parameters.with("username", username)).firstResultOptional();
LOG.info("Query user from file {}", file.getAbsolutePath());
return getUserFromFile(file);
}
public Optional<User> findOptionalByUsername(String username) throws IOException
{
File file = new File(getFilePathForUsername(username));
LOG.info("Query user from file {}", file.getAbsolutePath());
return getOptionalUserFromFile(file);
}
public List<User> findAll() throws IOException
{
try(Stream<Path> paths = Files.list(workdirProvider.getWorkdirPath(Path.of("users"))))
{
List<User> result = new ArrayList<>();
for(Path path : paths.toList())
{
result.add(getUserFromFile(new File(path.toString())));
}
return result;
}
}
private User getUserFromFile(File file) throws IOException
{
if(file.exists())
{
return OBJECT_MAPPER.readValue(file, User.class);
}
throw new NotFoundException("Did not find file " + file.getAbsolutePath());
}
private Optional<User> getOptionalUserFromFile(File file) throws IOException
{
if(file.exists())
{
return Optional.of(OBJECT_MAPPER.readValue(file, User.class));
}
return Optional.empty();
}
private String getFilePathForUsername(String username)
{
return workdirProvider.getWorkdir(Path.of("users", String.format("%s.json", username)));
}
public void persist(User user) throws IOException
{
try(FileWriter fw = new FileWriter(getFilePathForUsername(user.username()), false))
{
fw.write(OBJECT_MAPPER.writeValueAsString(user));
}
} }
} }

View File

@ -1,12 +1,15 @@
package dev.dinauer.utils; package dev.dinauer.utils;
import dev.dinauer.login.User; import dev.dinauer.login.User;
import dev.dinauer.login.UserEntity;
import dev.dinauer.login.UserRepo; import dev.dinauer.login.UserRepo;
import io.quarkus.elytron.security.common.BcryptUtil; import io.quarkus.elytron.security.common.BcryptUtil;
import io.quarkus.narayana.jta.QuarkusTransaction;
import io.quarkus.runtime.Startup; import io.quarkus.runtime.Startup;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import java.io.IOException; import java.io.IOException;
@ -26,17 +29,23 @@ public class StartupService
UserRepo userRepo; UserRepo userRepo;
@PostConstruct @PostConstruct
void init() throws IOException public void init()
{ {
if(userRepo.findOptionalByUsername(INITIAL_USERNAME).isEmpty()) if(userRepo.findOptionalByUsername(INITIAL_USERNAME).isEmpty())
{ {
QuarkusTransaction.begin();
userRepo.persist(buildInitialUser()); userRepo.persist(buildInitialUser());
QuarkusTransaction.commit();
LOG.infof("Initialized user 'admin'"); LOG.infof("Initialized user 'admin'");
} }
} }
private static User buildInitialUser() private static UserEntity buildInitialUser()
{ {
return new User(INITIAL_USERNAME, null, Set.of("admin"), BcryptUtil.bcryptHash(INITIAL_PASSWORD), true); UserEntity initialUser = UserEntity.init();
initialUser.setUsername(INITIAL_USERNAME);
initialUser.setPassword(BcryptUtil.bcryptHash(INITIAL_PASSWORD));
initialUser.setRoles(Set.of("admin"));
return initialUser;
} }
} }

View File

@ -20,4 +20,4 @@ dev.dinauer.kubooboo.work.dir=/var/lib/kubooboo/work
%dev.quarkus.datasource.username = postgres %dev.quarkus.datasource.username = postgres
%dev.quarkus.datasource.password = postgres %dev.quarkus.datasource.password = postgres
%dev.quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:6666/postgres %dev.quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:6666/postgres
%dev,test.quarkus.hibernate-orm.schema-management.strategy = none %dev,test.quarkus.hibernate-orm.schema-management.strategy=drop-and-create