chore: merge dev into main resolving conflicts

This commit is contained in:
GoHorse Deploy 2026-02-07 14:47:46 +00:00
commit cdecee1bea
11 changed files with 595 additions and 373 deletions

View file

@ -67,24 +67,67 @@ jobs:
# Injeta variáveis (Lembre-se de mudar DATABASE_URL para sslmode=disable no Forgejo!)
kubectl delete secret backend-secrets -n gohorsejobsdev --ignore-not-found
kubectl create secret generic backend-secrets -n gohorsejobsdev \
--from-literal=MTU="${{ vars.MTU }}" \
--from-literal=DATABASE_URL="${{ vars.DATABASE_URL }}" \
--from-literal=AMQP_URL="${{ vars.AMQP_URL }}" \
--from-literal=JWT_SECRET="${{ vars.JWT_SECRET }}" \
--from-literal=JWT_EXPIRATION="${{ vars.JWT_EXPIRATION }}" \
--from-literal=PASSWORD_PEPPER="${{ vars.PASSWORD_PEPPER }}" \
--from-literal=COOKIE_SECRET="${{ vars.COOKIE_SECRET }}" \
--from-literal=COOKIE_DOMAIN="${{ vars.COOKIE_DOMAIN }}" \
--from-literal=BACKEND_PORT="${{ vars.BACKEND_PORT }}" \
--from-literal=BACKEND_HOST="${{ vars.BACKEND_HOST }}" \
--from-literal=ENV="${{ vars.ENV }}" \
--from-literal=CORS_ORIGINS="${{ vars.CORS_ORIGINS }}" \
--from-literal=S3_BUCKET="${{ vars.S3_BUCKET }}" \
--from-literal=AWS_REGION="${{ vars.AWS_REGION }}" \
--from-literal=AWS_ENDPOINT="${{ vars.AWS_ENDPOINT }}" \
--from-literal=AWS_ACCESS_KEY_ID="${{ vars.AWS_ACCESS_KEY_ID }}" \
--from-literal=AWS_SECRET_ACCESS_KEY="${{ vars.AWS_SECRET_ACCESS_KEY }}"
# Prepare RSA key file if available (prefer secrets over vars)
if [ -n "${{ secrets.RSA_PRIVATE_KEY_BASE64 }}" ]; then
echo "Decoding RSA_PRIVATE_KEY_BASE64 from secrets"
printf '%b' "${{ secrets.RSA_PRIVATE_KEY_BASE64 }}" > /tmp/rsa_key.pem || true
# if it's base64-encoded PEM, decode it
if base64 -d /tmp/rsa_key.pem >/dev/null 2>&1; then
base64 -d /tmp/rsa_key.pem > /tmp/rsa_key_decoded.pem && mv /tmp/rsa_key_decoded.pem /tmp/rsa_key.pem || true
fi
elif [ -n "${{ vars.RSA_PRIVATE_KEY_BASE64 }}" ]; then
echo "Decoding RSA_PRIVATE_KEY_BASE64 from vars"
printf '%b' "${{ vars.RSA_PRIVATE_KEY_BASE64 }}" > /tmp/rsa_key.pem || true
if base64 -d /tmp/rsa_key.pem >/dev/null 2>&1; then
base64 -d /tmp/rsa_key.pem > /tmp/rsa_key_decoded.pem && mv /tmp/rsa_key_decoded.pem /tmp/rsa_key.pem || true
fi
fi
# Create secret: if rsa file exists, create secret from file (robust); otherwise fallback to from-literal
if [ -f /tmp/rsa_key.pem ]; then
kubectl create secret generic backend-secrets -n gohorsejobsdev \
--from-literal=MTU="${{ vars.MTU }}" \
--from-literal=DATABASE_URL="${{ vars.DATABASE_URL }}" \
--from-literal=AMQP_URL="${{ vars.AMQP_URL }}" \
--from-literal=JWT_SECRET="${{ vars.JWT_SECRET }}" \
--from-literal=JWT_EXPIRATION="${{ vars.JWT_EXPIRATION }}" \
--from-literal=PASSWORD_PEPPER="${{ vars.PASSWORD_PEPPER }}" \
--from-literal=COOKIE_SECRET="${{ vars.COOKIE_SECRET }}" \
--from-literal=COOKIE_DOMAIN="${{ vars.COOKIE_DOMAIN }}" \
--from-literal=BACKEND_PORT="${{ vars.BACKEND_PORT }}" \
--from-literal=BACKEND_HOST="${{ vars.BACKEND_HOST }}" \
--from-literal=ENV="${{ vars.ENV }}" \
--from-literal=CORS_ORIGINS="${{ vars.CORS_ORIGINS }}" \
--from-literal=S3_BUCKET="${{ vars.S3_BUCKET }}" \
--from-literal=AWS_REGION="${{ vars.AWS_REGION }}" \
--from-literal=AWS_ENDPOINT="${{ vars.AWS_ENDPOINT }}" \
--from-literal=AWS_ACCESS_KEY_ID="${{ vars.AWS_ACCESS_KEY_ID }}" \
--from-literal=AWS_SECRET_ACCESS_KEY="${{ vars.AWS_SECRET_ACCESS_KEY }}" \
--from-file=private_key.pem=/tmp/rsa_key.pem \
--dry-run=client -o yaml | kubectl apply -f -
else
kubectl create secret generic backend-secrets -n gohorsejobsdev \
--from-literal=MTU="${{ vars.MTU }}" \
--from-literal=DATABASE_URL="${{ vars.DATABASE_URL }}" \
--from-literal=AMQP_URL="${{ vars.AMQP_URL }}" \
--from-literal=JWT_SECRET="${{ vars.JWT_SECRET }}" \
--from-literal=JWT_EXPIRATION="${{ vars.JWT_EXPIRATION }}" \
--from-literal=PASSWORD_PEPPER="${{ vars.PASSWORD_PEPPER }}" \
--from-literal=COOKIE_SECRET="${{ vars.COOKIE_SECRET }}" \
--from-literal=COOKIE_DOMAIN="${{ vars.COOKIE_DOMAIN }}" \
--from-literal=BACKEND_PORT="${{ vars.BACKEND_PORT }}" \
--from-literal=BACKEND_HOST="${{ vars.BACKEND_HOST }}" \
--from-literal=ENV="${{ vars.ENV }}" \
--from-literal=CORS_ORIGINS="${{ vars.CORS_ORIGINS }}" \
--from-literal=S3_BUCKET="${{ vars.S3_BUCKET }}" \
--from-literal=AWS_REGION="${{ vars.AWS_REGION }}" \
--from-literal=AWS_ENDPOINT="${{ vars.AWS_ENDPOINT }}" \
--from-literal=AWS_ACCESS_KEY_ID="${{ vars.AWS_ACCESS_KEY_ID }}" \
--from-literal=AWS_SECRET_ACCESS_KEY="${{ vars.AWS_SECRET_ACCESS_KEY }}" \
--from-literal=RSA_PRIVATE_KEY_BASE64="${{ vars.RSA_PRIVATE_KEY_BASE64 }}" \
--dry-run=client -o yaml | kubectl apply -f -
fi
- name: Deploy to K3s
run: |

