Skip to content
Closed

Ads #10

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fd719b9
Initial work for ads on Modtale
Villagers654 Jan 24, 2026
923c0ae
Allow the super admin to implement new affiliate ads
Villagers654 Jan 24, 2026
afa7547
Try to fix auth on branch deployments
Villagers654 Jan 24, 2026
79f42c0
Try to fix CSRF issues on branch deployments
Villagers654 Jan 24, 2026
9d96482
Remove duplicate bean
Villagers654 Jan 24, 2026
239bcbd
Fix CORS in preview environments
Villagers654 Jan 24, 2026
0267262
PreAuthorize might be causing issues
Villagers654 Jan 24, 2026
85968ba
Attempt to fix more CSRF issues
Villagers654 Jan 24, 2026
f3de6ae
Attempt to fix even more CSRF issues
Villagers654 Jan 25, 2026
ec83fdd
Update ci-cd.yml
Villagers654 Jan 25, 2026
ce5ce05
Handle buckets better
Villagers654 Jan 25, 2026
43cc412
Merge remote-tracking branch 'origin/ads' into ads
Villagers654 Jan 25, 2026
9145f59
Attempt to fix R2 issues
Villagers654 Jan 25, 2026
55cb9be
Attempt to fix image files not showing up on staging
Villagers654 Jan 25, 2026
9f01765
Update ci-cd.yml
Villagers654 Jan 25, 2026
2e85a8c
Allow vertical ads
Villagers654 Jan 25, 2026
8663aa3
Merge remote-tracking branch 'origin/ads' into ads
Villagers654 Jan 25, 2026
159b015
Allow there to be multiple images per ad
Villagers654 Jan 25, 2026
a9aa1a8
Try to fix data seeding
Villagers654 Jan 25, 2026
735461b
Fix Mongo error
Villagers654 Jan 25, 2026
8a64ea3
Some placement adjustments
Villagers654 Jan 25, 2026
581e0f5
More placement adjustments
Villagers654 Jan 25, 2026
c6d9050
Try to fit the image sizes better
Villagers654 Jan 25, 2026
fd0f985
Unify the styling a little bit
Villagers654 Jan 25, 2026
64bc6b3
Last few visual tweaks
Villagers654 Jan 25, 2026
4e52bff
Last few visual tweaks
Villagers654 Jan 25, 2026
6ffa1a8
Always serve an ad of some variety
Villagers654 Jan 25, 2026
04d2853
Don't show external link context popup on ads
Villagers654 Jan 25, 2026
b33b8ab
A few final tweaks
Villagers654 Jan 28, 2026
1cb7aea
Implement source tracking
Villagers654 Jan 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
echo "SEEDING_ENABLED=true" >> $GITHUB_ENV
echo "TAG=$SLUG" >> $GITHUB_ENV
echo "R2_BUCKET_NAME=modtale-dev" >> $GITHUB_ENV
echo "R2_PUBLIC_DOMAIN=https://dev-cdn.modtale.net" >> $GITHUB_ENV
echo "R2_PUBLIC_DOMAIN=https://pub-dc8241faa0a343c080802bf00bc72bc5.r2.dev" >> $GITHUB_ENV
fi

- id: 'auth'
Expand All @@ -84,7 +84,7 @@ jobs:
"--update-env-vars" "APP_SEEDING_SOURCE_DB=modtale"
"--update-env-vars" "APP_WARDEN_URL=${{ secrets.WARDEN_URL }}"
"--update-env-vars" "FRONTEND_URL=placeholder"
"--update-env-vars" "R2_BUCKET_NAME=${{ env.R2_BUCKET_NAME }}"
"--update-env-vars" "R2_BUCKET_NAME=${{ env.R2_BUCKET_NAME }}"
"--update-env-vars" "R2_PUBLIC_DOMAIN=${{ env.R2_PUBLIC_DOMAIN }}"
)

