chore: merge dev into hml resolving conflicts
This commit is contained in:
commit
bf41617ac6
11 changed files with 595 additions and 361 deletions
|
|
@ -67,6 +67,25 @@ 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
|
||||
|
||||
# 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 }}" \
|
||||
|
|
@ -84,7 +103,31 @@ jobs:
|
|||
--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=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
51
.github/workflows/migrate.yml
vendored
Normal 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
|
||||
|
|
@ -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)
|
||||
|
||||
// Try multiple paths
|
||||
paths := []string{
|
||||
"migrations/" + migFile,
|
||||
"backend/migrations/" + migFile,
|
||||
"../migrations/" + migFile,
|
||||
"/home/yamamoto/lab/gohorsejobs/backend/migrations/" + migFile,
|
||||
}
|
||||
|
||||
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)
|
||||
var migrationsDir string
|
||||
for _, d := range possibleDirs {
|
||||
if fi, err := os.Stat(d); err == nil && fi.IsDir() {
|
||||
migrationsDir = d
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if content == nil {
|
||||
log.Fatalf("Could not find migration file %s. Last error: %v", migFile, readErr)
|
||||
if migrationsDir == "" {
|
||||
log.Fatal("Could not find migrations directory; looked in common locations")
|
||||
}
|
||||
|
||||
statements := strings.Split(string(content), ";")
|
||||
for _, stmt := range statements {
|
||||
trimmed := strings.TrimSpace(stmt)
|
||||
if trimmed == "" {
|
||||
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
|
||||
}
|
||||
log.Printf("Executing: %s", trimmed)
|
||||
_, err = db.Exec(trimmed)
|
||||
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 {
|
||||
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("Could not read migration %s: %v", fullPath, err)
|
||||
}
|
||||
|
||||
// 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.Fatalf("Failed applying migration %s: %v", migFile, err)
|
||||
}
|
||||
|
||||
log.Printf("Migration %s applied successfully", migFile)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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{} {
|
||||
|
|
|
|||
|
|
@ -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)';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
27
backend/scripts/validate_rsa_key.sh
Normal file
27
backend/scripts/validate_rsa_key.sh
Normal 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
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,77 +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,
|
||||
} 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);
|
||||
};
|
||||
|
|
@ -83,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'),
|
||||
}
|
||||
);
|
||||
|
|
@ -93,72 +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 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">
|
||||
<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>
|
||||
)}
|
||||
|
|
@ -166,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>
|
||||
|
|
@ -190,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>
|
||||
|
|
@ -221,3 +223,4 @@ export function JobCard({ job, isApplied, applicationStatus }: JobCardProps) {
|
|||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
28
private_key.pem
Normal file
28
private_key.pem
Normal 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
1
rsa_base64.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQ2M5c3VEb2J3SndDR0oKVkZ2Z2ExQnhLa21tR094b0Y4ek5pYnY2bDMzL1NDRkVCYjVmRmFlbnh2WW90RUdXVXcwZmVkNHpJY1gzczZoQQpxMnlMcjNuSXlncExwY09menB6UHhhczQ5UDE3TkEzQ2h2bzNrLzBlR2tCRDZQSE0xczYycVBQK2ZLRVp0d2xTCnExV2FGeGZjOTQ5aXFKQVF2VzZ3LzdXZ01aRGluZXEzSXpoVlZVQUZkdzNpY1pydTk3aENqUERVL3YzZUZUUzcKa3ZHckRZR0FIWlh6eWx1M0VyOWlmS1lIZEt4T3JXRm1HYVNzUHNZTWRLTnhXRmsrWjM4TlZVbndTSDNURWlWLwpTNGUzM3RUa2RNbU5wWSs2ZTlDaWdiMDlSbk9hbGo1bFBqRkdBOW5USE1KeHBzSHZTS3U4dk1CcitPWjRDTTNVClJIN01VWDAxQWdNQkFBRUNnZ0VBTUt4ZEZvLzRNZVBZNG05ODRCNFcvMGlZTnYvaWl6TGFLT0J0b0xzS2NMZUsKelQra3RYS1BIemxVeXZGK3B5RlEzL0pZQTI0VktBY1hoUnBEV2h1TGZjYWRJN0VlOVBiS2JLbUV1M0JKREVQcgpnbWQ5dnU5T25kK1JEeDMwb1VyNUplNUZYeVNCaG1wYVl6N0xHREhTRGd6Y2MwRUhENUhXZWQrSmtFZmVnRTd3Ck12dDlLSzQxbUdkYVF3aVBIUzQzdXpaaFFKRXF5YlAzaS82U1VuVjJDbnRPaHV0eExsUGsycnBIbm5zMHAvU3QKRHZsY3Y2MXZkdUlhZWo0SUZCcnBTd1RFNDVwdklma3ZOWngwcEphcE0xalpoZThGLzJUN0d0WERrb0ZRdmVvMQozWUIxYWFkcEN4N3UyOEl6UVR3QlpWd3FoQ3BpMmE1K3FWWVVUMEFVM3dLQmdRRFlZVXhRVUJpVW42YlhvQXN4CkpUb3pvWDBLNTBjWDJkOExWWTFPVXVocFJYYnp0UzJYWHR5ZmVvQVF0RVdvVDNVTzd2akVlZFdzd2ZvMmorTjMKWklYaWc3VnlqL0xONWxaeUN3V1luNFM0aW5FU2pLbHppNFB2OEQ0RitGa2dnMFdzVmd6YlRhNFA3ZmFIbkRObgplRUhkeUovWlE4K1hZeEJwU0FFOGVjV1Fsd0tCZ1FDNXRHYmZ6aDc3UkVzdjFoNmI4N3Z1bHJHSGMrT0JJVFRVCllGdTFZZlhwdmJYeDlnZVJmTkxEdFVoVWlzNnZnZmNRVjZzeFpWZjc4VWRscWlUQmViUkxwY3ZvQmxIVi9NUFoKVDNUc1pIMXZYd2lpdE9zQklGektrbjh4ZGp1TjZtTjVsTGpJNktrWWVWb1VMWWlVTmJpWitXaTdQWEJQbmM1SQpqQk81RWF5T0V3S0JnUURVMnBuc28yNGF2aGF0SktYOTJXWXdwaHBRb0lTQ0JQUHh2VjM4LzNmYkh0ZE9GQnRlClBaWUFWOHdsSW9FbmVjcG9QMUorVEcrU3UxcjlVM3hxMVhzVEFZZDd3L2tRN1JaNnB6Y0JGV0xFK29NU3dVWnMKQUlGd2hiOHR0a2xPdjNQSmZQaTJ2dXFNaHdVdUQ4MU5hckk0andRWUFTbnovU0tHdnF0Z3AxVmV6d0tCZ0RvTApET3grL0dnRTNJdERIYVlZOUhDS1lVcTVDaTdlTmlqN1JTN1lRNGlmWnpNTmR5Z2VIN0pVQXh1Smx6aDhJc0RVCjVnazJaOTJ6ZUdGcVlMcW9VNVloYUM1SmEySzY4bXdGemNIbFZ0OXNrTUpxVWRtMFI4eDVKWkJNS0NrZlRhQSsKdjlMc0JZNUV2OGIyeEcydXJOaFRnRXlsMDJqUEpoNit5WnRhenRoSkFvR0FIUklYL1cwSWx5YUxubzdXekF3TQpsU3NOZkpwVHZabWtyaTBVT0dYTTJZYUt1UVo2NTJ0NkVCRHRmTTdPMTZlVjNLTkJibHQxTGpJdHovUzhraUZpClE4dEdsdU8yN0huNS9hdWl4SmpsY1puem9VWHJFakFyYThsbWdBbzQxRG0waWNEcExVemhpeFowcVM4ZDZZZnAKUklUMUlvV1N1dTJmdk9PdnFlenE2Ymc9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K
|
||||
Loading…
Reference in a new issue