🗃️ 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
RUN sudo chmod -R 777 /var/lib/kubooboo
USER quarkus
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0"

View File

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

View File

@ -2,6 +2,6 @@ package dev.dinauer.login;
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;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.dinauer.WorkdirProvider;
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import io.quarkus.panache.common.Parameters;
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.stream.Stream;
@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
WorkdirProvider workdirProvider;
public User findByUsername(String username) throws IOException
public Optional<UserEntity> findOptionalByUsername(String username)
{
File file = new File(getFilePathForUsername(username));
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));
}
return find("username = :username", Parameters.with("username", username)).firstResultOptional();
}
}

View File

@ -1,12 +1,15 @@
package dev.dinauer.utils;
import dev.dinauer.login.User;
import dev.dinauer.login.UserEntity;
import dev.dinauer.login.UserRepo;
import io.quarkus.elytron.security.common.BcryptUtil;
import io.quarkus.narayana.jta.QuarkusTransaction;
import io.quarkus.runtime.Startup;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.jboss.logging.Logger;
import java.io.IOException;
@ -26,17 +29,23 @@ public class StartupService
UserRepo userRepo;
@PostConstruct
void init() throws IOException
public void init()
{
if(userRepo.findOptionalByUsername(INITIAL_USERNAME).isEmpty())
{
QuarkusTransaction.begin();
userRepo.persist(buildInitialUser());
QuarkusTransaction.commit();
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.password = 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