This commit is contained in:
2026-02-11 17:46:22 +03:00
commit eacfacb13b
266 changed files with 51337 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
package auditlog
import (
"context"
)
func ForcePasswordChangeStarted(
ctx context.Context,
targetUserID int64,
reason string, // admin_reset | login_enforced
) {
Write(ActivityLog{
UserID: IntUserIDToUUID(int(targetUserID)),
ActionType: "force_password_change_started",
ActionCategory: "auth",
Description: "kullanıcı için zorunlu parola değişimi başlatıldı",
IsSuccess: true,
ErrorMessage: reason,
})
}
func ForcePasswordChangeCompleted(
ctx context.Context,
userID int64,
source string, // reset_link | self_change | admin_reset
) {
Write(ActivityLog{
UserID: IntUserIDToUUID(int(userID)),
ActionType: "force_password_change_completed",
ActionCategory: "auth",
Description: "kullanıcı parolasını başarıyla güncelledi",
IsSuccess: true,
ErrorMessage: source,
})
}

View File

@@ -0,0 +1,73 @@
package auditlog
import (
"crypto/md5"
"fmt"
"time"
)
//
// =======================================================
// 🕵️ AUDIT LOG — HELPER FUNCTIONS
// =======================================================
// Bu dosya:
// - UUID bekleyen kolonlar için int → uuid dönüşümü
// - NULL-safe insert yardımcıları
// içerir
//
// -------------------------------------------------------
// 🔹 IntUserIDToUUID
// -------------------------------------------------------
// int user_id → deterministic UUID
// PostgreSQL uuid kolonu ile %100 uyumlu
//
// Aynı userID → her zaman aynı UUID
func IntUserIDToUUID(userID int) string {
if userID <= 0 {
return ""
}
sum := md5.Sum([]byte(fmt.Sprintf("bssapp-user-%d", userID)))
return fmt.Sprintf(
"%x-%x-%x-%x-%x",
sum[0:4],
sum[4:6],
sum[6:8],
sum[8:10],
sum[10:16],
)
}
// -------------------------------------------------------
// 🔹 nullIfZeroTime
// -------------------------------------------------------
// Zero time → NULL (SQL uyumlu)
func nullIfZeroTime(t time.Time) interface{} {
if t.IsZero() {
return nil
}
return t
}
// -------------------------------------------------------
// 🔹 nullIfZeroInt
// -------------------------------------------------------
// 0 → NULL (SQL uyumlu)
func nullIfZeroInt(v int) interface{} {
if v == 0 {
return nil
}
return v
}
func Int64UserIDToUUID(id int64) string {
return IntUserIDToUUID(int(id))
}
func nullIfEmpty(s string) any {
if s == "" {
return nil
}
return s
}

View File

@@ -0,0 +1,30 @@
package auditlog
import (
"database/sql"
"log"
"sync"
)
var (
logQueue chan ActivityLog
dbConn *sql.DB
once sync.Once
)
// Init → main.go içinden çağrılacak (tek sefer)
func Init(db *sql.DB, bufferSize int) {
log.Println("🟢 auditlog Init called, buffer:", bufferSize)
dbConn = db
logQueue = make(chan ActivityLog, bufferSize)
go logWorker()
}
// Optional: app kapanırken flush/stop istersen
func Close() {
if logQueue != nil {
close(logQueue)
}
}

View File

@@ -0,0 +1,37 @@
package auditlog
import "time"
type ActivityLog struct {
// identity
UserID string // UUID (auth)
DfUsrID int64 // DF user id (mk_dfusr.id)
Username string
RoleCode string
// action
ActionType string
ActionCategory string
ActionTarget string
Description string
// tech
IpAddress string
UserAgent string
SessionID string
// timing
RequestStartedAt time.Time
RequestFinishedAt time.Time
DurationMs int
HttpStatus int
// result
IsSuccess bool
ErrorMessage string
TargetDfUsrID int64
TargetUsername string
ChangeBefore any // map[string]any
ChangeAfter any
}

View File

