Current state
This commit is contained in:
310
.copilot/api-first.md
Normal file
310
.copilot/api-first.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# API-First Architecture – Canonical Rules (SkinBase)
|
||||
|
||||
> **This document defines how Copilot must generate API-first code.**
|
||||
> It applies to **Artworks and all future modules**.
|
||||
> If generated code conflicts with this file, **this file wins**.
|
||||
|
||||
---
|
||||
|
||||
## 1. What “API-First” Means (MANDATORY)
|
||||
|
||||
API-first means:
|
||||
|
||||
* Business logic lives in **Services**
|
||||
* Controllers are **thin adapters**
|
||||
* Output is defined by **API Resources**
|
||||
* Web (Blade), API, Admin all use **the same services**
|
||||
* No duplicated logic between web & API
|
||||
|
||||
Copilot MUST assume:
|
||||
|
||||
* Web UI exists (Blade / SSR)
|
||||
* API exists (JSON)
|
||||
* Both consume the **same backend logic**
|
||||
|
||||
---
|
||||
|
||||
## 2. Layered Architecture (STRICT)
|
||||
|
||||
Copilot MUST generate code following this flow:
|
||||
|
||||
```
|
||||
Request
|
||||
→ Controller (Web or API)
|
||||
→ Service (business rules)
|
||||
→ Models / Queries
|
||||
→ Resource (output shape)
|
||||
```
|
||||
|
||||
### Forbidden shortcuts
|
||||
|
||||
❌ Controller → Model directly
|
||||
❌ Controller → DB query
|
||||
❌ Resource contains logic
|
||||
❌ Model contains business logic
|
||||
|
||||
---
|
||||
|
||||
## 3. Directory Structure (REFERENCE)
|
||||
|
||||
Copilot MUST follow this structure:
|
||||
|
||||
```
|
||||
app/
|
||||
├── Http/
|
||||
│ ├── Controllers/
|
||||
│ │ ├── Api/
|
||||
│ │ │ └── ArtworkController.php
|
||||
│ │ └── Web/
|
||||
│ │ └── ArtworkController.php
|
||||
│ │
|
||||
│ ├── Resources/
|
||||
│ │ ├── ArtworkResource.php
|
||||
│ │ ├── ArtworkListResource.php
|
||||
│ │ └── CategoryResource.php
|
||||
│
|
||||
├── Services/
|
||||
│ ├── ArtworkService.php
|
||||
│ ├── ArtworkStatsService.php
|
||||
│ └── CategoryService.php
|
||||
│
|
||||
├── Policies/
|
||||
│ └── ArtworkPolicy.php
|
||||
│
|
||||
├── Models/
|
||||
│ └── Artwork.php
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Services Layer (CORE)
|
||||
|
||||
### 4.1 ArtworkService (MANDATORY)
|
||||
|
||||
Copilot MUST generate an `ArtworkService` that handles:
|
||||
|
||||
* Fetch public artwork by slug
|
||||
* Fetch artworks by category
|
||||
* Apply visibility rules
|
||||
* Apply soft delete rules
|
||||
* Throw domain-appropriate exceptions
|
||||
|
||||
Example responsibilities (NOT code):
|
||||
|
||||
* `getPublicArtworkBySlug(string $slug)`
|
||||
* `getCategoryArtworks(Category $category)`
|
||||
* `getLatestArtworks(int $limit)`
|
||||
|
||||
### Rules
|
||||
|
||||
* Services MUST NOT return JSON
|
||||
* Services MUST return models or collections
|
||||
* Services MUST enforce visibility rules
|
||||
|
||||
---
|
||||
|
||||
## 5. Controllers (ADAPTERS ONLY)
|
||||
|
||||
### 5.1 API Controllers
|
||||
|
||||
Location:
|
||||
|
||||
```
|
||||
app/Http/Controllers/Api/
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
* Return API Resources only
|
||||
* Never return models directly
|
||||
* No business logic
|
||||
* Stateless
|
||||
|
||||
Example:
|
||||
|
||||
```php
|
||||
return new ArtworkResource(
|
||||
$this->service->getPublicArtworkBySlug($slug)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Web Controllers
|
||||
|
||||
Location:
|
||||
|
||||
```
|
||||
app/Http/Controllers/Web/
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
* Use same services as API
|
||||
* Prepare data for Blade
|
||||
* No duplication of logic
|
||||
* SEO handled here (meta tags, schema)
|
||||
|
||||
Example:
|
||||
|
||||
```php
|
||||
return view('artworks.show', [
|
||||
'artwork' => new ArtworkResource($artwork),
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. API Resources (OUTPUT CONTRACT)
|
||||
|
||||
### Rules
|
||||
|
||||
Copilot MUST:
|
||||
|
||||
* Use Laravel `JsonResource`
|
||||
* Define explicit fields
|
||||
* Never expose internal fields accidentally
|
||||
* Avoid N+1 queries
|
||||
* Include relations conditionally
|
||||
|
||||
### ArtworkResource MUST include:
|
||||
|
||||
* slug
|
||||
* title
|
||||
* description
|
||||
* dimensions
|
||||
* categories
|
||||
* URLs (canonical)
|
||||
|
||||
### ArtworkListResource MUST:
|
||||
|
||||
* Be lightweight
|
||||
* Exclude heavy relations
|
||||
* Exclude stats unless requested
|
||||
|
||||
---
|
||||
|
||||
## 7. Routes (SEO-Safe)
|
||||
|
||||
### API routes
|
||||
|
||||
Location:
|
||||
|
||||
```
|
||||
routes/api.php
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
* Stateless
|
||||
* Slug-based
|
||||
* Versionable (`/api/v1/...`)
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
GET /api/v1/artworks/{slug}
|
||||
GET /api/v1/categories/{slug}/artworks
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Web routes
|
||||
|
||||
Location:
|
||||
|
||||
```
|
||||
routes/web.php
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
* Slug-based
|
||||
* No IDs
|
||||
* SEO-friendly
|
||||
* SSR output
|
||||
|
||||
---
|
||||
|
||||
## 8. Stats Handling (High Load Rule)
|
||||
|
||||
Copilot MUST:
|
||||
|
||||
* Use `ArtworkStatsService`
|
||||
* Increment stats via Jobs
|
||||
* Never mutate counters inline
|
||||
* Assume Redis may be present
|
||||
|
||||
Forbidden:
|
||||
|
||||
❌ `$artwork->increment('views')`
|
||||
❌ Updating stats inside controllers
|
||||
|
||||
---
|
||||
|
||||
## 9. Caching Rules
|
||||
|
||||
Copilot SHOULD assume:
|
||||
|
||||
* Redis is available
|
||||
* Cache keys are service-level
|
||||
* Resources are cacheable
|
||||
|
||||
Examples:
|
||||
|
||||
* `artwork:{slug}`
|
||||
* `category:{slug}:artworks`
|
||||
|
||||
Cache invalidation:
|
||||
|
||||
* On update
|
||||
* On delete
|
||||
* On restore
|
||||
|
||||
---
|
||||
|
||||
## 10. Error Handling Rules
|
||||
|
||||
Copilot MUST:
|
||||
|
||||
* Return 404 for missing public content
|
||||
* Return 410 or 301 for soft-deleted content (if requested)
|
||||
* Never expose private content via API
|
||||
|
||||
---
|
||||
|
||||
## 11. Testing Philosophy
|
||||
|
||||
Copilot MUST generate tests that:
|
||||
|
||||
* Hit API endpoints
|
||||
* Validate JSON structure
|
||||
* Test visibility & approval
|
||||
* Do NOT test Blade HTML
|
||||
|
||||
---
|
||||
|
||||
## 12. Forbidden Patterns (ABSOLUTE)
|
||||
|
||||
❌ Controllers with logic
|
||||
❌ Models with business rules
|
||||
❌ Duplicate logic between API & Web
|
||||
❌ Direct DB queries in controllers
|
||||
❌ Different rules for API vs Web
|
||||
|
||||
---
|
||||
|
||||
## 13. Final Instruction (NON-NEGOTIABLE)
|
||||
|
||||
> **API is the primary contract.**
|
||||
> Web UI, Admin UI, Mobile apps are **clients**.
|
||||
|
||||
Copilot MUST always ask:
|
||||
|
||||
> “Can this logic live in a service?”
|
||||
|
||||
If yes → put it there.
|
||||
|
||||
---
|
||||
|
||||
### ✅ End of API-First Architecture Instructions
|
||||
370
.copilot/artworks.md
Normal file
370
.copilot/artworks.md
Normal file
@@ -0,0 +1,370 @@
|
||||
# Artworks Module – Canonical Architecture (SkinBase)
|
||||
|
||||
> **Authoritative documentation for Copilot AI agent**
|
||||
> This file defines the **single source of truth** for the `artworks` domain.
|
||||
> All generated code **MUST follow this document**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
The **Artworks module** is the core content system of SkinBase.
|
||||
|
||||
It must support:
|
||||
|
||||
* high-read traffic (browse/search)
|
||||
* safe moderation (soft deletes, approvals)
|
||||
* multilingual content
|
||||
* SEO-friendly URLs
|
||||
* scalable statistics
|
||||
* future extensions (tags, EXIF, search engines)
|
||||
|
||||
Legacy tables **must NOT influence new code**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Core Design Principles (DO NOT VIOLATE)
|
||||
|
||||
1. **Single responsibility per table**
|
||||
2. **No counters on hot tables**
|
||||
3. **Soft deletes on user-generated content**
|
||||
4. **No legacy fields**
|
||||
5. **Slug-based routing only**
|
||||
6. **FK integrity everywhere**
|
||||
7. **Indexes optimized for browsing**
|
||||
8. **Stats updated asynchronously**
|
||||
|
||||
---
|
||||
|
||||
## 3. Database Schema (Canonical)
|
||||
|
||||
### 3.1 `artworks` (CORE TABLE)
|
||||
|
||||
**Purpose:**
|
||||
Stores the authoritative artwork entity.
|
||||
|
||||
```sql
|
||||
artworks
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
|
||||
| Field | Type | Notes |
|
||||
| ------------ | ------------ | ---------------- |
|
||||
| id | bigint | Primary key |
|
||||
| user_id | bigint | Owner |
|
||||
| title | varchar(150) | Default language |
|
||||
| slug | varchar(160) | UNIQUE, URL |
|
||||
| description | text | Optional |
|
||||
| file_name | varchar | Original file |
|
||||
| file_path | varchar | Storage path |
|
||||
| file_size | bigint | Bytes |
|
||||
| mime_type | varchar(64) | e.g. image/jpeg |
|
||||
| width | int | Pixels |
|
||||
| height | int | Pixels |
|
||||
| is_public | boolean | Visibility |
|
||||
| is_approved | boolean | Moderation |
|
||||
| published_at | datetime | SEO timing |
|
||||
| created_at | datetime | |
|
||||
| updated_at | datetime | |
|
||||
| deleted_at | datetime | Soft delete |
|
||||
|
||||
**Indexes:**
|
||||
|
||||
* `UNIQUE(slug)`
|
||||
* `(is_public, is_approved, published_at)`
|
||||
* `deleted_at`
|
||||
|
||||
---
|
||||
|
||||
### 3.2 `artwork_translations`
|
||||
|
||||
**Purpose:**
|
||||
Multilingual titles and descriptions.
|
||||
|
||||
```sql
|
||||
artwork_translations
|
||||
```
|
||||
|
||||
| Field | Type |
|
||||
| ----------- | -------- |
|
||||
| artwork_id | FK |
|
||||
| locale | char(2) |
|
||||
| title | varchar |
|
||||
| description | text |
|
||||
| deleted_at | datetime |
|
||||
|
||||
**Rules:**
|
||||
|
||||
* One row per `(artwork_id, locale)`
|
||||
* Default language lives in `artworks`
|
||||
|
||||
---
|
||||
|
||||
### 3.3 `artwork_stats`
|
||||
|
||||
**Purpose:**
|
||||
High-write counters isolated from core table.
|
||||
|
||||
```sql
|
||||
artwork_stats
|
||||
```
|
||||
|
||||
| Field | Type |
|
||||
| ------------ | ------- |
|
||||
| artwork_id | PK + FK |
|
||||
| views | bigint |
|
||||
| downloads | bigint |
|
||||
| favorites | bigint |
|
||||
| rating_avg | float |
|
||||
| rating_count | int |
|
||||
|
||||
**Rules:**
|
||||
|
||||
* NO soft deletes
|
||||
* Updated via jobs / async
|
||||
* Never eager-loaded by default
|
||||
|
||||
---
|
||||
|
||||
### 3.4 `artwork_category`
|
||||
|
||||
**Purpose:**
|
||||
Many-to-many relation with categories.
|
||||
|
||||
```sql
|
||||
artwork_category
|
||||
```
|
||||
|
||||
| Field | Type |
|
||||
| ----------- | ---- |
|
||||
| artwork_id | FK |
|
||||
| category_id | FK |
|
||||
|
||||
**Rules:**
|
||||
|
||||
* Categories handle hierarchy
|
||||
* Artworks can belong to multiple categories
|
||||
|
||||
---
|
||||
|
||||
### 3.5 `artwork_comments`
|
||||
|
||||
**Purpose:**
|
||||
User comments with moderation.
|
||||
|
||||
```sql
|
||||
artwork_comments
|
||||
```
|
||||
|
||||
| Field | Type |
|
||||
| ----------- | -------- |
|
||||
| artwork_id | FK |
|
||||
| user_id | FK |
|
||||
| content | text |
|
||||
| is_approved | boolean |
|
||||
| deleted_at | datetime |
|
||||
|
||||
---
|
||||
|
||||
### 3.6 `artwork_downloads`
|
||||
|
||||
**Purpose:**
|
||||
Audit log of downloads.
|
||||
|
||||
```sql
|
||||
artwork_downloads
|
||||
```
|
||||
|
||||
| Field | Type |
|
||||
| ---------- | ---------- |
|
||||
| artwork_id | FK |
|
||||
| user_id | nullable |
|
||||
| ip | binary(16) |
|
||||
| user_agent | varchar |
|
||||
| created_at | datetime |
|
||||
|
||||
**Rules:**
|
||||
|
||||
* Append-only
|
||||
* No soft deletes
|
||||
* Used for abuse detection & stats aggregation
|
||||
|
||||
---
|
||||
|
||||
## 4. Eloquent Models (REQUIRED)
|
||||
|
||||
### 4.1 Artwork Model
|
||||
|
||||
```php
|
||||
App\Models\Artwork
|
||||
```
|
||||
|
||||
**Traits:**
|
||||
|
||||
* `SoftDeletes`
|
||||
|
||||
**Relationships:**
|
||||
|
||||
```php
|
||||
belongsTo(User::class)
|
||||
hasMany(ArtworkTranslation::class)
|
||||
hasOne(ArtworkStats::class)
|
||||
belongsToMany(Category::class)
|
||||
hasMany(ArtworkComment::class)
|
||||
hasMany(ArtworkDownload::class)
|
||||
```
|
||||
|
||||
**Required Scopes:**
|
||||
|
||||
```php
|
||||
public function scopePublic($q)
|
||||
public function scopeApproved($q)
|
||||
public function scopePublished($q)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.2 ArtworkTranslation
|
||||
|
||||
```php
|
||||
App\Models\ArtworkTranslation
|
||||
```
|
||||
|
||||
* SoftDeletes
|
||||
* BelongsTo Artwork
|
||||
|
||||
---
|
||||
|
||||
### 4.3 ArtworkStats
|
||||
|
||||
```php
|
||||
App\Models\ArtworkStats
|
||||
```
|
||||
|
||||
* No SoftDeletes
|
||||
* BelongsTo Artwork
|
||||
|
||||
---
|
||||
|
||||
### 4.4 ArtworkComment
|
||||
|
||||
```php
|
||||
App\Models\ArtworkComment
|
||||
```
|
||||
|
||||
* SoftDeletes
|
||||
* BelongsTo Artwork
|
||||
* BelongsTo User
|
||||
|
||||
---
|
||||
|
||||
### 4.5 ArtworkDownload
|
||||
|
||||
```php
|
||||
App\Models\ArtworkDownload
|
||||
```
|
||||
|
||||
* Append-only
|
||||
* BelongsTo Artwork
|
||||
|
||||
---
|
||||
|
||||
## 5. Query Rules (IMPORTANT)
|
||||
|
||||
### Public browsing MUST always include:
|
||||
|
||||
```sql
|
||||
WHERE
|
||||
deleted_at IS NULL
|
||||
AND is_public = 1
|
||||
AND is_approved = 1
|
||||
```
|
||||
|
||||
### NEVER:
|
||||
|
||||
* eager-load stats on lists
|
||||
* update counters inline
|
||||
* expose IDs in URLs
|
||||
|
||||
---
|
||||
|
||||
## 6. Routing Rules
|
||||
|
||||
### Canonical URLs
|
||||
|
||||
```
|
||||
/{content_type}/{category_path}/{artwork_slug}
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
/photography/abstract/dark/night-city
|
||||
```
|
||||
|
||||
### Slug uniqueness is GLOBAL.
|
||||
|
||||
---
|
||||
|
||||
## 7. Search Rules
|
||||
|
||||
* Use MySQL FULLTEXT as fallback
|
||||
* Prefer external search engines later
|
||||
* Never search on `file_name` or paths
|
||||
|
||||
---
|
||||
|
||||
## 8. Caching Rules
|
||||
|
||||
* Category listings → Redis
|
||||
* Homepage feeds → Redis
|
||||
* Artwork stats → cached
|
||||
* DB is source of truth
|
||||
|
||||
---
|
||||
|
||||
## 9. Soft Delete Behavior
|
||||
|
||||
| Action | Result |
|
||||
| ------------------- | ------------------ |
|
||||
| Soft delete artwork | Hidden from public |
|
||||
| Restore | Fully restored |
|
||||
| Force delete | Rare, GDPR only |
|
||||
|
||||
SEO:
|
||||
|
||||
* Soft-deleted artworks → 410 or 301
|
||||
|
||||
---
|
||||
|
||||
## 10. Forbidden Patterns (NEVER GENERATE)
|
||||
|
||||
❌ Counters on `artworks`
|
||||
❌ IDs in URLs
|
||||
❌ Hard deletes
|
||||
❌ Category logic inside artworks
|
||||
❌ Mixed language columns
|
||||
❌ MyISAM
|
||||
❌ Polymorphic abuse
|
||||
|
||||
---
|
||||
|
||||
## 11. Future Extensions (Allowed)
|
||||
|
||||
Copilot MAY extend with:
|
||||
|
||||
* `artwork_tags`
|
||||
* `artwork_exif`
|
||||
* `artwork_versions`
|
||||
* `artwork_reports`
|
||||
* external search engines
|
||||
|
||||
BUT must not modify core tables without migrations.
|
||||
|
||||
---
|
||||
|
||||
## 12. Final Rule (MANDATORY)
|
||||
|
||||
> **If generated code conflicts with this document,
|
||||
> THIS DOCUMENT WINS.**
|
||||
259
.copilot/categories.md
Normal file
259
.copilot/categories.md
Normal file
@@ -0,0 +1,259 @@
|
||||
Nice, this is a **perfect use-case for Copilot AI Agent** 👍
|
||||
Below is a **clean, copy-paste ready Markdown file** you can put straight into your repo (e.g. `.copilot/categories.md`) and tell Copilot to follow.
|
||||
|
||||
This is written **explicitly for an AI agent**: clear goals, constraints, SQL, Laravel expectations.
|
||||
|
||||
---
|
||||
|
||||
````markdown
|
||||
# SkinBase – Category System (NEW SQL Structure)
|
||||
|
||||
This document defines the **new category & taxonomy system** for SkinBase.
|
||||
Copilot AI Agent must follow this structure strictly and MUST NOT reuse legacy logic.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Goals
|
||||
|
||||
- SEO-friendly URLs (no IDs in public routes)
|
||||
- Clear separation of content types (Photography, Skins, Wallpapers, etc.)
|
||||
- Unlimited category nesting
|
||||
- Laravel-friendly (Eloquent, migrations, relations)
|
||||
- Ready for sitemap, breadcrumbs, translations
|
||||
|
||||
---
|
||||
|
||||
## 🚫 Legacy System (DO NOT USE)
|
||||
|
||||
The old table `artworks_categories` is deprecated.
|
||||
|
||||
DO NOT:
|
||||
- use `section_id`
|
||||
- use `rootid`
|
||||
- use `num_artworks`
|
||||
- expose IDs in URLs
|
||||
- infer hierarchy from numeric hacks
|
||||
|
||||
---
|
||||
|
||||
## ✅ New Database Structure
|
||||
|
||||
### 1️⃣ content_types
|
||||
|
||||
Top-level sections (URL level 1)
|
||||
|
||||
```sql
|
||||
CREATE TABLE content_types (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
slug VARCHAR(64) NOT NULL UNIQUE,
|
||||
description TEXT NULL,
|
||||
created_at TIMESTAMP NULL,
|
||||
updated_at TIMESTAMP NULL
|
||||
) ENGINE=InnoDB;
|
||||
````
|
||||
|
||||
Examples:
|
||||
|
||||
* Photography → `photography`
|
||||
* Skins → `skins`
|
||||
* Wallpapers → `wallpapers`
|
||||
|
||||
Used in URLs as:
|
||||
|
||||
```
|
||||
/photography
|
||||
/skins
|
||||
/wallpapers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ categories
|
||||
|
||||
Hierarchical categories (unlimited depth)
|
||||
|
||||
```sql
|
||||
CREATE TABLE categories (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
content_type_id INT UNSIGNED NOT NULL,
|
||||
parent_id INT UNSIGNED NULL,
|
||||
|
||||
name VARCHAR(128) NOT NULL,
|
||||
slug VARCHAR(128) NOT NULL,
|
||||
|
||||
description TEXT NULL,
|
||||
image VARCHAR(255) NULL,
|
||||
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
sort_order INT DEFAULT 0,
|
||||
|
||||
created_at TIMESTAMP NULL,
|
||||
updated_at TIMESTAMP NULL,
|
||||
|
||||
UNIQUE KEY uniq_category_slug (content_type_id, slug),
|
||||
FOREIGN KEY (content_type_id) REFERENCES content_types(id),
|
||||
FOREIGN KEY (parent_id) REFERENCES categories(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
```
|
||||
|
||||
Hierarchy example:
|
||||
|
||||
```
|
||||
Photography
|
||||
├── Abstract
|
||||
├── Comic
|
||||
└── Dark
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3️⃣ artwork_category (pivot)
|
||||
|
||||
```sql
|
||||
CREATE TABLE artwork_category (
|
||||
artwork_id INT UNSIGNED NOT NULL,
|
||||
category_id INT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (artwork_id, category_id),
|
||||
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧭 Routing Rules (MANDATORY)
|
||||
|
||||
Public URLs MUST use slugs only.
|
||||
|
||||
### Category pages
|
||||
|
||||
```
|
||||
/{content_type}
|
||||
/{content_type}/{category}
|
||||
/{content_type}/{parent-category}/{category}
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
/photography
|
||||
/photography/abstract
|
||||
/skins/desktop-shell
|
||||
```
|
||||
|
||||
### Artwork pages
|
||||
|
||||
```
|
||||
/photography/abstract/fresh-red-apple
|
||||
/skins/darkstep/minimal-dark-pro
|
||||
```
|
||||
|
||||
❌ Never expose numeric IDs in URLs.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Laravel Eloquent Relations
|
||||
|
||||
Copilot MUST generate models with these relations:
|
||||
|
||||
```php
|
||||
ContentType hasMany Categories
|
||||
Category belongsTo ContentType
|
||||
Category hasMany Categories (children)
|
||||
Category belongsTo Category (parent)
|
||||
Artwork belongsToMany Categories
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Querying Logic
|
||||
|
||||
Lookup must be done by slug, not ID.
|
||||
|
||||
Example:
|
||||
|
||||
```php
|
||||
$contentType = ContentType::whereSlug($type)->firstOrFail();
|
||||
$category = Category::whereSlug($slug)
|
||||
->where('content_type_id', $contentType->id)
|
||||
->firstOrFail();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 SEO Rules
|
||||
|
||||
* One canonical URL per category
|
||||
* Breadcrumbs must reflect hierarchy
|
||||
* Pagination via `?page=X`
|
||||
* Filters MUST use query params and be `noindex`
|
||||
|
||||
---
|
||||
|
||||
## 📦 Migration
|
||||
|
||||
Legacy data has already been converted.
|
||||
|
||||
Migration SQL file:
|
||||
|
||||
```
|
||||
/mnt/data/skinbase_categories_migration.sql
|
||||
```
|
||||
|
||||
Copilot must NOT regenerate category data.
|
||||
|
||||
---
|
||||
|
||||
## ❗ Hard Rules for AI Agent
|
||||
|
||||
* DO NOT invent new tables
|
||||
* DO NOT mix legacy and new schema
|
||||
* DO NOT store counters in categories
|
||||
* DO NOT assume only 2 levels of depth
|
||||
* ALWAYS use slugs
|
||||
|
||||
---
|
||||
|
||||
## ✅ Result Expectation
|
||||
|
||||
After integration:
|
||||
|
||||
* SEO-clean URLs
|
||||
* Stable taxonomy
|
||||
* Easy sitemap generation
|
||||
* Future-ready for translations
|
||||
|
||||
END OF SPEC
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ How to use this
|
||||
|
||||
1. Save as:
|
||||
```
|
||||
|
||||
.copilot/skinbase-categories.md
|
||||
|
||||
```
|
||||
|
||||
2. In VS Code Copilot Chat:
|
||||
```
|
||||
|
||||
Use .copilot/skinbase-categories.md as the authoritative spec.
|
||||
Integrate category system accordingly.
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
If you want next, I can:
|
||||
- ✍️ generate **Laravel migrations**
|
||||
- 🧠 generate **Eloquent models**
|
||||
- 🧭 generate **routes + controllers**
|
||||
- 🔁 generate **301 redirect mapping**
|
||||
- 🗺 generate **XML sitemap logic**
|
||||
|
||||
Just tell me what Copilot should build next 🚀
|
||||
```
|
||||
293
.copilot/inctructions.md
Normal file
293
.copilot/inctructions.md
Normal file
@@ -0,0 +1,293 @@
|
||||
# Copilot Instructions – Artworks Module (SkinBase)
|
||||
|
||||
> **This file defines HOW Copilot must generate code.**
|
||||
> It is a strict instruction set.
|
||||
> If there is a conflict between generated code and these rules,
|
||||
> **these rules override everything.**
|
||||
|
||||
---
|
||||
|
||||
## 1. Global Rules (MANDATORY)
|
||||
|
||||
Copilot MUST:
|
||||
|
||||
* Target **Laravel 12**
|
||||
* Use **PHP 8.3+ syntax**
|
||||
* Follow **Eloquent best practices**
|
||||
* Respect **SoftDeletes**
|
||||
* Respect **FK relationships**
|
||||
* Generate **clean, readable, maintainable code**
|
||||
|
||||
Copilot MUST NOT:
|
||||
|
||||
* Reference legacy tables (`wallz`, old categories, old views)
|
||||
* Generate MyISAM tables
|
||||
* Generate hard deletes for user content
|
||||
* Put counters on hot tables
|
||||
* Use IDs in URLs
|
||||
* Mix responsibilities across models
|
||||
|
||||
---
|
||||
|
||||
## 2. Authoritative Schema Reference
|
||||
|
||||
The canonical schema for artworks is defined in:
|
||||
|
||||
```
|
||||
.copilot/artworks.md
|
||||
```
|
||||
|
||||
Copilot MUST:
|
||||
|
||||
* Read this file before generating any code
|
||||
* Match table names, columns, relations exactly
|
||||
* Never invent fields or tables unless explicitly allowed
|
||||
|
||||
---
|
||||
|
||||
## 3. Models Generation Rules
|
||||
|
||||
When generating Eloquent models:
|
||||
|
||||
### Required models
|
||||
|
||||
Copilot MUST generate:
|
||||
|
||||
* `App\Models\Artwork`
|
||||
* `App\Models\ArtworkTranslation`
|
||||
* `App\Models\ArtworkStats`
|
||||
* `App\Models\ArtworkComment`
|
||||
* `App\Models\ArtworkDownload`
|
||||
|
||||
### Model requirements
|
||||
|
||||
Each model MUST:
|
||||
|
||||
* Declare `$fillable`
|
||||
* Define all relationships
|
||||
* Use `SoftDeletes` **only when allowed**
|
||||
* Include PHPDoc blocks for relations
|
||||
* Use type-hinted return values
|
||||
|
||||
### Forbidden
|
||||
|
||||
Copilot MUST NOT:
|
||||
|
||||
* Add business logic into models
|
||||
* Perform stat mutations in models
|
||||
* Use observers unless explicitly requested
|
||||
|
||||
---
|
||||
|
||||
## 4. Relationships (STRICT)
|
||||
|
||||
Copilot MUST implement these exact relations:
|
||||
|
||||
### Artwork
|
||||
|
||||
```php
|
||||
belongsTo(User::class)
|
||||
hasMany(ArtworkTranslation::class)
|
||||
hasOne(ArtworkStats::class)
|
||||
belongsToMany(Category::class)
|
||||
hasMany(ArtworkComment::class)
|
||||
hasMany(ArtworkDownload::class)
|
||||
```
|
||||
|
||||
### ArtworkTranslation
|
||||
|
||||
```php
|
||||
belongsTo(Artwork::class)
|
||||
```
|
||||
|
||||
### ArtworkStats
|
||||
|
||||
```php
|
||||
belongsTo(Artwork::class)
|
||||
```
|
||||
|
||||
### ArtworkComment
|
||||
|
||||
```php
|
||||
belongsTo(Artwork::class)
|
||||
belongsTo(User::class)
|
||||
```
|
||||
|
||||
### ArtworkDownload
|
||||
|
||||
```php
|
||||
belongsTo(Artwork::class)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Query & Scope Rules
|
||||
|
||||
Copilot MUST define these scopes on `Artwork`:
|
||||
|
||||
```php
|
||||
scopePublic($query)
|
||||
scopeApproved($query)
|
||||
scopePublished($query)
|
||||
```
|
||||
|
||||
Public queries MUST always include:
|
||||
|
||||
```php
|
||||
deleted_at IS NULL
|
||||
is_public = true
|
||||
is_approved = true
|
||||
```
|
||||
|
||||
Copilot MUST NOT:
|
||||
|
||||
* Eager-load stats in list views
|
||||
* Use `offset` pagination for feeds
|
||||
* Load unnecessary relations by default
|
||||
|
||||
---
|
||||
|
||||
## 6. Controller Generation Rules
|
||||
|
||||
When generating controllers:
|
||||
|
||||
Copilot MUST:
|
||||
|
||||
* Use **thin controllers**
|
||||
* Delegate logic to services/actions if needed
|
||||
* Validate input using Form Requests
|
||||
* Use route-model binding with `slug`
|
||||
* Handle soft-deleted content properly
|
||||
|
||||
Copilot MUST NOT:
|
||||
|
||||
* Query raw DB tables directly
|
||||
* Bypass scopes
|
||||
* Return unfiltered content
|
||||
|
||||
---
|
||||
|
||||
## 7. Routing Rules
|
||||
|
||||
Routes MUST:
|
||||
|
||||
* Use **slug-based routing**
|
||||
* Never expose numeric IDs
|
||||
* Respect category hierarchy
|
||||
* Be SEO-friendly
|
||||
|
||||
Example (valid):
|
||||
|
||||
```
|
||||
/photography/abstract/dark/night-city
|
||||
```
|
||||
|
||||
Invalid:
|
||||
|
||||
```
|
||||
/artwork/123
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Soft Delete Rules
|
||||
|
||||
Copilot MUST:
|
||||
|
||||
* Use `delete()` (soft delete) for user content
|
||||
* Use `restore()` for recovery
|
||||
* Use `forceDelete()` **only** when explicitly requested
|
||||
|
||||
When content is soft-deleted:
|
||||
|
||||
* It must disappear from public browsing
|
||||
* It must remain accessible to admins
|
||||
|
||||
---
|
||||
|
||||
## 9. Stats & High-Load Rules
|
||||
|
||||
Copilot MUST:
|
||||
|
||||
* Treat stats as **derived data**
|
||||
* Update stats via jobs / services
|
||||
* Never increment counters inline in controllers
|
||||
* Assume Redis may be present
|
||||
|
||||
Copilot MUST NOT:
|
||||
|
||||
* Store counters on `artworks`
|
||||
* Use `increment()` directly on hot rows
|
||||
|
||||
---
|
||||
|
||||
## 10. Search Rules
|
||||
|
||||
Copilot MAY:
|
||||
|
||||
* Use MySQL FULLTEXT
|
||||
* Use external search engines (if requested)
|
||||
|
||||
Copilot MUST NOT:
|
||||
|
||||
* Search file paths
|
||||
* Search binary metadata
|
||||
* Assume Elasticsearch exists unless specified
|
||||
|
||||
---
|
||||
|
||||
## 11. Forbidden Patterns (NEVER GENERATE)
|
||||
|
||||
❌ Hard deletes on artworks
|
||||
❌ Legacy column names
|
||||
❌ Polymorphic abuse
|
||||
❌ Fat controllers
|
||||
❌ Magic numbers
|
||||
❌ Inline SQL in controllers
|
||||
❌ Business logic in migrations
|
||||
|
||||
---
|
||||
|
||||
## 12. Extension Rules
|
||||
|
||||
Copilot MAY generate new features ONLY if:
|
||||
|
||||
* They do not modify core tables
|
||||
* They follow the same architectural principles
|
||||
* They are isolated in new tables/services
|
||||
|
||||
Allowed examples:
|
||||
|
||||
* tags
|
||||
* EXIF metadata
|
||||
* versioning
|
||||
* reporting/flagging
|
||||
* API resources
|
||||
|
||||
---
|
||||
|
||||
## 13. Error Handling
|
||||
|
||||
Copilot MUST:
|
||||
|
||||
* Throw meaningful exceptions
|
||||
* Return proper HTTP codes
|
||||
* Use 404 for missing public content
|
||||
* Use 410 or 301 for deleted content (if requested)
|
||||
|
||||
---
|
||||
|
||||
## 14. Final Instruction (ABSOLUTE)
|
||||
|
||||
> **Copilot must treat this file and `artworks.md`
|
||||
> as non-negotiable contracts.**
|
||||
|
||||
If unsure:
|
||||
|
||||
* ask for clarification
|
||||
* do NOT guess
|
||||
* do NOT invent schema
|
||||
|
||||
---
|
||||
|
||||
### ✅ End of Copilot Instructions
|
||||
816
.copilot/legacy_users_migration.md
Normal file
816
.copilot/legacy_users_migration.md
Normal file
@@ -0,0 +1,816 @@
|
||||
# SkinBase – Legacy Users Migration to Laravel Auth (Authoritative Spec)
|
||||
|
||||
This document is the **single source of truth** for migrating users from the legacy SkinBase database (`projekti_old_skinbase`) into the new Laravel (SkinBase 2026) application using modern authentication.
|
||||
|
||||
It covers:
|
||||
- ✅ Target data model (Laravel-friendly)
|
||||
- ✅ What to keep / what to drop from legacy
|
||||
- ✅ Exact Laravel migrations
|
||||
- ✅ Import SQL + migration order
|
||||
- ✅ Old password compatibility (if possible)
|
||||
- ✅ First-login password reset flow (recommended)
|
||||
- ✅ Validation SQL & sanity checks
|
||||
- ✅ Role-based access control (RBAC)
|
||||
|
||||
> **Copilot AI Agent instructions:**
|
||||
> Follow this document strictly. Do not invent additional fields/tables unless explicitly allowed here.
|
||||
|
||||
---
|
||||
|
||||
## 0) Context: Legacy tables (what we’re migrating)
|
||||
|
||||
Legacy schema relevant for users:
|
||||
|
||||
### `users` (MyISAM, mixed responsibilities)
|
||||
- `user_id` (PK)
|
||||
- `uname` (username)
|
||||
- `password` (varchar 80) – legacy hash or plaintext (unknown)
|
||||
- `password2` (varchar 255) – sometimes present
|
||||
- `email`
|
||||
- `real_name`
|
||||
- `web`
|
||||
- `birth`, `gender`, `country`, `country_code`, `lang`
|
||||
- `picture`, `cover_art`
|
||||
- `signature`, `about_me`, `description`
|
||||
- `LastVisit`, `joinDate`
|
||||
- `user_type` (membership / level)
|
||||
- `active`, `authorized`
|
||||
- many legacy preferences and obsolete fields (ICQ etc.)
|
||||
|
||||
### `users_data` (mostly duplicate/overlap)
|
||||
This is redundant and will NOT be kept as-is.
|
||||
|
||||
### `users_statistics`
|
||||
Useful but not auth-related; will migrate to `user_statistics`.
|
||||
|
||||
### `users_types`
|
||||
Legacy user “levels”. We’ll map to modern roles.
|
||||
|
||||
---
|
||||
|
||||
## 1) Migration Goals
|
||||
|
||||
### Authentication goals
|
||||
- Use **Laravel default authentication** (Breeze/Fortify/Jetstream-compatible).
|
||||
- Allow login via:
|
||||
- username OR email
|
||||
- Preserve user accounts with minimal friction.
|
||||
- Handle legacy password format safely:
|
||||
- Prefer secure migration with password reset
|
||||
- Optionally support legacy hash verification if algorithm is known
|
||||
|
||||
### Data goals
|
||||
- Keep IDs stable where reasonable (`user_id` → `users.id`) to simplify future migrations.
|
||||
- Move non-auth profile data into a dedicated profile table.
|
||||
- Remove obsolete fields (ICQ etc.) and replace with modern social links.
|
||||
|
||||
### Security goals
|
||||
- Do not store weak hashes long-term.
|
||||
- If legacy password verification is implemented, rehash to bcrypt/argon immediately upon successful login.
|
||||
- Default to forcing password reset if legacy hash format is unknown.
|
||||
|
||||
---
|
||||
|
||||
## 2) Target Database Design (New System)
|
||||
|
||||
### 2.1 `users` (Auth + identity only)
|
||||
**Keep it clean.** This table should contain only identity/auth/security-critical fields.
|
||||
|
||||
Fields:
|
||||
- `id` (BIGINT)
|
||||
- `username` (unique)
|
||||
- `name`
|
||||
- `email` (unique)
|
||||
- `password` (bcrypt/argon hash)
|
||||
- `email_verified_at`
|
||||
- `remember_token`
|
||||
- `is_active` (legacy `active`)
|
||||
- `needs_password_reset` (new)
|
||||
- `role` (simple RBAC) OR use roles table/spatie later
|
||||
- timestamps
|
||||
|
||||
### 2.2 `user_profiles` (Profile data)
|
||||
- bio/about, avatar, cover image
|
||||
- country + language + birthdate + gender
|
||||
- website
|
||||
- timestamps
|
||||
|
||||
### 2.3 `user_social_links` (modern social replacement)
|
||||
Instead of ICQ, store dynamic social platforms:
|
||||
- github, twitter/x, instagram, youtube, discord, website, etc.
|
||||
|
||||
### 2.4 `user_statistics` (optional but useful)
|
||||
Migrated from legacy `users_statistics`.
|
||||
|
||||
---
|
||||
|
||||
## 3) What to Remove / Replace
|
||||
|
||||
### Remove (obsolete / not used / legacy UI junk)
|
||||
- `icq` (obsolete)
|
||||
- `zone`
|
||||
- `numboard`, `NumStats`, `numskin`, `section_style`
|
||||
- `menu`
|
||||
- `eicon`
|
||||
- `mlist`
|
||||
- various “board/menu” preferences that no longer exist
|
||||
|
||||
### Keep / migrate
|
||||
- username, email, name
|
||||
- last visit (optional)
|
||||
- active/authorized → `is_active` + `email_verified_at` strategy
|
||||
- about/bio
|
||||
- avatar/cover
|
||||
- country/language/gender/birthdate
|
||||
- website
|
||||
- statistics (optional)
|
||||
|
||||
### Replace ICQ with social links
|
||||
Add `user_social_links` table.
|
||||
|
||||
---
|
||||
|
||||
## 4) Role Mapping (Legacy `users_types` → Modern RBAC)
|
||||
|
||||
Legacy:
|
||||
- `users.user_type` references `users_types.id`
|
||||
|
||||
New (simple approach):
|
||||
- store a string `role` directly in `users.role`:
|
||||
- `user`
|
||||
- `moderator`
|
||||
- `admin`
|
||||
|
||||
Mapping recommendation (adjust if your legacy meaning differs):
|
||||
- `user_type` <= 0 → `user`
|
||||
- `user_type` in [1..X] with “moderator” meaning → `moderator`
|
||||
- special admin IDs → `admin`
|
||||
|
||||
> If you later need granular permissions, adopt **spatie/laravel-permission**.
|
||||
> For now, keep it simple.
|
||||
|
||||
---
|
||||
|
||||
## 5) Exact Laravel Migrations (Copy/Paste)
|
||||
|
||||
> These migrations are authoritative. Put them in `database/migrations/` in this order.
|
||||
|
||||
### 5.1 Create/extend `users` table
|
||||
If you already have Laravel’s default `users` migration, create a new migration to **modify** it.
|
||||
|
||||
**Migration: `2026_02_01_000010_update_users_table_for_skinbase.php`**
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
// Ensure big integer id is used in your app; Laravel default is bigIncrements already.
|
||||
// Add username for legacy uname
|
||||
if (!Schema::hasColumn('users', 'username')) {
|
||||
$table->string('username', 80)->nullable()->unique()->after('id');
|
||||
}
|
||||
|
||||
// If name exists, keep it. Ensure nullable for legacy.
|
||||
if (!Schema::hasColumn('users', 'name')) {
|
||||
$table->string('name')->nullable();
|
||||
} else {
|
||||
$table->string('name')->nullable()->change();
|
||||
}
|
||||
|
||||
// Email is important; legacy might have duplicates/NULLs -> handle in import script carefully.
|
||||
if (Schema::hasColumn('users', 'email')) {
|
||||
$table->string('email')->nullable()->change();
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('users', 'is_active')) {
|
||||
$table->boolean('is_active')->default(true)->after('remember_token');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('users', 'needs_password_reset')) {
|
||||
$table->boolean('needs_password_reset')->default(true)->after('is_active');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('users', 'role')) {
|
||||
$table->string('role', 32)->default('user')->after('needs_password_reset');
|
||||
}
|
||||
|
||||
// Optional: store legacy hash algorithm marker (only if doing compat)
|
||||
if (!Schema::hasColumn('users', 'legacy_password_algo')) {
|
||||
$table->string('legacy_password_algo', 32)->nullable()->after('role');
|
||||
}
|
||||
|
||||
// Optional: store legacy last visit
|
||||
if (!Schema::hasColumn('users', 'last_visit_at')) {
|
||||
$table->timestamp('last_visit_at')->nullable()->after('legacy_password_algo');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('users', 'username')) $table->dropColumn('username');
|
||||
if (Schema::hasColumn('users', 'is_active')) $table->dropColumn('is_active');
|
||||
if (Schema::hasColumn('users', 'needs_password_reset')) $table->dropColumn('needs_password_reset');
|
||||
if (Schema::hasColumn('users', 'role')) $table->dropColumn('role');
|
||||
if (Schema::hasColumn('users', 'legacy_password_algo')) $table->dropColumn('legacy_password_algo');
|
||||
if (Schema::hasColumn('users', 'last_visit_at')) $table->dropColumn('last_visit_at');
|
||||
});
|
||||
}
|
||||
};
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Create `user_profiles`
|
||||
|
||||
**Migration: `2026_02_01_000020_create_user_profiles_table.php`**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_profiles', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('user_id')->primary();
|
||||
|
||||
$table->text('bio')->nullable();
|
||||
$table->string('avatar', 255)->nullable();
|
||||
$table->string('cover_image', 255)->nullable();
|
||||
|
||||
$table->string('country', 80)->nullable();
|
||||
$table->char('country_code', 2)->nullable(); // normalize to ISO-3166-1 alpha-2
|
||||
$table->string('language', 10)->nullable();
|
||||
|
||||
$table->date('birthdate')->nullable();
|
||||
$table->enum('gender', ['M','F','X'])->default('X');
|
||||
|
||||
$table->string('website', 255)->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_profiles');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.3 Create `user_social_links`
|
||||
|
||||
**Migration: `2026_02_01_000030_create_user_social_links_table.php`**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_social_links', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('user_id');
|
||||
|
||||
$table->string('platform', 32); // e.g. github, twitter, instagram, youtube, discord, website
|
||||
$table->string('url', 255);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'platform']);
|
||||
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_social_links');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.4 Create `user_statistics` (optional)
|
||||
|
||||
**Migration: `2026_02_01_000040_create_user_statistics_table.php`**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_statistics', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('user_id')->primary();
|
||||
|
||||
$table->unsignedInteger('uploads')->default(0);
|
||||
$table->unsignedInteger('downloads')->default(0);
|
||||
$table->unsignedInteger('pageviews')->default(0);
|
||||
$table->unsignedInteger('awards')->default(0);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_statistics');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6) Auth Logic: Old Password Compatibility (Two Approaches)
|
||||
|
||||
### Approach A (Recommended): Force Reset for Everyone
|
||||
|
||||
**Safest**, because legacy hashing is unknown.
|
||||
|
||||
* Import users with a random password hash (or keep legacy hash in `password` temporarily but do not allow it).
|
||||
* Set `needs_password_reset = 1`
|
||||
* On login attempt, require reset.
|
||||
|
||||
This avoids accepting weak hashes forever.
|
||||
|
||||
### Approach B (Optional): Support legacy hash on first login, then rehash
|
||||
|
||||
Only do this if you can identify the algorithm used in legacy `password` / `password2`.
|
||||
|
||||
Common old hashes:
|
||||
|
||||
* MD5: 32 hex chars
|
||||
* SHA1: 40 hex chars
|
||||
* bcrypt: starts with `$2y$` or `$2a$`
|
||||
* phpBB style or custom salts: unknown
|
||||
|
||||
#### Detection hints (informational)
|
||||
|
||||
* If legacy `password` length is 32 and is hex → probably MD5
|
||||
* If length is 40 hex → probably SHA1
|
||||
* If starts with `$2y$` → already bcrypt
|
||||
|
||||
> **Action requirement:** Before implementing compat, confirm by checking a known user password against a sample hash.
|
||||
|
||||
---
|
||||
|
||||
## 7) Implementation: Login via Username OR Email
|
||||
|
||||
### 7.1 Breeze/Fortify login request logic
|
||||
|
||||
In your login controller (or Fortify authentication callback), accept a single input field:
|
||||
|
||||
* `login` (username or email)
|
||||
* `password`
|
||||
|
||||
Example lookup:
|
||||
|
||||
```php
|
||||
$user = User::query()
|
||||
->where('email', $login)
|
||||
->orWhere('username', $login)
|
||||
->first();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8) Implementation: Legacy Password Compatibility (If enabled)
|
||||
|
||||
### 8.1 Add a service: `app/Support/LegacyPassword.php`
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
class LegacyPassword
|
||||
{
|
||||
public static function detectAlgo(?string $hash): ?string
|
||||
{
|
||||
if (!$hash) return null;
|
||||
|
||||
if (str_starts_with($hash, '$2y$') || str_starts_with($hash, '$2a$')) return 'bcrypt';
|
||||
if (preg_match('/^[a-f0-9]{32}$/i', $hash)) return 'md5';
|
||||
if (preg_match('/^[a-f0-9]{40}$/i', $hash)) return 'sha1';
|
||||
|
||||
return null; // unknown
|
||||
}
|
||||
|
||||
public static function verify(string $plain, string $legacyHash, string $algo): bool
|
||||
{
|
||||
return match ($algo) {
|
||||
'md5' => md5($plain) === $legacyHash,
|
||||
'sha1' => sha1($plain) === $legacyHash,
|
||||
'bcrypt' => password_verify($plain, $legacyHash),
|
||||
default => false
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 Modify authentication (pseudo-code)
|
||||
|
||||
On login:
|
||||
|
||||
1. Try normal Laravel hash check (`Hash::check`)
|
||||
2. If fails AND `legacy_password_algo` is present (or detected), try legacy verify
|
||||
3. If legacy verify passes:
|
||||
|
||||
* set new password using `Hash::make($plain)`
|
||||
* set `needs_password_reset = 0`
|
||||
* clear `legacy_password_algo`
|
||||
|
||||
Example snippet:
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use App\Support\LegacyPassword;
|
||||
|
||||
if (Hash::check($password, $user->password)) {
|
||||
// ok
|
||||
} else {
|
||||
$algo = $user->legacy_password_algo ?: LegacyPassword::detectAlgo($user->password);
|
||||
if ($algo && LegacyPassword::verify($password, $user->password, $algo)) {
|
||||
$user->password = Hash::make($password);
|
||||
$user->needs_password_reset = false;
|
||||
$user->legacy_password_algo = null;
|
||||
$user->save();
|
||||
} else {
|
||||
// invalid credentials
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> If `detectAlgo()` returns null, do NOT allow login: require password reset via email.
|
||||
|
||||
---
|
||||
|
||||
## 9) First-login Password Reset Flow (Recommended & Secure)
|
||||
|
||||
### Requirements
|
||||
|
||||
* If `needs_password_reset = 1`, user must reset password before accessing account.
|
||||
* This can be enforced via middleware.
|
||||
|
||||
### 9.1 Middleware: `EnsurePasswordResetCompleted`
|
||||
|
||||
Create: `app/Http/Middleware/EnsurePasswordResetCompleted.php`
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EnsurePasswordResetCompleted
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if ($user && $user->needs_password_reset) {
|
||||
if (!$request->routeIs('password.reset.*')) {
|
||||
return redirect()->route('password.reset.notice');
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 Routes for reset notice & flow
|
||||
|
||||
Add routes:
|
||||
|
||||
* `password.reset.notice` → show "You must reset your password"
|
||||
* Use Laravel’s standard password reset email flow
|
||||
|
||||
### 9.3 UX recommendation
|
||||
|
||||
* When migrating, send optional mass email campaign:
|
||||
|
||||
* “SkinBase upgraded – set your new password”
|
||||
* But don’t require sending all at once; users can request reset when needed.
|
||||
|
||||
---
|
||||
|
||||
## 10) Migration / Import Process (Recommended Order)
|
||||
|
||||
### Step 1: Ensure old DB is accessible
|
||||
|
||||
Options:
|
||||
|
||||
* Import old DB into same MySQL server
|
||||
* Or create a read-only connection in Laravel (`config/database.php`) to `projekti_old_skinbase`
|
||||
|
||||
### Step 2: Run migrations for new schema
|
||||
|
||||
```bash
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
### Step 3: Import users
|
||||
|
||||
We keep `user_id` as `users.id` to preserve identity mapping.
|
||||
|
||||
---
|
||||
|
||||
## 11) Import SQL (Base migration)
|
||||
|
||||
> These SQL examples assume both databases are on the same MySQL server.
|
||||
> Adjust database names as needed.
|
||||
|
||||
### 11.1 Insert into `users`
|
||||
|
||||
**Important rules:**
|
||||
|
||||
* Some users may have NULL/duplicate emails → handle safely.
|
||||
* Username should be unique. If duplicates exist, add suffix.
|
||||
|
||||
Recommended initial import (conservative):
|
||||
|
||||
* Keep legacy hash in `password` temporarily
|
||||
* Mark `needs_password_reset = 1`
|
||||
* Set `legacy_password_algo` if detectable
|
||||
|
||||
```sql
|
||||
INSERT INTO users (id, username, name, email, password, is_active, needs_password_reset, role, legacy_password_algo, last_visit_at, created_at, updated_at)
|
||||
SELECT
|
||||
u.user_id AS id,
|
||||
NULLIF(u.uname, '') AS username,
|
||||
NULLIF(u.real_name, '') AS name,
|
||||
NULLIF(u.email, '') AS email,
|
||||
COALESCE(NULLIF(u.password2, ''), NULLIF(u.password, ''), '') AS password,
|
||||
CASE WHEN u.active = 1 THEN 1 ELSE 0 END AS is_active,
|
||||
1 AS needs_password_reset,
|
||||
'user' AS role,
|
||||
NULL AS legacy_password_algo,
|
||||
u.LastVisit AS last_visit_at,
|
||||
u.joinDate AS created_at,
|
||||
NOW() AS updated_at
|
||||
FROM projekti_old_skinbase.users u;
|
||||
```
|
||||
|
||||
> After import, you can populate `legacy_password_algo` using detection rules if you want compat.
|
||||
|
||||
Example:
|
||||
|
||||
```sql
|
||||
UPDATE users
|
||||
SET legacy_password_algo =
|
||||
CASE
|
||||
WHEN password LIKE '$2y$%' THEN 'bcrypt'
|
||||
WHEN password REGEXP '^[a-f0-9]{32}$' THEN 'md5'
|
||||
WHEN password REGEXP '^[a-f0-9]{40}$' THEN 'sha1'
|
||||
ELSE NULL
|
||||
END
|
||||
WHERE legacy_password_algo IS NULL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 11.2 Insert into `user_profiles`
|
||||
|
||||
```sql
|
||||
INSERT INTO user_profiles (user_id, bio, avatar, cover_image, country, country_code, language, birthdate, gender, website, created_at, updated_at)
|
||||
SELECT
|
||||
u.user_id,
|
||||
NULLIF(u.about_me, '') AS bio,
|
||||
NULLIF(u.picture, '') AS avatar,
|
||||
NULLIF(u.cover_art, '') AS cover_image,
|
||||
NULLIF(u.country, '') AS country,
|
||||
NULLIF(LEFT(u.country_code, 2), '') AS country_code,
|
||||
NULLIF(u.lang, '') AS language,
|
||||
u.birth AS birthdate,
|
||||
COALESCE(u.gender, 'X') AS gender,
|
||||
NULLIF(u.web, '') AS website,
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM projekti_old_skinbase.users u
|
||||
WHERE u.user_id IS NOT NULL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 11.3 Social links (only website initially)
|
||||
|
||||
Optionally insert website into social links (if you prefer everything in one place):
|
||||
|
||||
```sql
|
||||
INSERT INTO user_social_links (user_id, platform, url, created_at, updated_at)
|
||||
SELECT
|
||||
u.user_id,
|
||||
'website',
|
||||
u.web,
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM projekti_old_skinbase.users u
|
||||
WHERE u.web IS NOT NULL AND u.web <> '';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 11.4 Statistics
|
||||
|
||||
```sql
|
||||
INSERT INTO user_statistics (user_id, uploads, downloads, pageviews, awards, created_at, updated_at)
|
||||
SELECT
|
||||
s.user_id,
|
||||
s.uploads,
|
||||
s.downloads,
|
||||
s.pageviews,
|
||||
s.awards,
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM projekti_old_skinbase.users_statistics s;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12) Migration Validation SQL (Sanity Checks)
|
||||
|
||||
### 12.1 Count parity
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM projekti_old_skinbase.users) AS old_users,
|
||||
(SELECT COUNT(*) FROM users) AS new_users;
|
||||
```
|
||||
|
||||
### 12.2 Missing usernames
|
||||
|
||||
```sql
|
||||
SELECT id, email
|
||||
FROM users
|
||||
WHERE username IS NULL OR username = '';
|
||||
```
|
||||
|
||||
### 12.3 Duplicate usernames
|
||||
|
||||
```sql
|
||||
SELECT username, COUNT(*) c
|
||||
FROM users
|
||||
WHERE username IS NOT NULL AND username <> ''
|
||||
GROUP BY username
|
||||
HAVING c > 1;
|
||||
```
|
||||
|
||||
### 12.4 Duplicate emails
|
||||
|
||||
```sql
|
||||
SELECT email, COUNT(*) c
|
||||
FROM users
|
||||
WHERE email IS NOT NULL AND email <> ''
|
||||
GROUP BY email
|
||||
HAVING c > 1;
|
||||
```
|
||||
|
||||
### 12.5 Orphaned profiles
|
||||
|
||||
```sql
|
||||
SELECT p.user_id
|
||||
FROM user_profiles p
|
||||
LEFT JOIN users u ON u.id = p.user_id
|
||||
WHERE u.id IS NULL;
|
||||
```
|
||||
|
||||
### 12.6 Users inactive / unauthorized review
|
||||
|
||||
Legacy had `authorized`. If you want to incorporate:
|
||||
|
||||
```sql
|
||||
SELECT user_id, active, authorized
|
||||
FROM projekti_old_skinbase.users
|
||||
WHERE active = 0 OR authorized = 0
|
||||
LIMIT 200;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13) Role-based Access Control (RBAC)
|
||||
|
||||
### Option 1 (Simple, recommended now): string role on users
|
||||
|
||||
* `users.role` = `user|moderator|admin`
|
||||
* Add middleware checks:
|
||||
|
||||
* admin-only panels
|
||||
* moderator actions (approve uploads, etc.)
|
||||
|
||||
Example middleware:
|
||||
|
||||
```php
|
||||
public function handle($request, Closure $next, string $role)
|
||||
{
|
||||
$user = $request->user();
|
||||
if (!$user || $user->role !== $role) abort(403);
|
||||
return $next($request);
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2 (Advanced later): spatie/laravel-permission
|
||||
|
||||
Adopt if you need granular permissions:
|
||||
|
||||
* `approve_artwork`
|
||||
* `ban_user`
|
||||
* `edit_categories`
|
||||
* etc.
|
||||
|
||||
Not required for v1 migration.
|
||||
|
||||
---
|
||||
|
||||
## 14) Implementation Notes (Important)
|
||||
|
||||
### Email issues
|
||||
|
||||
Legacy allows NULL/duplicate emails. Laravel password reset requires unique emails.
|
||||
Strategy:
|
||||
|
||||
* If email missing: user must login with username and request support or add email.
|
||||
* If duplicate emails: resolve manually or append `+id` style (not recommended) or enforce unique by cleanup.
|
||||
|
||||
### Username issues
|
||||
|
||||
If duplicates exist, your import must resolve them.
|
||||
Recommended rule:
|
||||
|
||||
* if username duplicate, append `-<id>`
|
||||
|
||||
### MyISAM note
|
||||
|
||||
Legacy tables are MyISAM; importing into InnoDB is fine.
|
||||
Do not try to preserve MyISAM.
|
||||
|
||||
---
|
||||
|
||||
## 15) Recommended Laravel Auth Starter Kit
|
||||
|
||||
Use **Laravel Breeze** for simplest modern auth:
|
||||
|
||||
* login/register
|
||||
* password resets
|
||||
* email verification (optional)
|
||||
|
||||
Then customize:
|
||||
|
||||
* login field: username OR email
|
||||
* middleware to enforce password reset
|
||||
|
||||
---
|
||||
|
||||
## 16) Deliverables Checklist (What Copilot must implement)
|
||||
|
||||
1. ✅ Migrations (sections 5.1–5.4)
|
||||
2. ✅ Import strategy + SQL (section 11)
|
||||
3. ✅ Validation SQL queries (section 12)
|
||||
4. ✅ Login supports username/email (section 7)
|
||||
5. ✅ Password reset enforcement (section 9)
|
||||
6. ✅ Optional legacy password compatibility (section 8)
|
||||
7. ✅ RBAC (section 13 option 1)
|
||||
|
||||
---
|
||||
|
||||
## 17) Final Security Policy
|
||||
|
||||
* Default `needs_password_reset = 1` for all migrated users.
|
||||
* If legacy hash compatibility is used:
|
||||
|
||||
* accept legacy hash **only once**
|
||||
* rehash immediately to Laravel hash
|
||||
* clear legacy markers
|
||||
* Do not keep MD5/SHA1 hashes long-term.
|
||||
|
||||
---
|
||||
|
||||
END OF DOCUMENT
|
||||
19
.copilot/prompts.md
Normal file
19
.copilot/prompts.md
Normal file
@@ -0,0 +1,19 @@
|
||||
Generate all Eloquent models for the Artworks module.
|
||||
|
||||
Requirements:
|
||||
- Follow .copilot/artworks.md exactly
|
||||
- Follow .copilot/instructions.md strictly
|
||||
- Laravel 12, PHP 8.3+
|
||||
- Use SoftDeletes only where allowed
|
||||
- Define all relationships
|
||||
- Add fillable arrays
|
||||
- Add PHPDoc blocks for relations
|
||||
- Do NOT add business logic
|
||||
- Do NOT add observers
|
||||
|
||||
Models:
|
||||
- Artwork
|
||||
- ArtworkTranslation
|
||||
- ArtworkStats
|
||||
- ArtworkComment
|
||||
- ArtworkDownload
|
||||
240
.copilot/skinbase-category-links.md
Normal file
240
.copilot/skinbase-category-links.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# SkinBase – Category Link Building (AUTHORITATIVE SPEC)
|
||||
|
||||
This document defines the **ONLY valid way** to build public category and artwork URLs in SkinBase.
|
||||
Copilot AI Agent MUST follow this specification exactly.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Goal
|
||||
|
||||
- SEO-friendly URLs
|
||||
- No numeric IDs in public routes
|
||||
- Unlimited category depth
|
||||
- Predictable and deterministic link building
|
||||
- One canonical URL per resource
|
||||
|
||||
---
|
||||
|
||||
## 🚫 Forbidden Concepts
|
||||
|
||||
Copilot MUST NOT:
|
||||
- expose numeric IDs in URLs
|
||||
- use legacy paths (`/Photography/3`)
|
||||
- infer hierarchy from URL text
|
||||
- mix `content_type_id` and `parent_id`
|
||||
- create alternative URL formats
|
||||
- generate uppercase URLs
|
||||
|
||||
---
|
||||
|
||||
## 🧱 Data Model (Authoritative)
|
||||
|
||||
### content_types
|
||||
- `id`
|
||||
- `slug` → FIRST URL segment
|
||||
|
||||
Examples:
|
||||
```
|
||||
|
||||
photography
|
||||
wallpapers
|
||||
skins
|
||||
other
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### categories
|
||||
- `id`
|
||||
- `content_type_id`
|
||||
- `parent_id`
|
||||
- `slug`
|
||||
|
||||
Rules:
|
||||
- `parent_id = NULL` → root category
|
||||
- `parent_id != NULL` → child category
|
||||
- `parent_id` MUST reference `categories.id`
|
||||
- `content_type_id` MUST reference `content_types.id`
|
||||
|
||||
---
|
||||
|
||||
## 🧭 URL Structure (MANDATORY)
|
||||
|
||||
### Category URLs
|
||||
|
||||
```
|
||||
|
||||
/{content_type.slug}/{category-path}
|
||||
|
||||
```
|
||||
|
||||
Where:
|
||||
- `category-path` is built from category slugs in hierarchy order
|
||||
|
||||
Examples:
|
||||
```
|
||||
|
||||
/photography
|
||||
/photography/abstract
|
||||
/photography/abstract/dark
|
||||
/skins/media-players
|
||||
/other/art
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Artwork URLs
|
||||
|
||||
```
|
||||
|
||||
/{content_type.slug}/{category-path}/{artwork.slug}
|
||||
|
||||
```
|
||||
|
||||
Examples:
|
||||
```
|
||||
|
||||
/photography/abstract/dark/night-city
|
||||
/skins/media-players/zoom-player-dark
|
||||
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Artwork MUST belong to the last category in the path
|
||||
- Artwork slug is ALWAYS the final segment
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Category Path Construction (STRICT RULE)
|
||||
|
||||
Category paths MUST be constructed by walking parents.
|
||||
|
||||
Algorithm (conceptual):
|
||||
|
||||
1. Start with current category
|
||||
2. Collect its `slug`
|
||||
3. Move to `parent`
|
||||
4. Repeat until `parent_id = NULL`
|
||||
5. Reverse collected slugs
|
||||
6. Join with `/`
|
||||
|
||||
Example:
|
||||
```
|
||||
|
||||
Photography
|
||||
└── Abstract
|
||||
└── Dark
|
||||
|
||||
```
|
||||
|
||||
Produces:
|
||||
```
|
||||
|
||||
abstract/dark
|
||||
|
||||
```
|
||||
|
||||
Final URL:
|
||||
```
|
||||
|
||||
/photography/abstract/dark
|
||||
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Laravel Helper Contract
|
||||
|
||||
Category model MUST expose:
|
||||
```php
|
||||
$category->full_slug_path
|
||||
````
|
||||
|
||||
Which returns:
|
||||
|
||||
```
|
||||
abstract/dark
|
||||
```
|
||||
|
||||
Final URL generation:
|
||||
|
||||
```php
|
||||
'/' . $category->contentType->slug . '/' . $category->full_slug_path
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧭 Breadcrumb Rules
|
||||
|
||||
Breadcrumbs MUST reflect hierarchy exactly:
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
Home → Photography → Abstract → Dark
|
||||
```
|
||||
|
||||
Each breadcrumb link MUST use the same slug-based URL logic.
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Canonical URL RULE (SEO)
|
||||
|
||||
Every category and artwork page MUST include:
|
||||
|
||||
```html
|
||||
<link rel="canonical" href="https://skinbase.org/{full-slug-url}">
|
||||
```
|
||||
|
||||
Canonical URL MUST be:
|
||||
|
||||
* lowercase
|
||||
* slug-based
|
||||
* without IDs
|
||||
* without query parameters
|
||||
|
||||
---
|
||||
|
||||
## 🧨 Legacy URL Handling
|
||||
|
||||
Legacy URLs MUST be handled ONLY via **301 redirects**.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
/Photography/3
|
||||
/Photography/Business/564
|
||||
```
|
||||
|
||||
Redirect to:
|
||||
|
||||
```
|
||||
/photography/business
|
||||
```
|
||||
|
||||
Copilot MUST NOT generate new legacy URLs.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Validation Rules
|
||||
|
||||
Copilot MUST ensure:
|
||||
|
||||
* all URLs are lowercase
|
||||
* slugs are used exclusively
|
||||
* depth is unlimited
|
||||
* parent relationships are respected
|
||||
* only ONE URL exists per resource
|
||||
|
||||
---
|
||||
|
||||
## 🏁 FINAL STATEMENT
|
||||
|
||||
This document is the **single source of truth** for SkinBase category link building.
|
||||
|
||||
If any instruction conflicts with older code, documentation, or assumptions,
|
||||
THIS DOCUMENT WINS.
|
||||
|
||||
END OF SPEC
|
||||
309
.copilot/thumbnails.md
Normal file
309
.copilot/thumbnails.md
Normal file
@@ -0,0 +1,309 @@
|
||||
# Skinbase Thumbnails Generation Rules
|
||||
|
||||
## Project
|
||||
Skinbase.org – Artwork / Wallpapers / Skins CDN
|
||||
|
||||
CDN Base URL (Public):
|
||||
https://files.skinbase.org
|
||||
|
||||
All generated thumbnails must be publicly accessible under this domain.
|
||||
|
||||
---
|
||||
|
||||
## 1. Goals
|
||||
|
||||
- Generate fast-loading, high-quality thumbnails
|
||||
- Optimize for CDN delivery (Cloudflare + Apache)
|
||||
- Preserve visual quality
|
||||
- Keep consistent sizes
|
||||
- Use immutable filenames (hash-based)
|
||||
|
||||
---
|
||||
|
||||
## 2. Supported Input Formats
|
||||
|
||||
Source images may be:
|
||||
|
||||
- JPG / JPEG
|
||||
- PNG
|
||||
- WEBP
|
||||
- TIFF
|
||||
- PSD (flattened first)
|
||||
|
||||
Before thumbnail generation:
|
||||
- Strip EXIF metadata
|
||||
- Convert to RGB
|
||||
- Normalize orientation
|
||||
|
||||
---
|
||||
|
||||
## 3. Output Formats
|
||||
|
||||
Primary output:
|
||||
|
||||
- WEBP (preferred)
|
||||
- AVIF (optional, future use)
|
||||
- JPG (fallback only if WebP fails)
|
||||
|
||||
Default quality:
|
||||
|
||||
| Format | Quality |
|
||||
|--------|---------|
|
||||
| WebP | 82 |
|
||||
| AVIF | 45 |
|
||||
| JPG | 85 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Thumbnail Sizes
|
||||
|
||||
Generate the following sizes for every image:
|
||||
|
||||
| Type | Width | Height | Crop |
|
||||
|------|--------|--------|------|
|
||||
| xs | 150px | auto | no |
|
||||
| sm | 300px | auto | no |
|
||||
| md | 600px | auto | no |
|
||||
| lg | 1200px | auto | no |
|
||||
| sq | 400px | 400px | yes |
|
||||
|
||||
Rules:
|
||||
- Keep aspect ratio for non-square
|
||||
- Never upscale
|
||||
- Use center crop for sq
|
||||
|
||||
---
|
||||
|
||||
## 5. File Naming Convention
|
||||
|
||||
All thumbnails must use hash-based paths.
|
||||
|
||||
Format:
|
||||
|
||||
/lg/ff/2e/ff2e9ba2277b6b8296a0011c618ebf20c8c334a2.webp
|
||||
|
||||
Public URL example:
|
||||
|
||||
https://files.skinbase.org/lg/ff/2e/ff2e9ba2277b6b8296a0011c618ebf20c8c334a2.webp
|
||||
|
||||
Rules:
|
||||
|
||||
- Hash = SHA1(original_file + size + timestamp)
|
||||
- First 2 bytes = dir1
|
||||
- Next 2 bytes = dir2
|
||||
- Next 2 bytes = dir3
|
||||
- Filename = full hash
|
||||
|
||||
---
|
||||
|
||||
## 6. Directory Structure
|
||||
|
||||
Base directory (server):
|
||||
|
||||
/opt/www/virtual/skinbase/files/
|
||||
|
||||
Public mapping:
|
||||
|
||||
/opt/www/virtual/skinbase/files/lg/...
|
||||
→ https://files.skinbase.org/lg/...
|
||||
|
||||
Structure:
|
||||
|
||||
/xs/
|
||||
/sm/
|
||||
/md/
|
||||
/lg/
|
||||
/sq/
|
||||
|
||||
Each contains hashed subfolders.
|
||||
|
||||
Do not store flat files.
|
||||
|
||||
---
|
||||
|
||||
## 7. Image Processing Rules
|
||||
|
||||
When generating thumbnails:
|
||||
|
||||
1. Load source image
|
||||
2. Auto-orient
|
||||
3. Strip metadata
|
||||
4. Resize
|
||||
5. Apply mild sharpening
|
||||
6. Encode WebP
|
||||
7. Save to CDN path
|
||||
|
||||
Sharpening:
|
||||
|
||||
- Radius: 0.5
|
||||
- Amount: 0.3
|
||||
- Threshold: 0
|
||||
|
||||
No heavy filters allowed.
|
||||
|
||||
---
|
||||
|
||||
## 8. Background Handling
|
||||
|
||||
For transparent images:
|
||||
|
||||
- Preserve alpha channel
|
||||
- Do NOT add background
|
||||
- Keep transparent WebP
|
||||
|
||||
For JPG fallback:
|
||||
|
||||
- Background: #000000
|
||||
|
||||
---
|
||||
|
||||
## 9. Performance Constraints
|
||||
|
||||
Target limits:
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Max size (lg) | 400 KB |
|
||||
| Max size (md) | 180 KB |
|
||||
| Max size (sm) | 80 KB |
|
||||
| Max size (xs) | 30 KB |
|
||||
| Max size (sq) | 150 KB |
|
||||
|
||||
If exceeded:
|
||||
- Lower quality by 5
|
||||
- Re-encode
|
||||
|
||||
---
|
||||
|
||||
## 10. Security Rules
|
||||
|
||||
- Never execute embedded scripts
|
||||
- Reject SVG with scripts
|
||||
- Reject malformed images
|
||||
- Validate MIME type
|
||||
- Validate dimensions
|
||||
|
||||
Max source size: 100 MB
|
||||
|
||||
---
|
||||
|
||||
## 11. Cache Compatibility
|
||||
|
||||
All outputs must be CDN-ready.
|
||||
|
||||
Headers expected:
|
||||
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
Never generate filenames that change.
|
||||
|
||||
---
|
||||
|
||||
## 12. Regeneration Rules
|
||||
|
||||
Thumbnails must be regenerated when:
|
||||
|
||||
- Source image changes
|
||||
- Processing rules change
|
||||
- Quality profile updated
|
||||
|
||||
Old thumbnails must remain (cache-safe).
|
||||
|
||||
---
|
||||
|
||||
## 13. Laravel Integration
|
||||
|
||||
When thumbnail is created:
|
||||
|
||||
1. Save metadata to DB
|
||||
2. Store hash
|
||||
3. Store size
|
||||
4. Store extension
|
||||
5. Store public URL
|
||||
|
||||
Public URL format:
|
||||
|
||||
https://files.skinbase.org/{size}/{dir1}/{dir2}/{hash}.webp
|
||||
|
||||
Where:
|
||||
|
||||
{size} ∈ { xs, sm, md, lg, sq }
|
||||
|
||||
Model fields:
|
||||
|
||||
- thumb_hash
|
||||
- thumb_ext
|
||||
- thumb_size
|
||||
- thumb_width
|
||||
- thumb_height
|
||||
- thumb_url
|
||||
|
||||
---
|
||||
|
||||
## 14. Logging
|
||||
|
||||
Every generation must log:
|
||||
|
||||
- Source path
|
||||
- Output path
|
||||
- Public URL
|
||||
- Size
|
||||
- Time
|
||||
- Result
|
||||
|
||||
Format: JSON
|
||||
|
||||
Example:
|
||||
|
||||
{
|
||||
"source": "upload/a.jpg",
|
||||
"target": "lg/ff/2e/...",
|
||||
"url": "https://files.skinbase.org/lg/ff/2e/...",
|
||||
"size": "lg",
|
||||
"time_ms": 120,
|
||||
"status": "ok"
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
## 15. Error Handling
|
||||
|
||||
If generation fails:
|
||||
|
||||
- Log error
|
||||
- Mark record as failed
|
||||
- Do not retry more than 3x
|
||||
- Alert admin
|
||||
|
||||
---
|
||||
|
||||
## 16. Forbidden Actions
|
||||
|
||||
AI agents must NOT:
|
||||
|
||||
- Overwrite existing thumbnails
|
||||
- Change naming rules
|
||||
- Change directory layout
|
||||
- Serve via PHP
|
||||
- Store in public uploads
|
||||
- Generate relative URLs
|
||||
|
||||
All URLs must be absolute and use https://files.skinbase.org
|
||||
|
||||
---
|
||||
|
||||
## 17. Future Extensions
|
||||
|
||||
Planned:
|
||||
|
||||
- AVIF support
|
||||
- DPR variants (2x, 3x)
|
||||
- Smart cropping
|
||||
- Face detection
|
||||
- AI upscaling (optional)
|
||||
|
||||
Do not implement without approval.
|
||||
|
||||
---
|
||||
|
||||
## End of Rules
|
||||
Reference in New Issue
Block a user