mirror of
https://github.com/thomiceli/opengist.git
synced 2024-12-22 20:42:40 +00:00
Create invitations for closed registrations (#233)
This commit is contained in:
parent
3f5f4e01f1
commit
ef004675a5
14 changed files with 301 additions and 21 deletions
|
@ -42,7 +42,7 @@ func Setup(dbPath string, sharedCache bool) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}); err != nil {
|
||||
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
87
internal/db/invitation.go
Normal file
87
internal/db/invitation.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Invitation struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Code string
|
||||
ExpiresAt int64
|
||||
NbUsed uint
|
||||
NbMax uint
|
||||
}
|
||||
|
||||
func GetAllInvitations() ([]*Invitation, error) {
|
||||
var invitations []*Invitation
|
||||
err := db.
|
||||
Order("(((expires_at >= strftime('%s', 'now')) AND ((nb_max <= 0) OR (nb_used < nb_max)))) desc").
|
||||
Order("id asc").
|
||||
Find(&invitations).Error
|
||||
|
||||
return invitations, err
|
||||
}
|
||||
|
||||
func GetInvitationByID(id uint) (*Invitation, error) {
|
||||
invitation := new(Invitation)
|
||||
err := db.
|
||||
Where("id = ?", id).
|
||||
First(&invitation).Error
|
||||
return invitation, err
|
||||
}
|
||||
|
||||
func GetInvitationByCode(code string) (*Invitation, error) {
|
||||
invitation := new(Invitation)
|
||||
err := db.
|
||||
Where("code = ?", code).
|
||||
First(&invitation).Error
|
||||
return invitation, err
|
||||
}
|
||||
|
||||
func InvitationCodeExists(code string) (bool, error) {
|
||||
var count int64
|
||||
err := db.Model(&Invitation{}).Where("code = ?", code).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
func (i *Invitation) Create() error {
|
||||
i.Code = generateRandomCode()
|
||||
return db.Create(&i).Error
|
||||
}
|
||||
|
||||
func (i *Invitation) Update() error {
|
||||
return db.Save(&i).Error
|
||||
}
|
||||
|
||||
func (i *Invitation) Delete() error {
|
||||
return db.Delete(&i).Error
|
||||
}
|
||||
|
||||
func (i *Invitation) IsExpired() bool {
|
||||
return i.ExpiresAt < time.Now().Unix()
|
||||
}
|
||||
|
||||
func (i *Invitation) IsMaxedOut() bool {
|
||||
return i.NbMax > 0 && i.NbUsed >= i.NbMax
|
||||
}
|
||||
|
||||
func (i *Invitation) IsUsable() bool {
|
||||
return !i.IsExpired() && !i.IsMaxedOut()
|
||||
}
|
||||
|
||||
func (i *Invitation) Use() error {
|
||||
i.NbUsed++
|
||||
return i.Update()
|
||||
}
|
||||
|
||||
func generateRandomCode() string {
|
||||
const charset = "0123456789ABCDEF"
|
||||
var seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
result := make([]byte, 16)
|
||||
|
||||
for i := range result {
|
||||
result[i] = charset[seededRand.Intn(len(charset))]
|
||||
}
|
||||
return string(result)
|
||||
}
|
|
@ -162,6 +162,8 @@ admin.general: General
|
|||
admin.users: Users
|
||||
admin.gists: Gists
|
||||
admin.configuration: Configuration
|
||||
admin.invitations: Invitations
|
||||
admin.invitations.create: Create invitation
|
||||
admin.versions: Versions
|
||||
admin.ssh_keys: SSH keys
|
||||
admin.stats: Stats
|
||||
|
@ -195,3 +197,11 @@ admin.gists.private: Private ?
|
|||
admin.gists.nb-files: Nb. files
|
||||
admin.gists.nb-likes: Nb. likes
|
||||
admin.gists.delete_confirm: Do you want to delete this gist ?
|
||||
|
||||
admin.invitations.help: Invitations can be used to create an account even if signing up is disabled.
|
||||
admin.invitations.max_uses: Max uses
|
||||
admin.invitations.expires_at: Expires at
|
||||
admin.invitations.code: Code
|
||||
admin.invitations.copy_link: Copy link
|
||||
admin.invitations.uses: Uses
|
||||
admin.invitations.expired: Expired
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/thomiceli/opengist/internal/git"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func adminIndex(ctx echo.Context) error {
|
||||
|
@ -179,3 +180,59 @@ func adminSetConfig(ctx echo.Context) error {
|
|||
"success": true,
|
||||
})
|
||||
}
|
||||
|
||||
func adminInvitations(ctx echo.Context) error {
|
||||
setData(ctx, "title", "Invitations")
|
||||
setData(ctx, "htmlTitle", "Invitations - Admin panel")
|
||||
setData(ctx, "adminHeaderPage", "invitations")
|
||||
|
||||
var invitations []*db.Invitation
|
||||
var err error
|
||||
if invitations, err = db.GetAllInvitations(); err != nil {
|
||||
return errorRes(500, "Cannot get invites", err)
|
||||
}
|
||||
|
||||
setData(ctx, "invitations", invitations)
|
||||
return html(ctx, "admin_invitations.html")
|
||||
}
|
||||
|
||||
func adminInvitationsCreate(ctx echo.Context) error {
|
||||
code := ctx.FormValue("code")
|
||||
nbMax, err := strconv.ParseUint(ctx.FormValue("nbMax"), 10, 64)
|
||||
if err != nil {
|
||||
nbMax = 10
|
||||
}
|
||||
|
||||
expiresAtUnix, err := strconv.ParseInt(ctx.FormValue("expiredAtUnix"), 10, 64)
|
||||
if err != nil {
|
||||
expiresAtUnix = time.Now().Unix() + 604800 // 1 week
|
||||
}
|
||||
|
||||
invitation := &db.Invitation{
|
||||
Code: code,
|
||||
ExpiresAt: expiresAtUnix,
|
||||
NbMax: uint(nbMax),
|
||||
}
|
||||
|
||||
if err := invitation.Create(); err != nil {
|
||||
return errorRes(500, "Cannot create invitation", err)
|
||||
}
|
||||
|
||||
addFlash(ctx, "Invitation has been created", "success")
|
||||
return redirect(ctx, "/admin-panel/invitations")
|
||||
}
|
||||
|
||||
func adminInvitationsDelete(ctx echo.Context) error {
|
||||
id, _ := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
||||
invitation, err := db.GetInvitationByID(uint(id))
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot retrieve invitation", err)
|
||||
}
|
||||
|
||||
if err := invitation.Delete(); err != nil {
|
||||
return errorRes(500, "Cannot delete this invitation", err)
|
||||
}
|
||||
|
||||
addFlash(ctx, "Invitation has been deleted", "success")
|
||||
return redirect(ctx, "/admin-panel/invitations")
|
||||
}
|
||||
|
|
|
@ -36,15 +36,38 @@ const (
|
|||
var title = cases.Title(language.English)
|
||||
|
||||
func register(ctx echo.Context) error {
|
||||
disableSignup := getData(ctx, "DisableSignup")
|
||||
disableForm := getData(ctx, "DisableLoginForm")
|
||||
|
||||
code := ctx.QueryParam("code")
|
||||
if code != "" {
|
||||
if invitation, err := db.GetInvitationByCode(code); err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errorRes(500, "Cannot check for invitation code", err)
|
||||
} else if invitation != nil && invitation.IsUsable() {
|
||||
disableSignup = false
|
||||
}
|
||||
}
|
||||
|
||||
setData(ctx, "title", tr(ctx, "auth.new-account"))
|
||||
setData(ctx, "htmlTitle", "New account")
|
||||
setData(ctx, "disableForm", getData(ctx, "DisableLoginForm"))
|
||||
setData(ctx, "disableForm", disableForm)
|
||||
setData(ctx, "disableSignup", disableSignup)
|
||||
setData(ctx, "isLoginPage", false)
|
||||
return html(ctx, "auth_form.html")
|
||||
}
|
||||
|
||||
func processRegister(ctx echo.Context) error {
|
||||
if getData(ctx, "DisableSignup") == true {
|
||||
disableSignup := getData(ctx, "DisableSignup")
|
||||
|
||||
code := ctx.QueryParam("code")
|
||||
invitation, err := db.GetInvitationByCode(code)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errorRes(500, "Cannot check for invitation code", err)
|
||||
} else if invitation != nil && invitation.IsUsable() {
|
||||
disableSignup = false
|
||||
}
|
||||
|
||||
if disableSignup == true {
|
||||
return errorRes(403, "Signing up is disabled", nil)
|
||||
}
|
||||
|
||||
|
@ -90,6 +113,10 @@ func processRegister(ctx echo.Context) error {
|
|||
}
|
||||
}
|
||||
|
||||
if err := invitation.Use(); err != nil {
|
||||
return errorRes(500, "Cannot use invitation", err)
|
||||
}
|
||||
|
||||
sess.Values["user"] = user.ID
|
||||
saveSession(sess, ctx)
|
||||
|
||||
|
|
|
@ -74,21 +74,7 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
httpProtocol := "http"
|
||||
if ctx.Request().TLS != nil || ctx.Request().Header.Get("X-Forwarded-Proto") == "https" {
|
||||
httpProtocol = "https"
|
||||
}
|
||||
setData(ctx, "httpProtocol", strings.ToUpper(httpProtocol))
|
||||
|
||||
var baseHttpUrl string
|
||||
// if a custom external url is set, use it
|
||||
if config.C.ExternalUrl != "" {
|
||||
baseHttpUrl = config.C.ExternalUrl
|
||||
} else {
|
||||
baseHttpUrl = httpProtocol + "://" + ctx.Request().Host
|
||||
}
|
||||
|
||||
setData(ctx, "baseHttpUrl", baseHttpUrl)
|
||||
baseHttpUrl := getData(ctx, "baseHttpUrl").(string)
|
||||
|
||||
if config.C.HttpGit {
|
||||
setData(ctx, "httpCloneUrl", baseHttpUrl+"/"+userName+"/"+gistName+".git")
|
||||
|
|
|
@ -271,6 +271,9 @@ func NewServer(isDev bool) *Server {
|
|||
g2.POST("/users/:user/delete", adminUserDelete)
|
||||
g2.GET("/gists", adminGists)
|
||||
g2.POST("/gists/:gist/delete", adminGistDelete)
|
||||
g2.GET("/invitations", adminInvitations)
|
||||
g2.POST("/invitations", adminInvitationsCreate)
|
||||
g2.POST("/invitations/:id/delete", adminInvitationsDelete)
|
||||
g2.POST("/sync-fs", adminSyncReposFromFS)
|
||||
g2.POST("/sync-db", adminSyncReposFromDB)
|
||||
g2.POST("/gc-repos", adminGcRepos)
|
||||
|
@ -381,6 +384,22 @@ func dataInit(next echo.HandlerFunc) echo.HandlerFunc {
|
|||
setData(ctx, "giteaOauth", config.C.GiteaClientKey != "" && config.C.GiteaSecret != "")
|
||||
setData(ctx, "oidcOauth", config.C.OIDCClientKey != "" && config.C.OIDCSecret != "" && config.C.OIDCDiscoveryUrl != "")
|
||||
|
||||
httpProtocol := "http"
|
||||
if ctx.Request().TLS != nil || ctx.Request().Header.Get("X-Forwarded-Proto") == "https" {
|
||||
httpProtocol = "https"
|
||||
}
|
||||
setData(ctx, "httpProtocol", strings.ToUpper(httpProtocol))
|
||||
|
||||
var baseHttpUrl string
|
||||
// if a custom external url is set, use it
|
||||
if config.C.ExternalUrl != "" {
|
||||
baseHttpUrl = config.C.ExternalUrl
|
||||
} else {
|
||||
baseHttpUrl = httpProtocol + "://" + ctx.Request().Host
|
||||
}
|
||||
|
||||
setData(ctx, "baseHttpUrl", baseHttpUrl)
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
registerDomSetting(elem as HTMLElement)
|
||||
})
|
||||
}
|
||||
|
||||
let copyInviteButtons = Array.from(document.getElementsByClassName("copy-invitation-link"));
|
||||
for (let button of copyInviteButtons) {
|
||||
button.addEventListener('click', () => {
|
||||
navigator.clipboard.writeText((button as HTMLElement).dataset.link).catch((err) => {
|
||||
console.error('Could not copy text: ', err);
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
const setSetting = (key: string, value: string) => {
|
||||
|
|
|
@ -53,6 +53,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
e.innerHTML = dayjs.unix(parseInt(e.innerHTML)).format('DD/MM/YYYY HH:mm');
|
||||
});
|
||||
|
||||
document.querySelectorAll('form').forEach((form: HTMLFormElement) => {
|
||||
form.onsubmit = () => {
|
||||
form.querySelectorAll('input[type=datetime-local]').forEach((input: HTMLInputElement) => {
|
||||
console.log(dayjs(input.value).unix());
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = 'expiredAtUnix'
|
||||
hiddenInput.value = dayjs(input.value).unix().toString();
|
||||
form.appendChild(hiddenInput);
|
||||
});
|
||||
return true;
|
||||
};
|
||||
})
|
||||
|
||||
|
||||
|
||||
const rev = document.querySelector<HTMLElement>('.revision-text');
|
||||
if (rev) {
|
||||
const fullRev = rev.innerHTML;
|
||||
|
|
1
templates/base/admin_footer.html
vendored
1
templates/base/admin_footer.html
vendored
|
@ -8,6 +8,7 @@
|
|||
{{ template "_pagination" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
<script src="{{ asset "admin.ts" }}"></script>
|
||||
</main>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
|
2
templates/base/admin_header.html
vendored
2
templates/base/admin_header.html
vendored
|
@ -15,6 +15,8 @@
|
|||
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">{{ .locale.Tr "admin.users" }}</a>
|
||||
<a href="{{ $.c.ExternalUrl }}/admin-panel/gists" class="{{ if eq .adminHeaderPage "gists" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
|
||||
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">{{ .locale.Tr "admin.gists" }}</a>
|
||||
<a href="{{ $.c.ExternalUrl }}/admin-panel/invitations" class="{{ if eq .adminHeaderPage "invitations" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
|
||||
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">{{ .locale.Tr "admin.invitations" }}</a>
|
||||
<a href="{{ $.c.ExternalUrl }}/admin-panel/configuration" class="{{ if eq .adminHeaderPage "config" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
|
||||
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">{{ .locale.Tr "admin.configuration" }}</a>
|
||||
</nav>
|
||||
|
|
2
templates/pages/admin_config.html
vendored
2
templates/pages/admin_config.html
vendored
|
@ -120,7 +120,5 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="{{ asset "admin.ts" }}"></script>
|
||||
|
||||
{{ template "admin_footer" .}}
|
||||
{{ template "footer" .}}
|
||||
|
|
68
templates/pages/admin_invitations.html
vendored
Normal file
68
templates/pages/admin_invitations.html
vendored
Normal file
|
@ -0,0 +1,68 @@
|
|||
{{ template "header" .}}
|
||||
{{ template "admin_header" .}}
|
||||
|
||||
<h3 class="text-sm text-gray-600 dark:text-gray-400 italic mb-4">
|
||||
{{ .locale.Tr "admin.invitations.help" }}
|
||||
</h3>
|
||||
|
||||
<form method="POST">
|
||||
<div class="flex space-x-4">
|
||||
<div class="flex-1">
|
||||
<label for="nbMax" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">{{ .locale.Tr "admin.invitations.max_uses" }}</label>
|
||||
<input type="number" id="nbMax" name="nbMax" value="10" min="1" max="100" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label for="expiresAt" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">{{ .locale.Tr "admin.invitations.expires_at" }}</label>
|
||||
<input type="datetime-local" id="expiresAt" name="expiresAt" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{{ .locale.Tr "admin.invitations.create" }}</button>
|
||||
</div>
|
||||
{{ .csrfHtml }}
|
||||
</form>
|
||||
<hr class="my-4" />
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-700">
|
||||
<table class="min-w-full divide-y divide-slate-300 dark:divide-gray-500">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.invitations.code" }}</th>
|
||||
<th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.invitations.copy_link" }}</th>
|
||||
<th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.invitations.uses" }}</th>
|
||||
<th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.invitations.expires_at" }}</th>
|
||||
<th scope="col" class="relative whitespace-nowrap py-3.5 pl-3 pr-4 sm:pr-0">
|
||||
<span class="sr-only">{{ .locale.Tr "admin.delete" }}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-300 dark:divide-gray-500">
|
||||
{{ range $invitation := .invitations }}
|
||||
<tr class="{{ if $invitation.IsUsable }}text-slate-700 dark:text-slate-100{{ else }}text-gray-300 italic{{ end }}">
|
||||
<td class="whitespace-nowrap py-2 px-2 text-sm">{{ $invitation.Code }}</td>
|
||||
<td class="whitespace-nowrap py-2 px-2 text-sm items-center">
|
||||
{{ if $invitation.IsUsable }}
|
||||
<span class="copy-invitation-link" data-link="{{ $.baseHttpUrl }}/register?code={{ $invitation.Code }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 cursor-pointer">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5A3.375 3.375 0 0 0 6.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0 0 15 2.25h-1.5a2.251 2.251 0 0 0-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 0 0-9-9Z" />
|
||||
</svg>
|
||||
</span>
|
||||
{{ else }}
|
||||
<span class="italic">{{ $.locale.Tr "admin.invitations.expired" }}</span>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap py-2 px-2 text-sm">{{ $invitation.NbUsed }}/{{ $invitation.NbMax }}</td>
|
||||
<td class="whitespace-nowrap px-2 py-2 text-sm"><span class="moment-timestamp-date">{{ $invitation.ExpiresAt }}</span></td>
|
||||
<td class="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
|
||||
<form action="{{ $.c.ExternalUrl }}/admin-panel/invitations/{{ $invitation.ID }}/delete" method="POST" onsubmit="return confirm('{{ $.locale.Tr "admin.users.delete_confirm" }}')">
|
||||
{{ $.csrfHtml }}
|
||||
<button type="submit" class="text-rose-500 hover:text-rose-600">{{ $.locale.Tr "admin.delete" }}</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{ template "admin_footer" .}}
|
||||
{{ template "footer" .}}
|
2
templates/pages/auth_form.html
vendored
2
templates/pages/auth_form.html
vendored
|
@ -8,7 +8,7 @@
|
|||
|
||||
</header>
|
||||
<main class="mt-4">
|
||||
{{ if and .DisableSignup (not .isLoginPage) }}
|
||||
{{ if .disableSignup }}
|
||||
<p class="italic">{{ .locale.Tr "auth.signup-disabled" }}</p>
|
||||
{{ else }}
|
||||
<div class="sm:col-span-6">
|
||||
|
|
Loading…
Reference in a new issue