@@ -0,0 +1,141 @@
package auditlog
import (
"encoding/json"
"log"
)
func toJSONB(v any) any {
if v == nil {
return nil
}
b, err := json.Marshal(v)
if err != nil {
// JSON marshal hata olursa logu bozmayalım
log.Println("⚠️ auditlog json marshal error:", err)
return nil
}
return b // pq jsonb için []byte kabul eder
}
func logWorker() {
log.Println("🟢 auditlog worker STARTED")
for entry := range logQueue {
// ---------- DFUSR_ID ----------
var dfusrID any
if entry.DfUsrID > 0 {
dfusrID = entry.DfUsrID
} else {
dfusrID = nil
}
// ---------- USERNAME ----------
var username any
if entry.Username != "" {
username = entry.Username
} else {
username = nil
}
// ---------- ROLE CODE (SNAPSHOT) ----------
roleCode := entry.RoleCode
if roleCode == "" {
roleCode = "public"
}
// ---------- TARGET ----------
var targetDfUsrID any
if entry.TargetDfUsrID > 0 {
targetDfUsrID = entry.TargetDfUsrID
} else {
targetDfUsrID = nil
}
targetUsername := nullIfEmpty(entry.TargetUsername)
log.Printf(
"🧾 auditlog INSERT | actor_dfusr=%v actor_user=%v role=%s %s %s target=%v",
dfusrID,
username,
roleCode,
entry.ActionCategory,
entry.ActionTarget,
targetDfUsrID,
)
_, err := dbConn.Exec(`
INSERT INTO mk_user_activity_log (
log_id,
dfusr_id,
username,
role_code,
action_type,
action_category,
action_target,
description,
ip_address,
user_agent,
session_id,
request_started_at,
request_finished_at,
duration_ms,
http_status,
is_success,
error_message,
-- ✅ NEW
target_dfusr_id,
target_username,
change_before,
change_after,
created_at
) VALUES (
gen_random_uuid(),
$1,$2,$3,
$4,$5,$6,$7,
$8,$9,$10,
$11,$12,$13,$14,
$15,$16,
$17,$18,$19,$20,
now()
)
`,
dfusrID,
username,
roleCode,
entry.ActionType,
entry.ActionCategory,
entry.ActionTarget,
entry.Description,
entry.IpAddress,
entry.UserAgent,
entry.SessionID,
nullIfZeroTime(entry.RequestStartedAt),
nullIfZeroTime(entry.RequestFinishedAt),
nullIfZeroInt(entry.DurationMs),
nullIfZeroInt(entry.HttpStatus),
entry.IsSuccess,
entry.ErrorMessage,
// ✅ NEW
targetDfUsrID,
targetUsername,
toJSONB(entry.ChangeBefore),
toJSONB(entry.ChangeAfter),
)
if err != nil {
log.Println("❌ auditlog insert error:", err)
}
}
}

View File

@@ -0,0 +1,25 @@
package auditlog
import "context"
func Write(log ActivityLog) {
if logQueue == nil {
return // sistem henüz init edilmediyse sessizce çık
}
select {
case logQueue <- log:
// kuyruğa alındı
default:
// kuyruk dolu → drop edilir, ana akış bozulmaz
}
}
func Enqueue(ctx context.Context, al ActivityLog) {
select {
case logQueue <- al:
// ok
default:
// queue dolu → drop
}
}

View File

@@ -0,0 +1,36 @@
package authz
import "context"
type scopeKey string
const (
CtxDeptCodesKey scopeKey = "authz.dept_codes"
CtxPiyasaCodesKey scopeKey = "authz.piyasa_codes"
)
func WithDeptCodes(ctx context.Context, codes []string) context.Context {
return context.WithValue(ctx, CtxDeptCodesKey, codes)
}
func WithPiyasaCodes(ctx context.Context, codes []string) context.Context {
return context.WithValue(ctx, CtxPiyasaCodesKey, codes)
}
func GetDeptCodesFromCtx(ctx context.Context) []string {
if v := ctx.Value(CtxDeptCodesKey); v != nil {
if codes, ok := v.([]string); ok {
return codes
}
}
return nil
}
func GetPiyasaCodesFromCtx(ctx context.Context) []string {
if v := ctx.Value(CtxPiyasaCodesKey); v != nil {
if codes, ok := v.([]string); ok {
return codes
}
}
return nil
}

View File

@@ -0,0 +1,32 @@
package authz
import (
"context"
"fmt"
"strings"
)
func BuildMSSQLPiyasaFilter(
ctx context.Context,
column string,
) string {
codes := GetPiyasaCodesFromCtx(ctx)
if len(codes) == 0 {
return "1=1"
}
var quoted []string
for _, c := range codes {
quoted = append(quoted, "'"+c+"'")
}
return fmt.Sprintf(
"%s IN (%s)",
column,
strings.Join(quoted, ","),
)
}

View File

