Add password validation
This commit is contained in:
46
.air.toml
Normal file
46
.air.toml
Normal 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
17
.env
Normal 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
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
/tmp
|
||||
/uploads
|
||||
/public/output.css
|
||||
/log
|
||||
|
||||
*_templ.txt
|
||||
*_templ.go
|
9
Makefile
Normal file
9
Makefile
Normal 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
39
db/database.go
Normal 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
77
db/model/user/user.go
Normal 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
39
email/email.go
Normal 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
20
go.mod
Normal 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
25
go.sum
Normal 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=
|
62
handler/download/download.go
Normal file
62
handler/download/download.go
Normal 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
|
||||
}
|
||||
}
|
56
handler/download/file/file.go
Normal file
56
handler/download/file/file.go
Normal 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
23
handler/error/error.go
Normal 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
|
||||
}
|
||||
}
|
149
handler/forgotPassword/forgotPassword.go
Normal file
149
handler/forgotPassword/forgotPassword.go
Normal 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
|
||||
}
|
109
handler/forgotPassword/verify/verify.go
Normal file
109
handler/forgotPassword/verify/verify.go
Normal 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
23
handler/index/index.go
Normal 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
44
handler/logout/logout.go
Normal 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
25
handler/misc/misc.go
Normal 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
92
handler/signin/signin.go
Normal 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
179
handler/signup/signup.go
Normal 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
|
||||
}
|
54
handler/signup/verify/verify.go
Normal file
54
handler/signup/verify/verify.go
Normal 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
|
||||
}
|
||||
}
|
174
handler/upload/initialisation/initialisation.go
Normal file
174
handler/upload/initialisation/initialisation.go
Normal 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
142
handler/upload/upload.go
Normal 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
44
handler/user/user.go
Normal 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
60
logger/logger.go
Normal 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
23
main.go
Normal 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
146
middleware/middleware.go
Normal 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
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 943 B |
222
public/index.html
Normal file
222
public/index.html
Normal 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
3
public/input.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
1
public/robots.txt
Normal file
1
public/robots.txt
Normal file
@ -0,0 +1 @@
|
||||
User-agent: *
|
173
public/upload.js
Normal file
173
public/upload.js
Normal 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
|
||||
});
|
||||
}
|
||||
|
||||
|
97
public/validatePassword.js
Normal file
97
public/validatePassword.js
Normal 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
166
routes/routes.go
Normal 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
3
schema.sql
Normal 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
100
session/session.go
Normal 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
10
staging.bat
Normal 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
5
staging.sh
Normal 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
9
tailwind.config.js
Normal 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
28
types/models/models.go
Normal 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
46
types/types.go
Normal 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
142
utils/utils.go
Normal 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
|
||||
}
|
76
view/download/download.templ
Normal file
76
view/download/download.templ
Normal 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
111
view/email/email.templ
Normal 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
39
view/error/error.templ
Normal 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)
|
||||
}
|
169
view/forgotPassword/forgotPassword.templ
Normal file
169
view/forgotPassword/forgotPassword.templ
Normal 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
103
view/index/index.templ
Normal 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
16
view/layout/base.templ
Normal 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
55
view/signin/signin.templ
Normal 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
159
view/signup/signup.templ
Normal 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
56
view/upload/upload.templ
Normal 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
53
view/user/user.templ
Normal 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)
|
||||
}
|
Reference in New Issue
Block a user