Add password validation

This commit is contained in:
2024-04-25 22:51:37 +07:00
commit 2e2fbdf800
51 changed files with 3526 additions and 0 deletions

46
.air.toml Normal file
View File

@ -0,0 +1,46 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "tmp\\main.exe"
cmd = "go build -o ./tmp/main.exe ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", "uploads"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true

17
.env Normal file
View File

@ -0,0 +1,17 @@
SERVER_HOST=localhost
SERVER_PORT=8000
DOMAIN=filekeeper.fossy.my.id
CORS_PROTO=https
CORS_LIST=filekeeper.fossy.my.id:443,fossy.my.id:443
CORS_METHODS=POST,GET
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=test123
DB_NAME=filekeeper
SESSION_NAME=Session
SESSION_MAX_AGE=604800

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
/tmp
/uploads
/public/output.css
/log
*_templ.txt
*_templ.go

9
Makefile Normal file
View File

@ -0,0 +1,9 @@
run: generate tailwindcss
@go build -o ./tmp/main.exe
@./tmp/main.exe
generate:
@templ generate
tailwindcss:
@npx tailwindcss -i ./public/input.css -o ./public/output.css

39
db/database.go Normal file
View File

@ -0,0 +1,39 @@
package db
import (
"fmt"
"github.com/fossyy/filekeeper/logger"
"github.com/fossyy/filekeeper/utils"
"gorm.io/driver/mysql"
_ "gorm.io/driver/mysql"
"gorm.io/gorm"
gormLogger "gorm.io/gorm/logger"
"os"
"strings"
)
var DB *gorm.DB
var log *logger.AggregatedLogger
func init() {
var err error
connection := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", utils.Getenv("DB_USERNAME"), utils.Getenv("DB_PASSWORD"), utils.Getenv("DB_HOST"), utils.Getenv("DB_PORT"), utils.Getenv("DB_NAME"))
DB, err = gorm.Open(mysql.Open(connection), &gorm.Config{}, &gorm.Config{
Logger: gormLogger.Default.LogMode(gormLogger.Silent),
})
if err != nil {
panic("failed to connect database")
}
file, err := os.ReadFile("schema.sql")
if err != nil {
log.Error("Error opening file: %v", err)
}
querys := strings.Split(string(file), "\n")
for _, query := range querys {
err := DB.Exec(query).Error
if err != nil {
panic(err.Error())
}
}
}

77
db/model/user/user.go Normal file
View File

@ -0,0 +1,77 @@
package user
import (
"fmt"
"github.com/fossyy/filekeeper/db"
"github.com/fossyy/filekeeper/logger"
"github.com/fossyy/filekeeper/types"
"sync"
"time"
)
type Cache struct {
users map[string]*types.UserWithExpired
mu sync.Mutex
}
var log *logger.AggregatedLogger
var UserCache *Cache
func init() {
log = logger.Logger()
UserCache = &Cache{users: make(map[string]*types.UserWithExpired)}
ticker := time.NewTicker(time.Hour * 8)
go func() {
for {
<-ticker.C
currentTime := time.Now()
cacheClean := 0
log.Info(fmt.Sprintf("Cache cleanup initiated at %02d:%02d:%02d", currentTime.Hour(), currentTime.Minute(), currentTime.Second()))
UserCache.mu.Lock()
for _, user := range UserCache.users {
if currentTime.Sub(user.AccessAt) > time.Hour*8 {
delete(UserCache.users, user.Email)
cacheClean++
}
}
UserCache.mu.Unlock()
log.Info(fmt.Sprintf("Cache cleanup completed: %d entries removed. Finished at %s", cacheClean, time.Since(currentTime)))
}
}()
}
func Get(email string) (*types.UserWithExpired, error) {
UserCache.mu.Lock()
defer UserCache.mu.Unlock()
if user, ok := UserCache.users[email]; ok {
return user, nil
}
var userData types.UserWithExpired
err := db.DB.Table("users").Where("email = ?", email).First(&userData).Error
if err != nil {
return nil, err
}
UserCache.users[email] = &types.UserWithExpired{
UserID: userData.UserID,
Username: userData.Username,
Email: userData.Email,
Password: userData.Password,
AccessAt: time.Now(),
}
return &userData, nil
}
func DeleteCache(email string) {
UserCache.mu.Lock()
defer UserCache.mu.Unlock()
delete(UserCache.users, email)
}

39
email/email.go Normal file
View File

@ -0,0 +1,39 @@
package email
import (
"gopkg.in/gomail.v2"
)
type SmtpServer struct {
Host string
Port int
User string
Password string
}
type Email interface {
Send()
}
func NewSmtpServer(Host string, Port int, User string, Password string) *SmtpServer {
return &SmtpServer{
Host: Host,
Port: Port,
User: User,
Password: Password,
}
}
func (mail *SmtpServer) Send(dst string, subject string, body string) error {
m := gomail.NewMessage()
m.SetHeader("From", mail.User)
m.SetHeader("To", dst)
m.SetHeader("Subject", subject)
m.SetBody("text/html", body)
d := gomail.NewDialer(mail.Host, mail.Port, mail.User, mail.Password)
if err := d.DialAndSend(m); err != nil {
return err
}
return nil
}

20
go.mod Normal file
View File

@ -0,0 +1,20 @@
module github.com/fossyy/filekeeper
go 1.22.2
require (
github.com/a-h/templ v0.2.648
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
golang.org/x/crypto v0.21.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gorm.io/driver/mysql v1.5.6
gorm.io/gorm v1.25.8
)
require (
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
)

25
go.sum Normal file
View File

@ -0,0 +1,25 @@
github.com/a-h/templ v0.2.648 h1:A1ggHGIE7AONOHrFaDTM8SrqgqHL6fWgWCijQ21Zy9I=
github.com/a-h/templ v0.2.648/go.mod h1:SA7mtYwVEajbIXFRh3vKdYm/4FYyLQAtPH1+KxzGPA8=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.8 h1:WAGEZ/aEcznN4D03laj8DKnehe1e9gYQAjW8xyPRdeo=
gorm.io/gorm v1.25.8/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=

View File