51
.github/workflows/migrate.yml vendored Normal file
View file

@ -0,0 +1,51 @@
name: Validate RSA and Run Migrations
on:
push:
branches: [ dev ]
workflow_dispatch: {}
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.20'
- name: Validate RSA_PRIVATE_KEY_BASE64 secret
env:
RSA_B64: ${{ secrets.RSA_PRIVATE_KEY_BASE64 }}
run: |
if [ -z "$RSA_B64" ]; then
echo "RSA_PRIVATE_KEY_BASE64 secret is missing. Add it in repository secrets." >&2
exit 1
fi
echo "$RSA_B64" > rsa_base64.txt
if ! base64 -d rsa_base64.txt > /tmp/key.pem 2>/dev/null; then
# try convert literal \n
printf '%b' "$RSA_B64" > /tmp/key.pem || true
fi
if ! openssl pkey -in /tmp/key.pem -noout -text >/dev/null 2>&1; then
echo "RSA private key is invalid" >&2
exit 1
fi
- name: Validate DATABASE_URL secret
if: ${{ always() }}
run: |
if [ -z "${{ secrets.DATABASE_URL }}" ]; then
echo "DATABASE_URL secret is missing. Set up your DB connection string in secrets." >&2
exit 1
fi
- name: Run migrations
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: |
cd backend
go run ./cmd/manual_migrate

View file