@@ -0,0 +1,24 @@
package authz
import (
"fmt"
"strings"
)
func BuildINClause(column string, codes []string) string {
if len(codes) == 0 {
return "1=0"
}
var quoted []string
for _, c := range codes {
c = strings.TrimSpace(strings.ToUpper(c))
if c == "" {
continue
}
quoted = append(quoted, "'"+c+"'")
}
if len(quoted) == 0 {
return "1=0"
}
return fmt.Sprintf("%s IN (%s)", column, strings.Join(quoted, ","))
}

View File

@@ -0,0 +1,74 @@
package authz
import (
"database/sql"
"fmt"
"sync"
)
// =====================================================
// 🧠 PIYASA CACHE (USER → CODES)
// =====================================================
var (
piyasaCache = make(map[int][]string)
piyasaMu sync.RWMutex
)
// =====================================================
// 📌 GET USER PIYASA CODES (CACHED)
// =====================================================
func GetUserPiyasaCodes(pg *sql.DB, userID int) ([]string, error) {
// -----------------------------
// CACHE READ
// -----------------------------
piyasaMu.RLock()
if it, ok := piyasaCache[userID]; ok {
piyasaMu.RUnlock()
return it, nil
}
piyasaMu.RUnlock()
// -----------------------------
// DB QUERY
// -----------------------------
rows, err := pg.Query(`
SELECT piyasa_code
FROM dfusr_piyasa
WHERE dfusr_id = $1
AND is_allowed = true
`, userID)
if err != nil {
return nil, fmt.Errorf("pg piyasa query error: %w", err)
}
defer rows.Close()
var out []string
for rows.Next() {
var code string
if err := rows.Scan(&code); err == nil {
out = append(out, code)
}
}
// -----------------------------
// CACHE WRITE
// -----------------------------
piyasaMu.Lock()
piyasaCache[userID] = out
piyasaMu.Unlock()
return out, nil
}
// =====================================================
// 🧹 CLEAR USER PIYASA CACHE
// =====================================================
func ClearPiyasaCache(userID int) {
piyasaMu.Lock()
defer piyasaMu.Unlock()
delete(piyasaCache, userID)
}

View File

@@ -0,0 +1,50 @@
package mailer
import (
"os"
"strconv"
"strings"
)
type Config struct {
Host string
Port int
Username string
Password string
From string
StartTLS bool
}
func ConfigFromEnv() (Config, error) {
var cfg Config
cfg.Host = strings.TrimSpace(os.Getenv("SMTP_HOST"))
cfg.Username = strings.TrimSpace(os.Getenv("SMTP_USERNAME"))
cfg.Password = os.Getenv("SMTP_PASSWORD")
cfg.From = strings.TrimSpace(os.Getenv("SMTP_FROM"))
portStr := strings.TrimSpace(os.Getenv("SMTP_PORT"))
if portStr == "" {
cfg.Port = 587
} else {
p, err := strconv.Atoi(portStr)
if err != nil {
return Config{}, err
}
cfg.Port = p
}
startTLS := strings.TrimSpace(os.Getenv("SMTP_STARTTLS"))
if startTLS == "" {
cfg.StartTLS = true
} else {
cfg.StartTLS = strings.EqualFold(startTLS, "true") || startTLS == "1"
}
// minimal validation
if cfg.Host == "" || cfg.Username == "" || cfg.Password == "" || cfg.From == "" || cfg.Port <= 0 {
return Config{}, ErrInvalidConfig
}
return cfg, nil
}

View File