Expand Down
124 changes: 99 additions & 25 deletions backend/src/main/java/net/modtale/config/DataSeeder.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Filters;
import net.modtale.model.user.ApiKey;
import net.modtale.model.user.User;
import net.modtale.repository.user.UserRepository;
import org.bson.Document;
Expand Down Expand Up @@ -35,6 +36,7 @@ public class DataSeeder implements CommandLineRunner {

private static final int PROJECT_LIMIT = 50;
private static final int REPORT_LIMIT = 20;
private static final String SUPER_ADMIN_ID = "692620f7c2f3266e23ac0ded";

public DataSeeder(MongoTemplate mongoTemplate, UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.mongoTemplate = mongoTemplate;
Expand All @@ -48,30 +50,39 @@ public void run(String... args) {
return;
}

ensureSuperAdmin();
ensureNormalUser();

String currentDbName = mongoTemplate.getDb().getName();

if (currentDbName.equalsIgnoreCase(sourceDbName)) {
logger.warn("Seeding ABORTED: Current DB is same as Source DB ({})", currentDbName);
return;
}

if (userRepository.count() > 0) {
logger.info("Database '{}' already contains data. Skipping seed.", currentDbName);
if (mongoTemplate.getCollection("projects").countDocuments() > 0) {
logger.info("Database '{}' already contains projects. Skipping content clone.", currentDbName);
return;
}

logger.info("Initializing Preview Environment with relational subset from '{}'...", sourceDbName);
logger.info("Initializing Preview Environment...");

try {
logger.info("Attempting to clone relational subset from '{}'...", sourceDbName);

MongoDatabase sourceDb = mongoTemplate.getMongoDatabaseFactory().getMongoDatabase(sourceDbName);
MongoDatabase targetDb = mongoTemplate.getDb();

createDevAdmin();
long sourceProjectCount = sourceDb.getCollection("projects").countDocuments();
if (sourceProjectCount == 0) {
logger.warn("SOURCE DB '{}' IS EMPTY! Cannot clone data.", sourceDbName);
return;
}

List<Document> projects = fetchSubset(sourceDb, "projects", PROJECT_LIMIT);

if (projects.isEmpty()) {
logger.info("No projects found in source. Seeding finished early.");
logger.info("No projects found in source. Seeding finished.");
return;
}

Expand All @@ -81,16 +92,20 @@ public void run(String... args) {
.collect(Collectors.toSet());

Set<ObjectId> projectIds = projects.stream()
.map(doc -> doc.getObjectId("_id"))
.map(doc -> getSafeObjectId(doc.get("_id")))
.filter(Objects::nonNull)
.collect(Collectors.toSet());

logger.info("Identified {} projects and {} unique authors to clone.", projects.size(), authorIds.size());
logger.info("Found {} projects. Cloning referenced authors ({})...", projects.size(), authorIds.size());

cloneSpecificUsers(sourceDb, targetDb, authorIds);

targetDb.getCollection("projects").insertMany(projects);
logger.info("Cloned {} projects.", projects.size());
try {
targetDb.getCollection("projects").insertMany(projects);
logger.info("Cloned {} projects.", projects.size());
} catch (Exception e) {
logger.warn("Project insertion warning (duplicates might exist): {}", e.getMessage());
}

cloneProjectStats(sourceDb, targetDb, projectIds);

Expand All @@ -103,15 +118,48 @@ public void run(String... args) {
}
}

private void createDevAdmin() {
private ObjectId getSafeObjectId(Object id) {
if (id == null) return null;
if (id instanceof ObjectId) return (ObjectId) id;
if (id instanceof String) {
try {
return new ObjectId((String) id);
} catch (IllegalArgumentException e) {
return null;
}
}
return null;
}

private void ensureSuperAdmin() {
if (userRepository.existsById(SUPER_ADMIN_ID)) return;

userRepository.findByUsername("super_admin").ifPresent(userRepository::delete);

User user = new User();
user.setUsername("dev_admin");
user.setId(SUPER_ADMIN_ID);
user.setUsername("super_admin");
user.setEmail("admin@modtale.net");
user.setPassword(passwordEncoder.encode("password"));
user.setRoles(List.of("USER", "ADMIN"));
user.setBio("I am the generated admin for this preview environment.");
user.setBio("I am the Super Admin for this preview environment.");
user.setTier(ApiKey.Tier.ENTERPRISE);
userRepository.save(user);
logger.info("Created Default Admin: dev_admin / password");
logger.info("Created Super Admin: super_admin / password (ID: {})", SUPER_ADMIN_ID);
}

private void ensureNormalUser() {
if (userRepository.findByUsername("user").isPresent()) return;

User user = new User();
user.setUsername("user");
user.setEmail("user@modtale.net");
user.setPassword(passwordEncoder.encode("password"));
user.setRoles(List.of("USER"));
user.setBio("I am a standard user.");
user.setTier(ApiKey.Tier.USER);
userRepository.save(user);
logger.info("Created Normal User: user / password");
}

private List<Document> fetchSubset(MongoDatabase db, String collectionName, int limit) {
Expand All @@ -127,27 +175,45 @@ private void cloneSpecificUsers(MongoDatabase source, MongoDatabase target, Set<
MongoCollection<Document> targetCol = target.getCollection("users");

List<ObjectId> objectIds = userIds.stream()
.map(id -> {
try { return new ObjectId(id); } catch (IllegalArgumentException e) { return null; }
})
.map(this::getSafeObjectId)
.filter(Objects::nonNull)
.collect(Collectors.toList());

List<Document> usersToClone = new ArrayList<>();
if (objectIds.isEmpty()) return;

List<Document> usersToClone = new ArrayList<>();
sourceCol.find(Filters.in("_id", objectIds)).into(usersToClone);

if (usersToClone.isEmpty()) {
List<String> stringIds = userIds.stream().collect(Collectors.toList());
sourceCol.find(Filters.in("_id", stringIds)).into(usersToClone);
}

String defaultPasswordHash = passwordEncoder.encode("password");
List<Document> safeToInsert = new ArrayList<>();

for (Document user : usersToClone) {
String id = user.get("_id").toString();
String username = user.getString("username");

if (id.equals(SUPER_ADMIN_ID) || "user".equals(username) || "super_admin".equals(username)) {
continue;
}

user.put("password", defaultPasswordHash);
user.put("email", "scrubbed_" + user.getObjectId("_id").toString() + "@modtale.local");
user.put("email", "scrubbed_" + id + "@modtale.local");
user.put("githubAccessToken", null);
user.put("gitlabAccessToken", null);
safeToInsert.add(user);
}

if (!usersToClone.isEmpty()) {
targetCol.insertMany(usersToClone);
logger.info("Cloned and sanitized {} users.", usersToClone.size());
if (!safeToInsert.isEmpty()) {
try {
targetCol.insertMany(safeToInsert);
logger.info("Cloned and sanitized {} users.", safeToInsert.size());
} catch (Exception e) {
logger.warn("Partial user insertion error: {}", e.getMessage());
}
}
}

Expand All @@ -168,17 +234,25 @@ private void cloneProjectStats(MongoDatabase source, MongoDatabase target, Set<O
}

if (!stats.isEmpty()) {
target.getCollection("project_monthly_stats").insertMany(stats);
logger.info("Cloned {} project statistics records.", stats.size());
try {
target.getCollection("project_monthly_stats").insertMany(stats);
logger.info("Cloned {} project statistics records.", stats.size());
} catch (Exception e) {
logger.warn("Stats insertion error: {}", e.getMessage());
}
}
}

private void cloneCollectionSubset(MongoDatabase source, MongoDatabase target, String collectionName, int limit) {
try {
List<Document> docs = fetchSubset(source, collectionName, limit);
if (!docs.isEmpty()) {
target.getCollection(collectionName).insertMany(docs);
logger.info("Cloned {} documents from {}.", docs.size(), collectionName);
try {
target.getCollection(collectionName).insertMany(docs);
logger.info("Cloned {} documents from {}.", docs.size(), collectionName);
} catch (Exception e) {
logger.warn("Insertion error for {}: {}", collectionName, e.getMessage());
}
}
} catch (Exception e) {
logger.warn("Could not clone subset of {}", collectionName);
Expand Down
13 changes: 12 additions & 1 deletion backend/src/main/java/net/modtale/config/r2/R2Config.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package net.modtale.config.r2;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -14,6 +16,8 @@
@Configuration
public class R2Config {

private static final Logger logger = LoggerFactory.getLogger(R2Config.class);

@Value("${app.r2.access-key}")
private String accessKey;

Expand All @@ -25,8 +29,15 @@ public class R2Config {

@Bean
public S3Client s3Client() {
URI uri = URI.create(endpoint);
String cleanEndpoint = uri.getScheme() + "://" + uri.getAuthority();

if (!endpoint.equals(cleanEndpoint)) {
logger.warn("Corrected R2 Endpoint from '{}' to '{}' to prevent path duplication.", endpoint, cleanEndpoint);
}

return S3Client.builder()
.endpointOverride(URI.create(endpoint))
.endpointOverride(URI.create(cleanEndpoint))
.region(Region.US_EAST_1)
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey)
Expand Down
Loading