@ -5,6 +5,7 @@ import (
"fmt"
"log"
"os"
"sort"
"strings"
"github.com/joho/godotenv"
@ -44,60 +45,69 @@ func main() {
}
defer db.Close()
// List of migrations to run (in order)
migrations := []string{
"024_create_external_services_credentials.sql",
"025_create_chat_tables.sql",
"026_create_system_settings.sql",
"027_create_email_system.sql",
"028_add_avatar_url_to_users.sql",
// Discover migrations directory from several probable locations
possibleDirs := []string{
"migrations",
"backend/migrations",
"../migrations",
"/home/yamamoto/lab/gohorsejobs/backend/migrations",
}
for _, migFile := range migrations {
log.Printf("Processing migration: %s", migFile)
var migrationsDir string
for _, d := range possibleDirs {
if fi, err := os.Stat(d); err == nil && fi.IsDir() {
migrationsDir = d
break
}
}
// Try multiple paths
paths := []string{
"migrations/" + migFile,
"backend/migrations/" + migFile,
"../migrations/" + migFile,
"/home/yamamoto/lab/gohorsejobs/backend/migrations/" + migFile,
if migrationsDir == "" {
log.Fatal("Could not find migrations directory; looked in common locations")
}
entries, err := os.ReadDir(migrationsDir)
if err != nil {
log.Fatalf("Failed reading migrations dir %s: %v", migrationsDir, err)
}
var files []string
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
if strings.HasSuffix(name, ".sql") {
files = append(files, name)
}
}
// Sort filenames to ensure chronological order
sort.Strings(files)
for _, migFile := range files {
fullPath := migrationsDir + "/" + migFile
log.Printf("Processing migration: %s (from %s)", migFile, fullPath)
content, err := os.ReadFile(fullPath)
if err != nil {
log.Fatalf("Could not read migration %s: %v", fullPath, err)
}
var content []byte
var readErr error
for _, p := range paths {
content, readErr = os.ReadFile(p)
if readErr == nil {
log.Printf("Found migration at: %s", p)
break
}
}
if content == nil {
log.Fatalf("Could not find migration file %s. Last error: %v", migFile, readErr)
}
statements := strings.Split(string(content), ";")
for _, stmt := range statements {
trimmed := strings.TrimSpace(stmt)
if trimmed == "" {
// Execute the whole file. lib/pq supports multi-statement Exec.
sqlText := string(content)
log.Printf("Executing migration file %s", fullPath)
_, err = db.Exec(sqlText)
if err != nil {
errStr := err.Error()
// Tolerable errors: object already exists, column doesn't exist in some contexts,
// or duplicate key when updates are guarded by intent. Log and continue.
if strings.Contains(errStr, "already exists") || strings.Contains(errStr, "column") && strings.Contains(errStr, "does not exist") || strings.Contains(errStr, "duplicate key value violates unique constraint") {
log.Printf("Warning while applying %s: %v", migFile, err)
continue
}
log.Printf("Executing: %s", trimmed)
_, err = db.Exec(trimmed)
if err != nil {
if strings.Contains(err.Error(), "already exists") {
log.Printf("Warning (ignored): %v", err)
} else {
log.Printf("FAILED executing: %s\nError: %v", trimmed, err)
// Verify if we should stop. For now, continue best effort or fail?
// Use fatal for critical schema errors not "already exists"
log.Fatal(err)
}
}
log.Fatalf("Failed applying migration %s: %v", migFile, err)
}
log.Printf("Migration %s applied successfully", migFile)
}

View file

@ -10,6 +10,7 @@ import (
"encoding/base64"
"encoding/json"
"encoding/pem"
"strings"
"fmt"
"os"
"sync"
@ -92,10 +93,10 @@ func (s *CredentialsService) GetDecryptedKey(ctx context.Context, serviceName st
}
func (s *CredentialsService) decryptPayload(encryptedPayload string) (string, error) {
// 1. Decode Private Key from Env
rawPrivateKey, err := base64.StdEncoding.DecodeString(os.Getenv("RSA_PRIVATE_KEY_BASE64"))
// 1. Load Private Key bytes from env with fallbacks (base64, raw PEM, \n literals)
rawPrivateKey, err := getRawPrivateKeyBytes()
if err != nil {
return "", fmt.Errorf("failed to decode env RSA private key: %w", err)
return "", fmt.Errorf("failed to obtain RSA private key: %w", err)
}
block, _ := pem.Decode(rawPrivateKey)
@ -214,11 +215,10 @@ func (s *CredentialsService) DeleteCredentials(ctx context.Context, serviceName
// EncryptPayload encrypts a payload using the derived public key
func (s *CredentialsService) EncryptPayload(payload string) (string, error) {
// 1. Decode Private Key from Env (to derive Public Key)
// In a real scenario, you might store Public Key separately, but we can derive it.
rawPrivateKey, err := base64.StdEncoding.DecodeString(os.Getenv("RSA_PRIVATE_KEY_BASE64"))
// 1. Load Private Key bytes from env with fallbacks (base64, raw PEM, \n literals)
rawPrivateKey, err := getRawPrivateKeyBytes()
if err != nil {
return "", fmt.Errorf("failed to decode env RSA private key: %w", err)
return "", fmt.Errorf("failed to obtain RSA private key: %w", err)
}
block, _ := pem.Decode(rawPrivateKey)
@ -257,8 +257,54 @@ func (s *CredentialsService) EncryptPayload(payload string) (string, error) {
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// getRawPrivateKeyBytes attempts to load the RSA private key from the environment
// trying several fallbacks:
// 1) Treat env as base64 and decode
// 2) Treat env as a PEM string with literal "\n" escapes and replace them
// 3) Treat env as raw PEM
// 4) Trim and try base64 again
func getRawPrivateKeyBytes() ([]byte, error) {
env := os.Getenv("RSA_PRIVATE_KEY_BASE64")
if env == "" {
return nil, fmt.Errorf("RSA_PRIVATE_KEY_BASE64 environment variable is empty")
}
// Try base64 decode first
if b, err := base64.StdEncoding.DecodeString(env); err == nil {
if block, _ := pem.Decode(b); block != nil {
return b, nil
}
// Return decoded bytes even if pem.Decode returned nil; parsing later will catch it
return b, nil
}
// Try replacing literal \n with real newlines
envNew := strings.ReplaceAll(env, "\\n", "\n")
if block, _ := pem.Decode([]byte(envNew)); block != nil {
return []byte(envNew), nil
}
// Try raw env as PEM
if block, _ := pem.Decode([]byte(env)); block != nil {
return []byte(env), nil
}
// Trim and try base64 again
trimmed := strings.TrimSpace(env)
if b, err := base64.StdEncoding.DecodeString(trimmed); err == nil {
return b, nil
}
return nil, fmt.Errorf("could not decode RSA private key from env (tried base64 and PEM variants)")
}
// BootstrapCredentials checks if credentials are in DB, if not, migrates from Env
func (s *CredentialsService) BootstrapCredentials(ctx context.Context) error {
// If RSA private key is not available, skip migrating env credentials to DB.
if _, err := getRawPrivateKeyBytes(); err != nil {
fmt.Printf("[CredentialsBootstrap] RSA_PRIVATE_KEY_BASE64 missing or invalid: %v. Skipping ENV->DB credentials migration.\n", err)
return nil
}
// List of services and their env mapping
services := map[string]func() interface{}{
"stripe": func() interface{} {

View file

@ -11,17 +11,29 @@ CREATE TABLE IF NOT EXISTS regions (
CREATE TABLE IF NOT EXISTS cities (
id SERIAL PRIMARY KEY,
region_id INT NOT NULL,
region_id INT,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (region_id) REFERENCES regions(id) ON DELETE CASCADE
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Indexes
CREATE INDEX idx_regions_country ON regions(country_code);
CREATE INDEX idx_cities_region ON cities(region_id);
-- Ensure column and constraints exist when table already existed without them
ALTER TABLE cities
ADD COLUMN IF NOT EXISTS region_id INT;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name
WHERE tc.table_name = 'cities' AND tc.constraint_type = 'FOREIGN KEY' AND kcu.column_name = 'region_id'
) THEN
ALTER TABLE cities ADD CONSTRAINT fk_cities_region FOREIGN KEY (region_id) REFERENCES regions(id) ON DELETE CASCADE;
END IF;
END$$;
-- Indexes (safe if table/column already existed)
CREATE INDEX IF NOT EXISTS idx_regions_country ON regions(country_code);
CREATE INDEX IF NOT EXISTS idx_cities_region ON cities(region_id);
-- Comments
COMMENT ON TABLE regions IS 'Global Regions (States, Provinces, Prefectures)';

View file

@ -3,13 +3,25 @@
-- Increase status column length to support 'force_change_password' (21 chars)
ALTER TABLE users ALTER COLUMN status TYPE VARCHAR(50);
-- Safely change the superadmin identifier ONLY if 'lol' is not already taken.
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM users WHERE identifier = 'lol') THEN
UPDATE users
SET identifier = 'lol',
updated_at = CURRENT_TIMESTAMP
WHERE identifier = 'superadmin';
ELSE
RAISE NOTICE 'Skipping identifier change: ''lol'' already exists';
END IF;
END$$;
-- Update non-identifier fields for the superadmin row (if present)
UPDATE users
SET
identifier = 'lol',
email = 'lol@gohorsejobs.com',
full_name = 'Dr. Horse Expert',
name = 'Dr. Horse Expert',
status = 'force_change_password',
updated_at = CURRENT_TIMESTAMP
WHERE identifier = 'superadmin' OR email = 'admin@gohorsejobs.com';
WHERE identifier = 'superadmin' OR identifier = 'lol' OR email = 'admin@gohorsejobs.com';

View file

@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
if [ -z "${RSA_PRIVATE_KEY_BASE64:-}" ]; then
echo "RSA_PRIVATE_KEY_BASE64 is not set"
exit 2
fi
# Try decode base64
if echo "$RSA_PRIVATE_KEY_BASE64" | base64 -d > /tmp/rsa_key.pem 2>/dev/null; then
:
else
# Try replacing literal \n
echo "Attempting to replace literal \n and write PEM"
printf '%b' "$RSA_PRIVATE_KEY_BASE64" > /tmp/rsa_key.pem
fi
# Validate with openssl
if openssl pkey -in /tmp/rsa_key.pem -noout -text >/dev/null 2>&1; then
echo "RSA private key is valid PEM"
exit 0
else
echo "RSA private key is invalid"
echo "Preview (first 20 lines):"
sed -n '1,20p' /tmp/rsa_key.pem
exit 1
fi

View file

@ -1,48 +1,48 @@
"use client"
use client
import { Button } from "@/components/ui/button"
import { mockJobs } from "@/lib/mock-data"
import Link from "next/link"
import { Button } from @/components/ui/button
import { mockJobs } from @/lib/mock-data
import Link from next/link
import { ArrowRight, CheckCircle2 } from 'lucide-react'
import Image from "next/image"
import { motion } from "framer-motion"
import { useTranslation } from "@/lib/i18n"
import { Navbar } from "@/components/navbar"
import { Footer } from "@/components/footer"
import { HomeSearch } from "@/components/home-search"
import { JobCard } from "@/components/job-card"
import Image from next/image
import { motion } from framer-motion
import { useTranslation } from @/lib/i18n
import { Navbar } from @/components/navbar
import { Footer } from @/components/footer
import { HomeSearch } from @/components/home-search
import { JobCard } from @/components/job-card
export default function Home() {
const { t } = useTranslation()
return (
<div className="min-h-screen bg-gray-50 flex flex-col font-sans">
<div className=min-h-screen bg-gray-50 flex flex-col font-sans>
<Navbar />
<main className="flex-grow">
<main className=flex-grow>
{/* Hero Section */}
<section className="relative h-[500px] flex items-center justify-center bg-[#1F2F40]">
<section className=relative min-h-[500px] flex items-center justify-center bg-[#1F2F40] py-16 md:py-0>
{/* Background Image with Overlay */}
<div className="absolute inset-0 z-0">
<div className=absolute inset-0 z-0>
<Image
src="/10.png"
alt="Background"
src=/10.png
alt=Background
fill
className="object-cover opacity-60 contrast-125"
className=object-cover opacity-60 contrast-125
priority
/>
<div className="absolute inset-0 bg-gradient-to-r from-[#1F2F40] via-[#1F2F40]/90 to-transparent" />
<div className=absolute inset-0 bg-gradient-to-r from-[#1F2F40] via-[#1F2F40]/90 to-transparent />
</div>
<div className="container mx-auto px-4 relative z-10 text-center sm:text-left">
<div className="max-w-3xl">
<div className=container mx-auto px-4 relative z-10 text-center sm:text-left>
<div className=max-w-3xl>
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6 leading-tight tracking-tight"
className=text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6 leading-tight tracking-tight
>
Encontre a Vaga de TI <br className="hidden sm:block" />
Encontre a Vaga de TI <br className=hidden sm:block />
dos Seus Sonhos.
</motion.h1>
@ -50,9 +50,9 @@ export default function Home() {
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
className="text-lg md:text-xl text-gray-300 mb-8 max-w-xl leading-relaxed"
className=text-lg md:text-xl text-gray-300 mb-8 max-w-xl leading-relaxed mx-auto sm:mx-0
>
Conectamos você com as melhores empresas e techs.
Conectamos você com as melhores empresas e techs.
</motion.p>
<motion.div
@ -60,8 +60,8 @@ export default function Home() {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<Link href="/jobs">
<Button className="h-12 px-8 bg-orange-500 hover:bg-orange-600 text-white font-bold text-lg rounded-md shadow-lg transition-transform hover:scale-105">
<Link href=/jobs>
<Button className=h-12 px-8 bg-orange-500 hover:bg-orange-600 text-white font-bold text-lg rounded-md shadow-lg transition-transform hover:scale-105 w-full sm:w-auto>
Buscar Vagas
</Button>
</Link>
@ -71,20 +71,20 @@ export default function Home() {
</section>
{/* Search Section */}
<section className="px-4 mb-16">
<div className="container mx-auto">
<section className=px-4 mb-16>
<div className=container mx-auto>
<HomeSearch />
</div>
</section>
{/* Latest Jobs Section */}
<section className="py-12 bg-gray-50">
<div className="container mx-auto px-4">
<h2 className="text-2xl md:text-3xl font-bold text-gray-900 mb-8">
Últimas Vagas Cadastradas
<section className=py-12 bg-gray-50>
<div className=container mx-auto px-4>
<h2 className=text-2xl md:text-3xl font-bold text-gray-900 mb-8 text-center sm:text-left>
Últimas Vagas Cadastradas
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className=grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6>
{mockJobs.slice(0, 4).map((job, index) => (
<JobCard key={job.id} job={job} />
))}
@ -93,20 +93,20 @@ export default function Home() {
</section>
{/* More Jobs Section */}
<section className="py-12 bg-white">
<div className="container mx-auto px-4">
<div className="flex items-center justify-between mb-8">
<h2 className="text-2xl md:text-3xl font-bold text-gray-900">
<section className=py-12 bg-white>
<div className=container mx-auto px-4>
<div className=flex flex-col sm:flex-row items-center justify-between mb-8 gap-4>
<h2 className=text-2xl md:text-3xl font-bold text-gray-900>
Mais Vagas
</h2>
<Link href="/jobs">
<Button className="bg-orange-500 hover:bg-orange-600 text-white font-bold">
<Link href=/jobs className=w-full sm:w-auto>
<Button className=bg-orange-500 hover:bg-orange-600 text-white font-bold w-full sm:w-auto>
Ver Todas Vagas
</Button>
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className=grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6>
{mockJobs.slice(0, 8).map((job, index) => (
<JobCard key={`more-${job.id}-${index}`} job={job} />
))}
@ -116,36 +116,36 @@ export default function Home() {
{/* Bottom CTA Section */}
<section className="py-16 bg-white">
<div className="container mx-auto px-4">
<div className="bg-[#1F2F40] rounded-[2rem] p-8 md:p-16 relative overflow-hidden text-center md:text-left flex flex-col md:flex-row items-center justify-between min-h-[400px]">
<section className=py-16 bg-white>
<div className=container mx-auto px-4>
<div className=bg-[#1F2F40] rounded-[2rem] p-8 md:p-16 relative overflow-hidden text-center md:text-left flex flex-col md:flex-row items-center justify-between min-h-[400px]>
{/* Content */}
<div className="relative z-10 max-w-xl">
<h2 className="text-3xl md:text-4xl font-bold text-white mb-4 leading-tight">
Milhares de oportunidades <br/> esperam você.
<div className=relative z-10 max-w-xl>
<h2 className=text-3xl md:text-4xl font-bold text-white mb-4 leading-tight>
Milhares de oportunidades <br className=hidden sm:block/> esperam você.
</h2>
<p className="text-base text-gray-300 mb-8">
Conecte cargos, talentos, tomada de ações de vagas.
<p className=text-base text-gray-300 mb-8>
Conecte cargos, talentos, tomada de ações de vagas.
</p>
<Link href="/register/user">
<Button className="h-12 px-8 bg-white text-gray-900 hover:bg-gray-100 font-bold text-lg rounded-md">
<Link href=/register/user className=w-full sm:w-auto>
<Button className=h-12 px-8 bg-white text-gray-900 hover:bg-gray-100 font-bold text-lg rounded-md w-full sm:w-auto>
Cadastre-se
</Button>
</Link>
</div>
{/* Image Background for CTA */}
<div className="absolute inset-0 z-0">
<div className="absolute right-0 top-0 h-full w-full md:w-2/3 lg:w-1/2">
<div className=absolute inset-0 z-0>
<div className=absolute right-0 top-0 h-full w-full md:w-2/3 lg:w-1/2>
<Image
src="/muie.jpeg"
alt="Professional"
src=/muie.jpeg
alt=Professional
fill
className="object-cover object-center md:object-right opacity-40 md:opacity-100" // Opacity adjusted for mobile readability
className=object-cover object-center md:object-right opacity-40 md:opacity-100 // Opacity adjusted for mobile readability
/>
{/* Gradient Overlay to blend with dark background */}
<div className="absolute inset-0 bg-gradient-to-t md:bg-gradient-to-r from-[#1F2F40] via-[#1F2F40]/30 to-transparent" />
<div className=absolute inset-0 bg-gradient-to-t md:bg-gradient-to-r from-[#1F2F40] via-[#1F2F40]/30 to-transparent />
</div>
</div>
</div>
@ -157,3 +157,4 @@ export default function Home() {
</div>
)
}

View file

@ -1,78 +1,83 @@
"use client"
use client;
import type { Job } from "@/lib/types";
import { motion } from framer-motion;
import {
Building2,
MapPin,
Clock,
Heart,
} from lucide-react;
import Link from next/link;
import { useState } from react;
import {
Card,
CardHeader,
CardContent,
CardFooter,
CardHeader,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
MapPin,
Briefcase,
DollarSign,
Clock,
Building2,
Heart,
Zap,
} from "lucide-react";
import Link from "next/link";
import { motion } from "framer-motion";
import { useState } from "react";
import { useNotify } from "@/contexts/notification-context";
import { useTranslation } from "@/lib/i18n";
} from @/components/ui/card;
import { Button } from @/components/ui/button;
import { Badge } from @/components/ui/badge;
import { Avatar, AvatarImage, AvatarFallback } from @/components/ui/avatar;
import { formatTimeAgo } from @/lib/utils;
import { useTranslation } from @/lib/i18n;
import { useAuth } from @/contexts/AuthContext;
import { useNotification } from @/contexts/notification-context;
interface JobCardProps {
job: Job;
job: {
id: string;
title: string;
company: string;
location: string;
type: string;
postedAt: string | Date;
description: string;
salary?: string;
requirements?: string[];
};
isApplied?: boolean;
applicationStatus?: string;
}
export function JobCard({ job, isApplied, applicationStatus }: JobCardProps) {
const { t } = useTranslation();
const { user } = useAuth();
const notify = useNotification();
const [isFavorited, setIsFavorited] = useState(false);
const notify = useNotify();
const formatTimeAgo = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
const diffInMs = now.getTime() - date.getTime();
const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24));
if (diffInDays === 0) return t('jobs.posted.today');
if (diffInDays === 1) return t('jobs.posted.yesterday');
if (diffInDays < 7) return t('jobs.posted.daysAgo', { count: diffInDays });
if (diffInDays < 30) return t('jobs.posted.weeksAgo', { count: Math.floor(diffInDays / 7) });
return t('jobs.posted.monthsAgo', { count: Math.floor(diffInDays / 30) });
};
const getTypeLabel = (type: string) => {
return t(`jobs.types.${type}`) !== `jobs.types.${type}` ? t(`jobs.types.${type}`) : type;
switch (type.toLowerCase()) {
case full-time:
return CLT;
case contract:
return PJ;
case freelance:
return Freelancer;
case remote:
return Remoto;
default:
return type;
}
};
const getTypeBadgeVariant = (type: string) => {
switch (type) {
case "full-time":
return "default";
case "part-time":
return "secondary";
case "contract":
return "outline";
case "remote":
return "default";
const getTypeBadgeVariant = (type: string): default | secondary | outline | destructive | null => {
switch (type.toLowerCase()) {
case full-time:
return secondary;
case contract:
return outline;
case remote:
return default;
default:
return "outline";
return outline;
}
};
const getCompanyInitials = (company: string) => {
return company
.split(" ")
.split( )
.map((word) => word[0])
.join("")
.join(")
.toUpperCase()
.slice(0, 2);
};
@ -84,7 +89,7 @@ export function JobCard({ job, isApplied, applicationStatus }: JobCardProps) {
t('jobs.favorites.added.title'),
t('jobs.favorites.added.desc', { title: job.title }),
{
actionUrl: "/dashboard/favorites",
actionUrl: /dashboard/favorites,
actionLabel: t('jobs.favorites.action'),
}
);
@ -94,83 +99,68 @@ export function JobCard({ job, isApplied, applicationStatus }: JobCardProps) {
return (
<motion.div
whileHover={{ y: -2 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
transition={{ type: spring, stiffness: 300, damping: 20 }}
>
<Card className={`relative hover:shadow-lg transition-all duration-300 border-l-4 h-full flex flex-col ${job.isFeatured
? "border-l-amber-500 border-amber-200 shadow-md bg-amber-50/10"
: "border-l-primary/20 hover:border-l-primary"
}`}>
<CardHeader className="pb-4">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
{job.isFeatured && (
<div className="absolute -top-3 -right-3 z-10">
<Badge className="bg-gradient-to-r from-amber-400 to-orange-500 text-white border-0 shadow-lg gap-1 px-3 py-1">
<Zap className="h-3 w-3 fill-white" />
{t('home.featured.title')}
</Badge>
</div>
)}
<Avatar className="h-12 w-12">
<Card className=relative hover:shadow-lg transition-all duration-300 border-l-4 border-l-primary/20 hover:border-l-primary h-full flex flex-col>
<CardHeader className=pb-4>
<div className=flex items-start justify-between>
<div className=flex items-center gap-3>
<Avatar className=h-12 w-12>
<AvatarImage
src={`https://avatar.vercel.sh/${job.company}`}
alt={job.company}
/>
<AvatarFallback className="bg-primary/10 text-primary font-semibold">
<AvatarFallback className=bg-primary/10 text-primary font-semibold>
{getCompanyInitials(job.company)}
</AvatarFallback>
</Avatar>
<div>
<h3 className="font-semibold text-lg text-balance leading-tight hover:text-primary transition-colors">
<h3 className=font-semibold text-lg text-balance leading-tight hover:text-primary transition-colors>
{job.title}
</h3>
<div className="flex items-center gap-2 mt-1">
<Building2 className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground font-medium">
<div className=flex items-center gap-2 mt-1>
<Building2 className=h-4 w-4 text-muted-foreground />
<span className=text-muted-foreground font-medium>
{job.company}
</span>
</div>
</div>
</div>
<Button
variant="ghost"
size="icon"
<button
onClick={handleFavorite}
className="shrink-0"
className=shrink-0 p-2 hover:bg-muted rounded-full transition-colors
>
<Heart
className={`h-4 w-4 transition-colors ${isFavorited
? "fill-red-500 text-red-500"
: "text-muted-foreground hover:text-red-500"
? fill-red-500 text-red-500
: text-muted-foreground hover:text-red-500
}`}
/>
</Button>
</button>
</div>
</CardHeader>
<CardContent className="space-y-4 flex-1">
<CardContent className=space-y-4 flex-1>
{/* Job Meta Information */}
{/* Job Meta Information */}
<div className="flex flex-col gap-2 text-sm">
<div className="flex items-center gap-2 text-muted-foreground">
<MapPin className="h-4 w-4 shrink-0" />
<span className="truncate">{job.location}</span>
<div className=flex flex-col gap-2 text-sm>
<div className=flex items-center gap-2 text-muted-foreground>
<MapPin className=h-4 w-4 shrink-0 />
<span className=truncate>{job.location}</span>
</div>
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2">
<Briefcase className="h-4 w-4 shrink-0 text-muted-foreground" />
<div className=flex items-center justify-between w-full flex-wrap gap-2>
<div className=flex items-center gap-2>
<Badge
variant={getTypeBadgeVariant(job.type)}
className="text-xs"
className=text-xs
>
{getTypeLabel(job.type)}
</Badge>
</div>
{job.salary && (
<span className="font-medium text-foreground">
<span className=font-medium text-foreground whitespace-nowrap>
{job.salary}
</span>
)}
@ -178,22 +168,22 @@ export function JobCard({ job, isApplied, applicationStatus }: JobCardProps) {
</div>
{/* Job Description Preview */}
<div className="text-sm text-muted-foreground">
<p className="line-clamp-2">{job.description}</p>
<div className=text-sm text-muted-foreground>
<p className=line-clamp-2>{job.description}</p>
</div>
{/* Skills/Requirements Preview */}
{job.requirements && job.requirements.length > 0 && (
<div className="flex flex-wrap gap-2">
<div className=flex flex-wrap gap-2>
{job.requirements.slice(0, 3).map((requirement, index) => (
<Badge key={index} variant="outline" className="text-xs">
<Badge key={index} variant=outline className=text-xs>
{requirement}
</Badge>
))}
{job.requirements.length > 3 && (
<Badge
variant="outline"
className="text-xs text-muted-foreground"
variant=outline
className=text-xs text-muted-foreground
>
{t('jobs.requirements.more', { count: job.requirements.length - 3 })}
</Badge>
@ -202,29 +192,29 @@ export function JobCard({ job, isApplied, applicationStatus }: JobCardProps) {
)}
{/* Time Posted */}
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
<div className=flex items-center gap-2 text-xs text-muted-foreground>
<Clock className=h-3 w-3 />
<span>{formatTimeAgo(job.postedAt)}</span>
</div>
</CardContent>
<CardFooter className="pt-4 border-t">
<div className="flex gap-2 w-full">
<Link href={`/jobs/${job.id}`} className="flex-1">
<Button variant="outline" className="w-full cursor-pointer">
<CardFooter className=pt-4 border-t>
<div className=flex flex-col sm:flex-row gap-2 w-full>
<Link href={`/jobs/${job.id}`} className=w-full sm:flex-1>
<Button variant=outline className=w-full cursor-pointer>
{t('jobs.card.viewDetails')}
</Button>
</Link>
{isApplied ? (
<Button className="flex-1 w-full cursor-default bg-emerald-600 hover:bg-emerald-700 text-white" variant="secondary">
<Button className=w-full sm:flex-1 cursor-default bg-emerald-600 hover:bg-emerald-700 text-white variant=secondary>
{applicationStatus === 'pending' ? t('jobs.card.applied') :
applicationStatus === 'reviewing' ? t('jobs.card.reviewing') :
applicationStatus === 'interview' ? t('jobs.card.interview') :
t('jobs.card.applied')}
</Button>
) : (
<Link href={`/jobs/${job.id}/apply`} className="flex-1">
<Button className="w-full cursor-pointer">{t('jobs.card.apply')}</Button>
<Link href={`/jobs/${job.id}/apply`} className=w-full sm:flex-1>
<Button className=w-full cursor-pointer>{t('jobs.card.apply')}</Button>
</Link>
)}
</div>
@ -233,3 +223,4 @@ export function JobCard({ job, isApplied, applicationStatus }: JobCardProps) {
</motion.div>
);
}

28
private_key.pem Normal file
View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCc9suDobwJwCGJ
VFvga1BxKkmmGOxoF8zNibv6l33/SCFEBb5fFaenxvYotEGWUw0fed4zIcX3s6hA
q2yLr3nIygpLpcOfzpzPxas49P17NA3Chvo3k/0eGkBD6PHM1s62qPP+fKEZtwlS
q1WaFxfc949iqJAQvW6w/7WgMZDineq3IzhVVUAFdw3icZru97hCjPDU/v3eFTS7
kvGrDYGAHZXzylu3Er9ifKYHdKxOrWFmGaSsPsYMdKNxWFk+Z38NVUnwSH3TEiV/
S4e33tTkdMmNpY+6e9Cigb09RnOalj5lPjFGA9nTHMJxpsHvSKu8vMBr+OZ4CM3U
RH7MUX01AgMBAAECggEAMKxdFo/4MePY4m984B4W/0iYNv/iizLaKOBtoLsKcLeK
zT+ktXKPHzlUyvF+pyFQ3/JYA24VKAcXhRpDWhuLfcadI7Ee9PbKbKmEu3BJDEPr
gmd9vu9Ond+RDx30oUr5Je5FXySBhmpaYz7LGDHSDgzcc0EHD5HWed+JkEfegE7w
Mvt9KK41mGdaQwiPHS43uzZhQJEqybP3i/6SUnV2CntOhutxLlPk2rpHnns0p/St
Dvlcv61vduIaej4IFBrpSwTE45pvIfkvNZx0pJapM1jZhe8F/2T7GtXDkoFQveo1
3YB1aadpCx7u28IzQTwBZVwqhCpi2a5+qVYUT0AU3wKBgQDYYUxQUBiUn6bXoAsx
JTozoX0K50cX2d8LVY1OUuhpRXbztS2XXtyfeoAQtEWoT3UO7vjEedWswfo2j+N3
ZIXig7Vyj/LN5lZyCwWYn4S4inESjKlzi4Pv8D4F+Fkgg0WsVgzbTa4P7faHnDNn
eEHdyJ/ZQ8+XYxBpSAE8ecWQlwKBgQC5tGbfzh77REsv1h6b87vulrGHc+OBITTU
YFu1YfXpvbXx9geRfNLDtUhUis6vgfcQV6sxZVf78UdlqiTBebRLpcvoBlHV/MPZ
T3TsZH1vXwiitOsBIFzKkn8xdjuN6mN5lLjI6KkYeVoULYiUNbiZ+Wi7PXBPnc5I
jBO5EayOEwKBgQDU2pnso24avhatJKX92WYwphpQoISCBPPxvV38/3fbHtdOFBte
PZYAV8wlIoEnecpoP1J+TG+Su1r9U3xq1XsTAYd7w/kQ7RZ6pzcBFWLE+oMSwUZs
AIFwhb8ttklOv3PJfPi2vuqMhwUuD81NarI4jwQYASnz/SKGvqtgp1VezwKBgDoL
DOx+/GgE3ItDHaYY9HCKYUq5Ci7eNij7RS7YQ4ifZzMNdygeH7JUAxuJlzh8IsDU
5gk2Z92zeGFqYLqoU5YhaC5Ja2K68mwFzcHlVt9skMJqUdm0R8x5JZBMKCkfTaA+
v9LsBY5Ev8b2xG2urNhTgEyl02jPJh6+yZtazthJAoGAHRIX/W0IlyaLno7WzAwM
lSsNfJpTvZmkri0UOGXM2YaKuQZ652t6EBDtfM7O16eV3KNBblt1LjItz/S8kiFi
Q8tGluO27Hn5/auixJjlcZnzoUXrEjAra8lmgAo41Dm0icDpLUzhixZ0qS8d6Yfp
RIT1IoWSuu2fvOOvqezq6bg=
-----END PRIVATE KEY-----

1
rsa_base64.txt Normal file
View file

@ -0,0 +1 @@
LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQ2M5c3VEb2J3SndDR0oKVkZ2Z2ExQnhLa21tR094b0Y4ek5pYnY2bDMzL1NDRkVCYjVmRmFlbnh2WW90RUdXVXcwZmVkNHpJY1gzczZoQQpxMnlMcjNuSXlncExwY09menB6UHhhczQ5UDE3TkEzQ2h2bzNrLzBlR2tCRDZQSE0xczYycVBQK2ZLRVp0d2xTCnExV2FGeGZjOTQ5aXFKQVF2VzZ3LzdXZ01aRGluZXEzSXpoVlZVQUZkdzNpY1pydTk3aENqUERVL3YzZUZUUzcKa3ZHckRZR0FIWlh6eWx1M0VyOWlmS1lIZEt4T3JXRm1HYVNzUHNZTWRLTnhXRmsrWjM4TlZVbndTSDNURWlWLwpTNGUzM3RUa2RNbU5wWSs2ZTlDaWdiMDlSbk9hbGo1bFBqRkdBOW5USE1KeHBzSHZTS3U4dk1CcitPWjRDTTNVClJIN01VWDAxQWdNQkFBRUNnZ0VBTUt4ZEZvLzRNZVBZNG05ODRCNFcvMGlZTnYvaWl6TGFLT0J0b0xzS2NMZUsKelQra3RYS1BIemxVeXZGK3B5RlEzL0pZQTI0VktBY1hoUnBEV2h1TGZjYWRJN0VlOVBiS2JLbUV1M0JKREVQcgpnbWQ5dnU5T25kK1JEeDMwb1VyNUplNUZYeVNCaG1wYVl6N0xHREhTRGd6Y2MwRUhENUhXZWQrSmtFZmVnRTd3Ck12dDlLSzQxbUdkYVF3aVBIUzQzdXpaaFFKRXF5YlAzaS82U1VuVjJDbnRPaHV0eExsUGsycnBIbm5zMHAvU3QKRHZsY3Y2MXZkdUlhZWo0SUZCcnBTd1RFNDVwdklma3ZOWngwcEphcE0xalpoZThGLzJUN0d0WERrb0ZRdmVvMQozWUIxYWFkcEN4N3UyOEl6UVR3QlpWd3FoQ3BpMmE1K3FWWVVUMEFVM3dLQmdRRFlZVXhRVUJpVW42YlhvQXN4CkpUb3pvWDBLNTBjWDJkOExWWTFPVXVocFJYYnp0UzJYWHR5ZmVvQVF0RVdvVDNVTzd2akVlZFdzd2ZvMmorTjMKWklYaWc3VnlqL0xONWxaeUN3V1luNFM0aW5FU2pLbHppNFB2OEQ0RitGa2dnMFdzVmd6YlRhNFA3ZmFIbkRObgplRUhkeUovWlE4K1hZeEJwU0FFOGVjV1Fsd0tCZ1FDNXRHYmZ6aDc3UkVzdjFoNmI4N3Z1bHJHSGMrT0JJVFRVCllGdTFZZlhwdmJYeDlnZVJmTkxEdFVoVWlzNnZnZmNRVjZzeFpWZjc4VWRscWlUQmViUkxwY3ZvQmxIVi9NUFoKVDNUc1pIMXZYd2lpdE9zQklGektrbjh4ZGp1TjZtTjVsTGpJNktrWWVWb1VMWWlVTmJpWitXaTdQWEJQbmM1SQpqQk81RWF5T0V3S0JnUURVMnBuc28yNGF2aGF0SktYOTJXWXdwaHBRb0lTQ0JQUHh2VjM4LzNmYkh0ZE9GQnRlClBaWUFWOHdsSW9FbmVjcG9QMUorVEcrU3UxcjlVM3hxMVhzVEFZZDd3L2tRN1JaNnB6Y0JGV0xFK29NU3dVWnMKQUlGd2hiOHR0a2xPdjNQSmZQaTJ2dXFNaHdVdUQ4MU5hckk0andRWUFTbnovU0tHdnF0Z3AxVmV6d0tCZ0RvTApET3grL0dnRTNJdERIYVlZOUhDS1lVcTVDaTdlTmlqN1JTN1lRNGlmWnpNTmR5Z2VIN0pVQXh1Smx6aDhJc0RVCjVnazJaOTJ6ZUdGcVlMcW9VNVloYUM1SmEySzY4bXdGemNIbFZ0OXNrTUpxVWRtMFI4eDVKWkJNS0NrZlRhQSsKdjlMc0JZNUV2OGIyeEcydXJOaFRnRXlsMDJqUEpoNit5WnRhenRoSkFvR0FIUklYL1cwSWx5YUxubzdXekF3TQpsU3NOZkpwVHZabWtyaTBVT0dYTTJZYUt1UVo2NTJ0NkVCRHRmTTdPMTZlVjNLTkJibHQxTGpJdHovUzhraUZpClE4dEdsdU8yN0huNS9hdWl4SmpsY1puem9VWHJFakFyYThsbWdBbzQxRG0waWNEcExVemhpeFowcVM4ZDZZZnAKUklUMUlvV1N1dTJmdk9PdnFlenE2Ymc9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K