@@ -0,0 +1,270 @@
package mailer
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
"golang.org/x/oauth2/clientcredentials"
)
type GraphMailer struct {
httpClient *http.Client
from string // noreply@baggi.com.tr
replyTo string // opsiyonel
}
func NewGraphMailer() (*GraphMailer, error) {
tenantID := strings.TrimSpace(os.Getenv("AZURE_TENANT_ID"))
clientID := strings.TrimSpace(os.Getenv("AZURE_CLIENT_ID"))
clientSecret := strings.TrimSpace(os.Getenv("AZURE_CLIENT_SECRET"))
from := strings.TrimSpace(os.Getenv("MAIL_FROM"))
replyTo := strings.TrimSpace(os.Getenv("MAIL_REPLY_TO")) // opsiyonel
if tenantID == "" || clientID == "" || clientSecret == "" || from == "" {
return nil, fmt.Errorf("azure graph mailer env missing (AZURE_TENANT_ID/AZURE_CLIENT_ID/AZURE_CLIENT_SECRET/MAIL_FROM)")
}
conf := clientcredentials.Config{
ClientID: clientID,
ClientSecret: clientSecret,
TokenURL: fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantID),
Scopes: []string{"https://graph.microsoft.com/.default"},
}
httpClient := conf.Client(context.Background())
httpClient.Timeout = 25 * time.Second
log.Printf("✉️ Graph Mailer hazır (App-only token) | from=%s", from)
return &GraphMailer{
httpClient: httpClient,
from: from,
replyTo: replyTo,
}, nil
}
func (g *GraphMailer) Send(ctx context.Context, msg Message) error {
start := time.Now()
// ---------- validate ----------
cleanTo := make([]string, 0, len(msg.To))
for _, t := range msg.To {
t = strings.TrimSpace(t)
if t != "" {
cleanTo = append(cleanTo, t)
}
}
if len(cleanTo) == 0 {
return fmt.Errorf("recipient missing")
}
subject := strings.TrimSpace(msg.Subject)
if subject == "" {
return fmt.Errorf("subject missing")
}
// internal-safe adjustments
isInternal := allRecipientsInDomain(cleanTo, "baggi.com.tr")
if isInternal && !strings.HasPrefix(subject, "[BSSApp]") {
subject = "[BSSApp] " + subject
}
html := strings.TrimSpace(msg.BodyHTML)
text := strings.TrimSpace(msg.Body)
// Eğer sadece HTML geldiyse text üret (spam skorunu düşürür)
if text == "" && html != "" {
text = stripHTMLVerySimple(html)
}
// Eğer sadece text geldiyse basit HTML üret
if html == "" && text != "" {
html = "<pre style=\"font-family:Segoe UI, Arial, sans-serif; white-space:pre-wrap;\">" +
htmlEscape(text) + "</pre>"
}
if html == "" {
return fmt.Errorf("body missing (Body or BodyHTML)")
}
log.Printf("✉️ [MAIL] SEND START | from=%s | to=%v | internal=%v | subject=%s", g.from, cleanTo, isInternal, subject)
// ---------- build recipients ----------
type recipient struct {
EmailAddress struct {
Address string `json:"address"`
} `json:"emailAddress"`
}
toRecipients := make([]recipient, 0, len(cleanTo))
for _, m := range cleanTo {
r := recipient{}
r.EmailAddress.Address = m
toRecipients = append(toRecipients, r)
}
// ---------- headers to reduce auto-phish/spam signals ----------
headers := []map[string]string{
{
"name": "X-Mailer",
"value": "BSSApp Graph Mailer",
},
{
"name": "X-BSSApp-Internal",
"value": strconv.FormatBool(isInternal),
},
}
// replyTo (opsiyonel)
var replyToRecipients []recipient
if strings.TrimSpace(g.replyTo) != "" {
rt := recipient{}
rt.EmailAddress.Address = strings.TrimSpace(g.replyTo)
replyToRecipients = []recipient{rt}
}
// ---------- Graph payload ----------
message := map[string]any{
"subject": subject,
"body": map[string]string{
"contentType": "HTML",
"content": buildHTML(html, text, isInternal),
},
"toRecipients": toRecipients,
"internetMessageHeaders": headers,
"importance": "normal",
}
// replyTo SADECE doluysa ekle
if len(replyToRecipients) > 0 {
message["replyTo"] = replyToRecipients
}
payload := map[string]any{
"message": message,
"saveToSentItems": true,
}
b, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("json marshal: %w", err)
}
url := fmt.Sprintf(
"https://graph.microsoft.com/v1.0/users/%s/sendMail",
g.from,
)
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
url,
bytes.NewBuffer(b),
)
if err != nil {
return fmt.Errorf("new request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
res, err := g.httpClient.Do(req)
if err != nil {
return fmt.Errorf("graph request: %w", err)
}
defer res.Body.Close()
if res.StatusCode >= 300 {
bodyBytes, _ := io.ReadAll(res.Body)
log.Printf(
"❌ [MAIL] SEND FAILED | status=%s | body=%s",
res.Status,
string(bodyBytes),
)
return fmt.Errorf("graph send mail failed: %s", res.Status)
}
log.Printf(
"✅ [MAIL] SEND OK | to=%v | duration=%s",
cleanTo,
time.Since(start),
)
return nil
}
// ---------- helpers ----------
func allRecipientsInDomain(to []string, domain string) bool {
domain = strings.ToLower(strings.TrimSpace(domain))
for _, addr := range to {
addr = strings.ToLower(strings.TrimSpace(addr))
if !strings.HasSuffix(addr, "@"+domain) {
return false
}
}
return true
}
func buildHTML(htmlBody, textBody string, internal bool) string {
// Internal ise daha sade, daha az “marketing-like”
if internal {
return `
<div style="font-family:Segoe UI, Arial, sans-serif; font-size:14px; color:#1f1f1f;">
` + htmlBody + `
<hr style="border:none;border-top:1px solid #e5e5e5;margin:16px 0;" />
<div style="font-size:12px;color:#666;">
Bu e-posta BSSApp sistemi tarafından otomatik oluşturulmuştur.
</div>
</div>`
}
// External
return `
<div style="font-family:Segoe UI, Arial, sans-serif; font-size:14px; color:#1f1f1f;">
` + htmlBody + `
</div>`
}
// Çok basit “html -> text” (tam değil ama yeterli)
func stripHTMLVerySimple(s string) string {
s = strings.ReplaceAll(s, "<br>", "\n")
s = strings.ReplaceAll(s, "<br/>", "\n")
s = strings.ReplaceAll(s, "<br />", "\n")
s = strings.ReplaceAll(s, "</p>", "\n\n")
// kaba temizlik
for {
i := strings.Index(s, "<")
j := strings.Index(s, ">")
if i >= 0 && j > i {
s = s[:i] + s[j+1:]
continue
}
break
}
return strings.TrimSpace(s)
}
func htmlEscape(s string) string {
replacer := strings.NewReplacer(
"&", "&amp;",
"<", "&lt;",
">", "&gt;",
"\"", "&quot;",
"'", "&#39;",
)
return replacer.Replace(s)
}
func (g *GraphMailer) SendMail(to string, subject string, html string) error {
msg := Message{
To: []string{to},
Subject: subject,
BodyHTML: html,
}
// context burada internal olarak veriliyor
return g.Send(context.Background(), msg)
}

View File

@@ -0,0 +1,148 @@
package mailer
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/smtp"
"strings"
"time"
)
var ErrInvalidConfig = errors.New("invalid smtp config")
type Mailer struct {
cfg Config
}
type Message struct {
To []string
Subject string
Body string
BodyHTML string
}
func New(cfg Config) *Mailer {
return &Mailer{cfg: cfg}
}
func (m *Mailer) Send(ctx context.Context, msg Message) error {
if len(msg.To) == 0 {
return errors.New("recipient missing")
}
if strings.TrimSpace(msg.Subject) == "" {
return errors.New("subject missing")
}
// timeout kontrolü (ctx)
deadline, ok := ctx.Deadline()
if !ok {
// default timeout
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, 20*time.Second)
defer cancel()
deadline, _ = ctx.Deadline()
}
timeout := time.Until(deadline)
if timeout <= 0 {
return context.DeadlineExceeded
}
addr := fmt.Sprintf("%s:%d", m.cfg.Host, m.cfg.Port)
dialer := net.Dialer{Timeout: timeout}
conn, err := dialer.DialContext(ctx, "tcp", addr)
if err != nil {
return fmt.Errorf("smtp dial: %w", err)
}
defer conn.Close()
c, err := smtp.NewClient(conn, m.cfg.Host)
if err != nil {
return fmt.Errorf("smtp client: %w", err)
}
defer c.Close()
// STARTTLS
if m.cfg.StartTLS {
tlsCfg := &tls.Config{
ServerName: m.cfg.Host,
MinVersion: tls.VersionTLS12,
}
if err := c.StartTLS(tlsCfg); err != nil {
return fmt.Errorf("starttls: %w", err)
}
}
// AUTH
auth := smtp.PlainAuth("", m.cfg.Username, m.cfg.Password, m.cfg.Host)
if err := c.Auth(auth); err != nil {
return fmt.Errorf("smtp auth: %w", err)
}
// MAIL FROM
if err := c.Mail(m.cfg.From); err != nil {
return fmt.Errorf("mail from: %w", err)
}
// RCPT TO
for _, rcpt := range msg.To {
rcpt = strings.TrimSpace(rcpt)
if rcpt == "" {
continue
}
if err := c.Rcpt(rcpt); err != nil {
return fmt.Errorf("rcpt %s: %w", rcpt, err)
}
}
w, err := c.Data()
if err != nil {
return fmt.Errorf("data: %w", err)
}
// basit mime
contentType := "text/plain; charset=UTF-8"
body := msg.Body
if strings.TrimSpace(msg.BodyHTML) != "" {
contentType = "text/html; charset=UTF-8"
body = msg.BodyHTML
}
raw := buildMIME(m.cfg.From, msg.To, msg.Subject, contentType, body)
_, writeErr := w.Write([]byte(raw))
closeErr := w.Close()
if writeErr != nil {
return fmt.Errorf("write body: %w", writeErr)
}
if closeErr != nil {
return fmt.Errorf("close body: %w", closeErr)
}
if err := c.Quit(); err != nil {
return fmt.Errorf("quit: %w", err)
}
return nil
}
func buildMIME(from string, to []string, subject, contentType, body string) string {
// Subject UTF-8 basit hali (gerekirse sonra MIME encoded-word ekleriz)
headers := []string{
"From: " + from,
"To: " + strings.Join(to, ", "),
"Subject: " + subject,
"MIME-Version: 1.0",
"Content-Type: " + contentType,
"",
}
return strings.Join(headers, "\r\n") + "\r\n" + body + "\r\n"
}
type MailerInterface interface {
Send(ctx context.Context, msg Message) error
}

