diff --git a/api/invite.go b/api/invite.go
new file mode 100644
index 0000000..004b7b1
--- /dev/null
+++ b/api/invite.go
@@ -0,0 +1,54 @@
+package api
+
+import (
+ "strconv"
+ "time"
+
+ "go.jolheiser.com/invitea/database"
+)
+
+type Invite struct {
+ ID int64
+ Code string
+ Uses int64
+ Total int64
+ Expiration time.Time
+}
+
+func (i *Invite) TotalString() string {
+ if i.Total == 0 {
+ return "∞"
+ }
+ return strconv.FormatInt(i.Total, 10)
+}
+
+func (i *Invite) ExpirationString() string {
+ if i.Expiration.Unix() == 0 {
+ return "never"
+ }
+ return i.Expiration.Format("01/02/2006")
+}
+
+func (i *Invite) Valid() bool {
+ limited := i.Total != 0 && i.Uses >= i.Total
+ expired := i.Expiration.Unix() != 0 && time.Now().After(i.Expiration)
+ return !(limited || expired)
+}
+
+func InviteFromDB(i database.Invite) Invite {
+ return Invite{
+ ID: i.ID,
+ Code: i.Code,
+ Uses: i.Uses,
+ Total: i.Total.Int64,
+ Expiration: time.Unix(i.Expiration.Int64, 0).UTC(),
+ }
+}
+
+func InvitesFromDB(i ...database.Invite) []Invite {
+ invites := make([]Invite, 0, len(i))
+ for _, ii := range i {
+ invites = append(invites, InviteFromDB(ii))
+ }
+ return invites
+}
diff --git a/database/invites.sql.go b/database/invites.sql.go
index 26f02b7..10c08dd 100644
--- a/database/invites.sql.go
+++ b/database/invites.sql.go
@@ -7,8 +7,7 @@ package database
import (
"context"
-
- "go.jolheiser.com/invitea/database/sqlc"
+ "database/sql"
)
const countInvites = `-- name: CountInvites :one
@@ -34,8 +33,8 @@ RETURNING id, code, uses, total, expiration
type CreateInviteParams struct {
Code string
Uses int64
- Total int64
- Expiration sqlc.Timestamp
+ Total sql.NullInt64
+ Expiration sql.NullInt64
}
func (q *Queries) CreateInvite(ctx context.Context, arg CreateInviteParams) (Invite, error) {
@@ -68,11 +67,11 @@ func (q *Queries) DeleteInvite(ctx context.Context, id int64) error {
const getInvite = `-- name: GetInvite :one
SELECT id, code, uses, total, expiration FROM invites
-WHERE id = ? LIMIT 1
+WHERE code = ? LIMIT 1
`
-func (q *Queries) GetInvite(ctx context.Context, id int64) (Invite, error) {
- row := q.db.QueryRowContext(ctx, getInvite, id)
+func (q *Queries) GetInvite(ctx context.Context, code string) (Invite, error) {
+ row := q.db.QueryRowContext(ctx, getInvite, code)
var i Invite
err := row.Scan(
&i.ID,
@@ -86,7 +85,7 @@ func (q *Queries) GetInvite(ctx context.Context, id int64) (Invite, error) {
const listInvites = `-- name: ListInvites :many
SELECT id, code, uses, total, expiration FROM invites
-ORDER BY id
+ORDER BY id DESC
`
func (q *Queries) ListInvites(ctx context.Context) ([]Invite, error) {
@@ -117,3 +116,19 @@ func (q *Queries) ListInvites(ctx context.Context) ([]Invite, error) {
}
return items, nil
}
+
+const updateInvite = `-- name: UpdateInvite :exec
+UPDATE invites
+SET uses = ?
+WHERE id = ?
+`
+
+type UpdateInviteParams struct {
+ Uses int64
+ ID int64
+}
+
+func (q *Queries) UpdateInvite(ctx context.Context, arg UpdateInviteParams) error {
+ _, err := q.db.ExecContext(ctx, updateInvite, arg.Uses, arg.ID)
+ return err
+}
diff --git a/database/models.go b/database/models.go
index 514f0bd..e10298b 100644
--- a/database/models.go
+++ b/database/models.go
@@ -5,13 +5,13 @@
package database
import (
- "go.jolheiser.com/invitea/database/sqlc"
+ "database/sql"
)
type Invite struct {
ID int64
Code string
Uses int64
- Total int64
- Expiration sqlc.Timestamp
+ Total sql.NullInt64
+ Expiration sql.NullInt64
}
diff --git a/database/sqlc/migrations/001_schema.up.sql b/database/sqlc/migrations/001_schema.up.sql
index ec74e62..542a2ad 100644
--- a/database/sqlc/migrations/001_schema.up.sql
+++ b/database/sqlc/migrations/001_schema.up.sql
@@ -2,6 +2,6 @@ CREATE TABLE invites (
id INTEGER PRIMARY KEY,
code TEXT NOT NULL,
uses INTEGER NOT NULL,
- total INTEGER NOT NULL,
- expiration DATE NOT NULL
+ total INTEGER,
+ expiration INTEGER
);
\ No newline at end of file
diff --git a/database/sqlc/queries/invites.sql b/database/sqlc/queries/invites.sql
index 7c235ce..c085890 100644
--- a/database/sqlc/queries/invites.sql
+++ b/database/sqlc/queries/invites.sql
@@ -1,10 +1,10 @@
-- name: GetInvite :one
SELECT * FROM invites
-WHERE id = ? LIMIT 1;
+WHERE code = ? LIMIT 1;
-- name: ListInvites :many
SELECT * FROM invites
-ORDER BY id;
+ORDER BY id DESC;
-- name: CreateInvite :one
INSERT INTO invites (
@@ -14,6 +14,11 @@ INSERT INTO invites (
)
RETURNING *;
+-- name: UpdateInvite :exec
+UPDATE invites
+SET uses = ?
+WHERE id = ?;
+
-- name: DeleteInvite :exec
DELETE FROM invites
WHERE id = ?;
diff --git a/database/sqlc/sqlc.yaml b/database/sqlc/sqlc.yaml
index 6fb5ea8..25c1864 100644
--- a/database/sqlc/sqlc.yaml
+++ b/database/sqlc/sqlc.yaml
@@ -6,7 +6,4 @@ sql:
gen:
go:
package: "database"
- out: "../"
- overrides:
- - db_type: "DATE"
- go_type: "go.jolheiser.com/invitea/database/sqlc.Timestamp"
\ No newline at end of file
+ out: "../"
\ No newline at end of file
diff --git a/main.go b/main.go
index d40d097..a9e8c2c 100644
--- a/main.go
+++ b/main.go
@@ -4,15 +4,17 @@ import (
"database/sql"
"errors"
"fmt"
- "github.com/golang-migrate/migrate/v4"
"net/http"
"os"
"os/signal"
+ "strings"
"go.jolheiser.com/invitea/database"
"go.jolheiser.com/invitea/database/sqlc/migrations"
"go.jolheiser.com/invitea/router"
+ "code.gitea.io/sdk/gitea"
+ "github.com/golang-migrate/migrate/v4"
"github.com/peterbourgon/ff/v3"
"github.com/peterbourgon/ff/v3/ffyaml"
"github.com/rs/zerolog"
@@ -63,11 +65,17 @@ func main() {
log.Fatal().Err(err).Msg("could not migrate database")
}
+ client, err := gitea.NewClient(*giteaURLFlag, gitea.SetToken(*giteaTokenFlag))
+ if err != nil {
+ log.Fatal().Err(err).Msg("could not create gitea client")
+ }
+
r := router.New(router.Config{
Domain: *domainFlag,
SessionSecret: *sessionSecretFlag,
Database: database.New(db),
- GiteaURL: *giteaURLFlag,
+ GiteaClient: client,
+ GiteaURL: strings.TrimSuffix(*giteaURLFlag, "/"),
GiteaClientKey: *giteaClientKeyFlag,
GiteaClientSecret: *giteaClientSecretFlag,
GiteaToken: *giteaTokenFlag,
diff --git a/router/router.go b/router/router.go
index 52822bd..6506589 100644
--- a/router/router.go
+++ b/router/router.go
@@ -8,6 +8,7 @@ import (
"go.jolheiser.com/invitea/database"
"go.jolheiser.com/invitea/static"
+ sdk "code.gitea.io/sdk/gitea"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/gorilla/sessions"
@@ -21,6 +22,7 @@ type Config struct {
Domain string
SessionSecret string
Database *database.Queries
+ GiteaClient *sdk.Client
GiteaURL string
GiteaClientKey string
@@ -50,7 +52,9 @@ func New(cfg Config) *chi.Mux {
store := NewSessionStore(cfg.SessionSecret, cfg.GiteaURL)
routes := Routes{
- DB: cfg.Database,
+ DB: cfg.Database,
+ Gitea: cfg.GiteaClient,
+ GiteaURL: cfg.GiteaURL,
}
mux := chi.NewMux()
@@ -67,6 +71,12 @@ func New(cfg Config) *chi.Mux {
mux.Route("/", func(r chi.Router) {
r.Use(store.Middleware)
r.Get("/", routes.Index)
+ r.With(store.RequireAuth).Post("/", routes.IndexPost)
+ })
+
+ mux.Route("/invite/{code}", func(r chi.Router) {
+ r.Get("/", routes.Invite)
+ r.Post("/", routes.InvitePost)
})
mux.Route("/auth", func(r chi.Router) {
@@ -83,6 +93,17 @@ func New(cfg Config) *chi.Mux {
}
http.Redirect(w, r, cfg.Domain, http.StatusFound)
})
+ r.Get("/logout", func(w http.ResponseWriter, r *http.Request) {
+ if err := gothic.Logout(w, r); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if err := store.Logout(w, r); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ http.Redirect(w, r, cfg.Domain, http.StatusFound)
+ })
r.Get("/callback", func(w http.ResponseWriter, r *http.Request) {
user, err := gothic.CompleteUserAuth(w, r)
if err != nil {
diff --git a/router/routes.go b/router/routes.go
index f90f867..4e5fbe8 100644
--- a/router/routes.go
+++ b/router/routes.go
@@ -1,16 +1,26 @@
package router
import (
+ "crypto/rand"
+ "database/sql"
+ "encoding/hex"
"net/http"
+ "strconv"
+ "time"
+ "go.jolheiser.com/invitea/api"
"go.jolheiser.com/invitea/database"
"go.jolheiser.com/invitea/static"
+ "code.gitea.io/sdk/gitea"
+ "github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"
)
type Routes struct {
- DB *database.Queries
+ DB *database.Queries
+ Gitea *gitea.Client
+ GiteaURL string
}
func (ro *Routes) Index(w http.ResponseWriter, r *http.Request) {
@@ -19,15 +29,151 @@ func (ro *Routes) Index(w http.ResponseWriter, r *http.Request) {
isAdmin = ia
}
- invites, err := ro.DB.ListInvites(r.Context())
+ dbInvites, err := ro.DB.ListInvites(r.Context())
if err != nil {
log.Err(err).Msg("")
}
if err := static.Templates.ExecuteTemplate(w, "index.tmpl", map[string]any{
- "isAdmin": isAdmin,
- "invites": invites,
+ "isAdmin": isAdmin,
+ "username": r.Context().Value("username"),
+ "invites": api.InvitesFromDB(dbInvites...),
}); err != nil {
log.Err(err).Msg("")
}
}
+
+func (ro *Routes) IndexPost(w http.ResponseWriter, r *http.Request) {
+ action := r.FormValue("action")
+ if action == "" {
+ http.Redirect(w, r, "/", http.StatusBadRequest)
+ return
+ }
+
+ switch action {
+ case "delete":
+ id, err := strconv.ParseInt(r.FormValue("id"), 10, 64)
+ if err != nil {
+ http.Error(w, "invalid ID", http.StatusBadRequest)
+ return
+ }
+ if err := ro.DB.DeleteInvite(r.Context(), id); err != nil {
+ log.Err(err).Msg("could not delete invite")
+ http.Error(w, "could not delete invite", http.StatusInternalServerError)
+ return
+ }
+ case "create":
+ u := r.FormValue("uses")
+ var uses int64
+ if u != "" {
+ var err error
+ if uses, err = strconv.ParseInt(u, 10, 64); err != nil {
+ http.Error(w, "invalid uses", http.StatusBadRequest)
+ return
+ }
+ }
+
+ e := r.FormValue("expiration")
+ var expiration int64
+ if e != "" {
+ ex, err := time.Parse("2006-01-02", e)
+ if err != nil {
+ http.Error(w, "invalid expiration", http.StatusBadRequest)
+ return
+ }
+ expiration = ex.Unix()
+ }
+
+ code := make([]byte, 5)
+ if _, err := rand.Read(code); err != nil {
+ log.Err(err).Msg("could not generate code")
+ http.Error(w, "could not generate code", http.StatusInternalServerError)
+ return
+ }
+
+ if _, err := ro.DB.CreateInvite(r.Context(), database.CreateInviteParams{
+ Code: hex.EncodeToString(code),
+ Total: sql.NullInt64{
+ Int64: uses,
+ Valid: true,
+ },
+ Expiration: sql.NullInt64{
+ Int64: expiration,
+ Valid: true,
+ },
+ }); err != nil {
+ log.Err(err).Msg("could not create invite")
+ http.Error(w, "could not create invite", http.StatusInternalServerError)
+ return
+ }
+ }
+
+ http.Redirect(w, r, "/", http.StatusSeeOther)
+}
+
+func (ro *Routes) Invite(w http.ResponseWriter, r *http.Request) {
+ code := chi.URLParam(r, "code")
+
+ dbInvite, err := ro.DB.GetInvite(r.Context(), code)
+ if err != nil {
+ log.Err(err).Str("code", code).Msg("could not get invite")
+ http.NotFound(w, r)
+ return
+ }
+
+ invite := api.InviteFromDB(dbInvite)
+ if !invite.Valid() {
+ http.NotFound(w, r)
+ return
+ }
+
+ if err := static.Templates.ExecuteTemplate(w, "invite.tmpl", map[string]any{
+ "invite": invite,
+ }); err != nil {
+ log.Err(err).Msg("")
+ }
+}
+
+func (ro *Routes) InvitePost(w http.ResponseWriter, r *http.Request) {
+ code := chi.URLParam(r, "code")
+
+ dbInvite, err := ro.DB.GetInvite(r.Context(), code)
+ if err != nil {
+ log.Err(err).Msg(code)
+ http.NotFound(w, r)
+ return
+ }
+
+ invite := api.InviteFromDB(dbInvite)
+ if !invite.Valid() {
+ http.NotFound(w, r)
+ return
+ }
+
+ username := r.FormValue("username")
+ password := r.FormValue("password")
+ email := r.FormValue("email")
+
+ var b bool
+ if _, _, err := ro.Gitea.AdminCreateUser(gitea.CreateUserOption{
+ Username: username,
+ Email: email,
+ Password: password,
+ MustChangePassword: &b,
+ }); err != nil {
+ log.Err(err).Msg("could not create user")
+ http.Error(w, "could not create user", http.StatusInternalServerError)
+ return
+ }
+
+ if err := ro.DB.UpdateInvite(r.Context(), database.UpdateInviteParams{
+ ID: dbInvite.ID,
+ Uses: dbInvite.Uses + 1,
+ }); err != nil {
+ log.Err(err).Msg("could not update invite")
+ http.Error(w, "could not update invite", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, ro.GiteaURL+"/user/login", http.StatusSeeOther)
+}
diff --git a/router/session.go b/router/session.go
index 01e3ac9..6a458b5 100644
--- a/router/session.go
+++ b/router/session.go
@@ -25,6 +25,7 @@ func (s *SessionStore) Middleware(next http.Handler) http.Handler {
}
r = r.WithContext(context.WithValue(r.Context(), "isAdmin", sess.Values["isAdmin"]))
+ r = r.WithContext(context.WithValue(r.Context(), "username", sess.Values["username"]))
next.ServeHTTP(w, r)
})
}
@@ -62,11 +63,22 @@ func (s *SessionStore) Auth(w http.ResponseWriter, r *http.Request, token string
}
sess.Values["authenticated"] = true
sess.Values["isAdmin"] = profile.IsAdmin
+ sess.Values["username"] = profile.UserName
+ return s.Store.Save(r, w, sess)
+}
+
+func (s *SessionStore) Logout(w http.ResponseWriter, r *http.Request) error {
+ sess, err := s.Store.Get(r, sessionCookie)
+ if err != nil {
+ return err
+ }
+ sess.Options.MaxAge = -1
return s.Store.Save(r, w, sess)
}
func NewSessionStore(sessionSecret, giteURL string) *SessionStore {
store := sessions.NewCookieStore([]byte(sessionSecret))
+ store.MaxAge(0)
return &SessionStore{
Store: store,
GiteaURL: giteURL,
diff --git a/static/templates/admin.tmpl b/static/templates/admin.tmpl
index d77af0f..0cb0105 100644
--- a/static/templates/admin.tmpl
+++ b/static/templates/admin.tmpl
@@ -2,7 +2,7 @@
-
+
@@ -24,10 +24,22 @@
{{ range .invites }}
- {{ .Code }} |
- {{ .Uses }}/{{ .Total }} |
- {{ .Expiration }} |
- Delete |
+
+ {{ if .Valid -}}
+ {{ .Code }}
+ {{- else -}}
+ {{ .Code }}
+ {{- end }}
+ |
+ {{ .Uses }} / {{ .TotalString }} |
+ {{ .ExpirationString }} |
+
+
+ |
{{ end }}
diff --git a/static/templates/index.tmpl b/static/templates/index.tmpl
index cec60ae..6bf2270 100644
--- a/static/templates/index.tmpl
+++ b/static/templates/index.tmpl
@@ -12,6 +12,9 @@
{{ if .isAdmin }}
+
+ Hello, {{ .username }}. Log Out
+
{{ template "admin.tmpl" . }}
{{ else }}
diff --git a/static/templates/invite.tmpl b/static/templates/invite.tmpl
new file mode 100644
index 0000000..e234431
--- /dev/null
+++ b/static/templates/invite.tmpl
@@ -0,0 +1,50 @@
+
+
+ Invitea
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file