diff --git a/pkg/server/app/email.go b/pkg/server/app/email.go index 0897e415..2f721a6f 100644 --- a/pkg/server/app/email.go +++ b/pkg/server/app/email.go @@ -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{ diff --git a/pkg/server/app/email_test.go b/pkg/server/app/email_test.go index 1c68a96f..56270604 100644 --- a/pkg/server/app/email_test.go +++ b/pkg/server/app/email_test.go @@ -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() diff --git a/pkg/server/app/errors.go b/pkg/server/app/errors.go index 250de5d1..895fc40f 100644 --- a/pkg/server/app/errors.go +++ b/pkg/server/app/errors.go @@ -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." ) diff --git a/pkg/server/assets/package-lock.json b/pkg/server/assets/package-lock.json index a37c4305..ec63082c 100644 --- a/pkg/server/assets/package-lock.json +++ b/pkg/server/assets/package-lock.json @@ -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" } } } diff --git a/pkg/server/assets/styles/src/_books.scss b/pkg/server/assets/styles/src/_books.scss index 6f58ef94..c94c4347 100644 --- a/pkg/server/assets/styles/src/_books.scss +++ b/pkg/server/assets/styles/src/_books.scss @@ -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); } } } diff --git a/pkg/server/assets/styles/src/_buttons.scss b/pkg/server/assets/styles/src/_buttons.scss index ef1095d5..16059864 100644 --- a/pkg/server/assets/styles/src/_buttons.scss +++ b/pkg/server/assets/styles/src/_buttons.scss @@ -16,9 +16,11 @@ * along with Dnote. If not, see . */ -@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; } } diff --git a/pkg/server/assets/styles/src/_font.scss b/pkg/server/assets/styles/src/_font.scss index f64b7236..37bc3163 100644 --- a/pkg/server/assets/styles/src/_font.scss +++ b/pkg/server/assets/styles/src/_font.scss @@ -16,7 +16,7 @@ * along with Dnote. If not, see . */ -@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; } diff --git a/pkg/server/assets/styles/src/_global.scss b/pkg/server/assets/styles/src/_global.scss index a91a9a95..8c497658 100644 --- a/pkg/server/assets/styles/src/_global.scss +++ b/pkg/server/assets/styles/src/_global.scss @@ -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); } } } diff --git a/pkg/server/assets/styles/src/_header.scss b/pkg/server/assets/styles/src/_header.scss index 61c7b5bf..c952599e 100644 --- a/pkg/server/assets/styles/src/_header.scss +++ b/pkg/server/assets/styles/src/_header.scss @@ -16,8 +16,12 @@ * along with Dnote. If not, see . */ -@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); } } } diff --git a/pkg/server/assets/styles/src/_home.scss b/pkg/server/assets/styles/src/_home.scss index b1eac511..ce72a9f3 100644 --- a/pkg/server/assets/styles/src/_home.scss +++ b/pkg/server/assets/styles/src/_home.scss @@ -16,21 +16,23 @@ * along with Dnote. If not, see . */ -@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); } } diff --git a/pkg/server/assets/styles/src/_login.scss b/pkg/server/assets/styles/src/_login.scss index 49d5c42c..9200a911 100644 --- a/pkg/server/assets/styles/src/_login.scss +++ b/pkg/server/assets/styles/src/_login.scss @@ -16,11 +16,12 @@ * along with Dnote. If not, see . */ -@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); } } diff --git a/pkg/server/assets/styles/src/_note.scss b/pkg/server/assets/styles/src/_note.scss index 623d7969..edc83a34 100644 --- a/pkg/server/assets/styles/src/_note.scss +++ b/pkg/server/assets/styles/src/_note.scss @@ -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); } } } diff --git a/pkg/server/assets/styles/src/_rem.scss b/pkg/server/assets/styles/src/_rem.scss index e875d0cd..cd736925 100644 --- a/pkg/server/assets/styles/src/_rem.scss +++ b/pkg/server/assets/styles/src/_rem.scss @@ -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 { diff --git a/pkg/server/assets/styles/src/_responsive.scss b/pkg/server/assets/styles/src/_responsive.scss index 05188e4c..9aaf9a7f 100644 --- a/pkg/server/assets/styles/src/_responsive.scss +++ b/pkg/server/assets/styles/src/_responsive.scss @@ -16,39 +16,39 @@ * along with Dnote. If not, see . */ -@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; } } diff --git a/pkg/server/assets/styles/src/_settings.scss b/pkg/server/assets/styles/src/_settings.scss index 6b89ed7a..2f5d439d 100644 --- a/pkg/server/assets/styles/src/_settings.scss +++ b/pkg/server/assets/styles/src/_settings.scss @@ -16,17 +16,19 @@ * along with Dnote. If not, see . */ -@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); } } diff --git a/pkg/server/assets/styles/src/_shared.scss b/pkg/server/assets/styles/src/_shared.scss index 78d35e03..1b7697c1 100644 --- a/pkg/server/assets/styles/src/_shared.scss +++ b/pkg/server/assets/styles/src/_shared.scss @@ -16,8 +16,10 @@ * along with Dnote. If not, see . */ -@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 { diff --git a/pkg/server/assets/styles/src/_theme.scss b/pkg/server/assets/styles/src/_theme.scss index e17497a6..f37d81a5 100644 --- a/pkg/server/assets/styles/src/_theme.scss +++ b/pkg/server/assets/styles/src/_theme.scss @@ -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; diff --git a/pkg/server/assets/styles/src/main.scss b/pkg/server/assets/styles/src/main.scss index df45971a..4a5a7379 100644 --- a/pkg/server/assets/styles/src/main.scss +++ b/pkg/server/assets/styles/src/main.scss @@ -16,25 +16,25 @@ * along with Dnote. If not, see . */ -@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; diff --git a/pkg/server/controllers/helpers.go b/pkg/server/controllers/helpers.go index 0e88034b..72100c8b 100644 --- a/pkg/server/controllers/helpers.go +++ b/pkg/server/controllers/helpers.go @@ -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: diff --git a/pkg/server/controllers/routes.go b/pkg/server/controllers/routes.go index 0c873fa3..db8b706d 100644 --- a/pkg/server/controllers/routes.go +++ b/pkg/server/controllers/routes.go @@ -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}, diff --git a/pkg/server/controllers/users.go b/pkg/server/controllers/users.go index 4643f599..67baca6a 100644 --- a/pkg/server/controllers/users.go +++ b/pkg/server/controllers/users.go @@ -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) -} diff --git a/pkg/server/controllers/users_test.go b/pkg/server/controllers/users_test.go index e4c9ff2d..643cb016 100644 --- a/pkg/server/controllers/users_test.go +++ b/pkg/server/controllers/users_test.go @@ -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") - }) -} diff --git a/pkg/server/database/consts.go b/pkg/server/database/consts.go index b4a1db03..6a9abc0c 100644 --- a/pkg/server/database/consts.go +++ b/pkg/server/database/consts.go @@ -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 ( diff --git a/pkg/server/database/models.go b/pkg/server/database/models.go index ac3e4e56..98571e10 100644 --- a/pkg/server/database/models.go +++ b/pkg/server/database/models.go @@ -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 diff --git a/pkg/server/mailer/mailer.go b/pkg/server/mailer/mailer.go index d02d8911..1cf786a7 100644 --- a/pkg/server/mailer/mailer.go +++ b/pkg/server/mailer/mailer.go @@ -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 diff --git a/pkg/server/mailer/mailer_test.go b/pkg/server/mailer/mailer_test.go index df95b1f9..5837192a 100644 --- a/pkg/server/mailer/mailer_test.go +++ b/pkg/server/mailer/mailer_test.go @@ -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 diff --git a/pkg/server/mailer/templates/verify_email.txt b/pkg/server/mailer/templates/verify_email.txt deleted file mode 100644 index c21af88d..00000000 --- a/pkg/server/mailer/templates/verify_email.txt +++ /dev/null @@ -1,5 +0,0 @@ -Hi, - -Welcome to Dnote! To verify your email, visit the following link: - - {{ .WebURL }}/verify-email/{{ .Token }} diff --git a/pkg/server/mailer/types.go b/pkg/server/mailer/types.go index 3a371911..6ad862de 100644 --- a/pkg/server/mailer/types.go +++ b/pkg/server/mailer/types.go @@ -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 diff --git a/pkg/server/middleware/auth_test.go b/pkg/server/middleware/auth_test.go index 8451ae5d..1485befa 100644 --- a/pkg/server/middleware/auth_test.go +++ b/pkg/server/middleware/auth_test.go @@ -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) { diff --git a/pkg/server/session/session.go b/pkg/server/session/session.go index 8c55549c..a9494c58 100644 --- a/pkg/server/session/session.go +++ b/pkg/server/session/session.go @@ -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, } } diff --git a/pkg/server/session/session_test.go b/pkg/server/session/session_test.go index 107dacfe..dec674b7 100644 --- a/pkg/server/session/session_test.go +++ b/pkg/server/session/session_test.go @@ -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") diff --git a/pkg/server/token/token_test.go b/pkg/server/token/token_test.go index 922cc93d..426ff3d1 100644 --- a/pkg/server/token/token_test.go +++ b/pkg/server/token/token_test.go @@ -33,7 +33,7 @@ func TestCreate(t *testing.T) { kind string }{ { - kind: database.TokenTypeEmailVerification, + kind: database.TokenTypeResetPassword, }, } diff --git a/pkg/server/views/templates/users/email_verification.gohtml b/pkg/server/views/templates/users/email_verification.gohtml deleted file mode 100644 index 969688a4..00000000 --- a/pkg/server/views/templates/users/email_verification.gohtml +++ /dev/null @@ -1,2 +0,0 @@ -{{define "yield"}} -{{end}} diff --git a/pkg/server/views/templates/users/settings.gohtml b/pkg/server/views/templates/users/settings.gohtml index a9639468..9f747b3e 100644 --- a/pkg/server/views/templates/users/settings.gohtml +++ b/pkg/server/views/templates/users/settings.gohtml @@ -144,34 +144,6 @@ -
-
-
-

Email Verified

-
- -
- {{ if eq true false }} b{{end}} - - {{if .EmailVerified}} - Yes - {{else}} - No - - - {{end}} -
-
-
-
diff --git a/pkg/server/views/view.go b/pkg/server/views/view.go index 2b0e57a9..3484dca8 100644 --- a/pkg/server/views/view.go +++ b/pkg/server/views/view.go @@ -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