View File

@@ -0,0 +1,18 @@
package mailer
import "fmt"
func (m *GraphMailer) SendPasswordResetMail(toEmail string, resetURL string) error {
subject := "Parola Sıfırlama"
html := fmt.Sprintf(`
<p>Merhaba,</p>
<p>Parolanızı sıfırlamak için aşağıdaki bağlantıya tıklayın:</p>
<p>
<a href="%s">%s</a>
</p>
<p>Bu bağlantı <strong>30 dakika</strong> geçerlidir ve tek kullanımlıktır.</p>
`, resetURL, resetURL)
return m.SendMail(toEmail, subject, html)
}

View File

@@ -0,0 +1,11 @@
package security
import "errors"
var (
ErrPasswordTooShort = errors.New("password must be at least 8 characters")
ErrPasswordUpper = errors.New("password must contain an uppercase letter")
ErrPasswordLower = errors.New("password must contain a lowercase letter")
ErrPasswordDigit = errors.New("password must contain a digit")
ErrPasswordSpecial = errors.New("password must contain a special character")
)

View File

@@ -0,0 +1,35 @@
package security
import (
"errors"
"regexp"
"strings"
)
var (
reUpper = regexp.MustCompile(`[A-Z]`)
reLower = regexp.MustCompile(`[a-z]`)
reDigit = regexp.MustCompile(`[0-9]`)
reSpecial = regexp.MustCompile(`[^A-Za-z0-9]`)
)
func ValidatePassword(pw string) error {
pw = strings.TrimSpace(pw)
if len(pw) < 8 {
return errors.New("Parola en az 8 karakter olmalı")
}
if !reUpper.MatchString(pw) {
return errors.New("Parola en az 1 büyük harf içermeli")
}
if !reLower.MatchString(pw) {
return errors.New("Parola en az 1 küçük harf içermeli")
}
if !reDigit.MatchString(pw) {
return errors.New("Parola en az 1 rakam içermeli")
}
if !reSpecial.MatchString(pw) {
return errors.New("Parola en az 1 özel karakter içermeli")
}
return nil
}

