Files
INSServer/frontend/utils/RegistrarDao.java

490 lines
16 KiB
Java
Raw Normal View History

2026-02-11 23:22:20 +01:00
package ins.frontend.utils;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* Registrar DAO for schema:
* <ul>
* <li>tln(id, name, info, owner_id, is_public, allow_subdomains)</li>
* <li>infonames(id, info, tln_id, uid)</li>
* <li>subnames(id, name, infoname_id)</li>
* <li>records(id, infoname_id, subname_id, type, value, ttl, priority, port, weight)</li>
* </ul>
*
* <p>Rules:</p>
* <ul>
* <li>Create InfoName under TLN allowed if TLN is public OR owned by user.</li>
* <li>Subnames allowed for non-owner only if allow_subdomains=1. Owner always allowed.</li>
* <li>TLN name is not editable; only info/is_public/allow_subdomains are editable (owner-only).</li>
* </ul>
*/
public final class RegistrarDao {
private final UserDao.DataSourceProvider dataSource;
/**
* Creates the DAO.
*
* @param dataSource JDBC connection provider
*/
public RegistrarDao(UserDao.DataSourceProvider dataSource) {
this.dataSource = Objects.requireNonNull(dataSource, "dataSource");
}
// ---------- TLN ----------
/**
* TLN usable for creating InfoNames if public OR owned.
*
* @param tln TLN row
* @param userId current user id
* @return allowed
*/
public static boolean canUseTln(TlnRow tln, int userId) {
if (tln == null || userId <= 0) return false;
if (tln.isPublic) return true;
return tln.ownerId != null && tln.ownerId == userId;
}
/**
* Subdomain/subname allowed if:
* <ul>
* <li>no sub requested (root) OR</li>
* <li>TLN allows subdomains OR</li>
* <li>user is TLN owner</li>
* </ul>
*
* @param tln TLN row
* @param userId current user id
* @param sub sub label (nullable)
* @return allowed
*/
public static boolean canUseSubname(TlnRow tln, int userId, String sub) {
if (tln == null || userId <= 0) return false;
boolean wantsSub = sub != null && !sub.isBlank();
if (!wantsSub) return true;
if (tln.allowSubdomains) return true;
return tln.ownerId != null && tln.ownerId == userId;
}
private static TlnRow mapTln(ResultSet rs) throws SQLException {
return new TlnRow(
rs.getInt("id"),
rs.getString("name"),
rs.getString("info"),
(Integer) rs.getObject("owner_id"),
rs.getInt("is_public") == 1,
rs.getInt("allow_subdomains") == 1
);
}
/**
* Creates a TLN.
*
* @param name tln.name (unique)
* @param info tln.info (editable, can be null/blank)
* @param ownerId owner user id (nullable in DB, pass null to create unowned TLN)
* @param isPublic tln.is_public
* @param allowSubdomains tln.allow_subdomains
* @return new tln.id
* @throws SQLException on SQL errors
*/
public int createTln(String name, String info, Integer ownerId, boolean isPublic, boolean allowSubdomains) throws SQLException {
if (name == null || name.isBlank()) throw new IllegalArgumentException("name must not be blank");
String sql = "INSERT INTO tln (name, info, owner_id, is_public, allow_subdomains) VALUES (?, ?, ?, ?, ?)";
try (Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, name.trim());
ps.setString(2, info);
if (ownerId == null) ps.setNull(3, Types.INTEGER);
else ps.setInt(3, ownerId);
ps.setInt(4, isPublic ? 1 : 0);
ps.setInt(5, allowSubdomains ? 1 : 0);
ps.executeUpdate();
try (ResultSet rs = ps.getGeneratedKeys()) {
if (rs.next()) return rs.getInt(1);
}
}
throw new SQLException("No generated key returned for tln.id");
}
/**
* Finds TLN by name.
*
* @param name tln.name
* @return optional row
* @throws SQLException on SQL errors
*/
public Optional<TlnRow> findTlnByName(String name) throws SQLException {
if (name == null || name.isBlank()) return Optional.empty();
String sql = "SELECT id, name, info, owner_id, is_public, allow_subdomains FROM tln WHERE name = ? LIMIT 1";
try (Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, name.trim());
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return Optional.empty();
return Optional.of(mapTln(rs));
}
}
}
// ---------- Permissions ----------
/**
* Lists TLNs visible to a user: owned OR public OR unowned+public.
*
* @param userId current user id
* @return rows
* @throws SQLException on SQL errors
*/
public TlnRow[] listVisibleTlns(int userId) throws SQLException {
String sql = """
SELECT id, name, info, owner_id, is_public, allow_subdomains
FROM tln
WHERE is_public = 1 OR owner_id = ?
ORDER BY name ASC
""";
List<TlnRow> out = new ArrayList<>();
try (Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setInt(1, userId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) out.add(mapTln(rs));
}
}
return out.toArray(new TlnRow[0]);
}
/**
* Updates TLN fields except name (owner-only).
*
* @param tlnId tln.id
* @param ownerUserId users.id (must match tln.owner_id)
* @param newInfo new info text (nullable)
* @param isPublic is_public
* @param allowSubdomains allow_subdomains
* @return true if updated, false if not owned/not found
* @throws SQLException on SQL errors
*/
public boolean updateTlnOwned(int tlnId, int ownerUserId, String newInfo, boolean isPublic, boolean allowSubdomains) throws SQLException {
if (tlnId <= 0) throw new IllegalArgumentException("tlnId must be > 0");
if (ownerUserId <= 0) throw new IllegalArgumentException("ownerUserId must be > 0");
String sql = "UPDATE tln SET info = ?, is_public = ?, allow_subdomains = ? WHERE id = ? AND owner_id = ?";
try (Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, newInfo);
ps.setInt(2, isPublic ? 1 : 0);
ps.setInt(3, allowSubdomains ? 1 : 0);
ps.setInt(4, tlnId);
ps.setInt(5, ownerUserId);
return ps.executeUpdate() > 0;
}
}
// ---------- InfoNames ----------
/**
* Deletes TLN (owner-only).
*
* @param tlnId tln.id
* @param ownerUserId users.id
* @return true if deleted
* @throws SQLException on SQL errors
*/
public boolean deleteTlnOwned(int tlnId, int ownerUserId) throws SQLException {
if (tlnId <= 0) throw new IllegalArgumentException("tlnId must be > 0");
if (ownerUserId <= 0) throw new IllegalArgumentException("ownerUserId must be > 0");
String sql = "DELETE FROM tln WHERE id = ? AND owner_id = ?";
try (Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setInt(1, tlnId);
ps.setInt(2, ownerUserId);
return ps.executeUpdate() > 0;
}
}
/**
* Creates an InfoName under the given TLN.
*
* @param tln TLN row
* @param info infonames.info
* @param userId infonames.uid (users.id)
* @return new infonames.id
* @throws SQLException on SQL errors
*/
public int createInfoName(TlnRow tln, String info, int userId) throws SQLException {
if (tln == null) throw new IllegalArgumentException("tln must not be null");
if (userId <= 0) throw new IllegalArgumentException("userId must be > 0");
if (!canUseTln(tln, userId)) throw new SQLException("TLN not public and not owned by user.");
if (info == null || info.isBlank()) throw new IllegalArgumentException("info must not be blank");
String sql = "INSERT INTO infonames (info, tln_id, uid) VALUES (?, ?, ?)";
try (Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, info.trim());
ps.setInt(2, tln.id);
ps.setInt(3, userId);
ps.executeUpdate();
try (ResultSet rs = ps.getGeneratedKeys()) {
if (rs.next()) return rs.getInt(1);
}
}
throw new SQLException("No generated key returned for infonames.id");
}
/**
* Checks whether a user owns the infoname.
*
* @param infonameId infonames.id
* @param userId users.id
* @return true if owned
* @throws SQLException on SQL errors
*/
public boolean isOwnerOfInfoName(int infonameId, int userId) throws SQLException {
if (infonameId <= 0 || userId <= 0) return false;
String sql = "SELECT 1 FROM infonames WHERE id = ? AND uid = ? LIMIT 1";
try (Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setInt(1, infonameId);
ps.setInt(2, userId);
try (ResultSet rs = ps.executeQuery()) {
return rs.next();
}
}
}
/**
* Deletes an InfoName (owner-only).
*
* @param infonameId infonames.id
* @param ownerUserId users.id
* @return true if deleted
* @throws SQLException on SQL errors
*/
public boolean deleteInfoNameOwned(int infonameId, int ownerUserId) throws SQLException {
if (infonameId <= 0) throw new IllegalArgumentException("infonameId must be > 0");
if (ownerUserId <= 0) throw new IllegalArgumentException("ownerUserId must be > 0");
String sql = "DELETE FROM infonames WHERE id = ? AND uid = ?";
try (Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setInt(1, infonameId);
ps.setInt(2, ownerUserId);
return ps.executeUpdate() > 0;
}
}
// ---------- Subnames + Records ----------
/**
* Lists InfoNames owned by user (joined with TLN data).
*
* @param userId users.id
* @return rows
* @throws SQLException on SQL errors
*/
public InfoNameRow[] listOwnedInfoNames(int userId) throws SQLException {
if (userId <= 0) return new InfoNameRow[0];
String sql = """
SELECT i.id AS iid, i.info AS info, i.tln_id AS tln_id,
t.name AS tln_name, t.info AS tln_info, t.owner_id AS owner_id,
t.is_public AS is_public, t.allow_subdomains AS allow_subdomains
FROM infonames i
INNER JOIN tln t ON t.id = i.tln_id
WHERE i.uid = ?
ORDER BY t.name ASC, i.info ASC
""";
List<InfoNameRow> out = new ArrayList<>();
try (Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setInt(1, userId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
TlnRow tln = new TlnRow(
rs.getInt("tln_id"),
rs.getString("tln_name"),
rs.getString("tln_info"),
(Integer) rs.getObject("owner_id"),
rs.getInt("is_public") == 1,
rs.getInt("allow_subdomains") == 1
);
out.add(new InfoNameRow(
rs.getInt("iid"),
rs.getString("info"),
tln
));
}
}
}
return out.toArray(new InfoNameRow[0]);
}
/**
* Ensures a subname exists for an infoname; returns subnames.id.
*
* <p>If sub is null/blank, returns null (root).</p>
*
* @param infonameId infonames.id
* @param sub sub label (nullable)
* @return subnames.id or null for root
* @throws SQLException on SQL errors
*/
public Integer ensureSubname(int infonameId, String sub) throws SQLException {
if (sub == null || sub.isBlank()) return null;
if (infonameId <= 0) throw new IllegalArgumentException("infonameId must be > 0");
String name = sub.trim();
// 1) find
String find = "SELECT id FROM subnames WHERE infoname_id = ? AND name = ? LIMIT 1";
try (Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement(find)) {
ps.setInt(1, infonameId);
ps.setString(2, name);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) return rs.getInt("id");
}
}
// 2) insert
String ins = "INSERT INTO subnames (name, infoname_id) VALUES (?, ?)";
try (Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement(ins, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, name);
ps.setInt(2, infonameId);
ps.executeUpdate();
try (ResultSet rs = ps.getGeneratedKeys()) {
if (rs.next()) return rs.getInt(1);
}
}
throw new SQLException("No generated key returned for subnames.id");
}
/**
* Adds a DNS-like record.
*
* @param infonameId infonames.id
* @param subnameId subnames.id or null for root
* @param type enum type (A/AAAA/TXT/CNAME/MX/SRV/NS)
* @param value record value
* @param ttl ttl (default 3600)
* @param priority priority (MX/SRV)
* @param port port (SRV)
* @param weight weight (SRV)
* @return new records.id
* @throws SQLException on SQL errors
*/
public int addRecord(int infonameId,
Integer subnameId,
String type,
String value,
int ttl,
Integer priority,
Integer port,
Integer weight) throws SQLException {
if (infonameId <= 0) throw new IllegalArgumentException("infonameId must be > 0");
if (type == null || type.isBlank()) throw new IllegalArgumentException("type must not be blank");
if (value == null || value.isBlank()) throw new IllegalArgumentException("value must not be blank");
if (ttl <= 0) ttl = 3600;
String t = type.trim().toUpperCase();
String sql = """
INSERT INTO records (infoname_id, subname_id, type, value, ttl, priority, port, weight)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""";
try (Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setInt(1, infonameId);
if (subnameId == null) ps.setNull(2, Types.INTEGER);
else ps.setInt(2, subnameId);
ps.setString(3, t);
ps.setString(4, value.trim());
ps.setInt(5, ttl);
if (priority == null) ps.setNull(6, Types.INTEGER);
else ps.setInt(6, priority);
if (port == null) ps.setNull(7, Types.INTEGER);
else ps.setInt(7, port);
if (weight == null) ps.setNull(8, Types.INTEGER);
else ps.setInt(8, weight);
ps.executeUpdate();
try (ResultSet rs = ps.getGeneratedKeys()) {
if (rs.next()) return rs.getInt(1);
}
}
throw new SQLException("No generated key returned for records.id");
}
/**
* TLN row.
*/
public record TlnRow(int id, String name, String info, Integer ownerId, boolean isPublic, boolean allowSubdomains) {
}
/**
* InfoName row (includes TLN).
*/
public record InfoNameRow(int id, String info, TlnRow tln) {
}
}