Initial commit

This commit is contained in:
sas.fajri
2026-04-30 14:27:01 +07:00
commit e29e943c27
70 changed files with 8909 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
package auth
import (
"cpone-dashboard/db"
"embed"
"html/template"
"net/http"
)
var (
tmplFS *embed.FS
authSecret string
basePath string
)
func Init(fs *embed.FS, secret string) {
tmplFS = fs
authSecret = secret
}
func SetBasePath(p string) { basePath = p }
type loginData struct {
Error string
}
func ShowLogin(w http.ResponseWriter, r *http.Request) {
if getSession(r, authSecret) != "" {
http.Redirect(w, r, basePath+"/dashboard", http.StatusSeeOther)
return
}
render(w, loginData{})
}
func DoLogin(w http.ResponseWriter, r *http.Request) {
username := r.FormValue("username")
password := r.FormValue("password")
var hash, salt string
err := db.DB.QueryRow(
`SELECT User_Password, COALESCE(User_Salt, '')
FROM dashboard_user
WHERE User_Username = ? AND User_IsActive = 'Y'`,
username,
).Scan(&hash, &salt)
if err != nil || !checkPassword(password, salt, hash) {
w.WriteHeader(http.StatusUnauthorized)
render(w, loginData{Error: "Username atau password salah."})
return
}
SetSession(w, username, authSecret)
http.Redirect(w, r, basePath+"/projects", http.StatusSeeOther)
}
func DoLogout(w http.ResponseWriter, r *http.Request) {
ClearSession(w)
http.Redirect(w, r, basePath+"/mcu-login", http.StatusSeeOther)
}
func checkPassword(password, salt, storedHash string) bool {
return hashPassword(password, salt) == storedHash
}
func render(w http.ResponseWriter, data loginData) {
b := func(path string) string { return basePath + path }
t := template.Must(template.New("").Funcs(template.FuncMap{"b": b}).ParseFS(tmplFS, "templates/login/index.html"))
t.ExecuteTemplate(w, "login", data)
}

View File

@@ -0,0 +1,16 @@
package auth
import "net/http"
func Require(secret string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username := getSession(r, secret)
if username == "" {
http.Redirect(w, r, basePath+"/mcu-login", http.StatusSeeOther)
return
}
next.ServeHTTP(w, setContext(r, username))
})
}
}

View File

@@ -0,0 +1,79 @@
package auth
import (
"cpone-dashboard/db"
"crypto/sha256"
"encoding/hex"
"html/template"
"net/http"
)
type passwordData struct {
Username string
Error string
Success string
}
func ShowPassword(w http.ResponseWriter, r *http.Request) {
renderPassword(w, passwordData{Username: Username(r)})
}
func DoChangePassword(w http.ResponseWriter, r *http.Request) {
username := Username(r)
current := r.FormValue("current_password")
newPass := r.FormValue("new_password")
confirm := r.FormValue("confirm_password")
fail := func(msg string) {
w.WriteHeader(http.StatusUnprocessableEntity)
renderPassword(w, passwordData{Username: username, Error: msg})
}
if newPass != confirm {
fail("Password baru dan konfirmasi tidak cocok.")
return
}
if len(newPass) < 6 {
fail("Password baru minimal 6 karakter.")
return
}
// Verifikasi password lama
var storedHash, salt string
err := db.DB.QueryRow(
`SELECT User_Password, COALESCE(User_Salt, '')
FROM dashboard_user WHERE User_Username = ? AND User_IsActive = 'Y'`,
username,
).Scan(&storedHash, &salt)
if err != nil || !checkPassword(current, salt, storedHash) {
fail("Password saat ini tidak sesuai.")
return
}
// Generate salt + hash baru
newSalt := newUUID()
newHash := hashPassword(newPass, newSalt)
_, err = db.DB.Exec(
`UPDATE dashboard_user
SET User_Password = ?, User_Salt = ?, User_UpdatedAt = NOW()
WHERE User_Username = ?`,
newHash, newSalt, username,
)
if err != nil {
fail("Gagal menyimpan password baru, coba lagi.")
return
}
renderPassword(w, passwordData{Username: username, Success: "Password berhasil diubah."})
}
func renderPassword(w http.ResponseWriter, data passwordData) {
t := template.Must(template.ParseFS(tmplFS, "templates/auth/password.html"))
t.ExecuteTemplate(w, "password", data)
}
func hashPassword(password, salt string) string {
h := sha256.Sum256([]byte(salt + ":" + password))
return hex.EncodeToString(h[:])
}

View File

@@ -0,0 +1,14 @@
package auth
import "github.com/go-chi/chi/v5"
func Routes(r chi.Router) {
r.Get("/mcu-login", ShowLogin)
r.Post("/mcu-login", DoLogin)
r.Get("/logout", DoLogout)
}
func ProtectedRoutes(r chi.Router) {
r.Get("/password", ShowPassword)
r.Post("/password", DoChangePassword)
}

View File

@@ -0,0 +1,124 @@
package auth
import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"net/http"
"strconv"
"strings"
"time"
)
const cookieName = "cpone_session"
const projectCookieName = "cpone_project_mcu_id"
type ctxKey string
const userCtxKey ctxKey = "username"
// Username returns the logged-in username from the request context.
func Username(r *http.Request) string {
v, _ := r.Context().Value(userCtxKey).(string)
return v
}
func setContext(r *http.Request, username string) *http.Request {
return r.WithContext(context.WithValue(r.Context(), userCtxKey, username))
}
func SetSession(w http.ResponseWriter, username, secret string) {
val := encode(username) + "." + sign(username, secret)
http.SetCookie(w, &http.Cookie{
Name: cookieName,
Value: val,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Expires: time.Now().Add(24 * time.Hour),
})
}
func ClearSession(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: cookieName,
Value: "",
Path: "/",
MaxAge: -1,
})
}
func SetSelectedProject(w http.ResponseWriter, mcuID int) {
http.SetCookie(w, &http.Cookie{
Name: projectCookieName,
Value: strconv.Itoa(mcuID),
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Expires: time.Now().Add(7 * 24 * time.Hour),
})
}
func ClearSelectedProject(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: projectCookieName,
Value: "",
Path: "/",
MaxAge: -1,
})
}
func SelectedProjectID(r *http.Request) int {
c, err := r.Cookie(projectCookieName)
if err != nil {
return 0
}
id, err := strconv.Atoi(strings.TrimSpace(c.Value))
if err != nil {
return 0
}
return id
}
func getSession(r *http.Request, secret string) string {
c, err := r.Cookie(cookieName)
if err != nil {
return ""
}
parts := strings.SplitN(c.Value, ".", 2)
if len(parts) != 2 {
return ""
}
username, err := decode(parts[0])
if err != nil || username == "" {
return ""
}
if !hmac.Equal([]byte(sign(username, secret)), []byte(parts[1])) {
return ""
}
return username
}
func newUUID() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
func sign(username, secret string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(username))
return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
}
func encode(s string) string {
return base64.RawURLEncoding.EncodeToString([]byte(s))
}
func decode(s string) (string, error) {
b, err := base64.RawURLEncoding.DecodeString(s)
return string(b), err
}