View File

@@ -0,0 +1,13 @@
package security
import (
"os"
)
func BuildResetURL(token string) string {
base := os.Getenv("FRONTEND_URL")
if base == "" {
base = "http://localhost:9000"
}
return base + "/password-reset/" + token
}

View File

@@ -0,0 +1,23 @@
package security
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
)
func GenerateRefreshToken() (plain string, hash string, err error) {
b := make([]byte, 32) // 256 bit
if _, err = rand.Read(b); err != nil {
return
}
plain = hex.EncodeToString(b)
sum := sha256.Sum256([]byte(plain))
hash = hex.EncodeToString(sum[:])
return
}
func HashRefreshToken(plain string) string {
sum := sha256.Sum256([]byte(plain))
return hex.EncodeToString(sum[:])
}

View File

@@ -0,0 +1,26 @@
package security
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
)
func GenerateResetToken() (plain string, hash string, err error) {
b := make([]byte, 32) // 256 bit
if _, err = rand.Read(b); err != nil {
return
}
plain = hex.EncodeToString(b)
sum := sha256.Sum256([]byte(plain))
hash = hex.EncodeToString(sum[:])
return
}
func HashToken(plain string) string {
sum := sha256.Sum256([]byte(plain))
return hex.EncodeToString(sum[:])
}