@ -0,0 +1,62 @@
package downloadHandler
import (
"errors"
"github.com/fossyy/filekeeper/db"
"github.com/fossyy/filekeeper/logger"
"github.com/fossyy/filekeeper/middleware"
"github.com/fossyy/filekeeper/session"
"github.com/fossyy/filekeeper/types"
"github.com/fossyy/filekeeper/types/models"
"github.com/fossyy/filekeeper/utils"
downloadView "github.com/fossyy/filekeeper/view/download"
"net/http"
)
var log *logger.AggregatedLogger
func init() {
log = logger.Logger()
}
func GET(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("Session")
if err != nil {
if errors.Is(err, http.ErrNoCookie) {
http.Redirect(w, r, "/signin", http.StatusSeeOther)
return
}
log.Error(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
storeSession, err := session.Store.Get(cookie.Value)
if err != nil {
if errors.Is(err, &session.SessionNotFound{}) {
storeSession.Destroy(w)
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
userSession := middleware.GetUser(storeSession)
var files []models.File
db.DB.Table("files").Where("owner_id = ?", userSession.UserID).Find(&files)
var filesData []types.FileData
for i := 0; i < len(files); i++ {
filesData = append(filesData, types.FileData{
ID: files[i].ID.String(),
Name: files[i].Name,
Size: utils.ConvertFileSize(files[i].Size),
Downloaded: files[i].Downloaded,
})
}
component := downloadView.Main("Download Page", filesData)
err = component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
}

View File

@ -0,0 +1,56 @@
package downloadFileHandler
import (
"github.com/fossyy/filekeeper/db"
"github.com/fossyy/filekeeper/logger"
"github.com/fossyy/filekeeper/types/models"
"net/http"
"os"
"path/filepath"
)
var log *logger.AggregatedLogger
func init() {
log = logger.Logger()
}
func GET(w http.ResponseWriter, r *http.Request) {
fileID := r.PathValue("id")
var file models.File
err := db.DB.Table("files").Where("id = ?", fileID).First(&file).Error
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
}
uploadDir := "uploads"
currentDir, _ := os.Getwd()
basePath := filepath.Join(currentDir, uploadDir)
saveFolder := filepath.Join(basePath, file.OwnerID.String(), file.ID.String())
if filepath.Dir(saveFolder) != filepath.Join(basePath, file.OwnerID.String()) {
log.Error("invalid path")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
openFile, err := os.OpenFile(filepath.Join(saveFolder, file.Name), os.O_RDONLY, 0)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
}
defer openFile.Close()
stat, err := openFile.Stat()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
}
w.Header().Set("Content-Disposition", "attachment; filename="+stat.Name())
http.ServeContent(w, r, stat.Name(), stat.ModTime(), openFile)
return
}

23
handler/error/error.go Normal file
View File

@ -0,0 +1,23 @@
package errorHandler
import (
"github.com/fossyy/filekeeper/logger"
errorView "github.com/fossyy/filekeeper/view/error"
"net/http"
)
var log *logger.AggregatedLogger
func init() {
log = logger.Logger()
}
func ALL(w http.ResponseWriter, r *http.Request) {
component := errorView.Main("Not Found")
err := component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
}

View File

@ -0,0 +1,149 @@
package forgotPasswordHandler
import (
"bytes"
"context"
"errors"
"fmt"
"github.com/fossyy/filekeeper/db"
"github.com/fossyy/filekeeper/email"
"github.com/fossyy/filekeeper/logger"
"github.com/fossyy/filekeeper/types"
"github.com/fossyy/filekeeper/types/models"
"github.com/fossyy/filekeeper/utils"
emailView "github.com/fossyy/filekeeper/view/email"
forgotPasswordView "github.com/fossyy/filekeeper/view/forgotPassword"
"gorm.io/gorm"
"net/http"
"sync"
"time"
)
type ForgotPassword struct {
User *models.User
Code string
mu sync.Mutex
CreateTime time.Time
}
var log *logger.AggregatedLogger
var mailServer *email.SmtpServer
var ListForgotPassword map[string]*ForgotPassword
var UserForgotPassword = make(map[string]string)
func init() {
log = logger.Logger()
ListForgotPassword = make(map[string]*ForgotPassword)
mailServer = email.NewSmtpServer("mail.fossy.my.id", 25, "test@fossy.my.id", "Test123456")
ticker := time.NewTicker(time.Minute)
go func() {
for {
<-ticker.C
currentTime := time.Now()
cacheClean := 0
log.Info(fmt.Sprintf("Cache cleanup initiated at %02d:%02d:%02d", currentTime.Hour(), currentTime.Minute(), currentTime.Second()))
for _, data := range ListForgotPassword {
data.mu.Lock()
if currentTime.Sub(data.CreateTime) > time.Minute*1 {
delete(ListForgotPassword, data.User.Email)
delete(UserForgotPassword, data.Code)
cacheClean++
}
data.mu.Unlock()
}
log.Info(fmt.Sprintf("Cache cleanup completed: %d entries removed. Finished at %s", cacheClean, time.Since(currentTime)))
}
}()
}
func GET(w http.ResponseWriter, r *http.Request) {
component := forgotPasswordView.Main("Forgot Password Page", types.Message{
Code: 3,
Message: "",
})
err := component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
}
func POST(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, "Error parsing form", http.StatusBadRequest)
log.Error(err.Error())
return
}
emailForm := r.Form.Get("email")
var user models.User
err = db.DB.Table("users").Where("email = ?", emailForm).First(&user).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
component := forgotPasswordView.Main(fmt.Sprintf("Account with this email address %s is not found", emailForm), types.Message{
Code: 0,
Message: "",
})
err := component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
return
}
err = verifyForgot(&user)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
component := forgotPasswordView.EmailSend("Forgot Password Page")
err = component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
return
}
func verifyForgot(user *models.User) error {
var code string
var buffer bytes.Buffer
data, ok := ListForgotPassword[user.Email]
if !ok {
code = utils.GenerateRandomString(64)
} else {
code = data.Code
}
err := emailView.ForgotPassword(user.Username, fmt.Sprintf("https://%s/forgot-password/verify/%s", utils.Getenv("DOMAIN"), code)).Render(context.Background(), &buffer)
if err != nil {
return err
}
userData := &ForgotPassword{
User: user,
Code: code,
CreateTime: time.Now(),
}
UserForgotPassword[code] = user.Email
ListForgotPassword[user.Email] = userData
err = mailServer.Send(user.Email, "Password Change Request", buffer.String())
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,109 @@
package forgotPasswordVerifyHandler
import (
"github.com/fossyy/filekeeper/db"
"github.com/fossyy/filekeeper/db/model/user"
forgotPasswordHandler "github.com/fossyy/filekeeper/handler/forgotPassword"
"github.com/fossyy/filekeeper/logger"
"github.com/fossyy/filekeeper/session"
"github.com/fossyy/filekeeper/types"
"github.com/fossyy/filekeeper/utils"
forgotPasswordView "github.com/fossyy/filekeeper/view/forgotPassword"
signupView "github.com/fossyy/filekeeper/view/signup"
"net/http"
)
var log *logger.AggregatedLogger
func init() {
log = logger.Logger()
}
func GET(w http.ResponseWriter, r *http.Request) {
code := r.PathValue("code")
email := forgotPasswordHandler.UserForgotPassword[code]
_, ok := forgotPasswordHandler.ListForgotPassword[email]
if !ok {
w.WriteHeader(http.StatusNotFound)
return
}
component := forgotPasswordView.NewPasswordForm("Forgot Password Page", types.Message{
Code: 3,
Message: "",
})
err := component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
}
func POST(w http.ResponseWriter, r *http.Request) {
code := r.PathValue("code")
email := forgotPasswordHandler.UserForgotPassword[code]
data, ok := forgotPasswordHandler.ListForgotPassword[email]
if !ok {
w.WriteHeader(http.StatusNotFound)
return
}
err := r.ParseForm()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
password := r.Form.Get("password")
isValid := utils.ValidatePassword(password)
if !isValid {
component := signupView.Main("Sign up Page", types.Message{
Code: 0,
Message: "Password is invalid",
})
err := component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
return
}
hashedPassword, err := utils.HashPassword(password)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
err = db.DB.Table("users").Where("email = ?", data.User.Email).Update("password", hashedPassword).Error
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
delete(forgotPasswordHandler.ListForgotPassword, data.User.Email)
delete(forgotPasswordHandler.UserForgotPassword, data.Code)
session.RemoveAllSession(data.User.Email)
user.DeleteCache(data.User.Email)
component := forgotPasswordView.ChangeSuccess("Forgot Password Page")
err = component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
return
}

23
handler/index/index.go Normal file
View File

@ -0,0 +1,23 @@
package indexHandler
import (
"github.com/fossyy/filekeeper/logger"
"github.com/fossyy/filekeeper/view/index"
"net/http"
)
var log *logger.AggregatedLogger
func init() {
log = logger.Logger()
}
func GET(w http.ResponseWriter, r *http.Request) {
component := indexView.Main("main page")
err := component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
}

44
handler/logout/logout.go Normal file
View File

@ -0,0 +1,44 @@
package logoutHandler
import (
"errors"
"github.com/fossyy/filekeeper/logger"
"github.com/fossyy/filekeeper/session"
"github.com/fossyy/filekeeper/types"
"github.com/fossyy/filekeeper/utils"
"net/http"
)
var log *logger.AggregatedLogger
func init() {
log = logger.Logger()
}
func GET(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("Session")
if err != nil {
return
}
storeSession, err := session.Store.Get(cookie.Value)
if err != nil {
if errors.Is(err, &session.SessionNotFound{}) {
storeSession.Destroy(w)
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session.Store.Delete(cookie.Value)
session.RemoveSession(storeSession.Values["user"].(types.User).Email, cookie.Value)
http.SetCookie(w, &http.Cookie{
Name: utils.Getenv("SESSION_NAME"),
Value: "",
MaxAge: -1,
})
http.Redirect(w, r, "/signin", http.StatusSeeOther)
return
}

25
handler/misc/misc.go Normal file
View File

@ -0,0 +1,25 @@
package miscHandler
import (
"net/http"
)
func Robot(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/public/robots.txt", http.StatusSeeOther)
}
func Favicon(w http.ResponseWriter, r *http.Request) {
//currentDir, _ := os.Getwd()
//fmt.Println(currentDir)
//logo := "../../../favicon.ico"
//basePath := filepath.Join(currentDir, "public")
//logoPath := filepath.Join(basePath, logo)
//fmt.Println(filepath.Dir(logoPath))
//if filepath.Dir(logoPath) != basePath {
// log.Print("invalid logo path", logoPath)
// w.WriteHeader(500)
// return
//}
//http.ServeContent()
http.Redirect(w, r, "/public/favicon.ico", http.StatusSeeOther)
}

92
handler/signin/signin.go Normal file
View File

@ -0,0 +1,92 @@
package signinHandler
import (
"errors"
"github.com/fossyy/filekeeper/db/model/user"
"github.com/fossyy/filekeeper/logger"
"github.com/fossyy/filekeeper/session"
"github.com/fossyy/filekeeper/types"
"github.com/fossyy/filekeeper/utils"
signinView "github.com/fossyy/filekeeper/view/signin"
"net/http"
)
var log *logger.AggregatedLogger
func init() {
log = logger.Logger()
}
func GET(w http.ResponseWriter, r *http.Request) {
component := signinView.Main("Sign in Page", types.Message{
Code: 3,
Message: "",
})
err := component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
}
func POST(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, "Error parsing form", http.StatusBadRequest)
log.Error(err.Error())
return
}
email := r.Form.Get("email")
password := r.Form.Get("password")
userData, err := user.Get(email)
if err != nil {
component := signinView.Main("Sign in Page", types.Message{
Code: 0,
Message: "Incorrect Username or Password",
})
log.Error(err.Error())
err = component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
return
}
if email == userData.Email && utils.CheckPasswordHash(password, userData.Password) {
storeSession := session.Store.Create()
storeSession.Values["user"] = types.User{
UserID: userData.UserID,
Email: email,
Username: userData.Username,
Authenticated: true,
}
storeSession.Save(w)
session.AppendSession(email, storeSession)
cookie, err := r.Cookie("redirect")
if errors.Is(err, http.ErrNoCookie) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
http.SetCookie(w, &http.Cookie{
Name: "redirect",
MaxAge: -1,
})
http.Redirect(w, r, cookie.Value, http.StatusSeeOther)
return
}
component := signinView.Main("Sign in Page", types.Message{
Code: 0,
Message: "Incorrect Username or Password",
})
err = component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
}

179
handler/signup/signup.go Normal file
View File

@ -0,0 +1,179 @@
package signupHandler
import (
"bytes"
"context"
"errors"
"fmt"
"github.com/fossyy/filekeeper/db"
"github.com/fossyy/filekeeper/email"
"github.com/fossyy/filekeeper/logger"
"github.com/fossyy/filekeeper/types"
"github.com/fossyy/filekeeper/types/models"
"github.com/fossyy/filekeeper/utils"
emailView "github.com/fossyy/filekeeper/view/email"
signupView "github.com/fossyy/filekeeper/view/signup"
"github.com/google/uuid"
"gorm.io/gorm"
"net/http"
"sync"
"time"
)
type UnverifiedUser struct {
User *models.User
Code string
mu sync.Mutex
CreateTime time.Time
}
var log *logger.AggregatedLogger
var mailServer *email.SmtpServer
var VerifyUser map[string]*UnverifiedUser
var VerifyEmail map[string]string
func init() {
log = logger.Logger()
mailServer = email.NewSmtpServer("mail.fossy.my.id", 25, "test@fossy.my.id", "Test123456")
VerifyUser = make(map[string]*UnverifiedUser)
VerifyEmail = make(map[string]string)
ticker := time.NewTicker(time.Minute)
go func() {
for {
<-ticker.C
currentTime := time.Now()
cacheClean := 0
log.Info(fmt.Sprintf("Cache cleanup initiated at %02d:%02d:%02d", currentTime.Hour(), currentTime.Minute(), currentTime.Second()))
for _, data := range VerifyUser {
data.mu.Lock()
if currentTime.Sub(data.CreateTime) > time.Minute*1 {
delete(VerifyUser, data.Code)
delete(VerifyEmail, data.User.Email)
cacheClean++
}
data.mu.Unlock()
}
log.Info(fmt.Sprintf("Cache cleanup completed: %d entries removed. Finished at %s", cacheClean, time.Since(currentTime)))
}
}()
}
func GET(w http.ResponseWriter, r *http.Request) {
component := signupView.Main("Sign up Page", types.Message{
Code: 3,
Message: "",
})
err := component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
}
func POST(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
userEmail := r.Form.Get("email")
username := r.Form.Get("username")
password := r.Form.Get("password")
isValid := utils.ValidatePassword(password)
if !isValid {
component := signupView.Main("Sign up Page", types.Message{
Code: 0,
Message: "Password is invalid",
})
err := component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
return
}
hashedPassword, err := utils.HashPassword(password)
newUser := models.User{
UserID: uuid.New(),
Username: username,
Email: userEmail,
Password: hashedPassword,
}
var data models.User
err = db.DB.Table("users").Where("email = ? OR username = ?", userEmail, username).First(&data).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
err = verifyEmail(&newUser)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
component := signupView.EmailSend("Sign up Page")
err = component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
component := signupView.Main("Sign up Page", types.Message{
Code: 0,
Message: "Email or Username has been registered",
})
err = component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
return
}
func verifyEmail(user *models.User) error {
var buffer bytes.Buffer
var code string
code = VerifyEmail[user.Email]
userData, ok := VerifyUser[code]
if !ok {
code = utils.GenerateRandomString(64)
} else {
code = userData.Code
}
err := emailView.RegistrationEmail(user.Username, fmt.Sprintf("https://%s/signup/verify/%s", utils.Getenv("DOMAIN"), code)).Render(context.Background(), &buffer)
if err != nil {
return err
}
unverifiedUser := UnverifiedUser{
User: user,
Code: code,
CreateTime: time.Now(),
}
VerifyUser[code] = &unverifiedUser
VerifyEmail[user.Email] = code
err = mailServer.Send(user.Email, "Account Registration Verification", buffer.String())
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,54 @@
package signupVerifyHandler
import (
"github.com/fossyy/filekeeper/db"
signupHandler "github.com/fossyy/filekeeper/handler/signup"
"github.com/fossyy/filekeeper/logger"
"github.com/fossyy/filekeeper/types"
signupView "github.com/fossyy/filekeeper/view/signup"
"net/http"
)
var log *logger.AggregatedLogger
func init() {
log = logger.Logger()
}
func GET(w http.ResponseWriter, r *http.Request) {
code := r.PathValue("code")
data, ok := signupHandler.VerifyUser[code]
if !ok {
w.WriteHeader(http.StatusNotFound)
return
}
err := db.DB.Create(&data.User).Error
if err != nil {
component := signupView.Main("Sign up Page", types.Message{
Code: 0,
Message: "Email or Username has been registered",
})
err := component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
return
}
delete(signupHandler.VerifyUser, code)
delete(signupHandler.VerifyEmail, data.User.Email)
component := signupView.VerifySuccess("Verify page")
err = component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
}

View File

@ -0,0 +1,174 @@
package initialisation
import (
"encoding/json"
"errors"
"github.com/fossyy/filekeeper/db"
"github.com/fossyy/filekeeper/logger"
"github.com/fossyy/filekeeper/middleware"
"github.com/fossyy/filekeeper/session"
"github.com/fossyy/filekeeper/types"
"github.com/fossyy/filekeeper/types/models"
"github.com/google/uuid"
"gorm.io/gorm"
"io"
"net/http"
"os"
"path/filepath"
)
var log *logger.AggregatedLogger
func init() {
log = logger.Logger()
}
func POST(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("Session")
if err != nil {
handleError(w, err, http.StatusInternalServerError)
return
}
storeSession, err := session.Store.Get(cookie.Value)
if err != nil {
if errors.Is(err, &session.SessionNotFound{}) {
storeSession.Destroy(w)
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
userSession := middleware.GetUser(storeSession)
body, err := io.ReadAll(r.Body)
if err != nil {
handleError(w, err, http.StatusInternalServerError)
return
}
var fileInfo types.FileInfo
if err := json.Unmarshal(body, &fileInfo); err != nil {
handleError(w, err, http.StatusInternalServerError)
return
}
fileData, err := getFile(fileInfo.Name, userSession.UserID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
upload, err := handleNewUpload(userSession, fileInfo)
if err != nil {
handleError(w, err, http.StatusInternalServerError)
return
}
respondJSON(w, upload)
return
}
respondErrorJSON(w, err, http.StatusInternalServerError)
return
}
info, err := GetUploadInfo(fileData.ID.String())
if err != nil {
log.Error(err.Error())
return
}
if info.Done {
respondJSON(w, map[string]bool{"Done": true})
return
}
respondJSON(w, info)
}
func getFile(name string, ownerID uuid.UUID) (models.File, error) {
var data models.File
err := db.DB.Table("files").Where("name = ? AND owner_id = ?", name, ownerID).First(&data).Error
if err != nil {
return data, err
}
return data, nil
}
func handleNewUpload(user types.User, file types.FileInfo) (models.FilesUploaded, error) {
uploadDir := "uploads"
if _, err := os.Stat(uploadDir); os.IsNotExist(err) {
log.Error(err.Error())
err := os.Mkdir(uploadDir, os.ModePerm)
if err != nil {
log.Error(err.Error())
return models.FilesUploaded{}, err
}
}
fileID := uuid.New()
ownerID := user.UserID
currentDir, _ := os.Getwd()
basePath := filepath.Join(currentDir, uploadDir)
saveFolder := filepath.Join(basePath, ownerID.String(), fileID.String())
if filepath.Dir(saveFolder) != filepath.Join(basePath, ownerID.String()) {
return models.FilesUploaded{}, errors.New("invalid path")
}
err := os.MkdirAll(saveFolder, os.ModePerm)
if err != nil {
log.Error(err.Error())
return models.FilesUploaded{}, err
}
newFile := models.File{
ID: fileID,
OwnerID: ownerID,
Name: file.Name,
Size: file.Size,
Downloaded: 0,
}
err = db.DB.Create(&newFile).Error
if err != nil {
log.Error(err.Error())
return models.FilesUploaded{}, err
}
filesUploaded := models.FilesUploaded{
UploadID: uuid.New(),
FileID: fileID,
OwnerID: ownerID,
Name: file.Name,
Size: file.Size,
Uploaded: -1,
Done: false,
}
err = db.DB.Create(&filesUploaded).Error
if err != nil {
log.Error(err.Error())
return models.FilesUploaded{}, err
}
return filesUploaded, nil
}
func GetUploadInfo(fileID string) (*models.FilesUploaded, error) {
var data *models.FilesUploaded
err := db.DB.Table("files_uploadeds").Where("file_id = ?", fileID).First(&data).Error
if err != nil {
return data, err
}
return data, nil
}
func respondJSON(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(data); err != nil {
handleError(w, err, http.StatusInternalServerError)
}
}
func respondErrorJSON(w http.ResponseWriter, err error, statusCode int) {
w.WriteHeader(statusCode)
respondJSON(w, map[string]string{"error": err.Error()})
}
func handleError(w http.ResponseWriter, err error, statusCode int) {
http.Error(w, err.Error(), statusCode)
log.Error(err.Error())
}

142
handler/upload/upload.go Normal file
View File

@ -0,0 +1,142 @@
package uploadHandler
import (
"errors"
"github.com/fossyy/filekeeper/db"
"github.com/fossyy/filekeeper/handler/upload/initialisation"
"github.com/fossyy/filekeeper/logger"
"github.com/fossyy/filekeeper/middleware"
"github.com/fossyy/filekeeper/session"
filesView "github.com/fossyy/filekeeper/view/upload"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"sync"
)
var log *logger.AggregatedLogger
var mu sync.Mutex
func init() {
log = logger.Logger()
}
func GET(w http.ResponseWriter, r *http.Request) {
component := filesView.Main("upload page")
if err := component.Render(r.Context(), w); err != nil {
handleError(w, err, http.StatusInternalServerError)
return
}
}
func POST(w http.ResponseWriter, r *http.Request) {
fileID := r.PathValue("id")
if err := r.ParseMultipartForm(32 << 20); err != nil {
handleError(w, err, http.StatusInternalServerError)
return
}
cookie, err := r.Cookie("Session")
if err != nil {
handleCookieError(w, r, err)
return
}
storeSession, err := session.Store.Get(cookie.Value)
if err != nil {
if errors.Is(err, &session.SessionNotFound{}) {
storeSession.Destroy(w)
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
userSession := middleware.GetUser(storeSession)
if r.FormValue("done") == "true" {
finalizeFileUpload(fileID)
return
}
uploadDir := "uploads"
if err := createUploadDirectory(uploadDir); err != nil {
handleError(w, err, http.StatusInternalServerError)
return
}
file, err := initialisation.GetUploadInfo(fileID)
if err != nil {
log.Error("error getting upload info: " + err.Error())
return
}
currentDir, _ := os.Getwd()
basePath := filepath.Join(currentDir, uploadDir)
saveFolder := filepath.Join(basePath, userSession.UserID.String(), file.FileID.String())
if filepath.Dir(saveFolder) != filepath.Join(basePath, userSession.UserID.String()) {
log.Error("invalid path")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fileByte, _, err := r.FormFile("chunk")
if err != nil {
handleError(w, err, http.StatusInternalServerError)
return
}
defer fileByte.Close()
dst, err := os.OpenFile(filepath.Join(saveFolder, file.Name), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
if err != nil {
handleError(w, err, http.StatusInternalServerError)
return
}
defer dst.Close()
if _, err := io.Copy(dst, fileByte); err != nil {
handleError(w, err, http.StatusInternalServerError)
return
}
rawIndex := r.FormValue("index")
index, err := strconv.Atoi(rawIndex)
if err != nil {
return
}
updateIndex(index, fileID)
}
func finalizeFileUpload(fileID string) {
db.DB.Table("files_uploadeds").Where("file_id = ?", fileID).Updates(map[string]interface{}{
"Done": true,
})
}
func createUploadDirectory(uploadDir string) error {
if _, err := os.Stat(uploadDir); os.IsNotExist(err) {
if err := os.Mkdir(uploadDir, os.ModePerm); err != nil {
return err
}
}
return nil
}
func updateIndex(index int, fileID string) {
db.DB.Table("files_uploadeds").Where("file_id = ?", fileID).Updates(map[string]interface{}{
"Uploaded": index,
})
}
func handleCookieError(w http.ResponseWriter, r *http.Request, err error) {
if errors.Is(err, http.ErrNoCookie) {
http.Redirect(w, r, "/signin", http.StatusSeeOther)
return
}
handleError(w, err, http.StatusInternalServerError)
}
func handleError(w http.ResponseWriter, err error, status int) {
http.Error(w, err.Error(), status)
log.Error(err.Error())
}

44
handler/user/user.go Normal file
View File

@ -0,0 +1,44 @@
package userHandler
import (
"errors"
"github.com/fossyy/filekeeper/logger"
"github.com/fossyy/filekeeper/middleware"
"github.com/fossyy/filekeeper/session"
userView "github.com/fossyy/filekeeper/view/user"
"net/http"
)
var log *logger.AggregatedLogger
func init() {
log = logger.Logger()
}
func GET(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("Session")
if err != nil {
return
}
storeSession, err := session.Store.Get(cookie.Value)
if err != nil {
if errors.Is(err, &session.SessionNotFound{}) {
storeSession.Destroy(w)
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
userSession := middleware.GetUser(storeSession)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
component := userView.Main("User Page", userSession.Email, userSession.Username)
err = component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Error(err.Error())
return
}
}

60
logger/logger.go Normal file
View File

@ -0,0 +1,60 @@
package logger
import (
"fmt"
"io"
"log"
"os"
"time"
)
type AggregatedLogger struct {
infoLogger *log.Logger
warnLogger *log.Logger
errorLogger *log.Logger
panicLogger *log.Logger
}
func init() {
if _, err := os.Stat("log"); os.IsNotExist(err) {
os.Mkdir("log", os.ModePerm)
}
}
func Logger() *AggregatedLogger {
currentTime := time.Now()
formattedTime := currentTime.Format("2006-01-02-15-04")
file, err := os.OpenFile(fmt.Sprintf("log/log-%s.log", formattedTime), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return &AggregatedLogger{}
}
flag := log.Ldate | log.Ltime
writer := io.MultiWriter(os.Stdout, file)
infoLogger := log.New(writer, "INFO: ", flag)
warnLogger := log.New(writer, "WARN: ", flag)
errorLogger := log.New(writer, "ERROR: ", flag)
panicLogger := log.New(writer, "PANIC: ", flag)
return &AggregatedLogger{
infoLogger: infoLogger,
warnLogger: warnLogger,
errorLogger: errorLogger,
panicLogger: panicLogger,
}
}
func (l *AggregatedLogger) Info(v ...interface{}) {
l.infoLogger.Println(v...)
}
func (l *AggregatedLogger) Warn(v ...interface{}) {
l.warnLogger.Println(v...)
}
func (l *AggregatedLogger) Error(v ...interface{}) {
l.errorLogger.Println(v...)
}
func (l *AggregatedLogger) Panic(v ...interface{}) {
l.panicLogger.Panic(v...)
}

23
main.go Normal file
View File

@ -0,0 +1,23 @@
package main
import (
"fmt"
"github.com/fossyy/filekeeper/middleware"
"github.com/fossyy/filekeeper/routes"
"github.com/fossyy/filekeeper/utils"
"net/http"
)
func main() {
serverAddr := fmt.Sprintf("%s:%s", utils.Getenv("SERVER_HOST"), utils.Getenv("SERVER_PORT"))
server := http.Server{
Addr: serverAddr,
Handler: middleware.Handler(routes.SetupRoutes()),
}
fmt.Printf("Listening on http://%s\n", serverAddr)
err := server.ListenAndServe()
if err != nil {
return
}
}

146
middleware/middleware.go Normal file
View File

@ -0,0 +1,146 @@
package middleware
import (
"errors"
"fmt"
errorHandler "github.com/fossyy/filekeeper/handler/error"
"github.com/fossyy/filekeeper/logger"
"github.com/fossyy/filekeeper/session"
"github.com/fossyy/filekeeper/types"
"github.com/fossyy/filekeeper/utils"
"net/http"
"strings"
)
var log *logger.AggregatedLogger
func init() {
log = logger.Logger()
}
type wrapper struct {
http.ResponseWriter
request *http.Request
statusCode int
}
func (w *wrapper) WriteHeader(code int) {
if code == http.StatusNotFound {
w.Header().Set("Content-Type", "text/html")
errorHandler.ALL(w.ResponseWriter, w.request)
return
}
w.ResponseWriter.WriteHeader(code)
w.statusCode = code
return
}
func Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
address := strings.Split(utils.Getenv("CORS_LIST"), ",")
for _, addr := range address {
if request.Host == addr {
writer.Header().Set("Access-Control-Allow-Origin", fmt.Sprintf("%s://%s", utils.Getenv("CORS_PROTO"), addr))
}
}
wrappedWriter := &wrapper{
ResponseWriter: writer,
request: request,
statusCode: http.StatusOK,
}
writer.Header().Set("Access-Control-Allow-Methods", fmt.Sprintf("%s, OPTIONS", utils.Getenv("CORS_METHODS")))
writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
next.ServeHTTP(wrappedWriter, request)
log.Info(fmt.Sprintf("%s %s %s %v \n", utils.ClientIP(request), request.Method, request.RequestURI, wrappedWriter.statusCode))
})
}
func Auth(next http.HandlerFunc, w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("Session")
if err != nil {
if errors.Is(err, http.ErrNoCookie) {
http.SetCookie(w, &http.Cookie{
Name: "redirect",
Value: r.RequestURI,
Path: "/",
})
http.Redirect(w, r, "/signin", http.StatusSeeOther)
return
}
log.Error(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
storeSession, err := session.Store.Get(cookie.Value)
if err != nil {
if errors.Is(err, &session.SessionNotFound{}) {
storeSession.Destroy(w)
http.Redirect(w, r, "/signin", http.StatusSeeOther)
return
}
log.Error(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
userSession := GetUser(storeSession)
if userSession.Authenticated {
next.ServeHTTP(w, r)
return
}
http.SetCookie(w, &http.Cookie{
Name: "redirect",
Value: r.RequestURI,
Path: "/",
})
http.Redirect(w, r, "/signin", http.StatusSeeOther)
return
}
func Guest(next http.HandlerFunc, w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("Session")
if err != nil {
if errors.Is(err, http.ErrNoCookie) {
next.ServeHTTP(w, r)
return
}
log.Error(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
storeSession, err := session.Store.Get(cookie.Value)
if err != nil {
if errors.Is(err, &session.SessionNotFound{}) {
http.SetCookie(w, &http.Cookie{
Name: "Session",
Value: "",
MaxAge: -1,
})
next.ServeHTTP(w, r)
return
} else {
log.Error(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
userSession := GetUser(storeSession)
if !userSession.Authenticated {
next.ServeHTTP(w, r)
return
}
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
func GetUser(s *session.Session) types.User {
val := s.Values["user"]
var userSession = types.User{}
userSession, ok := val.(types.User)
if !ok {
return types.User{Authenticated: false}
}
return userSession
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 B

222
public/index.html Normal file
View File

@ -0,0 +1,222 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: 'Arimo', sans-serif;
--font-sans: 'Arimo';
}
</style>
<style>
body {
font-family: 'Libre Franklin', sans-serif;
--font-sans: 'Libre Franklin';
}
</style>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: 'Arimo', sans-serif;
--font-sans: 'Arimo';
}
</style>
<style>
body {
font-family: 'Libre Franklin', sans-serif;
--font-sans: 'Libre Franklin';
}
</style>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div class="dark flex items-center min-h-screen p-4 sm:p-6 bg-gray-900">
<div class="mx-auto w-full max-w-md space-y-8">
<header class="text-center">
<div class="space-y-2">
<h1 class="text-3xl font-bold text-white">Forgot password</h1>
<p class="text-gray-500 dark:text-gray-400">Enter your email below to reset your password</p>
</div>
</header>
<form class="space-y-4" method="post" action="">
<div class="space-y-2">
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="password">Password</label>
<input type="password" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="password" name="password" required />
</div>
<div class="space-y-2">
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="confirmPassword">Confirm Password</label>
<input type="password" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="confirmPassword" required />
</div>
<div class="flex justify-start mt-3 ml-4 p-1">
<ul>
<li class="flex items-center py-1">
<div id="matchSvgContainer" class="rounded-full p-1 fill-current bg-red-200 text-green-700">
<svg id="matchSvgIcon" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path id="matchGoodPath" style="display: none;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
<path id="matchBadPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
<span id="matchStatusText" class="font-medium text-sm ml-3 text-red-700"> Passwords do not match</span>
</li>
<li class="flex items-center py-1">
<div id="lengthSvgContainer" class="rounded-full p-1 fill-current bg-red-200 text-green-700">
<svg id="lengthSvgIcon" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path id="lengthGoodPath" style="display: none;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
<path id="lengthBadPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
<span id="lengthStatusText" class="font-medium text-sm ml-3 text-red-700"> Password length must be at least 8 characters</span>
</li>
<li class="flex items-center py-1">
<div id="requirementsSvgContainer" class="rounded-full p-1 fill-current bg-red-200 text-green-700">
<svg id="requirementsSvgIcon" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path id="requirementsGoodPath" style="display: none;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
<path id="requirementsBadPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
<span id="requirementsStatusText" class="font-medium text-sm ml-3 text-red-700">The password must contain at least one symbol (!@#$%^&*), one uppercase letter, and three numbers</span>
</li>
</ul>
</div>
<button class="bg-slate-200 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full" type="submit" id="submit" name="submit" disabled>
Submit
</button>
</form>
</div>
</div>
<script>
var isMatch = false
var isValid = false
var isSecure = false
function validatePasswords() {
var password = document.getElementById('password').value;
var confirmPassword = document.getElementById('confirmPassword').value;
var matchGoodPath = document.getElementById('matchGoodPath');
var matchBadPath = document.getElementById('matchBadPath');
var lengthGoodPath = document.getElementById('lengthGoodPath');
var lengthBadPath = document.getElementById('lengthBadPath');
var requirementsGoodPath = document.getElementById('requirementsGoodPath');
var requirementsBadPath = document.getElementById('requirementsBadPath');
var matchSvgContainer = document.getElementById('matchSvgContainer');
var lengthSvgContainer = document.getElementById('lengthSvgContainer');
var requirementsSvgContainer = document.getElementById('requirementsSvgContainer');
var matchStatusText = document.getElementById('matchStatusText');
var lengthStatusText = document.getElementById('lengthStatusText');
var requirementsStatusText = document.getElementById('requirementsStatusText');
var symbolRegex = /[!@#$%^&*]/;
var uppercaseRegex = /[A-Z]/;
var numberRegex = /\d/g;
if (password === confirmPassword && password.length > 0 && confirmPassword.length > 0 && password.length === confirmPassword.length) {
matchSvgContainer.classList.remove('bg-red-200');
matchSvgContainer.classList.add('bg-green-200');
matchStatusText.classList.remove('text-red-700');
matchStatusText.classList.add('text-green-700');
matchGoodPath.style.display = 'inline';
matchBadPath.style.display = 'none';
matchStatusText.textContent = "Passwords match";
console.log("anjay")
isMatch = true
} else {
matchSvgContainer.classList.remove('bg-green-200');
matchSvgContainer.classList.add('bg-red-200');
matchStatusText.classList.remove('text-green-700');
matchStatusText.classList.add('text-red-700');
matchGoodPath.style.display = 'none';
matchBadPath.style.display = 'inline';
matchStatusText.textContent = "Passwords do not match";
isMatch = false
}
if (password.length >= 8) {
lengthSvgContainer.classList.remove('bg-red-200');
lengthSvgContainer.classList.add('bg-green-200');
lengthStatusText.classList.remove('text-red-700');
lengthStatusText.classList.add('text-green-700');
lengthGoodPath.style.display = 'inline';
lengthBadPath.style.display = 'none';
lengthStatusText.textContent = "Password length meets requirement";
isValid = true
} else {
lengthSvgContainer.classList.remove('bg-green-200');
lengthSvgContainer.classList.add('bg-red-200');
lengthStatusText.classList.remove('text-green-700');
lengthStatusText.classList.add('text-red-700');
lengthGoodPath.style.display = 'none';
lengthBadPath.style.display = 'inline';
lengthStatusText.textContent = "Password length must be at least 8 characters";
isValid = false
}
var symbolCheck = symbolRegex.test(password);
var uppercaseCheck = uppercaseRegex.test(password);
var numberCount = (password.match(numberRegex) || []).length;
if (symbolCheck && uppercaseCheck && numberCount >= 3) {
requirementsSvgContainer.classList.remove('bg-red-200');
requirementsSvgContainer.classList.add('bg-green-200');
requirementsStatusText.classList.remove('text-red-700');
requirementsStatusText.classList.add('text-green-700');
requirementsGoodPath.style.display = 'inline';
requirementsBadPath.style.display = 'none';
requirementsStatusText.textContent = "Password meets additional requirements";
isSecure = true
} else {
requirementsSvgContainer.classList.remove('bg-green-200');
requirementsSvgContainer.classList.add('bg-red-200');
requirementsStatusText.classList.remove('text-green-700');
requirementsStatusText.classList.add('text-red-700');
requirementsGoodPath.style.display = 'none';
requirementsBadPath.style.display = 'inline';
requirementsStatusText.textContent = "The password must contain at least one symbol (!@#$%^&*), one uppercase letter, and three numbers";
isSecure = false
}
console.log(isSecure)
console.log(isValid)
console.log(isSecure)
if (isSecure && isValid && isSecure && password === confirmPassword) {
document.getElementById("submit").disabled = false;
} else {
document.getElementById("submit").disabled = true;
}
}
document.getElementById('password').addEventListener('input', validatePasswords);
document.getElementById('confirmPassword').addEventListener('input', validatePasswords);
</script>
</body>
</html>
</body>
</html>

3
public/input.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

1
public/robots.txt Normal file
View File

@ -0,0 +1 @@
User-agent: *

173
public/upload.js Normal file
View File

@ -0,0 +1,173 @@
document.addEventListener("dragover", function (event) {
event.preventDefault();
});
document.addEventListener("drop", async function (event) {
event.preventDefault();
const file = event.dataTransfer.files[0]
await handleFile(file)
});
document.getElementById('dropzone-file').addEventListener('change', async function(event) {
event.preventDefault();
const file = event.target.files[0]
await handleFile(file)
});
async function handleFile(file){
const chunkSize = 2 * 1024 * 1024;
const chunks = Math.ceil(file.size / chunkSize);
const data = JSON.stringify({
"name": file.name,
"size": file.size,
"chunk": chunks,
});
fetch('/upload/init', {
method: 'POST',
body: data,
}).then(async response => {
const responseData = await response.json()
console.log(responseData)
if (responseData.Done === false) {
addNewUploadElement(file)
const fileChunks = await splitFile(file, chunkSize);
await uploadChunks(file.name,file.size, fileChunks, responseData.Uploaded, responseData.FileID);
} else {
alert("file already uploaded")
}
}).catch(error => {
console.error('Error uploading file:', error);
});
}
function addNewUploadElement(file){
const newDiv = document.createElement('div');
newDiv.innerHTML = `
<div class="p-6 rounded-lg shadow bg-gray-800 border-gray-700">
<div class="mb-2 flex justify-between items-center">
<div class="flex items-center gap-x-3">
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100" height="100" viewBox="0 0 48 48">
<path fill="#90CAF9" d="M40 45L8 45 8 3 30 3 40 13z"></path>
<path fill="#E1F5FE" d="M38.5 14L29 14 29 4.5z"></path>
</svg>
<div>
<p class="text-sm font-medium text-white">${ file.name }</p>
<p class="text-xs text-gray-500">${ convertFileSize(file.size) }</p>
</div>
</div>
<div class="inline-flex items-center gap-x-2">
<a class="text-gray-500 hover:text-gray-800" href="#">
<svg class="flex-shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<rect width="4" height="16" x="6" y="4" />
<rect width="4" height="16" x="14" y="4" />
</svg>
</a>
<a class="text-gray-500 hover:text-gray-800" href="#">
<svg class="flex-shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M3 6h18" />
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
<line x1="10" x2="10" y1="11" y2="17" />
<line x1="14" x2="14" y1="11" y2="17" />
</svg>
</a>
</div>
</div>
<div class="flex items-center gap-x-3 whitespace-nowrap">
<div id="progress-${ file.name }-1" class="flex w-full h-2 rounded-full overflow-hidden bg-gray-200"
role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100">
<div id="progress-${ file.name }-2"
class="flex flex-col justify-center rounded-full overflow-hidden bg-teal-500 text-xs text-white text-center whitespace-nowrap transition duration-500">
</div>
</div>
<span id="progress-${ file.name }-3" class="text-sm text-white ">Starting...</span>
</div>
<div id="progress-${ file.name }-4" class="text-sm text-gray-500">Uploading 0%</div>
</div>
`;
document.getElementById('container').appendChild(newDiv);
}
function convertFileSize(sizeInBytes) {
if (sizeInBytes < 1024) {
return sizeInBytes + ' B';
} else if (sizeInBytes < 1024 * 1024) {
return (sizeInBytes / 1024).toFixed(2) + ' KB';
} else if (sizeInBytes < 1024 * 1024 * 1024) {
return (sizeInBytes / (1024 * 1024)).toFixed(2) + ' MB';
} else {
return (sizeInBytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
}
}
async function splitFile(file, chunkSize) {
const fileSize = file.size;
const chunks = Math.ceil(fileSize / chunkSize);
const fileChunks = [];
for (let i = 0; i < chunks; i++) {
const start = i * chunkSize;
const end = Math.min(fileSize, start + chunkSize);
const chunk = file.slice(start, end);
fileChunks.push(chunk);
}
return fileChunks;
}
async function uploadChunks(name, size, chunks, uploadedChunk= -1, FileID) {
let byteUploaded = 0
var progress1 = document.getElementById(`progress-${name}-1`);
var progress2 = document.getElementById(`progress-${name}-2`);
var progress3 = document.getElementById(`progress-${name}-3`);
var progress4 = document.getElementById(`progress-${name}-4`);
for (let index = 0; index < chunks.length; index++) {
const percentComplete = Math.round((index + 1) / chunks.length * 100);
const chunk = chunks[index];
if (!(index <= uploadedChunk)) {
const formData = new FormData();
formData.append('name', name);
formData.append('chunk', chunk);
formData.append('index', index);
formData.append('done', false);
progress1.setAttribute("aria-valuenow", percentComplete);
progress2.style.width = `${percentComplete}%`;
const startTime = performance.now();
await fetch(`/upload/${FileID}`, {
method: 'POST',
body: formData
});
const endTime = performance.now();
const totalTime = (endTime - startTime) / 1000;
const uploadSpeed = chunk.size / totalTime / 1024 / 1024;
byteUploaded += chunk.size
progress3.innerText = `${uploadSpeed.toFixed(2)} MB/s`;
progress4.innerText = `Uploading ${percentComplete}% - ${convertFileSize(byteUploaded)} of ${ convertFileSize(size)}`;
} else {
progress1.setAttribute("aria-valuenow", percentComplete);
progress2.style.width = `${percentComplete}%`;
byteUploaded += chunk.size
}
}
const formData = new FormData();
formData.append('name', name);
formData.append('done', true);
return fetch(`/upload/${FileID}`, {
method: 'POST',
body: formData
});
}

View File

@ -0,0 +1,97 @@
var isMatch = false
var isValid = false
var isSecure = false
function validatePasswords() {
var password = document.getElementById('password').value;
var confirmPassword = document.getElementById('confirmPassword').value;
var matchGoodPath = document.getElementById('matchGoodPath');
var matchBadPath = document.getElementById('matchBadPath');
var lengthGoodPath = document.getElementById('lengthGoodPath');
var lengthBadPath = document.getElementById('lengthBadPath');
var requirementsGoodPath = document.getElementById('requirementsGoodPath');
var requirementsBadPath = document.getElementById('requirementsBadPath');
var matchSvgContainer = document.getElementById('matchSvgContainer');
var lengthSvgContainer = document.getElementById('lengthSvgContainer');
var requirementsSvgContainer = document.getElementById('requirementsSvgContainer');
var matchStatusText = document.getElementById('matchStatusText');
var lengthStatusText = document.getElementById('lengthStatusText');
var requirementsStatusText = document.getElementById('requirementsStatusText');
var symbolRegex = /[!@#$%^&*]/;
var uppercaseRegex = /[A-Z]/;
var numberRegex = /\d/g;
if (password === confirmPassword && password.length > 0 && confirmPassword.length > 0 && password.length === confirmPassword.length) {
matchSvgContainer.classList.remove('bg-red-200');
matchSvgContainer.classList.add('bg-green-200');
matchStatusText.classList.remove('text-red-700');
matchStatusText.classList.add('text-green-700');
matchGoodPath.style.display = 'inline';
matchBadPath.style.display = 'none';
matchStatusText.textContent = "Passwords match";
console.log("anjay")
isMatch = true
} else {
matchSvgContainer.classList.remove('bg-green-200');
matchSvgContainer.classList.add('bg-red-200');
matchStatusText.classList.remove('text-green-700');
matchStatusText.classList.add('text-red-700');
matchGoodPath.style.display = 'none';
matchBadPath.style.display = 'inline';
matchStatusText.textContent = "Passwords do not match";
isMatch = false
}
if (password.length >= 8) {
lengthSvgContainer.classList.remove('bg-red-200');
lengthSvgContainer.classList.add('bg-green-200');
lengthStatusText.classList.remove('text-red-700');
lengthStatusText.classList.add('text-green-700');
lengthGoodPath.style.display = 'inline';
lengthBadPath.style.display = 'none';
lengthStatusText.textContent = "Password length meets requirement";
isValid = true
} else {
lengthSvgContainer.classList.remove('bg-green-200');
lengthSvgContainer.classList.add('bg-red-200');
lengthStatusText.classList.remove('text-green-700');
lengthStatusText.classList.add('text-red-700');
lengthGoodPath.style.display = 'none';
lengthBadPath.style.display = 'inline';
lengthStatusText.textContent = "Password length must be at least 8 characters";
isValid = false
}
var symbolCheck = symbolRegex.test(password);
var uppercaseCheck = uppercaseRegex.test(password);
var numberCount = (password.match(numberRegex) || []).length;
if (symbolCheck && uppercaseCheck && numberCount >= 3) {
requirementsSvgContainer.classList.remove('bg-red-200');
requirementsSvgContainer.classList.add('bg-green-200');
requirementsStatusText.classList.remove('text-red-700');
requirementsStatusText.classList.add('text-green-700');
requirementsGoodPath.style.display = 'inline';
requirementsBadPath.style.display = 'none';
requirementsStatusText.textContent = "Password meets additional requirements";
isSecure = true
} else {
requirementsSvgContainer.classList.remove('bg-green-200');
requirementsSvgContainer.classList.add('bg-red-200');
requirementsStatusText.classList.remove('text-green-700');
requirementsStatusText.classList.add('text-red-700');
requirementsGoodPath.style.display = 'none';
requirementsBadPath.style.display = 'inline';
requirementsStatusText.textContent = "The password must contain at least one symbol (!@#$%^&*), one uppercase letter, and three numbers";
isSecure = false
}
if (isSecure && isValid && isSecure && password === confirmPassword) {
document.getElementById("submit").disabled = false;
} else {
document.getElementById("submit").disabled = true;
}
}
document.getElementById('password').addEventListener('input', validatePasswords);
document.getElementById('confirmPassword').addEventListener('input', validatePasswords);

166
routes/routes.go Normal file
View File

@ -0,0 +1,166 @@
package routes
import (
downloadHandler "github.com/fossyy/filekeeper/handler/download"
downloadFileHandler "github.com/fossyy/filekeeper/handler/download/file"
forgotPasswordHandler "github.com/fossyy/filekeeper/handler/forgotPassword"
forgotPasswordVerifyHandler "github.com/fossyy/filekeeper/handler/forgotPassword/verify"
indexHandler "github.com/fossyy/filekeeper/handler/index"
logoutHandler "github.com/fossyy/filekeeper/handler/logout"
miscHandler "github.com/fossyy/filekeeper/handler/misc"
signinHandler "github.com/fossyy/filekeeper/handler/signin"
signupHandler "github.com/fossyy/filekeeper/handler/signup"
signupVerifyHandler "github.com/fossyy/filekeeper/handler/signup/verify"
uploadHandler "github.com/fossyy/filekeeper/handler/upload"
"github.com/fossyy/filekeeper/handler/upload/initialisation"
userHandler "github.com/fossyy/filekeeper/handler/user"
"github.com/fossyy/filekeeper/middleware"
"net/http"
)
func SetupRoutes() *http.ServeMux {
handler := http.NewServeMux()
handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/":
switch r.Method {
case http.MethodGet:
indexHandler.GET(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
default:
w.WriteHeader(http.StatusNotFound)
}
})
handler.HandleFunc("/signin", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
middleware.Guest(signinHandler.GET, w, r)
case http.MethodPost:
middleware.Guest(signinHandler.POST, w, r)
}
})
signupRouter := http.NewServeMux()
handler.Handle("/signup/", http.StripPrefix("/signup", signupRouter))
signupRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
middleware.Guest(signupHandler.GET, w, r)
case http.MethodPost:
middleware.Guest(signupHandler.POST, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
signupRouter.HandleFunc("/verify/{code}", func(w http.ResponseWriter, r *http.Request) {
middleware.Guest(signupVerifyHandler.GET, w, r)
})
forgotPasswordRouter := http.NewServeMux()
handler.Handle("/forgot-password/", http.StripPrefix("/forgot-password", forgotPasswordRouter))
forgotPasswordRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
middleware.Guest(forgotPasswordHandler.GET, w, r)
case http.MethodPost:
middleware.Guest(forgotPasswordHandler.POST, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
forgotPasswordRouter.HandleFunc("/verify/{code}", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
middleware.Guest(forgotPasswordVerifyHandler.GET, w, r)
case http.MethodPost:
middleware.Guest(forgotPasswordVerifyHandler.POST, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
handler.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
middleware.Auth(userHandler.GET, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
// Upload router
uploadRouter := http.NewServeMux()
handler.Handle("/upload/", http.StripPrefix("/upload", uploadRouter))
uploadRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
middleware.Auth(uploadHandler.GET, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
uploadRouter.HandleFunc("/{id}", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
middleware.Auth(uploadHandler.POST, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
uploadRouter.HandleFunc("/init", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
middleware.Auth(initialisation.POST, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
// Download router
downloadRouter := http.NewServeMux()
handler.Handle("/download/", http.StripPrefix("/download", downloadRouter))
downloadRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
middleware.Auth(downloadHandler.GET, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
downloadRouter.HandleFunc("/{id}", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
downloadFileHandler.GET(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
handler.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
middleware.Auth(logoutHandler.GET, w, r)
})
handler.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
miscHandler.Robot(w, r)
})
handler.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/public/favicon.ico", http.StatusSeeOther)
})
fileServer := http.FileServer(http.Dir("./public"))
handler.Handle("/public/", http.StripPrefix("/public", fileServer))
return handler
}

3
schema.sql Normal file
View File

@ -0,0 +1,3 @@
CREATE TABLE IF NOT EXISTS users (user_id VARCHAR(255) PRIMARY KEY NOT NULL,username VARCHAR(255) UNIQUE NOT NULL,email VARCHAR(255) UNIQUE NOT NULL,password TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS files (id VARCHAR(255) PRIMARY KEY NOT NULL,owner_id VARCHAR(255) NOT NULL,name TEXT NOT NULL,size BIGINT NOT NULL,downloaded BIGINT NOT NULL,FOREIGN KEY (owner_id) REFERENCES users(user_id));
CREATE TABLE IF NOT EXISTS files_uploadeds (upload_id VARCHAR(255) PRIMARY KEY NOT NULL,file_id VARCHAR(255) NOT NULL,owner_id VARCHAR(255) NOT NULL,name TEXT NOT NULL,size INT NOT NULL,uploaded INT NOT NULL DEFAULT 0,done BOOLEAN NOT NULL DEFAULT FALSE,FOREIGN KEY (file_id) REFERENCES files(id),FOREIGN KEY (owner_id) REFERENCES users(user_id));

100
session/session.go Normal file
View File

@ -0,0 +1,100 @@
package session
import (
"github.com/fossyy/filekeeper/utils"
"net/http"
"strconv"
"sync"
)
type Session struct {
ID string
Values map[string]interface{}
}
type StoreSession struct {
Sessions map[string]*Session
mu sync.Mutex
}
var Store = StoreSession{Sessions: make(map[string]*Session)}
var userSessions = make(map[string][]string)
type SessionNotFound struct{}
func (e *SessionNotFound) Error() string {
return "session not found"
}
func (s *StoreSession) Get(id string) (*Session, error) {
s.mu.Lock()
defer s.mu.Unlock()
if session, ok := s.Sessions[id]; ok {
return session, nil
}
return nil, &SessionNotFound{}
}
func (s *StoreSession) Create() *Session {
id := utils.GenerateRandomString(128)
session := &Session{
ID: id,
Values: make(map[string]interface{}),
}
s.Sessions[id] = session
return session
}
func (s *StoreSession) Delete(id string) {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.Sessions, id)
}
func (s *Session) Save(w http.ResponseWriter) {
maxAge, _ := strconv.Atoi(utils.Getenv("SESSION_MAX_AGE"))
http.SetCookie(w, &http.Cookie{
Name: utils.Getenv("SESSION_NAME"),
Value: s.ID,
MaxAge: maxAge,
})
}
func (s *Session) Destroy(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: utils.Getenv("SESSION_NAME"),
Value: "",
MaxAge: -1,
})
}
func AppendSession(email string, session *Session) {
userSessions[email] = append(userSessions[email], session.ID)
}
func RemoveSession(email string, id string) {
sessions := userSessions[email]
var updatedSessions []string
for _, userSession := range sessions {
if userSession != id {
updatedSessions = append(updatedSessions, userSession)
}
}
if len(updatedSessions) > 0 {
userSessions[email] = updatedSessions
return
}
delete(userSessions, email)
}
func RemoveAllSession(email string) {
sessions := userSessions[email]
for _, session := range sessions {
delete(Store.Sessions, session)
}
delete(userSessions, email)
}
func Getses() map[string][]string {
return userSessions
}

10
staging.bat Normal file
View File

@ -0,0 +1,10 @@
@echo off
REM Start the Go server using Air
start "" air
REM Watch for changes in Tailwind CSS
start "" npx tailwindcss -i ./public/input.css -o ./public/output.css --watch
REM Watch for changes in templates and proxy to Go server
start "" cmd /k "templ generate -watch -proxy=http://localhost:8000"

5
staging.sh Normal file
View File

@ -0,0 +1,5 @@
#!/bin/bash
air &
npx tailwindcss -i ./public/input.css -o ./public/output.css --watch &
templ generate -watch -proxy=http://localhost:8000

9
tailwind.config.js Normal file
View File

@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./view/**/*.{html,js,templ}"],
theme: {
extend: {},
},
plugins: [],
}

28
types/models/models.go Normal file
View File

@ -0,0 +1,28 @@
package models
import "github.com/google/uuid"
type User struct {
UserID uuid.UUID `gorm:"primaryKey;not null;unique"`
Username string `gorm:"unique;not null"`
Email string `gorm:"unique;not null"`
Password string `gorm:"not null"`
}
type File struct {
ID uuid.UUID `gorm:"primaryKey;not null;unique"`
OwnerID uuid.UUID `gorm:"not null"`
Name string `gorm:"not null"`
Size int `gorm:"not null"`
Downloaded int `gorm:"not null;default=0"`
}
type FilesUploaded struct {
UploadID uuid.UUID `gorm:"primaryKey;not null;unique"`
FileID uuid.UUID `gorm:"not null"`
OwnerID uuid.UUID `gorm:"not null"`
Name string `gorm:"not null"`
Size int `gorm:"not null"`
Uploaded int `gorm:"not null;default=0"`
Done bool `gorm:"not null;default=false"`
}

46
types/types.go Normal file
View File

@ -0,0 +1,46 @@
package types
import (
"github.com/google/uuid"
"time"
)
type Message struct {
Code int
Message string
}
type User struct {
UserID uuid.UUID
Email string
Username string
Authenticated bool
}
type UserWithExpired struct {
UserID uuid.UUID
Username string
Email string
Password string
AccessAt time.Time
}
type FileInfo struct {
Name string `json:"name"`
Size int `json:"size"`
Chunk int `json:"chunk"`
}
type FileInfoUploaded struct {
Name string `json:"name"`
Size int `json:"size"`
Chunk int `json:"chunk"`
UploadedChunk int `json:"uploaded_chunk"`
}
type FileData struct {
ID string
Name string
Size string
Downloaded int
}

142
utils/utils.go Normal file
View File

@ -0,0 +1,142 @@
package utils
import (
"fmt"
"github.com/fossyy/filekeeper/logger"
"github.com/joho/godotenv"
"golang.org/x/crypto/bcrypt"
"math/rand"
"net/http"
"os"
"strings"
"sync"
"time"
"unicode"
)
type Env struct {
value map[string]string
mu sync.Mutex
}
var env *Env
var log *logger.AggregatedLogger
func init() {
env = &Env{value: map[string]string{}}
}
func ClientIP(request *http.Request) string {
ip := request.Header.Get("X-Real-IP")
if ip == "" {
ip = request.Header.Get("X-Forwarded-For")
if ip == "" {
ip = request.RemoteAddr
}
}
if strings.Contains(ip, ",") {
ips := strings.Split(ip, ",")
ip = strings.TrimSpace(ips[0])
}
if strings.Contains(ip, ":") {
ips := strings.Split(ip, ":")
ip = ips[0]
}
return ip
}
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
func ValidatePassword(password string) bool {
if len(password) < 6 {
return false
}
var (
hasSymbol bool
hasNumber int
hasUppercase bool
)
symbols := []string{"!", "@", "#", "$", "%", "^", "&", "*"}
for _, symbol := range symbols {
if strings.Contains(password, symbol) {
hasSymbol = true
}
}
for _, char := range password {
switch {
case unicode.IsNumber(char):
hasNumber++
case unicode.IsUpper(char):
hasUppercase = true
}
}
return hasSymbol && hasNumber >= 3 && hasUppercase
}
func ConvertFileSize(byte int) string {
if byte < 1024 {
return fmt.Sprintf("%d B", byte)
} else if byte < 1024*1024 {
return fmt.Sprintf("%d KB", byte/1024)
} else if byte < 1024*1024*1024 {
return fmt.Sprintf("%d MB", byte/(1024*1024))
} else {
return fmt.Sprintf("%d GB", byte/(1024*1024*1024))
}
}
func Getenv(key string) string {
env.mu.Lock()
defer env.mu.Unlock()
if val, ok := env.value[key]; ok {
return val
}
err := godotenv.Load(".env")
if err != nil {
log.Error("Error loading .env file: %s", err)
}
val := os.Getenv(key)
env.value[key] = val
return val
}
func GenerateRandomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
seededRand := rand.New(rand.NewSource(time.Now().UnixNano()))
var result strings.Builder
for i := 0; i < length; i++ {
randomIndex := seededRand.Intn(len(charset))
result.WriteString(string(charset[randomIndex]))
}
return result.String()
}
func SanitizeFilename(filename string) string {
invalidChars := []string{"\\", "/", ":", "*", "?", "\"", "<", ">", "|"}
for _, char := range invalidChars {
filename = strings.ReplaceAll(filename, char, "_")
}
return filename
}

View File

@ -0,0 +1,76 @@
package downloadView
import (
"github.com/fossyy/filekeeper/view/layout"
"github.com/fossyy/filekeeper/types"
)
templ component(title string, files []types.FileData){
@layout.Base(title){
<div class="dark min-h-screen p-4 sm:p-6 bg-gray-900 text-white">
<div class="space-y-4">
<header class="text-center">
<div class="space-y-2">
<h1 class="text-3xl font-bold tracking-tighter sm:text-4xl">Download Files</h1>
</div>
</header>
<div class="mx-auto grid w-full max-w-3xl gap-4 px-4">
for _, file := range files {
<div class="rounded-lg border bg-card text-card-foreground shadow-sm">
<div class="flex space-y-4 flex-col p-4">
<div class="space-y-1">
<h2 class="text-lg font-bold tracking-wide">{ file.Name }</h2>
<p class="text-sm leading-none"> { file.Size }</p>
</div>
<div class="space-x-2">
<button class="inline-flex items-center justify-center whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-9 rounded-md px-3">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
>
<path d="M4 13.5V4a2 2 0 0 1 2-2h8.5L20 7.5V20a2 2 0 0 1-2 2h-5.5"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<path d="M10.42 12.61a2.1 2.1 0 1 1 2.97 2.97L7.95 21 4 22l.99-3.95 5.43-5.44Z"></path>
</svg>
<span class="sr-only">Edit</span>
</button>
<a href={ templ.SafeURL("/download/" + file.ID) } class="inline-flex items-center justify-center p-5 text-base font-medium text-gray-500 rounded-lg bg-gray-50 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-700 dark:hover:text-white">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" x2="12" y1="15" y2="3"></line>
</svg>
</a>
</div>
</div>
</div>
}
</div>
</div>
</div>
}
}
templ Main(title string, files []types.FileData){
@component(title, files)
}

111
view/email/email.templ Normal file
View File

@ -0,0 +1,111 @@
package emailView
templ RegistrationEmail(name string, link string) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Email Verification</title>
<style>
body, html {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
font-size: 16px;
line-height: 1.6;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
h1 {
color: #333;
}
.button {
display: inline-block;
padding: 10px 20px;
background-color: black;
color: white;
text-decoration: none;
border-radius: 5px;
}
.button:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="container">
<h1>Email Verification</h1>
<p>Dear {name},</p>
<p>Please verify your email address by clicking the button below:</p>
<a href={ templ.SafeURL(link)} class="button">Verify Email</a>
<p>Or copy and paste this URL into a new tab of your browser: <a href={ templ.SafeURL(link)}><br/>{link}</a></p>
<p>If you did not request this verification, please disregard this email.</p>
<p>Thank you, <br/> The Filekeeper Team</p>
</div>
</body>
</html>
}
templ ForgotPassword(name string, link string) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Email Verification</title>
<style>
body, html {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
font-size: 16px;
line-height: 1.6;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
h1 {
color: #333;
}
.button {
display: inline-block;
padding: 10px 20px;
background-color: black;
color: white;
text-decoration: none;
border-radius: 5px;
}
.button:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="container">
<h1>Password Change Request</h1>
<p>Dear {name},</p>
<p>Please verify your password change request by clicking the button below:</p>
<a href={ templ.SafeURL(link)} class="button">Verify Password Change</a>
<p>Or copy and paste this URL into a new tab of your browser: <a href={ templ.SafeURL(link)}><br/>{link}</a></p>
<p>If you did not request this password change, please disregard this email.</p>
<p>Thank you, <br/> The Filekeeper Team</p>
</div>
</body>
</html>
}

39
view/error/error.templ Normal file
View File

@ -0,0 +1,39 @@
package errorView
import "github.com/fossyy/filekeeper/view/layout"
templ content(title string){
@layout.Base(title){
<div class="flex flex-col items-center justify-center w-full min-h-[calc(100vh-1rem)] py-10 text-center gap-4 md:gap-8">
<div class="space-y-2">
<h1 class="text-4xl font-bold tracking-tighter sm:text-5xl">404 Not Found</h1>
<p class="max-w-[600px] text-gray-500 md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed dark:text-gray-400">
The page you are looking for does not exist. It might have been moved or deleted.
</p>
</div>
<a
class="inline-flex h-10 items-center rounded-md border border-gray-200 border-gray-200 bg-white px-8 text-sm font-medium shadow-sm gap-2 transition-colors hover:bg-gray-100 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-950 dark:border-gray-800 dark:border-gray-800 dark:bg-gray-950 dark:hover:bg-gray-800 dark:hover:text-gray-50 dark:focus-visible:ring-gray-300"
href="/"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="w-4 h-4"
>
<path d="m9 18 6-6-6-6"></path>
</svg>
</a>
</div>
}
}
templ Main(title string) {
@content(title)
}

View File

@ -0,0 +1,169 @@
package forgotPasswordView
import (
"github.com/fossyy/filekeeper/types"
"github.com/fossyy/filekeeper/view/layout"
)
templ content(title string, err types.Message) {
@layout.Base(title){
<div class="dark flex items-center min-h-screen p-4 sm:p-6 bg-gray-900">
<div class="mx-auto w-full max-w-md space-y-8">
<header class="text-center">
<div class="space-y-2">
<h1 class="text-3xl font-bold text-white">Forgot password</h1>
<p class="text-gray-500 dark:text-gray-400">Enter your email below to reset your password</p>
switch err.Code {
case 0:
<div class="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
<span class="font-medium">Danger alert!</span> {err.Message}
</div>
}
</div>
</header>
<form class="space-y-4" method="post" action="">
<div class="space-y-2">
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="email">Email</label>
<input type="email" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="email" name="email" placeholder="m@example.com" required="" />
</div>
<button class="bg-slate-200 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full" type="submit">
Submit
</button>
</form>
</div>
</div>
}
}
templ Main(title string, err types.Message) {
@content(title, err)
}
templ NewPasswordForm(title string, err types.Message) {
@layout.Base(title){
<div class="dark flex items-center min-h-screen p-4 sm:p-6 bg-gray-900">
<div class="mx-auto w-full max-w-md space-y-8">
<header class="text-center">
<div class="space-y-2">
<h1 class="text-3xl font-bold text-white">Forgot password</h1>
<p class="text-gray-500 dark:text-gray-400">Enter your email below to reset your password</p>
switch err.Code {
case 0:
<div class="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
<span class="font-medium">Danger alert!</span> {err.Message}
</div>
}
</div>
</header>
<form class="space-y-4" method="post" action="">
<div class="space-y-2">
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="password">Password</label>
<input type="password" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="password" name="password" required />
</div>
<div class="space-y-2">
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="confirmPassword">Confirm Password</label>
<input type="password" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="confirmPassword" required />
</div>
<div class="flex justify-start mt-3 ml-4 p-1">
<ul>
<li class="flex items-center py-1">
<div id="matchSvgContainer" class="rounded-full p-1 fill-current bg-red-200 text-green-700">
<svg id="matchSvgIcon" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path id="matchGoodPath" style="display: none;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
<path id="matchBadPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
<span id="matchStatusText" class="font-medium text-sm ml-3 text-red-700"> Passwords do not match</span>
</li>
<li class="flex items-center py-1">
<div id="lengthSvgContainer" class="rounded-full p-1 fill-current bg-red-200 text-green-700">
<svg id="lengthSvgIcon" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path id="lengthGoodPath" style="display: none;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
<path id="lengthBadPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
<span id="lengthStatusText" class="font-medium text-sm ml-3 text-red-700"> Password length must be at least 8 characters</span>
</li>
<li class="flex items-center py-1">
<div id="requirementsSvgContainer" class="rounded-full p-1 fill-current bg-red-200 text-green-700">
<svg id="requirementsSvgIcon" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path id="requirementsGoodPath" style="display: none;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
<path id="requirementsBadPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
<span id="requirementsStatusText" class="font-medium text-sm ml-3 text-red-700">The password must contain at least one symbol (!@#$%^&*), one uppercase letter, and three numbers</span>
</li>
</ul>
</div>
<button class="bg-slate-200 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full" type="submit" id="submit" name="submit" disabled>
Submit
</button>
</form>
</div>
</div>
<script src="/public/validatePassword.js" />
}
}
templ EmailSend(title string) {
@layout.Base(title){
<style>h1, h2, h3, h4, h5, h6 { font-family: 'Arimo', sans-serif; --font-sans: 'Arimo'; }</style>
<style>body { font-family: 'Libre Franklin', sans-serif; --font-sans: 'Libre Franklin'; }</style>
<div class="flex flex-col items-center justify-center min-h-[80vh] gap-6">
<div class="flex items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-16 w-16 text-gray-500 dark:text-gray-400"
>
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
</svg>
</div>
<div class="space-y-2 text-center">
<h1 class="text-3xl font-bold">Email Verification Sent</h1>
<p class="text-gray-500 dark:text-gray-400">
We've sent a verification email to your inbox. Please check your email and follow the instructions to change your password.
</p>
</div>
</div>
}
}
templ ChangeSuccess(title string) {
@layout.Base(title){
<style>h1, h2, h3, h4, h5, h6 { font-family: 'Arimo', sans-serif; --font-sans: 'Arimo'; }</style>
<style>body { font-family: 'Libre Franklin', sans-serif; --font-sans: 'Libre Franklin'; }</style>
<div class="flex flex-col items-center justify-center min-h-[80vh] gap-6">
<div class="bg-green-500 text-white rounded-full p-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-8 w-8"
>
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</div>
<div class="space-y-2 text-center">
<h1 class="text-3xl font-bold">Password Changed Successfully</h1>
<p class="text-gray-500 dark:text-gray-400">
Your password has been successfully updated. Feel free to continue enjoying our platform.
</p>
</div>
</div>
}
}

103
view/index/index.templ Normal file
View File

@ -0,0 +1,103 @@
package indexView
import (
"github.com/fossyy/filekeeper/view/layout"
)
templ content(title string) {
@layout.Base(title){
<div class="bg-white">
<nav class="container mx-auto px-6 py-4 flex justify-between items-center">
<a href="#" class="text-blue-500 text-xl font-semibold">
C.
</a>
<div class="flex space-x-4">
<a href="/signup" class="text-gray-600 hover:text-gray-800">
Sign up
</a>
<a href="/signin" class="text-gray-600 hover:text-gray-800">
Sign in
</a>
</div>
</nav>
<header class="container mx-auto px-6 py-16 text-center">
<h1 class="text-5xl font-bold text-gray-900 mb-2">Your files, always within reach.</h1>
<p class="text-gray-700 text-lg mb-8">
Store, access, and share your files from anywhere. We offer secure and reliable file storage, so you can
focus on what matters most.
</p>
<div class="flex justify-center items-center space-x-4">
<div class="sm:flex sm:justify-center lg:justify-start">
<div class="rounded-md shadow">
<a class="w-full flex items-center justify-center px-8 py-3 text-base leading-6 font-medium rounded-md text-white bg-pink-400 hover:bg-pink-500 hover:text-white focus:ring ring-offset-2 ring-pink-400 focus:outline-none transition duration-150 ease-in-out md:py-4 md:text-lg md:px-10"
href="/signup">
Get started
</a>
</div>
</div>
</div>
<div class="flex justify-center items-center space-x-4 mt-8">
<div class="grid gap-4 md:gap-8 lg:gap-4 items-start sm:grid-cols-2">
<div class="flex items-center space-x-4">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="w-6 h-6 flex-shrink-0 text-gray-500">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
<div class="text-sm font-medium leading-none md:text-base">
Secure encryption to protect your data
</div>
</div>
<div class="flex items-center space-x-4">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="w-6 h-6 flex-shrink-0 text-gray-500">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
<div class="text-sm font-medium leading-none md:text-base">
Easy file sharing with customizable permissions
</div>
</div>
<div class="flex items-center space-x-4">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="w-6 h-6 flex-shrink-0 text-gray-500">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
<div class="text-sm font-medium leading-none md:text-base">
Unlimited storage capacity for your files
</div>
</div>
<div class="flex items-center space-x-4">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="w-6 h-6 flex-shrink-0 text-gray-500">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
<div class="text-sm font-medium leading-none md:text-base">
Seamless collaboration with built-in productivity tools
</div>
</div>
</div>
</div>
</header>
</div>
<footer class="w-full border-t">
<div class="container grid-inset py-12 gap-4 text-center md:gap-8 md:py-16">
<div class="flex items-center justify-center gap-2 text-xs tracking-wide md:gap-4">
<p class="text-gray-500 dark:text-gray-400">© 2023 Acme, Inc. All rights reserved.</p>
<p class="text-gray-500 dark:text-gray-400">Terms of Service</p>
<p class="text-gray-500 dark:text-gray-400">Privacy Policy</p>
</div>
</div>
</footer>
}
}
templ Main(title string) {
@content(title)
}

16
view/layout/base.templ Normal file
View File

@ -0,0 +1,16 @@
package layout
templ Base(title string){
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="/public/output.css" rel="stylesheet"/>
<title>{ title }</title>
</head>
<body>
{ children... }
</body>
</html>
}

55
view/signin/signin.templ Normal file
View File

@ -0,0 +1,55 @@
package signinView
import (
"github.com/fossyy/filekeeper/types"
"github.com/fossyy/filekeeper/view/layout"
)
templ content(err types.Message, title string) {
@layout.Base(title){
<div class="dark flex items-center min-h-screen p-4 sm:p-6 bg-gray-900">
<div class="mx-auto w-full max-w-md space-y-8">
<header class="text-center">
<div class="space-y-2">
<h1 class="text-3xl font-bold text-white">Sign In</h1>
<p class="text-gray-500 dark:text-gray-400">Enter your email or username below to login to your account</p>
switch err.Code {
case 0:
<div class="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
<span class="font-medium">Danger alert!</span> {err.Message}
</div>
}
</div>
</header>
<form class="space-y-4" method="post" action="">
<div class="space-y-2">
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="email">Email</label>
<input type="email" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="email" name="email" placeholder="m@example.com" required="" />
</div>
<div class="space-y-2">
<div class="flex items-center">
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="password">Password</label>
<a class="ml-auto inline-block text-sm underline text-gray-300 dark:text-gray-400" href="/forgot-password" rel="ugc">
Forgot your password?
</a>
</div>
<input type="password" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="password" name="password" required="" />
</div>
<button class="bg-slate-200 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full" type="submit">
Login
</button>
</form>
<div class="text-center text-sm text-white">
Don't have an account?
<a class="underline" href="/signup" rel="ugc">
Sign up
</a>
</div>
</div>
</div>
}
}
templ Main(title string, err types.Message) {
@content(err, title)
}

159
view/signup/signup.templ Normal file
View File

@ -0,0 +1,159 @@
package signup
import (
"github.com/fossyy/filekeeper/types"
"github.com/fossyy/filekeeper/view/layout"
)
templ form(err types.Message, title string) {
@layout.Base(title){
<div class="dark flex items-center min-h-screen p-4 sm:p-6 bg-gray-900">
<div class="mx-auto w-full max-w-md space-y-8">
<header class="text-center">
<div class="space-y-2">
<h1 class="text-3xl font-bold text-white">Sign Up</h1>
<p class="text-gray-500 dark:text-gray-400">Enter your email below to login to your account</p>
switch err.Code {
case 0:
<div class="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
<span class="font-medium">Danger alert!</span> {err.Message}
</div>
case 1:
<div class="p-4 mb-4 text-sm text-green-800 rounded-lg bg-green-50 dark:bg-gray-800 dark:text-green-400" role="alert">
<span class="font-medium">Success alert!</span> {err.Message}
</div>
}
</div>
</header>
<form class="space-y-4" method="post" action="">
<div class="space-y-2">
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="email">Email</label>
<input type="email" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="email" name="email" placeholder="m@example.com" required="" />
</div>
<div class="space-y-2">
<div class="flex items-center">
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="password">Username</label>
</div>
<input type="text" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="username" name="username" required="" />
</div>
<div class="space-y-2">
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="password">Password</label>
<input type="password" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="password" name="password" required />
</div>
<div class="space-y-2">
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="confirmPassword">Confirm Password</label>
<input type="password" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="confirmPassword" required />
</div>
<div class="flex justify-start mt-3 ml-4 p-1">
<ul>
<li class="flex items-center py-1">
<div id="matchSvgContainer" class="rounded-full p-1 fill-current bg-red-200 text-green-700">
<svg id="matchSvgIcon" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path id="matchGoodPath" style="display: none;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
<path id="matchBadPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
<span id="matchStatusText" class="font-medium text-sm ml-3 text-red-700"> Passwords do not match</span>
</li>
<li class="flex items-center py-1">
<div id="lengthSvgContainer" class="rounded-full p-1 fill-current bg-red-200 text-green-700">
<svg id="lengthSvgIcon" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path id="lengthGoodPath" style="display: none;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
<path id="lengthBadPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
<span id="lengthStatusText" class="font-medium text-sm ml-3 text-red-700"> Password length must be at least 8 characters</span>
</li>
<li class="flex items-center py-1">
<div id="requirementsSvgContainer" class="rounded-full p-1 fill-current bg-red-200 text-green-700">
<svg id="requirementsSvgIcon" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path id="requirementsGoodPath" style="display: none;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
<path id="requirementsBadPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
<span id="requirementsStatusText" class="font-medium text-sm ml-3 text-red-700">The password must contain at least one symbol (!@#$%^&*), one uppercase letter, and three numbers</span>
</li>
</ul>
</div>
<button class="bg-slate-200 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full" type="submit" id="submit" name="submit" disabled>
Sign up
</button>
</form>
<div class="text-center text-sm text-white">
Already have an account?
<a class="underline" href="/signin" rel="ugc">
Sign in
</a>
</div>
</div>
</div>
<script src="/public/validatePassword.js" />
}
}
templ Main(title string, err types.Message) {
@form(err, title)
}
templ EmailSend(title string) {
@layout.Base(title){
<style>h1, h2, h3, h4, h5, h6 { font-family: 'Arimo', sans-serif; --font-sans: 'Arimo'; }</style>
<style>body { font-family: 'Libre Franklin', sans-serif; --font-sans: 'Libre Franklin'; }</style>
<div class="flex flex-col items-center justify-center min-h-[80vh] gap-6">
<div class="flex items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-16 w-16 text-gray-500 dark:text-gray-400"
>
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
</svg>
</div>
<div class="space-y-2 text-center">
<h1 class="text-3xl font-bold">Email Verification Sent</h1>
<p class="text-gray-500 dark:text-gray-400">
We've sent a verification email to your inbox. Please check your email and follow the instructions to verify your account.
</p>
</div>
</div>
}
}
templ VerifySuccess(title string) {
@layout.Base(title){
<style>h1, h2, h3, h4, h5, h6 { font-family: 'Arimo', sans-serif; --font-sans: 'Arimo'; }</style>
<style>body { font-family: 'Libre Franklin', sans-serif; --font-sans: 'Libre Franklin'; }</style>
<div class="flex flex-col items-center justify-center min-h-[80vh] gap-6">
<div class="bg-green-500 text-white rounded-full p-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-8 w-8"
>
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</div>
<div class="space-y-2 text-center">
<h1 class="text-3xl font-bold">Account Verified</h1>
<p class="text-gray-500 dark:text-gray-400">
Your account has been successfully verified. You can now access all the features of our platform.
</p>
</div>
</div>
}
}

56
view/upload/upload.templ Normal file
View File

@ -0,0 +1,56 @@
package uploadView
import "github.com/fossyy/filekeeper/view/layout"
templ content(title string) {
@layout.Base(title){
<div class="flex items-center min-h-screen p-4 sm:p-6 bg-gray-900 text-white">
<div class="mx-auto w-full max-w-md space-y-8">
<div class="rounded-lg border bg-card text-card-foreground shadow-sm w-full max-w-md" data-v0-t="card">
<div class="flex flex-col space-y-1.5 p-4">
<div class="flex items-center justify-center w-full">
<label for="dropzone-file"
class="flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:hover:bg-bray-800 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600">
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<svg class="w-8 h-8 mb-4 text-gray-400" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2" />
</svg>
<p class="mb-2 text-sm text-gray-400"><span
class="font-semibold">Click to upload</span> or drag and drop</p>
<p class="text-xs text-gray-400">SVG, PNG, JPG or GIF (MAX.
800x400px)</p>
</div>
<input id="dropzone-file" type="file" class="hidden" />
</label>
</div>
<div>
<div hidden>
<div class="flex items-center gap-x-3 whitespace-nowrap">
<div id="progress-fake" class="flex w-full h-2 rounded-full overflow-hidden bg-gray-700"
role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100">
<div id="progress-fake"
class="flex flex-col justify-center rounded-full overflow-hidden bg-teal-500 text-xs text-white text-center whitespace-nowrap transition duration-500">
</div>
</div>
<div class="w-6 text-end">
<span id="progress-fake" class="text-sm text-white">Starting...</span>
</div>
</div>
</div>
<div id="container"></div>
</div>
</div>
</div>
</div>
</div>
<script src="/public/upload.js" />
}
}
templ Main(title string) {
@content(title)
}

53
view/user/user.templ Normal file
View File

@ -0,0 +1,53 @@
package userView
import "github.com/fossyy/filekeeper/view/layout"
templ content(email string, username string, title string) {
@layout.Base(title){
<div class="bg-white overflow-hidden shadow rounded-lg border">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
User Profile
</h3>
<p class="mt-1 max-w-2xl text-sm text-gray-500">
This is some information about the user.
</p>
</div>
<div class="border-t border-gray-200 px-4 py-5 sm:p-0">
<dl class="sm:divide-y sm:divide-gray-200">
<div class="py-3 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">
Full name
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{ username }
</dd>
</div>
<div class="py-3 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">
Email address
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{ email }
</dd>
</div>
<div class="py-3 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">
Password
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
ntah lah
</dd>
</div>
</dl>
</div>
<form action="/logout" method="get">
<button type="submit" class="focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900">Logout</button>
</form>
</div>
}
}
templ Main(title string, email string, username string) {
@content(email, username, title)
}