mirror of
https://github.com/dnote/dnote
synced 2026-03-14 22:45:50 +01:00
Remove email verification (#688)
* Remove email verified flag * Fix sass deprecation warnings
This commit is contained in:
parent
ca5af5e34a
commit
fd7b2a78b2
35 changed files with 733 additions and 916 deletions
|
|
@ -65,28 +65,6 @@ func getNoreplySender(webURL string) (string, error) {
|
|||
return addr, nil
|
||||
}
|
||||
|
||||
// SendVerificationEmail sends verification email
|
||||
func (a *App) SendVerificationEmail(email, tokenValue string) error {
|
||||
body, err := a.EmailTemplates.Execute(mailer.EmailTypeEmailVerification, mailer.EmailKindText, mailer.EmailVerificationTmplData{
|
||||
Token: tokenValue,
|
||||
WebURL: a.WebURL,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "executing reset verification template for %s", email)
|
||||
}
|
||||
|
||||
from, err := GetSenderEmail(a.WebURL, defaultSender)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting the sender email")
|
||||
}
|
||||
|
||||
if err := a.EmailBackend.Queue("Verify your Dnote email address", from, []string{email}, mailer.EmailKindText, body); err != nil {
|
||||
return errors.Wrapf(err, "queueing email for %s", email)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendWelcomeEmail sends welcome email
|
||||
func (a *App) SendWelcomeEmail(email string) error {
|
||||
body, err := a.EmailTemplates.Execute(mailer.EmailTypeWelcome, mailer.EmailKindText, mailer.WelcomeTmplData{
|
||||
|
|
|
|||
|
|
@ -26,22 +26,6 @@ import (
|
|||
"github.com/dnote/dnote/pkg/server/testutils"
|
||||
)
|
||||
|
||||
func TestSendVerificationEmail(t *testing.T) {
|
||||
emailBackend := testutils.MockEmailbackendImplementation{}
|
||||
a := NewTest()
|
||||
a.EmailBackend = &emailBackend
|
||||
a.WebURL = "http://example.com"
|
||||
|
||||
if err := a.SendVerificationEmail("alice@example.com", "mockTokenValue"); err != nil {
|
||||
t.Fatal(err, "failed to perform")
|
||||
}
|
||||
|
||||
assert.Equalf(t, len(emailBackend.Emails), 1, "email queue count mismatch")
|
||||
assert.Equal(t, emailBackend.Emails[0].From, "noreply@example.com", "email sender mismatch")
|
||||
assert.DeepEqual(t, emailBackend.Emails[0].To, []string{"alice@example.com"}, "email sender mismatch")
|
||||
|
||||
}
|
||||
|
||||
func TestSendWelcomeEmail(t *testing.T) {
|
||||
emailBackend := testutils.MockEmailbackendImplementation{}
|
||||
a := NewTest()
|
||||
|
|
|
|||
|
|
@ -79,7 +79,4 @@ var (
|
|||
ErrInvalidPassword appError = "Invalid currnet password."
|
||||
// ErrEmailTooLong is an error for email length exceeding the limit
|
||||
ErrEmailTooLong appError = "Email is too long."
|
||||
|
||||
// ErrEmailAlreadyVerified is an error for trying to verify email that is already verified
|
||||
ErrEmailAlreadyVerified appError = "Email is already verified."
|
||||
)
|
||||
|
|
|
|||
529
pkg/server/assets/package-lock.json
generated
529
pkg/server/assets/package-lock.json
generated
|
|
@ -1,158 +1,493 @@
|
|||
{
|
||||
"name": "assets",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"anymatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
|
||||
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "assets",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"sass": "^1.50.1"
|
||||
}
|
||||
},
|
||||
"binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"dev": true
|
||||
"node_modules/@parcel/watcher": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^1.0.3",
|
||||
"is-glob": "^4.0.3",
|
||||
"micromatch": "^4.0.5",
|
||||
"node-addon-api": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher-android-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
"@parcel/watcher-freebsd-x64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||
"@parcel/watcher-win32-ia32": "2.5.1",
|
||||
"@parcel/watcher-win32-x64": "2.5.1"
|
||||
}
|
||||
},
|
||||
"braces": {
|
||||
"node_modules/@parcel/watcher-android-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-freebsd-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-ia32": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
|
||||
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
}
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||
"node_modules/detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"fsevents": "~2.3.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"detect-libc": "bin/detect-libc.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-glob": "^4.0.1"
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"immutable": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz",
|
||||
"integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==",
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz",
|
||||
"integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
|
||||
"dev": true
|
||||
},
|
||||
"is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"is-extglob": {
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
|
||||
"dev": true
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"is-glob": {
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"is-number": {
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"picomatch": {
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true
|
||||
},
|
||||
"readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"picomatch": "^2.2.1"
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.50.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.50.1.tgz",
|
||||
"integrity": "sha512-noTnY41KnlW2A9P8sdwESpDmo+KBNkukI1i8+hOK3footBUcohNHtdOJbckp46XO95nuvcHDDZ+4tmOnpK3hjw==",
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
|
||||
"integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"source-map-js": {
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"to-regex-range": {
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
@use "rem";
|
||||
@use "theme";
|
||||
|
||||
/* Copyright (C) 2019, 2020, 2021, 2022, 2023, 2024, 2025 Dnote contributors
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
|
|
@ -18,12 +21,12 @@
|
|||
|
||||
.books-page {
|
||||
.books-content {
|
||||
padding: rem(16px) rem(24px);
|
||||
margin-top: rem(16px);
|
||||
padding: rem.rem(16px) rem.rem(24px);
|
||||
margin-top: rem.rem(16px);
|
||||
|
||||
h1 {
|
||||
border-bottom: 1px solid $lighter-gray;
|
||||
margin-bottom: rem(12px);
|
||||
border-bottom: 1px solid theme.$lighter-gray;
|
||||
margin-bottom: rem.rem(12px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,11 @@
|
|||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@import './theme';
|
||||
@import './rem';
|
||||
@import './font';
|
||||
@use "sass:color";
|
||||
@use 'theme';
|
||||
@use 'rem';
|
||||
@use 'font';
|
||||
@use "responsive";
|
||||
|
||||
@mixin button($text-color, $background-color) {
|
||||
color: $text-color;
|
||||
|
|
@ -26,7 +28,7 @@
|
|||
|
||||
&:not(:disabled):hover {
|
||||
color: $text-color;
|
||||
background-color: darken($background-color, 5%);
|
||||
background-color: color.adjust($background-color, $lightness: -5%);
|
||||
box-shadow: 0px 0px 4px 2px #cacaca;
|
||||
}
|
||||
}
|
||||
|
|
@ -87,40 +89,40 @@ button:disabled {
|
|||
}
|
||||
|
||||
.button-small {
|
||||
@include font-size('small');
|
||||
padding: rem(4px) rem(12px);
|
||||
@include font.font-size('small');
|
||||
padding: rem.rem(4px) rem.rem(12px);
|
||||
}
|
||||
|
||||
.button-normal {
|
||||
// @include font-size('small');
|
||||
padding: rem(8px) rem(16px);
|
||||
padding: rem.rem(8px) rem.rem(16px);
|
||||
}
|
||||
|
||||
.button-large {
|
||||
@include font-size('medium');
|
||||
@include font.font-size('medium');
|
||||
|
||||
padding: rem(8px) rem(24px);
|
||||
padding: rem.rem(8px) rem.rem(24px);
|
||||
|
||||
@include breakpoint(md) {
|
||||
padding: rem(12px) rem(36px);
|
||||
@include responsive.breakpoint(md) {
|
||||
padding: rem.rem(12px) rem.rem(36px);
|
||||
}
|
||||
|
||||
@include breakpoint(lg) {
|
||||
padding: rem(12px) rem(48px);
|
||||
@include responsive.breakpoint(lg) {
|
||||
padding: rem.rem(12px) rem.rem(48px);
|
||||
}
|
||||
}
|
||||
|
||||
.button-xlarge {
|
||||
@include font-size('x-large');
|
||||
@include font.font-size('x-large');
|
||||
|
||||
padding: rem(16px) rem(24px);
|
||||
padding: rem.rem(16px) rem.rem(24px);
|
||||
|
||||
@include breakpoint(md) {
|
||||
padding: rem(12px) rem(36px);
|
||||
@include responsive.breakpoint(md) {
|
||||
padding: rem.rem(12px) rem.rem(36px);
|
||||
}
|
||||
|
||||
@include breakpoint(lg) {
|
||||
padding: rem(16px) rem(48px);
|
||||
@include responsive.breakpoint(lg) {
|
||||
padding: rem.rem(16px) rem.rem(48px);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,23 +135,23 @@ button:disabled {
|
|||
}
|
||||
|
||||
.button-second {
|
||||
@include button($black, $second);
|
||||
@include button(theme.$black, theme.$second);
|
||||
}
|
||||
|
||||
.button-second-outline {
|
||||
@include button-outline($black, $second);
|
||||
@include button-outline(theme.$black, theme.$second);
|
||||
}
|
||||
|
||||
.button-third {
|
||||
@include button(#ffffff, $third);
|
||||
@include button(#ffffff, theme.$third);
|
||||
}
|
||||
|
||||
.button-third-outline {
|
||||
@include button-outline($third, $third);
|
||||
@include button-outline(theme.$third, theme.$third);
|
||||
}
|
||||
|
||||
.button-danger {
|
||||
@include button-outline($danger-text, $danger-text);
|
||||
@include button-outline(theme.$danger-text, theme.$danger-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +160,7 @@ button:disabled {
|
|||
}
|
||||
|
||||
.button ~ .button {
|
||||
margin-left: rem(12px);
|
||||
margin-left: rem.rem(12px);
|
||||
}
|
||||
|
||||
.button-no-ui {
|
||||
|
|
@ -173,10 +175,10 @@ button:disabled {
|
|||
}
|
||||
|
||||
.button-link {
|
||||
color: $link;
|
||||
color: theme.$link;
|
||||
|
||||
&:hover {
|
||||
color: $link-hover;
|
||||
color: theme.$link-hover;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@import './responsive';
|
||||
@use 'responsive';
|
||||
|
||||
$lowDecay: 0.1;
|
||||
$medDecay: 0.15;
|
||||
|
|
@ -95,12 +95,12 @@ $highDecay: 0.2;
|
|||
font-size: $smSizeValue * 1px;
|
||||
font-size: $smSizeValue * 0.1rem;
|
||||
|
||||
@include breakpoint(md) {
|
||||
@include responsive.breakpoint(md) {
|
||||
font-size: $mdSizeValue * 1px;
|
||||
font-size: $mdSizeValue * 0.1rem;
|
||||
}
|
||||
|
||||
@include breakpoint(lg) {
|
||||
@include responsive.breakpoint(lg) {
|
||||
font-size: $lgSizeValue * 1px;
|
||||
font-size: $lgSizeValue * 0.1rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
@use "font";
|
||||
@use "rem";
|
||||
@use "responsive";
|
||||
@use "theme";
|
||||
@use "variables";
|
||||
|
||||
/* Copyright (C) 2019, 2020, 2021, 2022, 2023, 2024, 2025 Dnote contributors
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
|
|
@ -20,8 +26,8 @@
|
|||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: $lighter-gray;
|
||||
min-height: calc(100vh - #{$header-height});
|
||||
background: theme.$lighter-gray;
|
||||
min-height: calc(100vh - #{variables.$header-height});
|
||||
// margin-bottom: $footer-height;
|
||||
|
||||
&.nofooter {
|
||||
|
|
@ -29,49 +35,49 @@
|
|||
}
|
||||
|
||||
&.noheader:not(.nofooter) {
|
||||
min-height: calc(100vh - #{$footer-height});
|
||||
min-height: calc(100vh - #{variables.$footer-height});
|
||||
}
|
||||
&.nofooter:not(.noheader) {
|
||||
min-height: calc(100vh - #{$header-height});
|
||||
min-height: calc(100vh - #{variables.$header-height});
|
||||
}
|
||||
&.nofooter.noheader {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@include breakpoint(lg) {
|
||||
@include responsive.breakpoint(lg) {
|
||||
margin-bottom: 0;
|
||||
min-height: calc(100vh - #{$header-height});
|
||||
min-height: calc(100vh - #{variables.$header-height});
|
||||
}
|
||||
}
|
||||
|
||||
/* partials */
|
||||
.partial--time {
|
||||
color: $gray;
|
||||
@include font-size('small');
|
||||
color: theme.$gray;
|
||||
@include font.font-size('small');
|
||||
|
||||
.mobile-text {
|
||||
@include breakpoint(md) {
|
||||
@include responsive.breakpoint(md) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.text {
|
||||
display: none;
|
||||
|
||||
@include breakpoint(md) {
|
||||
@include responsive.breakpoint(md) {
|
||||
display: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.partial--page-toolbar {
|
||||
@include breakpoint(lg) {
|
||||
height: rem(48px);
|
||||
border-radius: rem(4px);
|
||||
background: $light;
|
||||
@include responsive.breakpoint(lg) {
|
||||
height: rem.rem(48px);
|
||||
border-radius: rem.rem(4px);
|
||||
background: theme.$light;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.14);
|
||||
|
||||
&.bottom {
|
||||
margin-top: rem(12px);
|
||||
margin-top: rem.rem(12px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,12 @@
|
|||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@import './theme';
|
||||
@import './variables';
|
||||
@use "sass:color";
|
||||
@use 'theme';
|
||||
@use 'variables';
|
||||
@use "font";
|
||||
@use "rem";
|
||||
@use "responsive";
|
||||
|
||||
.header-wrapper {
|
||||
padding: 0;
|
||||
|
|
@ -25,7 +29,7 @@
|
|||
position: relative;
|
||||
display: flex;
|
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);
|
||||
background: $first;
|
||||
background: theme.$first;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
|
|
@ -33,13 +37,13 @@
|
|||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 4;
|
||||
height: $header-height;
|
||||
height: variables.$header-height;
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@include breakpoint(md) {
|
||||
@include responsive.breakpoint(md) {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
|
|
@ -60,15 +64,15 @@
|
|||
.search-wrapper {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-left: rem(32px);
|
||||
margin-left: rem.rem(32px);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: rem(356px);
|
||||
width: rem.rem(356px);
|
||||
border: 0;
|
||||
padding: 4px 12px;
|
||||
border-radius: rem(4px);
|
||||
@include font-size('small');
|
||||
border-radius: rem.rem(4px);
|
||||
@include font.font-size('small');
|
||||
}
|
||||
|
||||
.brand {
|
||||
|
|
@ -81,7 +85,7 @@
|
|||
}
|
||||
|
||||
.main-nav {
|
||||
margin-left: rem(32px);
|
||||
margin-left: rem.rem(32px);
|
||||
display: flex;
|
||||
|
||||
.list {
|
||||
|
|
@ -94,22 +98,22 @@
|
|||
}
|
||||
|
||||
.nav-link {
|
||||
@include font-size('small');
|
||||
@include font.font-size('small');
|
||||
display: flex;
|
||||
font-weight: 600;
|
||||
align-items: center;
|
||||
padding: 0 rem(16px);
|
||||
color: $white;
|
||||
padding: 0 rem.rem(16px);
|
||||
color: theme.$white;
|
||||
|
||||
&:hover {
|
||||
color: $white;
|
||||
color: theme.$white;
|
||||
text-decoration: none;
|
||||
background: lighten($first, 10%);
|
||||
background: color.adjust(theme.$first, $lightness: 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
@include font-size('small');
|
||||
@include font.font-size('small');
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
|
@ -131,7 +135,7 @@
|
|||
display: none;
|
||||
position: absolute;
|
||||
background-color: #f1f1f1;
|
||||
width: rem(240px);
|
||||
width: rem.rem(240px);
|
||||
background: #fff;
|
||||
border: 1px solid #d8d8d8;
|
||||
border-radius: 4px;
|
||||
|
|
@ -154,15 +158,15 @@
|
|||
}
|
||||
|
||||
.account-dropdown-header {
|
||||
@include font-size('small');
|
||||
color: $light-gray;
|
||||
padding: rem(8px) rem(12px);
|
||||
@include font.font-size('small');
|
||||
color: theme.$light-gray;
|
||||
padding: rem.rem(8px) rem.rem(12px);
|
||||
display: block;
|
||||
margin-bottom: 0;
|
||||
white-space: nowrap;
|
||||
|
||||
svg {
|
||||
fill: $light-gray;
|
||||
fill: theme.$light-gray;
|
||||
}
|
||||
|
||||
.email {
|
||||
|
|
@ -173,15 +177,15 @@
|
|||
}
|
||||
|
||||
.dropdown-link {
|
||||
@include font-size('small');
|
||||
@include font.font-size('small');
|
||||
white-space: pre;
|
||||
padding: rem(8px) rem(14px);
|
||||
padding: rem.rem(8px) rem.rem(14px);
|
||||
width: 100%;
|
||||
display: block;
|
||||
color: black;
|
||||
|
||||
&:hover {
|
||||
background: $lighter-gray;
|
||||
background: theme.$lighter-gray;
|
||||
text-decoration: none;
|
||||
color: #0056b3;
|
||||
}
|
||||
|
|
@ -192,7 +196,7 @@
|
|||
}
|
||||
|
||||
&:not(.disabled):focus {
|
||||
background: $lighter-gray;
|
||||
background: theme.$lighter-gray;
|
||||
color: #0056b3;
|
||||
outline: 1px dotted gray;
|
||||
}
|
||||
|
|
@ -204,7 +208,7 @@
|
|||
}
|
||||
|
||||
.session-notice {
|
||||
margin-left: rem(4px);
|
||||
margin-left: rem.rem(4px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,21 +16,23 @@
|
|||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@import './theme';
|
||||
@import './font';
|
||||
@use 'theme';
|
||||
@use 'font';
|
||||
@use "rem";
|
||||
@use "responsive";
|
||||
|
||||
.home-page {
|
||||
.note-group-list {
|
||||
flex-grow: 1;
|
||||
|
||||
@include breakpoint(lg) {
|
||||
margin-top: rem(16px);
|
||||
@include responsive.breakpoint(lg) {
|
||||
margin-top: rem.rem(16px);
|
||||
}
|
||||
|
||||
.note-group-list-empty {
|
||||
padding: rem(40px) rem(16px);
|
||||
padding: rem.rem(40px) rem.rem(16px);
|
||||
text-align: center;
|
||||
color: $gray;
|
||||
color: theme.$gray;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -40,29 +42,29 @@
|
|||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.14);
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: rem(20px);
|
||||
margin-top: rem.rem(20px);
|
||||
|
||||
@include breakpoint(md) {
|
||||
margin-top: rem(24px);
|
||||
@include responsive.breakpoint(md) {
|
||||
margin-top: rem.rem(24px);
|
||||
}
|
||||
}
|
||||
|
||||
.note-group-header {
|
||||
@include font-size('small');
|
||||
@include font.font-size('small');
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: white;
|
||||
padding: rem(12px) rem(16px);
|
||||
background: $light;
|
||||
color: $black;
|
||||
border-bottom: 1px solid $border-color;
|
||||
padding: rem.rem(12px) rem.rem(16px);
|
||||
background: theme.$light;
|
||||
color: theme.$black;
|
||||
border-bottom: 1px solid theme.$border-color;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-weight: 600;
|
||||
@include font-size('small');
|
||||
@include font.font-size('small');
|
||||
}
|
||||
|
||||
.mask {
|
||||
|
|
@ -78,7 +80,7 @@
|
|||
|
||||
.header-date {
|
||||
font-weight: 600;
|
||||
@include font-size('regular');
|
||||
@include font.font-size('regular');
|
||||
}
|
||||
.header-count {
|
||||
font-weight: 300;
|
||||
|
|
@ -101,23 +103,23 @@
|
|||
background: white;
|
||||
position: relative;
|
||||
|
||||
border-bottom: 1px solid $border-color;
|
||||
border-bottom: 1px solid theme.$border-color;
|
||||
|
||||
.link {
|
||||
color: $black;
|
||||
color: theme.$black;
|
||||
display: block;
|
||||
padding: rem(12px) rem(16px);
|
||||
padding: rem.rem(12px) rem.rem(16px);
|
||||
border: 2px solid transparent;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background: $light-blue;
|
||||
background: theme.$light-blue;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.meta {
|
||||
line-height: rem(16px);
|
||||
line-height: rem.rem(16px);
|
||||
}
|
||||
|
||||
.body {
|
||||
|
|
@ -131,11 +133,11 @@
|
|||
}
|
||||
|
||||
.note-content {
|
||||
margin-top: rem(12px);
|
||||
margin-top: rem.rem(12px);
|
||||
line-height: 1.6rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: $gray;
|
||||
color: theme.$gray;
|
||||
}
|
||||
|
||||
.book-label {
|
||||
|
|
@ -143,11 +145,11 @@
|
|||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 700;
|
||||
@include font-size('small');
|
||||
@include font.font-size('small');
|
||||
|
||||
width: 212px;
|
||||
|
||||
@include breakpoint('md') {
|
||||
@include responsive.breakpoint('md') {
|
||||
width: 320px;
|
||||
}
|
||||
}
|
||||
|
|
@ -155,7 +157,7 @@
|
|||
.match {
|
||||
display: inline-block;
|
||||
background: #f7f77d;
|
||||
padding: rem(4px) rem(4px);
|
||||
padding: rem.rem(4px) rem.rem(4px);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -168,12 +170,12 @@
|
|||
align-items: center;
|
||||
|
||||
.paginator-info {
|
||||
@include font-size('small');
|
||||
color: $gray;
|
||||
@include font.font-size('small');
|
||||
color: theme.$gray;
|
||||
}
|
||||
|
||||
.paginator-link {
|
||||
padding: rem(12px) rem(12px);
|
||||
padding: rem.rem(12px) rem.rem(12px);
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
|
|
@ -181,10 +183,10 @@
|
|||
}
|
||||
|
||||
.paginator-link-prev {
|
||||
margin-left: rem(8px);
|
||||
margin-left: rem.rem(8px);
|
||||
|
||||
@include breakpoint(md) {
|
||||
margin-left: rem(20px);
|
||||
@include responsive.breakpoint(md) {
|
||||
margin-left: rem.rem(20px);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,11 +16,12 @@
|
|||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@import './theme';
|
||||
@import './font';
|
||||
@use 'theme';
|
||||
@use 'font';
|
||||
@use "rem";
|
||||
|
||||
.auth-page {
|
||||
background: $lighter-gray;
|
||||
background: theme.$lighter-gray;
|
||||
text-align: center;
|
||||
min-height: 100vh;
|
||||
padding: 50px 0;
|
||||
|
|
@ -30,8 +31,8 @@
|
|||
}
|
||||
|
||||
.heading {
|
||||
color: $black;
|
||||
@include font-size('2x-large');
|
||||
color: theme.$black;
|
||||
@include font.font-size('2x-large');
|
||||
font-weight: 300;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 0;
|
||||
|
|
@ -58,15 +59,15 @@
|
|||
|
||||
.callout {
|
||||
color: #7c7c7c;
|
||||
@include font-size('small');
|
||||
@include font.font-size('small');
|
||||
}
|
||||
.cta {
|
||||
@include font-size('small');
|
||||
@include font.font-size('small');
|
||||
}
|
||||
|
||||
.panel {
|
||||
border: 1px solid $border-color;
|
||||
background: $white;
|
||||
border: 1px solid theme.$border-color;
|
||||
background: theme.$white;
|
||||
border-radius: 2px;
|
||||
padding: 20px;
|
||||
text-align: left;
|
||||
|
|
@ -82,21 +83,21 @@
|
|||
}
|
||||
}
|
||||
.label {
|
||||
@include font-size('small');
|
||||
@include font.font-size('small');
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.forgot {
|
||||
@include font-size('small');
|
||||
@include font.font-size('small');
|
||||
float: right;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&.password-reset-page {
|
||||
.email-input {
|
||||
margin-top: rem(16px);
|
||||
margin-top: rem.rem(16px);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
@use "font";
|
||||
@use "rem";
|
||||
@use "responsive";
|
||||
@use "theme";
|
||||
|
||||
/* Copyright (C) 2019, 2020, 2021, 2022, 2023, 2024, 2025 Dnote contributors
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
|
|
@ -18,7 +23,7 @@
|
|||
|
||||
.note-page {
|
||||
// min-height: calc(100vh - 57px);
|
||||
background: $lighter-gray;
|
||||
background: theme.$lighter-gray;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
|
||||
|
|
@ -38,8 +43,8 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: rem(12px) rem(16px);
|
||||
border-bottom: 1px solid $border-color;
|
||||
padding: rem.rem(12px) rem.rem(16px);
|
||||
border-bottom: 1px solid theme.$border-color;
|
||||
}
|
||||
.header-left,
|
||||
.header-right {
|
||||
|
|
@ -52,27 +57,27 @@
|
|||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: rem(12px) rem(16px);
|
||||
padding: rem.rem(12px) rem.rem(16px);
|
||||
}
|
||||
|
||||
.collapsed-content {
|
||||
color: $light-gray;
|
||||
color: theme.$light-gray;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@include font-size('small');
|
||||
padding: rem(12px) rem(16px);
|
||||
@include font.font-size('small');
|
||||
padding: rem.rem(12px) rem.rem(16px);
|
||||
}
|
||||
|
||||
.ts {
|
||||
color: $light-gray;
|
||||
color: theme.$light-gray;
|
||||
}
|
||||
.ts-lead {
|
||||
display: none;
|
||||
@include breakpoint(md) {
|
||||
@include responsive.breakpoint(md) {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
|
@ -83,13 +88,13 @@
|
|||
}
|
||||
|
||||
.book-label {
|
||||
@include font-size('medium');
|
||||
@include font.font-size('medium');
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: $black;
|
||||
color: theme.$black;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
|
|
@ -103,17 +108,17 @@
|
|||
// header
|
||||
.header {
|
||||
.book-label {
|
||||
max-width: rem(200px);
|
||||
margin-left: rem(12px);
|
||||
max-width: rem.rem(200px);
|
||||
margin-left: rem.rem(12px);
|
||||
|
||||
@include breakpoint(sm) {
|
||||
max-width: rem(200px);
|
||||
@include responsive.breakpoint(sm) {
|
||||
max-width: rem.rem(200px);
|
||||
}
|
||||
@include breakpoint(md) {
|
||||
max-width: rem(420px);
|
||||
@include responsive.breakpoint(md) {
|
||||
max-width: rem.rem(420px);
|
||||
}
|
||||
@include breakpoint(lg) {
|
||||
max-width: rem(600px);
|
||||
@include responsive.breakpoint(lg) {
|
||||
max-width: rem.rem(600px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
@use "sass:list";
|
||||
@use "sass:map";
|
||||
@use "sass:meta";
|
||||
/* Copyright (C) 2019, 2020, 2021, 2022, 2023, 2024, 2025 Dnote contributors
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
|
|
@ -24,6 +27,8 @@ The above copyright notice and this permission notice shall be included in all c
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
@use "sass:math";
|
||||
|
||||
// assume 1 rem = 10 px
|
||||
// achieved by body { font-size: 62.5%; )
|
||||
$rem-baseline: 10px !default;
|
||||
|
|
@ -32,24 +37,24 @@ $rem-px-only: false !default;
|
|||
|
||||
@function rem-separator($list, $separator: false) {
|
||||
@if $separator == "comma" or $separator == "space" {
|
||||
@return append($list, null, $separator);
|
||||
@return list.append($list, null, $separator);
|
||||
}
|
||||
|
||||
@if function-exists("list-separator") == true {
|
||||
@return list-separator($list);
|
||||
@if meta.function-exists("list-separator") == true {
|
||||
@return list.separator($list);
|
||||
}
|
||||
|
||||
// list-separator polyfill by Hugo Giraudel (https://sass-compatibility.github.io/#list_separator_function)
|
||||
$test-list: ();
|
||||
@each $item in $list {
|
||||
$test-list: append($test-list, $item, space);
|
||||
$test-list: list.append($test-list, $item, space);
|
||||
}
|
||||
|
||||
@return if($test-list == $list, space, comma);
|
||||
}
|
||||
|
||||
@mixin rem-baseline($zoom: 100%) {
|
||||
font-size: $zoom / 16px * $rem-baseline;
|
||||
font-size: math.div($zoom, 16px) * $rem-baseline;
|
||||
}
|
||||
|
||||
@function rem-convert($to, $values...) {
|
||||
|
|
@ -57,28 +62,28 @@ $rem-px-only: false !default;
|
|||
$separator: rem-separator($values);
|
||||
|
||||
@each $value in $values {
|
||||
@if type-of($value) == "number" and unit($value) == "rem" and $to == "px" {
|
||||
$result: append($result, $value / 1rem * $rem-baseline, $separator);
|
||||
@if meta.type-of($value) == "number" and math.unit($value) == "rem" and $to == "px" {
|
||||
$result: list.append($result, math.div($value, 1rem) * $rem-baseline, $separator);
|
||||
} @else if
|
||||
type-of($value) ==
|
||||
meta.type-of($value) ==
|
||||
"number" and
|
||||
unit($value) ==
|
||||
math.unit($value) ==
|
||||
"px" and
|
||||
$to ==
|
||||
"rem"
|
||||
{
|
||||
$result: append($result, $value / $rem-baseline * 1rem, $separator);
|
||||
} @else if type-of($value) == "list" {
|
||||
$result: list.append($result, math.div($value, $rem-baseline) * 1rem, $separator);
|
||||
} @else if meta.type-of($value) == "list" {
|
||||
$value-separator: rem-separator($value);
|
||||
$value: rem-convert($to, $value...);
|
||||
$value: rem-separator($value, $value-separator);
|
||||
$result: append($result, $value, $separator);
|
||||
$result: list.append($result, $value, $separator);
|
||||
} @else {
|
||||
$result: append($result, $value, $separator);
|
||||
$result: list.append($result, $value, $separator);
|
||||
}
|
||||
}
|
||||
|
||||
@return if(length($result) == 1, nth($result, 1), $result);
|
||||
@return if(list.length($result) == 1, list.nth($result, 1), $result);
|
||||
}
|
||||
|
||||
@function rem($values...) {
|
||||
|
|
@ -90,9 +95,9 @@ $rem-px-only: false !default;
|
|||
}
|
||||
|
||||
@mixin rem($properties, $values...) {
|
||||
@if type-of($properties) == "map" {
|
||||
@each $property in map-keys($properties) {
|
||||
@include rem($property, map-get($properties, $property));
|
||||
@if meta.type-of($properties) == "map" {
|
||||
@each $property in map.keys($properties) {
|
||||
@include rem($property, map.get($properties, $property));
|
||||
}
|
||||
} @else {
|
||||
@each $property in $properties {
|
||||
|
|
|
|||
|
|
@ -16,39 +16,39 @@
|
|||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@import './variables';
|
||||
@use 'variables';
|
||||
|
||||
@mixin breakpoint($point) {
|
||||
@if $point == xl {
|
||||
@media (min-width: $xl-breakpoint) {
|
||||
@media (min-width: variables.$xl-breakpoint) {
|
||||
@content;
|
||||
}
|
||||
} @else if $point == lg {
|
||||
@media (min-width: $lg-breakpoint) {
|
||||
@media (min-width: variables.$lg-breakpoint) {
|
||||
@content;
|
||||
}
|
||||
} @else if $point == md {
|
||||
@media (min-width: $md-breakpoint) {
|
||||
@media (min-width: variables.$md-breakpoint) {
|
||||
@content;
|
||||
}
|
||||
} @else if $point == sm {
|
||||
@media (min-width: $sm-breakpoint) {
|
||||
@media (min-width: variables.$sm-breakpoint) {
|
||||
@content;
|
||||
}
|
||||
} @else if $point == smonly {
|
||||
@media (min-width: $sm-breakpoint) and (max-width: $md-breakpoint - 1px) {
|
||||
@media (min-width: variables.$sm-breakpoint) and (max-width: variables.$md-breakpoint - 1px) {
|
||||
@content;
|
||||
}
|
||||
} @else if $point == smdown {
|
||||
@media (max-width: $md-breakpoint - 1px) {
|
||||
@media (max-width: variables.$md-breakpoint - 1px) {
|
||||
@content;
|
||||
}
|
||||
} @else if $point == mdonly {
|
||||
@media (min-width: $md-breakpoint) and (max-width: $lg-breakpoint - 1px) {
|
||||
@media (min-width: variables.$md-breakpoint) and (max-width: variables.$lg-breakpoint - 1px) {
|
||||
@content;
|
||||
}
|
||||
} @else if $point == mddown {
|
||||
@media (max-width: $lg-breakpoint - 1px) {
|
||||
@media (max-width: variables.$lg-breakpoint - 1px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,17 +16,19 @@
|
|||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@import './theme';
|
||||
@import './font';
|
||||
@use 'theme';
|
||||
@use 'font';
|
||||
@use "rem";
|
||||
@use "responsive";
|
||||
|
||||
.settings-page {
|
||||
.sidebar {
|
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);
|
||||
background: white;
|
||||
margin-bottom: rem(20px);
|
||||
margin-top: rem(20px);
|
||||
margin-bottom: rem.rem(20px);
|
||||
margin-top: rem.rem(20px);
|
||||
|
||||
@include breakpoint(lg) {
|
||||
@include responsive.breakpoint(lg) {
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
|
@ -34,30 +36,30 @@
|
|||
|
||||
.sidebar-item {
|
||||
display: block;
|
||||
padding: rem(12px) rem(16px);
|
||||
padding: rem.rem(12px) rem.rem(16px);
|
||||
border-left: 4px solid transparent;
|
||||
@include font-size('regular');
|
||||
@include font.font-size('regular');
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background: $light;
|
||||
background: theme.$light;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: 600;
|
||||
border-left-color: $first;
|
||||
border-left-color: theme.$first;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-section-wrapper {
|
||||
.header {
|
||||
@include breakpoint(lg) {
|
||||
@include responsive.breakpoint(lg) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-section {
|
||||
margin-top: rem(24px);
|
||||
margin-top: rem.rem(24px);
|
||||
background: white;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.14);
|
||||
|
||||
|
|
@ -67,27 +69,27 @@
|
|||
}
|
||||
|
||||
.section-heading {
|
||||
@include font-size('regular');
|
||||
@include font.font-size('regular');
|
||||
font-weight: 600;
|
||||
padding-bottom: rem(4px);
|
||||
background: $light;
|
||||
padding: rem(16px) rem(20px);
|
||||
padding-bottom: rem.rem(4px);
|
||||
background: theme.$light;
|
||||
padding: rem.rem(16px) rem.rem(20px);
|
||||
}
|
||||
.section-content {
|
||||
margin-top: rem(20px);
|
||||
margin-top: rem.rem(20px);
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: rem(18px);
|
||||
margin-top: rem.rem(18px);
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
padding: rem(16px) rem(20px);
|
||||
padding: rem.rem(16px) rem.rem(20px);
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid $border-color;
|
||||
border-bottom: 1px solid theme.$border-color;
|
||||
}
|
||||
|
||||
.setting-row-summary {
|
||||
|
|
@ -95,7 +97,7 @@
|
|||
flex-direction: column;
|
||||
// align-items: flex-start;
|
||||
|
||||
@include breakpoint(md) {
|
||||
@include responsive.breakpoint(md) {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
|
@ -103,24 +105,24 @@
|
|||
}
|
||||
|
||||
.setting-row-main {
|
||||
padding-top: rem(24px);
|
||||
padding-top: rem.rem(24px);
|
||||
}
|
||||
|
||||
.setting-name {
|
||||
font-weight: 400;
|
||||
@include font-size('regular');
|
||||
@include font.font-size('regular');
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.setting-desc {
|
||||
margin-bottom: 0;
|
||||
@include font-size('small');
|
||||
color: $gray;
|
||||
@include font.font-size('small');
|
||||
color: theme.$gray;
|
||||
}
|
||||
.setting-action {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@include breakpoint(md) {
|
||||
@include responsive.breakpoint(md) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
|
@ -130,9 +132,9 @@
|
|||
word-break: break-all;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: rem(4px);
|
||||
margin-top: rem.rem(4px);
|
||||
|
||||
@include breakpoint(md) {
|
||||
@include responsive.breakpoint(md) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 0;
|
||||
|
|
@ -140,26 +142,26 @@
|
|||
}
|
||||
|
||||
.setting-edit {
|
||||
color: $link;
|
||||
color: theme.$link;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
color: $link-hover;
|
||||
color: theme.$link-hover;
|
||||
}
|
||||
@include breakpoint(md) {
|
||||
margin-left: rem(16px);
|
||||
@include responsive.breakpoint(md) {
|
||||
margin-left: rem.rem(16px);
|
||||
}
|
||||
}
|
||||
|
||||
.input-row {
|
||||
& ~ .input-row,
|
||||
.input-row {
|
||||
margin-top: rem(12px);
|
||||
margin-top: rem.rem(12px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.email-verification-form {
|
||||
margin-left: rem(12px);
|
||||
margin-left: rem.rem(12px);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,10 @@
|
|||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@import './font';
|
||||
@import './responsive';
|
||||
@use 'font';
|
||||
@use 'responsive';
|
||||
@use "rem";
|
||||
@use "theme";
|
||||
|
||||
@keyframes holderPulse {
|
||||
0% {
|
||||
|
|
@ -46,7 +48,7 @@ input[type='email']:disabled,
|
|||
input[type='number']:disabled,
|
||||
input[type='password']:disabled,
|
||||
textarea:disabled {
|
||||
background-color: $lighter-gray;
|
||||
background-color: theme.$lighter-gray;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
|
@ -76,17 +78,17 @@ button {
|
|||
}
|
||||
|
||||
.text-input {
|
||||
border: 1px solid $border-color;
|
||||
padding: rem(8px) rem(12px);
|
||||
border: 1px solid theme.$border-color;
|
||||
padding: rem.rem(8px) rem.rem(12px);
|
||||
position: relative;
|
||||
border-radius: rem(4px);
|
||||
border-radius: rem.rem(4px);
|
||||
display: block;
|
||||
|
||||
&::placeholder {
|
||||
color: $gray;
|
||||
color: theme.$gray;
|
||||
}
|
||||
&:focus {
|
||||
border-color: $light-blue;
|
||||
border-color: theme.$light-blue;
|
||||
box-shadow: inset 0 1px 2px rgba(24, 31, 35, 0.075),
|
||||
0 0 0 0.2em rgba(4, 100, 210, 0.3);
|
||||
outline: none;
|
||||
|
|
@ -94,11 +96,11 @@ button {
|
|||
}
|
||||
|
||||
.text-input-small {
|
||||
padding: rem(4px) rem(12px);
|
||||
padding: rem.rem(4px) rem.rem(12px);
|
||||
}
|
||||
|
||||
.text-input-medium {
|
||||
padding: rem(8px) rem(12px);
|
||||
padding: rem.rem(8px) rem.rem(12px);
|
||||
}
|
||||
|
||||
.text-input-stretch {
|
||||
|
|
@ -110,10 +112,10 @@ button {
|
|||
}
|
||||
|
||||
a {
|
||||
color: $link;
|
||||
color: theme.$link;
|
||||
|
||||
&:hover {
|
||||
color: $link-hover;
|
||||
color: theme.$link-hover;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -129,12 +131,12 @@ h6 {
|
|||
|
||||
// grid
|
||||
.container.mobile-fw {
|
||||
@include breakpoint(mddown) {
|
||||
@include responsive.breakpoint(mddown) {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
.container.mobile-nopadding {
|
||||
@include breakpoint(mddown) {
|
||||
@include responsive.breakpoint(mddown) {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
|
|
@ -154,30 +156,30 @@ html body {
|
|||
}
|
||||
|
||||
.page {
|
||||
padding-top: rem(20px);
|
||||
padding-bottom: rem(20px);
|
||||
padding-top: rem.rem(20px);
|
||||
padding-bottom: rem.rem(20px);
|
||||
|
||||
&.page-mobile-full {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
|
||||
@include breakpoint(lg) {
|
||||
padding-top: rem(32px);
|
||||
padding-bottom: rem(32px);
|
||||
@include responsive.breakpoint(lg) {
|
||||
padding-top: rem.rem(32px);
|
||||
padding-bottom: rem.rem(32px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-top: rem(20px);
|
||||
margin-top: rem.rem(20px);
|
||||
|
||||
&.page-header-full {
|
||||
margin-bottom: rem(20px);
|
||||
margin-bottom: rem.rem(20px);
|
||||
}
|
||||
|
||||
@include breakpoint(lg) {
|
||||
@include responsive.breakpoint(lg) {
|
||||
// padding: 0;
|
||||
margin-bottom: rem(20px);
|
||||
margin-bottom: rem.rem(20px);
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -189,7 +191,7 @@ html body {
|
|||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
background-size: 8px 10px;
|
||||
border: 1px solid $border-color;
|
||||
border: 1px solid theme.$border-color;
|
||||
min-height: 34px;
|
||||
padding: 6px 8px;
|
||||
padding-right: 24px;
|
||||
|
|
@ -207,7 +209,7 @@ html body {
|
|||
&:disabled,
|
||||
&.form-select-disabled {
|
||||
background-image: url('data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAABAAAAAUCAYAAACEYr13AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAEKSURBVHgBzVTNDYIwFC4NB46OwAi4gY7gETgoE6gTGCcwTgAJ4efGCLCBjMAIXrmA3yOhQazQhJj4JQ0v7fte3/e1hbFfIk3TYxzHp6kc7dtCFEUW5/xBcdM0a9d1S1kel00mSWKCnIkkxDSnXADIMYYEU9O0zPf91WwB6L6NyB3atrUMw7hNFkCbFyROmXYYmypMDMNwo+t6ztSwtW27oEAXrXBuwu2rCht+WPgU7C8gPCBzYOBKhQS5FTwIKBYeQFeJoWyiKNYH5Co6OCuQr/0JdBuPVyElQCd7GRMb3B3HebsHHzexrmvyQvZwqjFZWsDzvCc62BFhSGYD3UMsfs6ToKOd+6EsxgtrtWLW4gUN3AAAAABJRU5ErkJggg==');
|
||||
background-color: $lighter-gray;
|
||||
background-color: theme.$lighter-gray;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -215,12 +217,12 @@ html body {
|
|||
// width: 100%;
|
||||
width: auto;
|
||||
font-weight: 600;
|
||||
margin-bottom: rem(4px);
|
||||
@include font-size('small');
|
||||
margin-bottom: rem.rem(4px);
|
||||
@include font.font-size('small');
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
@include font-size('x-large');
|
||||
@include font.font-size('x-large');
|
||||
}
|
||||
|
||||
.dropdown-caret {
|
||||
|
|
@ -231,7 +233,7 @@ html body {
|
|||
border-right: 4px solid transparent;
|
||||
border-bottom: 0 solid transparent;
|
||||
border-left: 4px solid transparent;
|
||||
margin-left: rem(8px);
|
||||
margin-left: rem.rem(8px);
|
||||
}
|
||||
|
||||
.divider {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
@use "sass:color";
|
||||
/* Copyright (C) 2019, 2020, 2021, 2022, 2023, 2024, 2025 Dnote contributors
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
|
|
@ -26,7 +27,7 @@ $lighter-gray: #f3f3f3;
|
|||
$dark-gray: #637283;
|
||||
|
||||
// primary colors
|
||||
$first: #072a40;
|
||||
$first: #333745;
|
||||
$second: #e7e7e7;
|
||||
$third: #0a4b73;
|
||||
|
||||
|
|
@ -35,7 +36,7 @@ $border-color: #d8d8d8;
|
|||
$border-color-light: $lighter-gray;
|
||||
|
||||
$link: #6f53c0;
|
||||
$link-hover: darken($link, 5%);
|
||||
$link-hover: color.adjust($link, $lightness: -5%);
|
||||
|
||||
$danger-text: #cb2431;
|
||||
$danger-background: #f8d7da;
|
||||
|
|
|
|||
|
|
@ -16,25 +16,25 @@
|
|||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@import './reboot';
|
||||
@import './grid';
|
||||
@import './bootstrap';
|
||||
@import './buttons';
|
||||
@import './responsive';
|
||||
@import './select';
|
||||
@import './shared';
|
||||
@import './marker';
|
||||
@import './rem';
|
||||
@import './markdown';
|
||||
@import './hljs';
|
||||
@use 'reboot';
|
||||
@use 'grid';
|
||||
@use 'bootstrap';
|
||||
@use 'buttons';
|
||||
@use 'responsive';
|
||||
@use 'select';
|
||||
@use 'shared';
|
||||
@use 'marker';
|
||||
@use 'rem';
|
||||
@use 'markdown';
|
||||
@use 'hljs';
|
||||
|
||||
@import './login';
|
||||
@import './home';
|
||||
@import './note';
|
||||
@import './books';
|
||||
@import './settings';
|
||||
@import './header';
|
||||
@import './global';
|
||||
@use 'login';
|
||||
@use 'home';
|
||||
@use 'note';
|
||||
@use 'books';
|
||||
@use 'settings';
|
||||
@use 'header';
|
||||
@use 'global';
|
||||
|
||||
html {
|
||||
font-size: 62.5%; /* 1.0 rem = 10px */
|
||||
|
|
@ -74,11 +74,11 @@ img {
|
|||
}
|
||||
|
||||
.container.mobile-nopadding {
|
||||
@include breakpoint(mdonly) {
|
||||
@include responsive.breakpoint(mdonly) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@include breakpoint(mddown) {
|
||||
@include responsive.breakpoint(mddown) {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
|
|
@ -137,7 +137,7 @@ img {
|
|||
}
|
||||
|
||||
.input {
|
||||
border-radius: rem(4px);
|
||||
border-radius: rem.rem(4px);
|
||||
background-clip: padding-box;
|
||||
border: 1px solid #ced4da;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
|
|
|
|||
|
|
@ -232,8 +232,6 @@ func getStatusCode(err error) int {
|
|||
return http.StatusUnauthorized
|
||||
case app.ErrEmailTooLong:
|
||||
return http.StatusBadRequest
|
||||
case app.ErrEmailAlreadyVerified:
|
||||
return http.StatusConflict
|
||||
case app.ErrMissingToken:
|
||||
return http.StatusBadRequest
|
||||
case app.ErrExpiredToken:
|
||||
|
|
|
|||
|
|
@ -58,8 +58,6 @@ func NewWebRoutes(a *app.App, c *Controllers) []Route {
|
|||
{"PATCH", "/password-reset", c.Users.PasswordReset, true},
|
||||
{"GET", "/password-reset/{token}", c.Users.PasswordResetConfirm, true},
|
||||
{"POST", "/reset-token", c.Users.CreateResetToken, true},
|
||||
{"POST", "/verification-token", mw.Auth(a.DB, c.Users.CreateEmailVerificationToken, redirectGuest), true},
|
||||
{"GET", "/verify-email/{token}", mw.Auth(a.DB, c.Users.VerifyEmail, redirectGuest), true},
|
||||
{"PATCH", "/account/profile", mw.Auth(a.DB, c.Users.ProfileUpdate, nil), true},
|
||||
{"PATCH", "/account/password", mw.Auth(a.DB, c.Users.PasswordUpdate, nil), true},
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ import (
|
|||
"github.com/dnote/dnote/pkg/server/database"
|
||||
"github.com/dnote/dnote/pkg/server/helpers"
|
||||
"github.com/dnote/dnote/pkg/server/log"
|
||||
"github.com/dnote/dnote/pkg/server/mailer"
|
||||
"github.com/dnote/dnote/pkg/server/token"
|
||||
"github.com/dnote/dnote/pkg/server/views"
|
||||
"github.com/gorilla/mux"
|
||||
|
|
@ -80,10 +79,6 @@ func NewUsers(app *app.App, viewEngine *views.Engine) *Users {
|
|||
views.Config{Title: "About", Layout: "base", HelperFuncs: commonHelpers, HeaderTemplate: "navbar"},
|
||||
"users/settings_about",
|
||||
),
|
||||
EmailVerificationView: viewEngine.NewView(app,
|
||||
views.Config{Layout: "base", HelperFuncs: commonHelpers, HeaderTemplate: "navbar"},
|
||||
"users/email_verification",
|
||||
),
|
||||
app: app,
|
||||
}
|
||||
}
|
||||
|
|
@ -96,7 +91,6 @@ type Users struct {
|
|||
AboutView *views.View
|
||||
PasswordResetView *views.View
|
||||
PasswordResetConfirmView *views.View
|
||||
EmailVerificationView *views.View
|
||||
app *app.App
|
||||
}
|
||||
|
||||
|
|
@ -599,10 +593,6 @@ func (u *Users) ProfileUpdate(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// check if email was changed
|
||||
if form.Email != account.Email.String {
|
||||
account.EmailVerified = false
|
||||
}
|
||||
account.Email.String = form.Email
|
||||
|
||||
if err := tx.Save(&account).Error; err != nil {
|
||||
|
|
@ -620,119 +610,3 @@ func (u *Users) ProfileUpdate(w http.ResponseWriter, r *http.Request) {
|
|||
views.RedirectAlert(w, r, "/", http.StatusFound, alert)
|
||||
}
|
||||
|
||||
func (u *Users) VerifyEmail(w http.ResponseWriter, r *http.Request) {
|
||||
vd := views.Data{}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
tokenValue := vars["token"]
|
||||
|
||||
if tokenValue == "" {
|
||||
handleHTMLError(w, r, app.ErrMissingToken, "Missing email verification token", u.EmailVerificationView, vd)
|
||||
return
|
||||
}
|
||||
|
||||
var token database.Token
|
||||
if err := u.app.DB.
|
||||
Where("value = ? AND type = ?", tokenValue, database.TokenTypeEmailVerification).
|
||||
First(&token).Error; err != nil {
|
||||
handleHTMLError(w, r, app.ErrInvalidToken, "Finding token", u.EmailVerificationView, vd)
|
||||
return
|
||||
}
|
||||
|
||||
if token.UsedAt != nil {
|
||||
handleHTMLError(w, r, app.ErrInvalidToken, "Token has already been used.", u.EmailVerificationView, vd)
|
||||
return
|
||||
}
|
||||
|
||||
// Expire after ttl
|
||||
if time.Since(token.CreatedAt).Minutes() > 30 {
|
||||
handleHTMLError(w, r, app.ErrExpiredToken, "Token has expired.", u.EmailVerificationView, vd)
|
||||
return
|
||||
}
|
||||
|
||||
var account database.Account
|
||||
if err := u.app.DB.Where("user_id = ?", token.UserID).First(&account).Error; err != nil {
|
||||
handleHTMLError(w, r, err, "finding account", u.EmailVerificationView, vd)
|
||||
return
|
||||
}
|
||||
if account.EmailVerified {
|
||||
handleHTMLError(w, r, app.ErrEmailAlreadyVerified, "Already verified", u.EmailVerificationView, vd)
|
||||
return
|
||||
}
|
||||
|
||||
tx := u.app.DB.Begin()
|
||||
account.EmailVerified = true
|
||||
if err := tx.Save(&account).Error; err != nil {
|
||||
tx.Rollback()
|
||||
handleHTMLError(w, r, err, "updating email_verified", u.EmailVerificationView, vd)
|
||||
return
|
||||
}
|
||||
if err := tx.Model(&token).Update("used_at", time.Now()).Error; err != nil {
|
||||
tx.Rollback()
|
||||
handleHTMLError(w, r, err, "updating reset token", u.EmailVerificationView, vd)
|
||||
return
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
var user database.User
|
||||
if err := u.app.DB.Where("id = ?", token.UserID).First(&user).Error; err != nil {
|
||||
handleHTMLError(w, r, err, "finding user", u.EmailVerificationView, vd)
|
||||
return
|
||||
}
|
||||
|
||||
session, err := u.app.SignIn(&user)
|
||||
if err != nil {
|
||||
handleHTMLError(w, r, err, "Creating session", u.EmailVerificationView, vd)
|
||||
}
|
||||
|
||||
setSessionCookie(w, session.Key, session.ExpiresAt)
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
func (u *Users) CreateEmailVerificationToken(w http.ResponseWriter, r *http.Request) {
|
||||
vd := views.Data{}
|
||||
|
||||
user := context.User(r.Context())
|
||||
if user == nil {
|
||||
handleHTMLError(w, r, app.ErrLoginRequired, "No authenticated user found", u.SettingView, vd)
|
||||
return
|
||||
}
|
||||
|
||||
var account database.Account
|
||||
err := u.app.DB.Where("user_id = ?", user.ID).First(&account).Error
|
||||
if err != nil {
|
||||
handleHTMLError(w, r, err, "finding account", u.SettingView, vd)
|
||||
return
|
||||
}
|
||||
|
||||
if account.EmailVerified {
|
||||
handleHTMLError(w, r, app.ErrEmailAlreadyVerified, "email is already verified.", u.SettingView, vd)
|
||||
return
|
||||
}
|
||||
if account.Email.String == "" {
|
||||
handleHTMLError(w, r, app.ErrEmailRequired, "email is empty.", u.SettingView, vd)
|
||||
return
|
||||
}
|
||||
|
||||
tok, err := token.Create(u.app.DB, account.UserID, database.TokenTypeEmailVerification)
|
||||
if err != nil {
|
||||
handleHTMLError(w, r, err, "saving token", u.SettingView, vd)
|
||||
return
|
||||
}
|
||||
|
||||
if err := u.app.SendVerificationEmail(account.Email.String, tok.Value); err != nil {
|
||||
if pkgErrors.Cause(err) == mailer.ErrSMTPNotConfigured {
|
||||
handleHTMLError(w, r, app.ErrInvalidSMTPConfig, "SMTP config is not configured correctly.", u.SettingView, vd)
|
||||
} else {
|
||||
handleHTMLError(w, r, err, "sending verification email", u.SettingView, vd)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
alert := views.Alert{
|
||||
Level: views.AlertLvlSuccess,
|
||||
Message: "Please check your email for the verification",
|
||||
}
|
||||
views.RedirectAlert(w, r, "/", http.StatusFound, alert)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -578,12 +578,6 @@ func TestResetPassword(t *testing.T) {
|
|||
Type: database.TokenTypeResetPassword,
|
||||
}
|
||||
testutils.MustExec(t, db.Save(&tok), "preparing token")
|
||||
otherTok := database.Token{
|
||||
UserID: u.ID,
|
||||
Value: "somerandomvalue",
|
||||
Type: database.TokenTypeEmailVerification,
|
||||
}
|
||||
testutils.MustExec(t, db.Save(&otherTok), "preparing another token")
|
||||
|
||||
s1 := database.Session{
|
||||
Key: "some-session-key-1",
|
||||
|
|
@ -618,16 +612,14 @@ func TestResetPassword(t *testing.T) {
|
|||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusFound, "Status code mismatch")
|
||||
|
||||
var resetToken, verificationToken database.Token
|
||||
var resetToken database.Token
|
||||
var account database.Account
|
||||
testutils.MustExec(t, db.Where("value = ?", "MivFxYiSMMA4An9dP24DNQ==").First(&resetToken), "finding reset token")
|
||||
testutils.MustExec(t, db.Where("value = ?", "somerandomvalue").First(&verificationToken), "finding reset token")
|
||||
testutils.MustExec(t, db.Where("id = ?", acc.ID).First(&account), "finding account")
|
||||
|
||||
assert.NotEqual(t, resetToken.UsedAt, nil, "reset_token UsedAt mismatch")
|
||||
passwordErr := bcrypt.CompareHashAndPassword([]byte(account.Password.String), []byte("newpassword"))
|
||||
assert.Equal(t, passwordErr, nil, "Password mismatch")
|
||||
assert.Equal(t, verificationToken.UsedAt, (*time.Time)(nil), "verificationToken UsedAt mismatch")
|
||||
|
||||
var s1Count, s2Count int64
|
||||
testutils.MustExec(t, db.Model(&database.Session{}).Where("id = ?", s1.ID).Count(&s1Count), "counting s1")
|
||||
|
|
@ -777,46 +769,6 @@ func TestResetPassword(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("using wrong type token: email_verification", func(t *testing.T) {
|
||||
db := testutils.InitMemoryDB(t)
|
||||
|
||||
// Setup
|
||||
a := app.NewTest()
|
||||
a.Clock = clock.NewMock()
|
||||
a.DB = db
|
||||
server := MustNewServer(t, &a)
|
||||
defer server.Close()
|
||||
|
||||
u := testutils.SetupUserData(db)
|
||||
acc := testutils.SetupAccountData(db, u, "alice@example.com", "somepassword")
|
||||
tok := database.Token{
|
||||
UserID: u.ID,
|
||||
Value: "MivFxYiSMMA4An9dP24DNQ==",
|
||||
Type: database.TokenTypeEmailVerification,
|
||||
}
|
||||
testutils.MustExec(t, db.Save(&tok), "Failed to prepare reset_token")
|
||||
testutils.MustExec(t, db.Model(&tok).Update("created_at", time.Now().Add(time.Minute*-11)), "Failed to prepare reset_token created_at")
|
||||
|
||||
dat := url.Values{}
|
||||
dat.Set("token", "MivFxYiSMMA4An9dP24DNQ==")
|
||||
dat.Set("password", "oldpassword")
|
||||
dat.Set("password_confirmation", "oldpassword")
|
||||
req := testutils.MakeFormReq(server.URL, "PATCH", "/password-reset", dat)
|
||||
|
||||
// Execute
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusBadRequest, "Status code mismatch")
|
||||
|
||||
var resetToken database.Token
|
||||
var account database.Account
|
||||
testutils.MustExec(t, db.Where("value = ?", "MivFxYiSMMA4An9dP24DNQ==").First(&resetToken), "failed to find reset_token")
|
||||
testutils.MustExec(t, db.Where("id = ?", acc.ID).First(&account), "failed to find account")
|
||||
|
||||
assert.Equal(t, acc.Password, account.Password, "password should not have been updated")
|
||||
assert.Equal(t, resetToken.UsedAt, (*time.Time)(nil), "used_at should be nil")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateResetToken(t *testing.T) {
|
||||
|
|
@ -1018,9 +970,7 @@ func TestUpdateEmail(t *testing.T) {
|
|||
defer server.Close()
|
||||
|
||||
u := testutils.SetupUserData(db)
|
||||
acc := testutils.SetupAccountData(db, u, "alice@example.com", "pass1234")
|
||||
acc.EmailVerified = true
|
||||
testutils.MustExec(t, db.Save(&acc), "updating email_verified")
|
||||
testutils.SetupAccountData(db, u, "alice@example.com", "pass1234")
|
||||
|
||||
// Execute
|
||||
dat := url.Values{}
|
||||
|
|
@ -1039,7 +989,6 @@ func TestUpdateEmail(t *testing.T) {
|
|||
testutils.MustExec(t, db.Where("user_id = ?", u.ID).First(&account), "finding account")
|
||||
|
||||
assert.Equal(t, account.Email.String, "alice-new@example.com", "email mismatch")
|
||||
assert.Equal(t, account.EmailVerified, false, "EmailVerified mismatch")
|
||||
})
|
||||
|
||||
t.Run("password mismatch", func(t *testing.T) {
|
||||
|
|
@ -1053,9 +1002,7 @@ func TestUpdateEmail(t *testing.T) {
|
|||
defer server.Close()
|
||||
|
||||
u := testutils.SetupUserData(db)
|
||||
acc := testutils.SetupAccountData(db, u, "alice@example.com", "pass1234")
|
||||
acc.EmailVerified = true
|
||||
testutils.MustExec(t, db.Save(&acc), "updating email_verified")
|
||||
testutils.SetupAccountData(db, u, "alice@example.com", "pass1234")
|
||||
|
||||
// Execute
|
||||
dat := url.Values{}
|
||||
|
|
@ -1074,238 +1021,6 @@ func TestUpdateEmail(t *testing.T) {
|
|||
testutils.MustExec(t, db.Where("user_id = ?", u.ID).First(&account), "finding account")
|
||||
|
||||
assert.Equal(t, account.Email.String, "alice@example.com", "email mismatch")
|
||||
assert.Equal(t, account.EmailVerified, true, "EmailVerified mismatch")
|
||||
})
|
||||
}
|
||||
|
||||
func TestVerifyEmail(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
db := testutils.InitMemoryDB(t)
|
||||
|
||||
// Setup
|
||||
a := app.NewTest()
|
||||
a.Clock = clock.NewMock()
|
||||
a.DB = db
|
||||
server := MustNewServer(t, &a)
|
||||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData(db)
|
||||
testutils.SetupAccountData(db, user, "alice@example.com", "pass1234")
|
||||
tok := database.Token{
|
||||
UserID: user.ID,
|
||||
Type: database.TokenTypeEmailVerification,
|
||||
Value: "someTokenValue",
|
||||
}
|
||||
testutils.MustExec(t, db.Save(&tok), "preparing token")
|
||||
|
||||
// Execute
|
||||
req := testutils.MakeReq(server.URL, "GET", fmt.Sprintf("/verify-email/%s", "someTokenValue"), "")
|
||||
res := testutils.HTTPAuthDo(t, db, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusFound, "Status code mismatch")
|
||||
|
||||
var account database.Account
|
||||
var token database.Token
|
||||
var tokenCount int64
|
||||
testutils.MustExec(t, db.Where("user_id = ?", user.ID).First(&account), "finding account")
|
||||
testutils.MustExec(t, db.Where("user_id = ? AND type = ?", user.ID, database.TokenTypeEmailVerification).First(&token), "finding token")
|
||||
testutils.MustExec(t, db.Model(&database.Token{}).Count(&tokenCount), "counting token")
|
||||
|
||||
assert.Equal(t, account.EmailVerified, true, "email_verified mismatch")
|
||||
assert.NotEqual(t, token.Value, "", "token value should not have been updated")
|
||||
assert.Equal(t, tokenCount, int64(1), "token count mismatch")
|
||||
assert.NotEqual(t, token.UsedAt, (*time.Time)(nil), "token should have been used")
|
||||
})
|
||||
|
||||
t.Run("used token", func(t *testing.T) {
|
||||
db := testutils.InitMemoryDB(t)
|
||||
|
||||
// Setup
|
||||
a := app.NewTest()
|
||||
a.Clock = clock.NewMock()
|
||||
a.DB = db
|
||||
server := MustNewServer(t, &a)
|
||||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData(db)
|
||||
testutils.SetupAccountData(db, user, "alice@example.com", "pass1234")
|
||||
|
||||
usedAt := time.Now().Add(time.Hour * -11).UTC()
|
||||
tok := database.Token{
|
||||
UserID: user.ID,
|
||||
Type: database.TokenTypeEmailVerification,
|
||||
Value: "someTokenValue",
|
||||
UsedAt: &usedAt,
|
||||
}
|
||||
testutils.MustExec(t, db.Save(&tok), "preparing token")
|
||||
|
||||
// Execute
|
||||
req := testutils.MakeReq(server.URL, "GET", fmt.Sprintf("/verify-email/%s", "someTokenValue"), "")
|
||||
res := testutils.HTTPAuthDo(t, db, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusBadRequest, "")
|
||||
|
||||
var account database.Account
|
||||
var token database.Token
|
||||
var tokenCount int64
|
||||
testutils.MustExec(t, db.Where("user_id = ?", user.ID).First(&account), "finding account")
|
||||
testutils.MustExec(t, db.Where("user_id = ? AND type = ?", user.ID, database.TokenTypeEmailVerification).First(&token), "finding token")
|
||||
testutils.MustExec(t, db.Model(&database.Token{}).Count(&tokenCount), "counting token")
|
||||
|
||||
assert.Equal(t, account.EmailVerified, false, "email_verified mismatch")
|
||||
assert.NotEqual(t, token.UsedAt, nil, "token used_at mismatch")
|
||||
assert.Equal(t, tokenCount, int64(1), "token count mismatch")
|
||||
assert.NotEqual(t, token.UsedAt, (*time.Time)(nil), "token should have been used")
|
||||
})
|
||||
|
||||
t.Run("expired token", func(t *testing.T) {
|
||||
db := testutils.InitMemoryDB(t)
|
||||
|
||||
// Setup
|
||||
a := app.NewTest()
|
||||
a.Clock = clock.NewMock()
|
||||
a.DB = db
|
||||
server := MustNewServer(t, &a)
|
||||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData(db)
|
||||
testutils.SetupAccountData(db, user, "alice@example.com", "pass1234")
|
||||
|
||||
tok := database.Token{
|
||||
UserID: user.ID,
|
||||
Type: database.TokenTypeEmailVerification,
|
||||
Value: "someTokenValue",
|
||||
}
|
||||
testutils.MustExec(t, db.Save(&tok), "preparing token")
|
||||
testutils.MustExec(t, db.Model(&tok).Update("created_at", time.Now().Add(time.Minute*-31)), "Failed to prepare token created_at")
|
||||
|
||||
// Execute
|
||||
req := testutils.MakeReq(server.URL, "GET", fmt.Sprintf("/verify-email/%s", "someTokenValue"), "")
|
||||
res := testutils.HTTPAuthDo(t, db, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusGone, "")
|
||||
|
||||
var account database.Account
|
||||
var token database.Token
|
||||
var tokenCount int64
|
||||
testutils.MustExec(t, db.Where("user_id = ?", user.ID).First(&account), "finding account")
|
||||
testutils.MustExec(t, db.Where("user_id = ? AND type = ?", user.ID, database.TokenTypeEmailVerification).First(&token), "finding token")
|
||||
testutils.MustExec(t, db.Model(&database.Token{}).Count(&tokenCount), "counting token")
|
||||
|
||||
assert.Equal(t, account.EmailVerified, false, "email_verified mismatch")
|
||||
assert.Equal(t, tokenCount, int64(1), "token count mismatch")
|
||||
assert.Equal(t, token.UsedAt, (*time.Time)(nil), "token should have not been used")
|
||||
})
|
||||
|
||||
t.Run("already verified", func(t *testing.T) {
|
||||
db := testutils.InitMemoryDB(t)
|
||||
|
||||
// Setup
|
||||
a := app.NewTest()
|
||||
a.Clock = clock.NewMock()
|
||||
a.DB = db
|
||||
server := MustNewServer(t, &a)
|
||||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData(db)
|
||||
acc := testutils.SetupAccountData(db, user, "alice@example.com", "oldpass1234")
|
||||
acc.EmailVerified = true
|
||||
testutils.MustExec(t, db.Save(&acc), "preparing account")
|
||||
|
||||
tok := database.Token{
|
||||
UserID: user.ID,
|
||||
Type: database.TokenTypeEmailVerification,
|
||||
Value: "someTokenValue",
|
||||
}
|
||||
testutils.MustExec(t, db.Save(&tok), "preparing token")
|
||||
|
||||
// Execute
|
||||
req := testutils.MakeReq(server.URL, "GET", fmt.Sprintf("/verify-email/%s", "someTokenValue"), "")
|
||||
res := testutils.HTTPAuthDo(t, db, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusConflict, "")
|
||||
|
||||
var account database.Account
|
||||
var token database.Token
|
||||
var tokenCount int64
|
||||
testutils.MustExec(t, db.Where("user_id = ?", user.ID).First(&account), "finding account")
|
||||
testutils.MustExec(t, db.Where("user_id = ? AND type = ?", user.ID, database.TokenTypeEmailVerification).First(&token), "finding token")
|
||||
testutils.MustExec(t, db.Model(&database.Token{}).Count(&tokenCount), "counting token")
|
||||
|
||||
assert.Equal(t, account.EmailVerified, true, "email_verified mismatch")
|
||||
assert.Equal(t, tokenCount, int64(1), "token count mismatch")
|
||||
assert.Equal(t, token.UsedAt, (*time.Time)(nil), "token should have not been used")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateVerificationToken(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
db := testutils.InitMemoryDB(t)
|
||||
|
||||
// Setup
|
||||
emailBackend := testutils.MockEmailbackendImplementation{}
|
||||
a := app.NewTest()
|
||||
a.Clock = clock.NewMock()
|
||||
a.DB = db
|
||||
a.EmailBackend = &emailBackend
|
||||
server := MustNewServer(t, &a)
|
||||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData(db)
|
||||
testutils.SetupAccountData(db, user, "alice@example.com", "pass1234")
|
||||
|
||||
// Execute
|
||||
req := testutils.MakeReq(server.URL, "POST", "/verification-token", "")
|
||||
res := testutils.HTTPAuthDo(t, db, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusFound, "status code mismatch")
|
||||
|
||||
var account database.Account
|
||||
var token database.Token
|
||||
var tokenCount int64
|
||||
testutils.MustExec(t, db.Where("user_id = ?", user.ID).First(&account), "finding account")
|
||||
testutils.MustExec(t, db.Where("user_id = ? AND type = ?", user.ID, database.TokenTypeEmailVerification).First(&token), "finding token")
|
||||
testutils.MustExec(t, db.Model(&database.Token{}).Count(&tokenCount), "counting token")
|
||||
|
||||
assert.Equal(t, account.EmailVerified, false, "email_verified should not have been updated")
|
||||
assert.NotEqual(t, token.Value, "", "token Value mismatch")
|
||||
assert.Equal(t, tokenCount, int64(1), "token count mismatch")
|
||||
assert.Equal(t, token.UsedAt, (*time.Time)(nil), "token UsedAt mismatch")
|
||||
assert.Equal(t, len(emailBackend.Emails), 1, "email queue count mismatch")
|
||||
})
|
||||
|
||||
t.Run("already verified", func(t *testing.T) {
|
||||
db := testutils.InitMemoryDB(t)
|
||||
// Setup
|
||||
a := app.NewTest()
|
||||
a.Clock = clock.NewMock()
|
||||
a.DB = db
|
||||
server := MustNewServer(t, &a)
|
||||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData(db)
|
||||
acc := testutils.SetupAccountData(db, user, "alice@example.com", "pass1234")
|
||||
acc.EmailVerified = true
|
||||
testutils.MustExec(t, db.Save(&acc), "preparing account")
|
||||
|
||||
// Execute
|
||||
req := testutils.MakeReq(server.URL, "POST", "/verification-token", "")
|
||||
res := testutils.HTTPAuthDo(t, db, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusConflict, "Status code mismatch")
|
||||
|
||||
var account database.Account
|
||||
var tokenCount int64
|
||||
testutils.MustExec(t, db.Where("user_id = ?", user.ID).First(&account), "finding account")
|
||||
testutils.MustExec(t, db.Model(&database.Token{}).Count(&tokenCount), "counting token")
|
||||
|
||||
assert.Equal(t, account.EmailVerified, true, "email_verified should not have been updated")
|
||||
assert.Equal(t, tokenCount, int64(0), "token count mismatch")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ package database
|
|||
const (
|
||||
// TokenTypeResetPassword is a type of a token for reseting password
|
||||
TokenTypeResetPassword = "reset_password"
|
||||
// TokenTypeEmailVerification is a type of a token for verifying email
|
||||
TokenTypeEmailVerification = "email_verification"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -73,10 +73,9 @@ type User struct {
|
|||
// Account is a model for an account
|
||||
type Account struct {
|
||||
Model
|
||||
UserID int `gorm:"index"`
|
||||
Email NullString
|
||||
EmailVerified bool `gorm:"default:false"`
|
||||
Password NullString
|
||||
UserID int `gorm:"index"`
|
||||
Email NullString
|
||||
Password NullString
|
||||
}
|
||||
|
||||
// Token is a model for a token
|
||||
|
|
|
|||
|
|
@ -34,8 +34,6 @@ var (
|
|||
EmailTypeResetPassword = "reset_password"
|
||||
// EmailTypeResetPasswordAlert represents a password change notification email
|
||||
EmailTypeResetPasswordAlert = "reset_password_alert"
|
||||
// EmailTypeEmailVerification represents an email verification email
|
||||
EmailTypeEmailVerification = "verify_email"
|
||||
// EmailTypeWelcome represents an welcome email
|
||||
EmailTypeWelcome = "welcome"
|
||||
)
|
||||
|
|
@ -79,10 +77,6 @@ func NewTemplates() Templates {
|
|||
if err != nil {
|
||||
panic(errors.Wrap(err, "initializing welcome template"))
|
||||
}
|
||||
verifyEmailText, err := initTextTmpl(EmailTypeEmailVerification)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "initializing email verification template"))
|
||||
}
|
||||
passwordResetText, err := initTextTmpl(EmailTypeResetPassword)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "initializing password reset template"))
|
||||
|
|
@ -95,7 +89,6 @@ func NewTemplates() Templates {
|
|||
T := Templates{}
|
||||
T.set(EmailTypeResetPassword, EmailKindText, passwordResetText)
|
||||
T.set(EmailTypeResetPasswordAlert, EmailKindText, passwordResetAlertText)
|
||||
T.set(EmailTypeEmailVerification, EmailKindText, verifyEmailText)
|
||||
T.set(EmailTypeWelcome, EmailKindText, welcomeText)
|
||||
|
||||
return T
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ func TestAllTemplatesInitialized(t *testing.T) {
|
|||
emailTypes := []string{
|
||||
EmailTypeResetPassword,
|
||||
EmailTypeResetPasswordAlert,
|
||||
EmailTypeEmailVerification,
|
||||
EmailTypeWelcome,
|
||||
}
|
||||
|
||||
|
|
@ -46,44 +45,6 @@ func TestAllTemplatesInitialized(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestEmailVerificationEmail(t *testing.T) {
|
||||
testCases := []struct {
|
||||
token string
|
||||
webURL string
|
||||
}{
|
||||
{
|
||||
token: "someRandomToken1",
|
||||
webURL: "http://localhost:3000",
|
||||
},
|
||||
{
|
||||
token: "someRandomToken2",
|
||||
webURL: "http://localhost:3001",
|
||||
},
|
||||
}
|
||||
|
||||
tmpl := NewTemplates()
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("with WebURL %s", tc.webURL), func(t *testing.T) {
|
||||
dat := EmailVerificationTmplData{
|
||||
Token: tc.token,
|
||||
WebURL: tc.webURL,
|
||||
}
|
||||
body, err := tmpl.Execute(EmailTypeEmailVerification, EmailKindText, dat)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "executing"))
|
||||
}
|
||||
|
||||
if ok := strings.Contains(body, tc.webURL); !ok {
|
||||
t.Errorf("email body did not contain %s", tc.webURL)
|
||||
}
|
||||
if ok := strings.Contains(body, tc.token); !ok {
|
||||
t.Errorf("email body did not contain %s", tc.token)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetPasswordEmail(t *testing.T) {
|
||||
testCases := []struct {
|
||||
token string
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
Hi,
|
||||
|
||||
Welcome to Dnote! To verify your email, visit the following link:
|
||||
|
||||
{{ .WebURL }}/verify-email/{{ .Token }}
|
||||
|
|
@ -18,12 +18,6 @@
|
|||
|
||||
package mailer
|
||||
|
||||
// EmailVerificationTmplData is a template data for email verification emails
|
||||
type EmailVerificationTmplData struct {
|
||||
Token string
|
||||
WebURL string
|
||||
}
|
||||
|
||||
// EmailResetPasswordTmplData is a template data for reset password emails
|
||||
type EmailResetPasswordTmplData struct {
|
||||
AccountEmail string
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ func TestTokenAuth(t *testing.T) {
|
|||
user := testutils.SetupUserData(db)
|
||||
tok := database.Token{
|
||||
UserID: user.ID,
|
||||
Type: database.TokenTypeEmailVerification,
|
||||
Type: database.TokenTypeResetPassword,
|
||||
Value: "xpwFnc0MdllFUePDq9DLeQ==",
|
||||
}
|
||||
testutils.MustExec(t, db.Save(&tok), "preparing token")
|
||||
|
|
@ -193,7 +193,7 @@ func TestTokenAuth(t *testing.T) {
|
|||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
server := httptest.NewServer(TokenAuth(db, handler, database.TokenTypeEmailVerification, nil))
|
||||
server := httptest.NewServer(TokenAuth(db, handler, database.TokenTypeResetPassword, nil))
|
||||
defer server.Close()
|
||||
|
||||
t.Run("with token", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -24,16 +24,14 @@ import (
|
|||
|
||||
// Session represents user session
|
||||
type Session struct {
|
||||
UUID string `json:"uuid"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
UUID string `json:"uuid"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// New returns a new session for the given user
|
||||
func New(user database.User, account database.Account) Session {
|
||||
return Session{
|
||||
UUID: user.UUID,
|
||||
Email: account.Email.String,
|
||||
EmailVerified: account.EmailVerified,
|
||||
UUID: user.UUID,
|
||||
Email: account.Email.String,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@ import (
|
|||
|
||||
func TestNew(t *testing.T) {
|
||||
u1 := database.User{UUID: "0f5f0054-d23f-4be1-b5fb-57673109e9cb"}
|
||||
a1 := database.Account{Email: database.ToNullString("alice@example.com"), EmailVerified: false}
|
||||
a1 := database.Account{Email: database.ToNullString("alice@example.com")}
|
||||
|
||||
u2 := database.User{UUID: "718a1041-bbe6-496e-bbe4-ea7e572c295e"}
|
||||
a2 := database.Account{Email: database.ToNullString("bob@example.com"), EmailVerified: false}
|
||||
a2 := database.Account{Email: database.ToNullString("bob@example.com")}
|
||||
|
||||
testCases := []struct {
|
||||
user database.User
|
||||
|
|
@ -52,9 +52,8 @@ func TestNew(t *testing.T) {
|
|||
// Execute
|
||||
got := New(tc.user, tc.account)
|
||||
expected := Session{
|
||||
UUID: tc.user.UUID,
|
||||
Email: tc.account.Email.String,
|
||||
EmailVerified: tc.account.EmailVerified,
|
||||
UUID: tc.user.UUID,
|
||||
Email: tc.account.Email.String,
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, got, expected, "result mismatch")
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ func TestCreate(t *testing.T) {
|
|||
kind string
|
||||
}{
|
||||
{
|
||||
kind: database.TokenTypeEmailVerification,
|
||||
kind: database.TokenTypeResetPassword,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
{{define "yield"}}
|
||||
{{end}}
|
||||
|
|
@ -144,34 +144,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-row-summary">
|
||||
<div>
|
||||
<h3 class="setting-name">Email Verified</h3>
|
||||
</div>
|
||||
|
||||
<div class="setting-right">
|
||||
{{ if eq true false }} b{{end}}
|
||||
|
||||
{{if .EmailVerified}}
|
||||
Yes
|
||||
{{else}}
|
||||
No
|
||||
|
||||
<form action="/verification-token" method="POST" class="email-verification-form">
|
||||
<button
|
||||
id="T-send-verification-button"
|
||||
class="button button-second button-small"
|
||||
type="submit"
|
||||
>
|
||||
Send verification email
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-row-summary">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -116,8 +116,6 @@ func (v *View) Render(w http.ResponseWriter, r *http.Request, data *Data, status
|
|||
}
|
||||
if vd.Account != nil {
|
||||
vd.Yield["Email"] = vd.Account.Email.String
|
||||
vd.Yield["EmailVerified"] = vd.Account.EmailVerified
|
||||
vd.Yield["EmailVerified"] = vd.Account.EmailVerified
|
||||
}
|
||||
vd.Yield["CurrentPath"] = r.URL.Path
|
||||
vd.Yield["Standalone"] = buildinfo.Standalone
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue