diff --git a/Cargo.lock b/Cargo.lock index e74ae87..0a24801 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -798,7 +798,7 @@ checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "salvo-cli" -version = "0.1.5" +version = "0.1.8" dependencies = [ "ansi_term", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index a3450ee..4ab71af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "salvo-cli" -version = "0.1.5" +version = "0.1.8" edition = "2021" authors = ["Fankai Liu liufankai137@outlook.com"] -keywords = ["salvo", "cli"] -description = "This CLI tool aims to streamline the process of setting up a new project by generating a template structure. Key features might include generating boilerplate code, configuring necessary dependencies, and setting up a basic project structure suitable for Salvo-based applications" +keywords = ["salvo", "cli","template"] +description = "This CLI tool is designed to streamline the creation of new Salvo web projects through the generation of template structures. It offers the flexibility to select from web API templates or web site templates, and the convenience of choosing a database connector. It auto-generates foundational code to give users a head start in their development process." license = "MIT/Apache-2.0" repository = "https://github.com/fankaiLiu/salvo-cli" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/README.md b/README.md index a20c87b..35eb877 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ + # Salvo CLI -Salvo CLI is a command-line interface tool for the Salvo web framework. It helps streamline the process of setting up a new Salvo project by generating a template structure. +Salvo CLI is a command-line interface tool for the [Salvo](https://github.com/salvo-rs/salvo) web framework. It helps streamline the process of setting up a new Salvo project by generating a template structure. ## Installation -Before you can use the Salvo CLI, you need to install it. Assuming you have Rust and Cargo installed, you can install Salvo CLI with the following command: - ```bash cargo install salvo-cli ``` @@ -15,13 +14,24 @@ To create a new Salvo project, use the new command followed by the name of your ```bash salvo-cli new project_name ``` -This will create a new directory named project_name, and it will set up a basic project structure suitable for a Salvo-based application. - ## Update ```bashs cargo install --force salvo-cli ``` +### Feature Development Plan + +| Status |Task | +|:---:|:---:| +|✅| web api template | +|✅| web site with htmlx template | +|✅|with sqlx template| +|✅|basic midware | +|✅|suport sqlite,pgsql,mysql| +||with seaorm template| +|| better web site | +|| with diese template| +|| with Rbatis template| ## License This project is licensed under the MIT OR Apache-2.0 License. diff --git a/locales/app.yml b/locales/app.yml index 8605862..ff37518 100644 --- a/locales/app.yml +++ b/locales/app.yml @@ -257,3 +257,129 @@ error_project_path_exist: th: ปลายทาง `%{path}` มีอยู่แล้ว el: ο προορισμός `%{path}` υπάρχει ήδη da: destination `%{path}` findes allerede +select_db_conn_type: + en: select database connection type + zh_CN: 选择数据库连接类型 + zh_TW: 選擇資料庫連接類型 + fr: sélectionnez le type de connexion à la base de données + ja: データベース接続タイプを選択 + es: seleccione el tipo de conexión a la base de datos + de: wählen Sie den Datenbankverbindungstyp aus + ru: выберите тип подключения к базе данных + it: seleziona il tipo di connessione al database + pt: selecione o tipo de conexão ao banco de dados + ko: 데이터베이스 연결 유형을 선택하십시오 + no: velg databaseforbindelsestype + is: veldu gagnagrunns tengingar tegund + uk: виберіть тип підключення до бази даних + th: เลือกประเภทการเชื่อมต่อฐานข้อมูล + el: επιλέξτε τύπο σύνδεσης βάσης δεδομένων + da: vælg databaseforbindelsestype +db_conn_types_sqlx: + en: SQLx is an async, pure Rust† SQL crate featuring compile-time checked queries without a DSL. 10K⭐ + zh_CN: SQLx 是一个异步、纯 Rust† 的 SQL 库,具有无 DSL 的编译时检查查询。10K⭐ + zh_TW: SQLx 是一個非同步、純 Rust† 的 SQL 函式庫,具有無 DSL 的編譯時檢查查詢。10K⭐ + fr: SQLx est une caisse SQL asynchrone et pure Rust† avec des requêtes vérifiées au moment de la compilation sans DSL. 10K⭐ + ja: SQLx は、DSL なしでコンパイル時にクエリをチェックする、非同期で純粋な Rust† SQL クレートです。10K⭐ + es: SQLx es una caja SQL asíncrona y pura Rust† con consultas verificadas en tiempo de compilación sin DSL. 10K⭐ + de: SQLx ist eine asynchrone, reine Rust† SQL-Kiste mit kompilierten Abfragen ohne DSL. 10K⭐ + ru: SQLx - это асинхронный, чистый Rust† SQL ящик с проверкой запросов во время компиляции без DSL. 10K⭐ + it: SQLx è una cassa SQL asincrona e pura Rust† con query verificate in fase di compilazione senza DSL. 10K⭐ + pt: SQLx é uma caixa SQL assíncrona e pura Rust† com consultas verificadas no momento da compilação sem DSL. 10K⭐ + ko: SQLx는 DSL 없이 컴파일 시간에 쿼리를 확인하는 비동기, 순수 Rust† SQL 크레이트입니다. 10K⭐ + no: SQLx er en asynkron, ren Rust† SQL-kasse med kompilerte spørringer uten DSL. 10K⭐ + is: SQLx er asynkron, hrein Rust† SQL kassa með samþjöppuð fyrirspurnir án DSL. 10K⭐ + uk: SQLx - це асинхронний, чистий Rust† SQL ящик з перевіреними на етапі компіляції запитами без DSL. 10K⭐ + th: SQLx เป็น SQL crate แบบ async, pure Rust† ที่มีการตรวจสอบคิวรีในเวลาคอมไพล์โดยไม่มี DSL. 10K⭐ + el: το SQLx είναι ένα ασύγχρονο, καθαρό Rust† SQL κιβώτιο με ερωτήματα ελεγμένα κατά τη σύνταξη χωρίς DSL. 10K⭐ + da: SQLx er en asynkron, ren Rust† SQL-kasse med kompilerede forespørgsler uden DSL. 10K⭐ +db_conn_types_diesel: + en: diesel:A safe, extensible ORM and Query Builder for Rust 11K⭐ + zh_CN: diesel:Rust 的安全、可扩展的 ORM 和查询生成器 11K⭐ + zh_TW: diesel:Rust 的安全、可擴展的 ORM 和查詢生成器 11K⭐ + fr: diesel:ORM et générateur de requêtes extensible et sûr pour Rust 11K⭐ + ja: diesel:Rust の安全で拡張可能な ORM とクエリビルダー 11K⭐ + es: diesel:ORM y generador de consultas seguro y extensible para Rust 11K⭐ + de: diesel:Eine sichere, erweiterbare ORM und Abfragegenerator für Rust 11K⭐ + ru: diesel:ORM и генератор запросов безопасны и расширяемы для Rust 11K⭐ + it: diesel:ORM e generatore di query sicuro ed estensibile per Rust 11K⭐ + pt: diesel:ORM e gerador de consultas seguro e extensível para Rust 11K⭐ + ko: diesel:Rust의 안전하고 확장 가능한 ORM 및 쿼리 빌더 11K⭐ + no: diesel:En sikker, utvidbar ORM og spørringsbygger for Rust 11K⭐ + is: diesel:Öruggt, útvíkkandi ORM og fyrirspurnarbyggir fyrir Rust 11K⭐ + uk: diesel:ORM та генератор запитів безпечні та розширювані для Rust 11K⭐ + th: diesel:ORM และ Query Builder ที่ปลอดภัยและสามารถขยายได้สำหรับ Rust 11K⭐ + el: diesel:ένα ασφαλές, επεκτάσιμο ORM και Query Builder για Rust 11K⭐ + da: diesel:En sikker, udvidelig ORM og Query Builder til Rust 11K⭐ +db_conn_types_sea_orm: + en: sea-orm:🐚 An async & dynamic ORM for Rust (I like it) + zh_CN: sea-orm:🐚 Rust 的异步和动态 ORM(我喜欢它) + zh_TW: sea-orm:🐚 Rust 的非同步和動態 ORM(我喜歡它) + fr: sea-orm:🐚 Un ORM asynchrone et dynamique pour Rust (je l'aime) + ja: sea-orm:🐚 Rust の非同期 & 動的 ORM(気に入っています) + es: sea-orm:🐚 Un ORM asíncrono y dinámico para Rust (me gusta) + de: sea-orm:🐚 Ein asynchrones und dynamisches ORM für Rust (ich mag es) + ru: sea-orm:🐚 Асинхронный и динамический ORM для Rust (мне нравится) + it: sea-orm:🐚 Un ORM asincrono e dinamico per Rust (mi piace) + pt: sea-orm:🐚 Um ORM assíncrono e dinâmico para Rust (eu gosto) + ko: sea-orm:🐚 Rust의 비동기 및 동적 ORM (나는 좋아) + no: sea-orm:🐚 En asynkron og dynamisk ORM for Rust (jeg liker det) + is: sea-orm:🐚 Asynkron og hreyfanleg ORM fyrir Rust (mér líkar það) + uk: sea-orm:🐚 Асинхронний та динамічний ORM для Rust (мені подобається) + th: sea-orm:🐚 ORM แบบ async และ dynamic สำหรับ Rust (ฉันชอบมัน) + el: sea-orm:🐚 ένα ασύγχρονο και δυναμικό ORM για Rust (μου αρέσει) + da: sea-orm:🐚 En asynkron og dynamisk ORM til Rust (jeg kan lide det) +db_conn_types_rbatis: + en: rbatis:A similar Mybatis (java) asynchronous ORM framework + zh_CN: rbatis:类似 Mybatis (java) 的异步 ORM 框架 + zh_TW: rbatis:類似 Mybatis (java) 的非同步 ORM 框架 + fr: rbatis:Un cadre ORM asynchrone similaire à Mybatis (java) + ja: rbatis:Mybatis (java) に似た非同期 ORM フレームワーク + es: rbatis:Un marco ORM asíncrono similar a Mybatis (java) + de: rbatis:Ein ähnliches Mybatis (java) asynchrones ORM-Framework + ru: rbatis:Асинхронный ORM-фреймворк, похожий на Mybatis (java) + it: rbatis:Un framework ORM asincrono simile a Mybatis (java) + pt: rbatis:Um framework ORM assíncrono semelhante ao Mybatis (java) + ko: rbatis:Mybatis (java)와 유사한 비동기 ORM 프레임 워크 + no: rbatis:Et lignende Mybatis (java) asynkront ORM-rammeverk + is: rbatis:Asynkron ORM kerfi sem líkist Mybatis (java) + uk: rbatis:Асинхронний ORM-фреймворк, схожий на Mybatis (java) + th: rbatis:เฟรมเวิร์ก ORM แบบ async ที่คล้ายกับ Mybatis (java) + el: rbatis:ένα παρόμοιο Mybatis (java) ασύγχρονο πλαίσιο ORM + da: rbatis:Et lignende Mybatis (java) asynkront ORM-rammeverk +db_conn_types_nothing: + en: unnecessary + zh_CN: 不需要 + zh_TW: 不需要 + fr: inutile + ja: 不要 + es: innecesario + de: unnötig + ru: ненужный + it: inutile + pt: desnecessário + ko: 불필요한 + no: unødvendig + is: óþarfi + uk: непотрібний + th: ไม่จำเป็น + el: περιττός + da: unødvendig +select_db_type: + en: select database type + zh_CN: 选择数据库类型 + zh_TW: 選擇資料庫類型 + fr: sélectionnez le type de base de données + ja: データベースタイプを選択 + es: seleccione el tipo de base de datos + de: wählen Sie den Datenbanktyp aus + ru: выберите тип базы данных + it: seleziona il tipo di database + pt: selecione o tipo de banco de dados + ko: 데이터베이스 유형을 선택하십시오 + no: velg databasetype + is: veldu gagnagrunnstegund + uk: виберіть тип бази даних + th: เลือกประเภทฐานข้อมูล + el: επιλέξτε τύπο βάσης δεδομένων + da: vælg databasetype \ No newline at end of file diff --git a/locales/code_comment.yml b/locales/code_comment.yml index 3979f5a..b5a14ef 100644 --- a/locales/code_comment.yml +++ b/locales/code_comment.yml @@ -89,3 +89,386 @@ config_error_read_failed: th: อ่านไฟล์ล้มเหลว el: απέτυχε η ανάγνωση του αρχείου da: kunne ikke læse filen +user_does_not_exist: + en: User does not exist. + zh_CN: 用户不存在。 + zh_TW: 用戶不存在。 + fr: L'utilisateur n'existe pas. + ja: ユーザーが存在しません。 + es: El usuario no existe. + de: Benutzer existiert nicht. + ru: Пользователь не существует. + it: L'utente non esiste. + pt: O usuário não existe. + ko: 사용자가 존재하지 않습니다. + no: Brukeren eksisterer ikke. + is: Notandinn er ekki til. + uk: Користувача не існує. + th: ไม่มีผู้ใช้นี้ + el: Ο χρήστης δεν υπάρχει. + da: Brugeren findes ikke. + +incorrect_password: + en: Incorrect password. + zh_CN: 密码不正确。 + zh_TW: 密碼不正確。 + fr: Mot de passe incorrect. + ja: パスワードが間違っています。 + es: Contraseña incorrecta. + de: Falsches Passwort. + ru: Неверный пароль. + it: Password errata. + pt: Senha incorreta. + ko: 잘못된 비밀번호입니다. + no: Feil passord. + is: Rangt lykilorð. + uk: Невірний пароль. + th: รหัสผ่านไม่ถูกต้อง + el: Λάθος κωδικός πρόσβασης. + da: Forkert adgangskode. + +database_connection_failed: + en: Database connection failed. + zh_CN: 数据库连接失败。 + zh_TW: 數據庫連接失敗。 + fr: La connexion à la base de données a échoué. + ja: データベースへの接続に失敗しました。 + es: Falló la conexión a la base de datos. + de: Datenbankverbindung fehlgeschlagen. + ru: Сбой подключения к базе данных. + it: Connessione al database fallita. + pt: Falha na conexão com o banco de dados. + ko: 데이터베이스 연결에 실패했습니다. + no: Tilkobling til databasen mislyktes. + is: Tókst ekki að tengjast gagnagrunninum. + uk: Помилка підключення до бази даних. + th: การเชื่อมต่อฐานข้อมูลล้มเหลว + el: Η σύνδεση με τη βάση δεδομένων απέτυχε. + da: Databasetilkobling mislykkedes. +account: + en: Account + zh_CN: 账号 + zh_TW: 帳號 + fr: Compte + ja: アカウント + es: Cuenta + de: Konto + ru: Учетная запись + it: Account + pt: Conta + ko: 계정 + no: Konto + is: Reikningur + uk: Обліковий запис + th: บัญชี + el: Λογαριασμός + da: Konto + +password: + en: Password + zh_CN: 密码 + zh_TW: 密碼 + fr: Mot de passe + ja: パスワード + es: Contraseña + de: Passwort + ru: Пароль + it: Password + pt: Senha + ko: 비밀번호 + no: Passord + is: Lykilorð + uk: Пароль + th: รหัสผ่าน + el: Κωδικός + da: Adgangskode + +login: + en: Login + zh_CN: 登录 + zh_TW: 登入 + fr: Connexion + ja: ログイン + es: Iniciar sesión + de: Anmelden + ru: Войти + it: Accedi + pt: Login + ko: 로그인 + no: Logg inn + is: Innskráning + uk: Увійти + th: เข้าสู่ระบบ + el: Σύνδεση + da: Log ind +user_list: + en: User list + zh_CN: 用户列表 + zh_TW: 使用者列表 + fr: Liste d'utilisateurs + ja: ユーザーリスト + es: Lista de usuarios + de: Benutzerliste + ru: Список пользователей + it: Lista utenti + pt: Lista de usuários + ko: 사용자 목록 + no: Brukerliste + is: Notendalisti + uk: Список користувачів + th: รายชื่อผู้ใช้ + el: Λίστα χρηστών + da: Brugerliste + +delete: + en: Delete + zh_CN: 删除 + zh_TW: 刪除 + fr: Supprimer + ja: 削除 + es: Eliminar + de: Löschen + ru: Удалить + it: Elimina + pt: Deletar + ko: 삭제 + no: Slett + is: Eyða + uk: Видалити + th: ลบ + el: Διαγραφή + da: Slet +return_to_homepage: + en: Return to homepage + zh_CN: 返回主页 + zh_TW: 返回主頁 + fr: Retour à la page d'accueil + ja: ホームページに戻る + es: Volver a la página principal + de: Zur Startseite zurückkehren + ru: Вернуться на главную страницу + it: Torna alla homepage + pt: Retornar à página inicial + ko: 홈페이지로 돌아가기 + no: Returner til hovedsiden + is: Fara aftur á heimasíðu + uk: Повернутися на головну сторінку + th: กลับสู่หน้าหลัก + el: Επιστροφή στην αρχική σελίδα + da: Returner til hjemmesiden +username: + en: Username + zh_CN: 用户名 + zh_TW: 用戶名 + fr: Nom d'utilisateur + ja: ユーザーネーム + es: Nombre de usuario + de: Benutzername + ru: Имя пользователя + it: Nome utente + pt: Nome de usuário + ko: 사용자 이름 + no: Brukernavn + is: Notendanafn + uk: Ім'я користувача + th: ชื่อผู้ใช้ + el: Όνομα χρήστη + da: Brugernavn +generate_a_string_of_a_specified_length: + en: Generate a string of a specified length + zh_CN: 生成指定长度的字符串 + zh_TW: 生成指定長度的字串 + fr: Générer une chaîne de caractères d'une longueur spécifiée + ja: 指定した長さの文字列を生成する + es: Generar una cadena de una longitud especificada + de: Erzeugen Sie eine Zeichenkette einer bestimmten Länge + ru: Генерировать строку заданной длины + it: Generare una stringa di una lunghezza specificata + pt: Gerar uma string de um comprimento especificado + ko: 지정된 길이의 문자열 생성 + no: Generer en streng av en spesifisert lengde + is: Búa til streng af tiltekinni lengd + uk: Генерувати рядок заданої довжини + th: สร้างสตริงความยาวที่ระบุ + el: Δημιουργήστε μια συμβολοσειρά ενός καθορισμένου μήκους + da: Generer en streng af en bestemt længde +contact_support: + en: Contact Support + zh_CN: 联系支持 + zh_TW: 聯繫支援 + fr: Contacter le support + ja: サポートに連絡する + es: Contactar con soporte + de: Support kontaktieren + ru: Связаться со службой поддержки + it: Contatta il supporto + pt: Contato com o suporte + ko: 지원에 문의하세요 + no: Kontaktstøtte + is: Hafa samband við þjónustu + uk: Звернутися до служби підтримки + th: ติดต่อฝ่ายสนับสนุน + el: Επικοινωνία με την υποστήριξη + da: Kontakt support +page_not_found: + en: Page Not Found + zh_CN: 页面未找到 + zh_TW: 找不到網頁 + fr: Page non trouvée + ja: ページが見つかりません + es: Página no encontrada + de: Seite nicht gefunden + ru: Страница не найдена + it: Pagina non trovata + pt: Página não encontrada + ko: 페이지를 찾을 수 없습니다 + no: Siden ble ikke funnet + is: Síða fannst ekki + uk: Сторінку не знайдено + th: ไม่พบหน้านี้ + el: Η σελίδα δεν βρέθηκε + da: Side ikke fundet +are_you_sure_you_want_to_delete: + en: Are you sure you want to delete? + zh_CN: 确定删除吗? + zh_TW: 確定要刪除嗎? + fr: Êtes-vous sûr de vouloir supprimer ? + ja: 削除してもよろしいですか? + es: ¿Estás seguro de que quieres eliminar? + de: Sind Sie sicher, dass Sie löschen möchten? + ru: Вы уверены, что хотите удалить? + it: Sei sicuro di voler eliminare? + pt: Tem certeza de que deseja excluir? + ko: 정말로 삭제하시겠습니까? + no: Er du sikker på at du vil slette? + is: Ertu viss um að þú viljir eyða? + uk: Ви впевнені, що хочете видалити? + th: คุณแน่ใจหรือว่าต้องการลบ? + el: Είστε σίγουροι ότι θέλετε να διαγράψετε; + da: Er du sikker på, at du vil slette? +yes: + en: Yes + zh_CN: 是 + zh_TW: 是 + fr: Oui + ja: はい + es: Sí + de: Ja + ru: Да + it: Sì + pt: Sim + ko: 예 + no: Ja + is: Já + uk: Так + th: ใช่ + el: Ναι + da: Ja +cancel: + en: Cancel + zh_CN: 取消 + zh_TW: 取消 + fr: Annuler + ja: キャンセル + es: Cancelar + de: Abbrechen + ru: Отмена + it: Annulla + pt: Cancelar + ko: 취소 + no: Avbryt + is: Hætta við + uk: Відмінити + th: ยกเลิก + el: Ακύρωση + da: Annuller +operation: + en: Operation + zh_CN: 操作 + zh_TW: 操作 + fr: Opération + ja: 操作 + es: Operación + de: Betrieb + ru: Операция + it: Operazione + pt: Operação + ko: 작업 + no: Operasjon + is: Aðgerð + uk: Операція + th: การดำเนินการ + el: Λειτουργία + da: Operation +create_success_sqlx: + en: 🎯 You have chosen sqlx, documentation can be viewed here:https://github.com/launchbadge/sqlx + zh_CN: 🎯 您选择了sqlx,文档可以在这里查看:https://github.com/launchbadge/sqlx + zh_TW: 🎯 您選擇了sqlx,文檔可以在這裡查看:https://github.com/launchbadge/sqlx + fr: 🎯 Vous avez choisi sqlx, la documentation peut être consultée ici:https://github.com/launchbadge/sqlx + ja: 🎯 sqlxを選択しました。ドキュメントはこちらからご覧ください:https://github.com/launchbadge/sqlx + es: 🎯 Ha elegido sqlx, la documentación se puede ver aquí:https://github.com/launchbadge/sqlx + de: 🎯 Sie haben sqlx gewählt, die Dokumentation kann hier eingesehen werden:https://github.com/launchbadge/sqlx + ru: 🎯 Вы выбрали sqlx, документацию можно посмотреть здесь:https://github.com/launchbadge/sqlx + it: 🎯 Hai scelto sqlx, la documentazione può essere visualizzata qui:https://github.com/launchbadge/sqlx + pt: 🎯 Você escolheu sqlx, a documentação pode ser visualizada aqui:https://github.com/launchbadge/sqlx + ko: 🎯 sqlx를 선택했습니다. 여기에서 문서를 볼 수 있습니다:https://github.com/launchbadge/sqlx + no: 🎯 Du har valgt sqlx, dokumentasjonen kan ses her:https://github.com/launchbadge/sqlx + is: 🎯 Þú hefur valið sqlx, hægt er að skoða kennslu hér:https://github.com/launchbadge/sqlx + uk: 🎯 Ви вибрали sqlx, документацію можна переглянути тут:https://github.com/launchbadge/sqlx + th: 🎯 คุณได้เลือก sqlx คุณสามารถดูเอกสารได้ที่นี่:https://github.com/launchbadge/sqlx + el: 🎯 Επιλέξατε sqlx, η τεκμηρίωση μπορεί να προβληθεί εδώ:https://github.com/launchbadge/sqlx + da: 🎯 Du har valgt sqlx, dokumentationen kan ses her:https://github.com/launchbadge/sqlx +create_success_sqlx_sqlite: + en: 🎯 Default database created in `/data/demo.db`.\n After running it, you can access /login with the default username:'zhangsan' and password:'123'. + zh_CN: 🎯 默认数据库创建在`/data/demo.db`。\n 运行后,您可以使用默认用户名:'zhangsan'和密码:'123'访问/login。 + zh_TW: 🎯 預設資料庫創建在`/data/demo.db`。\n 運行後,您可以使用預設用戶名:'zhangsan'和密碼:'123'訪問/login。 + fr: 🎯 Base de données par défaut créée dans `/data/demo.db`. \n Après l'avoir exécuté, vous pouvez accéder à /login avec le nom d'utilisateur par défaut:'zhangsan' et le mot de passe:'123'. + ja: 🎯 デフォルトのデータベースが`/data/demo.db`に作成されました。\n 実行後、デフォルトのユーザー名:'zhangsan'とパスワード:'123'で/loginにアクセスできます。 + es: 🎯 Base de datos predeterminada creada en `/data/demo.db`. \n Después de ejecutarlo, puede acceder a /login con el nombre de usuario predeterminado:'zhangsan' y la contraseña:'123'. + de: 🎯 Standarddatenbank erstellt in `/data/demo.db`. \n Nach dem Ausführen können Sie mit dem Standardbenutzernamen:'zhangsan' und dem Passwort:'123' auf /login zugreifen. + ru: 🎯 База данных по умолчанию создана в `/data/demo.db`. \n После запуска вы можете получить доступ к /login с именем пользователя по умолчанию:'zhangsan' и паролем:'123'. + it: 🎯 Database predefinita creata in `/data/demo.db`. \n Dopo l'esecuzione, è possibile accedere a /login con l'username predefinito:'zhangsan' e la password:'123'. + pt: 🎯 Banco de dados padrão criado em `/data/demo.db`. \n Após executá-lo, você pode acessar /login com o nome de usuário padrão:'zhangsan' e a senha:'123'. + ko: 🎯 기본 데이터베이스가 `/data/demo.db`에 생성되었습니다. \n 실행 후 기본 사용자 이름:'zhangsan'과 비밀번호:'123'으로 /login에 접속할 수 있습니다. + no: 🎯 Standarddatabasen er opprettet i `/data/demo.db`. \n Etter å ha kjørt det, kan du få tilgang til /login med standard brukernavn:'zhangsan' og passord:'123'. + is: 🎯 Sjálfgefin gagnagrunn búin til í `/data/demo.db`. \n Eftir að hafa keyrt það, getur þú fengið aðgang að /login með sjálfgefnum notandanafni:'zhangsan' og lykilorði:'123'. + uk: 🎯 База даних за замовчуванням створена в `/data/demo.db`. \n Після запуску ви можете отримати доступ до /login за допомогою імені користувача за замовчуванням:'zhangsan' та пароля:'123'. + th: 🎯 สร้างฐานข้อมูลเริ่มต้นใน `/data/demo.db`. \n หลังจากเรียกใช้งาน คุณสามารถเข้าถึง /login ด้วยชื่อผู้ใช้เริ่มต้น:'zhangsan' และรหัสผ่าน:'123'. + el: 🎯 Δημιουργήθηκε η προεπιλεγμένη βάση δεδομένων στο `/data/demo.db`. \n Μετά την εκτέλεσή του, μπορείτε να αποκτήσετε πρόσβαση στο /login με το όνομα χρήστη:'zhangsan' και τον κωδικό πρόσβασης:'123'. + da: 🎯 Standarddatabasen er oprettet i `/data/demo.db`. \n Efter at have kørt det, kan du få adgang til /login med standardbrugernavnet:'zhangsan' og adgangskoden:'123'. +create_success_mysql_or_pgsql: + en: 📊 Follow the instructions in the data/init_sql.sql file to complete the initialization of data + zh_CN: 📊 按照data/init_sql.sql文件中的说明完成数据初始化 + zh_TW: 📊 按照data/init_sql.sql文件中的說明完成資料初始化 + fr: 📊 Suivez les instructions du fichier data/init_sql.sql pour terminer l'initialisation des données + ja: 📊 data/init_sql.sqlファイルの指示に従って、データの初期化を完了します + es: 📊 Siga las instrucciones del archivo data/init_sql.sql para completar la inicialización de datos + de: 📊 Befolgen Sie die Anweisungen in der Datei data/init_sql.sql, um die Initialisierung der Daten abzuschließen + ru: 📊 Следуйте инструкциям в файле data/init_sql.sql, чтобы завершить инициализацию данных + it: 📊 Seguire le istruzioni nel file data/init_sql.sql per completare l'inizializzazione dei dati + pt: 📊 Siga as instruções no arquivo data/init_sql.sql para concluir a inicialização dos dados + ko: 📊 data/init_sql.sql 파일의 지침에 따라 데이터 초기화를 완료하십시오 + no: 📊 Følg instruksjonene i data/init_sql.sql-filen for å fullføre initialiseringen av data + is: 📊 Fylgdu leiðbeiningunum í skránni data/init_sql.sql til að ljúka upphafsstillingu gagna + uk: 📊 Дотримуйтесь інструкцій у файлі data/init_sql.sql, щоб завершити ініціалізацію даних + th: 📊 ปฏิบัติตามคำแนะนำในไฟล์ data/init_sql.sql เพื่อเสร็จสิ้นการเริ่มต้นข้อมูล + el: 📊 Ακολουθήστε τις οδηγίες στο αρχείο data/init_sql.sql για να ολοκληρώσετε την αρχικοποίηση των δεδομένων + da: 📊 Følg instruktionerne i data/init_sql.sql-filen for at fuldføre initialiseringen af data +create_success_mysql_or_pgsql_fist_use: + en: -- Please first use cargo install sqlx-cli \n -- Modify the database connection string in .env and config/config.toml \n -- Then execute sqlx database create to create the database \n -- Execute sqlx migrate run to restore the database, run the following SQL in the database to add default data. \n -- After running, you can access /login with the default username:'zhangsan' and password:'123'. + zh_CN: -- 请先使用 cargo install sqlx-cli \n -- 修改.env和config/config.toml中的数据库连接字符串 \n -- 然后执行sqlx database create 创建数据库 \n -- 执行sqlx migrate run 还原数据库, 在数据库中运以下SQL以添加默认数据。 \n -- 运行后,您可以使用默认用户名:'zhangsan'和密码:'123'访问/login。 + zh_TW: -- 首先,使用 cargo install sqlx-cli \n -- 修改.env和config/config.toml中的數據庫連接字符串 \n -- 然後執行sqlx database create 創建數據庫 \n -- 執行sqlx migrate run 恢复數據庫, 在數據庫中運行以下SQL以添加默認數據。 \n -- 運行後,您可以使用默認用戶名:'zhangsan'和密碼:'123'訪問/login。 + fr: -- D'abord, utilisez cargo install sqlx-cli \n -- Modifiez la chaîne de connexion à la base de données dans .env et config/config.toml \n -- Ensuite, exécutez sqlx database create pour créer la base de données \n -- Exécutez sqlx migrate run pour restaurer la base de données, exécutez le SQL suivant dans la base de données pour ajouter les données par défaut. \n -- Après l'exécution, vous pouvez accéder à /login avec le nom d'utilisateur par défaut:'zhangsan' et le mot de passe:'123'. + ja: -- まず、cargo install sqlx-cliを使用してください \n -- .envとconfig/config.tomlのデータベース接続文字列を変更します \n -- 次に、sqlx database createを実行してデータベースを作成します \n -- sqlx migrate runを実行してデータベースを復元し、次のSQLをデータベースで実行してデフォルトデータを追加します。 \n -- 実行後、デフォルトのユーザー名:'zhangsan'とパスワード:'123'で/loginにアクセスできます。 + es: -- Primero, use cargo install sqlx-cli \n -- Modifique la cadena de conexión de la base de datos en .env y config/config.toml \n -- Luego, ejecute sqlx database create para crear la base de datos \n -- Ejecute sqlx migrate run para restaurar la base de datos, ejecute el siguiente SQL en la base de datos para agregar datos predeterminados. \n -- Después de ejecutarlo, puede acceder a /login con el nombre de usuario predeterminado:'zhangsan' y la contraseña:'123'. + de: -- Bitte verwenden Sie zuerst cargo install sqlx-cli \n -- Ändern Sie die Verbindungszeichenfolge zur Datenbank in .env und config/config.toml \n -- Führen Sie dann sqlx database create aus, um die Datenbank zu erstellen \n -- Führen Sie sqlx migrate run aus, um die Datenbank wiederherzustellen. Führen Sie den folgenden SQL in der Datenbank aus, um Standarddaten hinzuzufügen. \n -- Nach dem Ausführen können Sie mit dem Standardbenutzernamen:'zhangsan' und dem Passwort:'123' auf /login zugreifen. + ru: -- Пожалуйста, сначала используйте cargo install sqlx-cli \n -- Измените строку подключения к базе данных в .env и config/config.toml \n -- Затем выполните sqlx database create, чтобы создать базу данных \n -- Выполните sqlx migrate run, чтобы восстановить базу данных, выполните следующий SQL в базе данных, чтобы добавить данные по умолчанию. \n -- После запуска вы можете получить доступ к /login с именем пользователя по умолчанию:'zhangsan' и паролем:'123'. + it: -- Si prega di utilizzare prima cargo install sqlx-cli \n -- Modifica la stringa di connessione al database in .env e config/config.toml \n -- Quindi esegui sqlx database create per creare il database \n -- Esegui sqlx migrate run per ripristinare il database, esegui il seguente SQL nel database per aggiungere dati predefiniti. \n -- Dopo l'esecuzione, è possibile accedere a /login con l'username predefinito:'zhangsan' e la password:'123'. + pt: -- Por favor, use primeiro cargo install sqlx-cli \n -- Modifique a string de conexão do banco de dados em .env e config/config.toml \n -- Em seguida, execute sqlx database create para criar o banco de dados \n -- Execute sqlx migrate run para restaurar o banco de dados, execute o seguinte SQL no banco de dados para adicionar dados padrão. \n -- Após a execução, você pode acessar /login com o nome de usuário padrão:'zhangsan' e a senha:'123'. + ko: -- 먼저 cargo install sqlx-cli를 사용하십시오. \n -- .env 및 config/config.toml에서 데이터베이스 연결 문자열을 수정하십시오. \n -- 그런 다음 sqlx database create를 실행하여 데이터베이스를 만듭니다. \n -- 데이터베이스를 복원하려면 sqlx migrate run을 실행하고 다음 SQL을 데이터베이스에서 실행하여 기본 데이터를 추가하십시오. \n -- 실행 후 기본 사용자 이름:'zhangsan'과 비밀번호:'123'으로 /login에 액세스 할 수 있습니다. + no: -- Vennligst bruk først cargo install sqlx-cli \n -- Endre database-tilkoblingsstrengen i .env og config/config.toml \n -- Kjør deretter sqlx database create for å opprette databasen \n -- Kjør sqlx migrate run for å gjenopprette databasen, kjør følgende SQL i databasen for å legge til standarddata. \n -- Etter å ha kjørt det, kan du få tilgang til /login med standard brukernavn:'zhangsan' og passord:'123'. + is: -- Vinsamlegast notaðu fyrst cargo install sqlx-cli \n -- Breyttu tengingu strenginn í .env og config/config.toml \n -- Keyrið svo sqlx database create til að búa til gagnagrunninn \n -- Keyrið sqlx migrate run til að endurheimta gagnagrunninn, keyrið eftirfarandi SQL í gagnagrunninum til að bæta við sjálfgefnum gögnum. \n -- Eftir að hafa keyrt það, getur þú fengið aðgang að /login með sjálfgefnum notandanafni:'zhangsan' og lykilorði:'123'. + uk: -- Будь ласка, спочатку використовуйте cargo install sqlx-cli \n -- Змініть рядок підключення до бази даних в .env та config/config.toml \n -- Потім виконайте sqlx database create, щоб створити базу даних \n -- Виконайте sqlx migrate run, щоб відновити базу даних, виконайте наступний SQL в базі даних, щоб додати дані за замовчуванням. \n -- Після запуску ви можете отримати доступ до /login за допомогою імені користувача за замовчуванням:'zhangsan' та пароля:'123'. + th: -- โปรดใช้ก่อน cargo install sqlx-cli \n -- แก้ไขสตริงการเชื่อมต่อฐานข้อมูลใน .env และ config/config.toml \n -- จากนั้นเรียกใช้ sqlx database create เพื่อสร้างฐานข้อมูล \n -- รัน sqlx migrate run เพื่อกู้คืนฐานข้อมูล รัน SQL ต่อไปนี้ในฐานข้อมูลเพื่อเพิ่มข้อมูลเริ่มต้น \n -- หลังจากเรียกใช้งาน คุณสามารถเข้าถึง /login ด้วยชื่อผู้ใช้เริ่มต้น:'zhangsan' และรหัสผ่าน:'123'. + el: -- Παρακαλώ χρησιμοποιήστε πρώτα cargo install sqlx-cli \n -- Τροποποιήστε τη συμβολοσειρά σύνδεσης της βάσης δεδομένων στα .env και config/config.toml \n -- Στη συνέχεια, εκτελέστε sqlx database create για να δημιουργήσετε τη βάση δεδομένων \n -- Εκτελέστε το sqlx migrate run για να επαναφέρετε τη βάση δεδομένων, εκτελέστε το ακόλουθο SQL στη βάση δεδομένων για να προσθέσετε προεπιλεγμένα δεδομένα. \n -- Μετά την εκτέλεση, μπορείτε να έχετε πρόσβαση στο /login με προεπιλεγμένο όνομα χρήστη:'zhangsan' και κωδικό πρόσβασης:'123'. + da: -- Brug venligst først cargo install sqlx-cli \n -- Rediger databaseforbindelsesstrengen i .env og config/config.toml \n -- Kør derefter sqlx database create for at oprette databasen \n -- Kør sqlx migrate run for at gendanne databasen, kør følgende SQL i databasen for at tilføje standarddata. \n -- Efter at have kørt det, kan du få adgang til /login med standardbrugernavnet:'zhangsan' og adgangskoden:'123'. \ No newline at end of file diff --git a/src/template/.env.hbs b/src/template/.env.hbs new file mode 100644 index 0000000..78fd725 --- /dev/null +++ b/src/template/.env.hbs @@ -0,0 +1,9 @@ +{{#if is_postgres}} +DATABASE_URL=postgresql://liufankai:1@localhost/salvo_demo +{{/if}} +{{#if is_sqlite}} +DATABASE_URL="sqlite:data/demo.db" +{{/if}} +{{#if is_mysql}} +DATABASE_URL="mysql://root:981109@localhost/salvo_demo" +{{/if}} \ No newline at end of file diff --git a/src/template/config/config.hbs b/src/template/config/config.hbs index 3336df4..fe495b9 100644 --- a/src/template/config/config.hbs +++ b/src/template/config/config.hbs @@ -2,6 +2,18 @@ name = "{{project_name}}" address = "0.0.0.0:5800" ssl = false +{{#if need_db_conn}} +[database] +{{#if is_sqlite}} +database_url= "sqlite:data/demo.db" +{{/if}} +{{#if is_postgres}} +database_url="postgresql://liufankai:1@localhost/salvo_demo" +{{/if}} +{{#if is_mysql}} +database_url="mysql://root:981109@localhost/salvo_demo" +{{/if}} +{{/if}} [jwt] jwt_secret = "secret" jwt_exp = 6000 diff --git a/src/template/data/demo.db b/src/template/data/demo.db new file mode 100644 index 0000000..9e3f7f1 Binary files /dev/null and b/src/template/data/demo.db differ diff --git a/src/template/data/init_sql_sql.hbs b/src/template/data/init_sql_sql.hbs new file mode 100644 index 0000000..9367c2b --- /dev/null +++ b/src/template/data/init_sql_sql.hbs @@ -0,0 +1,4 @@ +{{create_success_mysql_or_pgsql_fist_use}} +BEGIN; +INSERT INTO "users" ("id", "username", "password") VALUES ('cdd0e080-5bb1-4442-b6f7-2ba60dbd0555', 'zhangsan', '$argon2id$v=19$m=19456,t=2,p=1$rcosL5pOPdA2c7i4ZuLA4Q$s0JGh78UzMmu1qZMpVUA3b8kWYLXcZhw7uBfwhYDJ4A'); +COMMIT; diff --git a/src/template/migrations/20231001143156_users.sql b/src/template/migrations/20231001143156_users.sql new file mode 100644 index 0000000..2e17ad1 --- /dev/null +++ b/src/template/migrations/20231001143156_users.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS users +( + id TEXT PRIMARY KEY NOT NULL, + username VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(511) NOT NULL +); \ No newline at end of file diff --git a/src/template/src/app_error.rs b/src/template/src/app_error.hbs similarity index 56% rename from src/template/src/app_error.rs rename to src/template/src/app_error.hbs index af030aa..84b507c 100644 --- a/src/template/src/app_error.rs +++ b/src/template/src/app_error.hbs @@ -1,7 +1,20 @@ +{{#if is_web_site}} use salvo::{ - async_trait, http::ParseError, prelude::EndpointOutRegister, writing::Json, Depot, Request, - Response, Writer, + async_trait, + http::ParseError, + prelude::{EndpointOutRegister, StatusCode}, + writing::Json, + Depot, Request, Response, Writer, }; +{{else}} +use salvo::{ + async_trait, + http::ParseError, + prelude::EndpointOutRegister, + writing::Json, + Depot, Request, Response, Writer, +}; +{{/if}} use thiserror::Error; #[derive(Error, Debug)] @@ -10,6 +23,10 @@ pub enum AppError { AnyHow(#[from] anyhow::Error), #[error("http::ParseError:`{0}`")] ParseError(#[from] ParseError), + {{#if is_sqlx}} + #[error("sqlx::Error:`{0}`")] + SqlxError(#[from] sqlx::Error), + {{/if}} } pub type AppResult = Result; @@ -17,7 +34,11 @@ pub type AppResult = Result; #[async_trait] impl Writer for AppError { async fn write(mut self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) { + {{#if is_web_site}} + res.stuff(StatusCode::INTERNAL_SERVER_ERROR, Json(self.to_string())); + {{else}} res.render(Json(self.to_string())); + {{/if}} } } diff --git a/src/template/src/app_response.hbs b/src/template/src/app_response.hbs new file mode 100644 index 0000000..8c72fa5 --- /dev/null +++ b/src/template/src/app_response.hbs @@ -0,0 +1,53 @@ +use salvo::{prelude::StatusCode, writing::Json, Response}; +use serde::Serialize; + +#[derive(Debug, Serialize, Default)] +pub struct Res { + pub code: i32, + pub data: T, + pub msg: String, +} + +#[derive(Debug, Serialize, Default)] +pub struct ErrRes { + pub code: i32, + pub msg: String, +} + +impl Res { + pub fn with_data(data: T) -> Self { + Self { + code: 200, + data, + msg: "success".to_string(), + } + } + #[allow(dead_code)] + pub fn with_data_msg(data: T, msg: &str) -> Self { + Self { + code: 200, + data, + msg: msg.to_string(), + } + } +} + +impl ErrRes { + pub fn with_err(err: &str) -> Self { + Self { + code: 500, + msg: err.to_string(), + } + } +} +impl Res { + pub fn into_response(self, res: &mut Response) { + res.render(Json(self)); + } +} + +impl ErrRes { + pub fn into_response(self, res: &mut Response) { + res.stuff(StatusCode::INTERNAL_SERVER_ERROR, Json(self)); + } +} diff --git a/src/template/src/config_template.hbs b/src/template/src/config_template.hbs index 8988709..c9f2626 100644 --- a/src/template/src/config_template.hbs +++ b/src/template/src/config_template.hbs @@ -6,6 +6,9 @@ use std::{fs::File, io::Read, path::Path}; pub struct Configs { pub server: Server, pub log: Log, + {{#if need_db_conn}} + pub database: DataBase, + {{/if}} pub cert: Cert, pub jwt: Jwt, } @@ -16,6 +19,12 @@ pub struct Server { pub address: String, pub ssl: bool, } +{{#if need_db_conn}} +#[derive(Debug, Deserialize)] +pub struct DataBase { + pub database_url: String, +} +{{/if}} #[derive(Debug, Deserialize)] pub struct Log { diff --git a/src/template/src/db.hbs b/src/template/src/db.hbs new file mode 100644 index 0000000..0d8864d --- /dev/null +++ b/src/template/src/db.hbs @@ -0,0 +1,47 @@ +{{#if is_sqlx}} +{{#if is_mysql}} +use sqlx::MySqlPool; +{{/if}} +{{#if is_postgres}} +use sqlx::PgPool; +{{/if}} +{{#if is_sqlite}} +use sqlx::SqlitePool; +{{/if}} +{{/if}} +use tokio::sync::OnceCell; + +use crate::config::CFG; +{{#if is_sqlx}} +{{#if is_sqlite}} +pub static DB: OnceCell = OnceCell::const_new(); +{{/if}} +{{#if is_postgres}} +pub static DB: OnceCell = OnceCell::const_new(); +{{/if}} +{{#if is_mysql}} +pub static DB: OnceCell = OnceCell::const_new(); +{{/if}} +{{/if}} +pub async fn init_db_conn() { + DB.get_or_init(|| async { + {{#if is_sqlx}} + {{#if is_sqlite}} + SqlitePool::connect(&CFG.database.database_url) + .await + .expect("{{database_connection_failed}}") + {{/if}} + {{#if is_postgres}} + PgPool::connect(&CFG.database.database_url) + .await + .expect("{{database_connection_failed}}") + {{/if}} + {{#if is_mysql}} + MySqlPool::connect(&CFG.database.database_url) + .await + .expect("{{database_connection_failed}}") + {{/if}} + {{/if}} + }) + .await; +} diff --git a/src/template/src/dtos/mod.hbs b/src/template/src/dtos/mod.hbs new file mode 100644 index 0000000..22d12a3 --- /dev/null +++ b/src/template/src/dtos/mod.hbs @@ -0,0 +1 @@ +pub mod user; diff --git a/src/template/src/dtos/user.hbs b/src/template/src/dtos/user.hbs new file mode 100644 index 0000000..c45fea8 --- /dev/null +++ b/src/template/src/dtos/user.hbs @@ -0,0 +1,40 @@ +use salvo::prelude::ToSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Debug, ToSchema, Default)] +pub struct UserAddRequest { + pub username: String, + pub password: String, +} + +#[derive(Debug, Deserialize, ToSchema, Default)] +pub struct UserLoginRequest { + pub username: String, + pub password: String, +} + +#[derive(Debug, Deserialize, ToSchema, Default)] +pub struct UserUpdateRequest { + pub id: String, + pub username: String, + pub password: String, +} + +#[derive(Debug, Deserialize, ToSchema, Default)] +pub struct UserDeleteRequest { + pub id: String, +} + +#[derive(Debug, Serialize, ToSchema, Default)] +pub struct UserResponse { + pub id: String, + pub username: String, +} + +#[derive(Debug, Serialize, ToSchema, Default)] +pub struct UserLoginResponse { + pub id: String, + pub username: String, + pub token: String, + pub exp: i64, +} diff --git a/src/template/src/main_template.hbs b/src/template/src/main_template.hbs index 20433e7..0d998c8 100644 --- a/src/template/src/main_template.hbs +++ b/src/template/src/main_template.hbs @@ -1,71 +1,50 @@ -use crate::app_error::AppResult; +{{#if need_db_conn}} +use crate::db::init_db_conn; +{{/if}} +use crate::middleware::handle_404::handle_404; +use crate::routers::router; use config::{CERT_KEY, CFG}; -use middleware::jwt::jwt_hoop; use salvo::catcher::Catcher; use salvo::conn::rustls::{Keycert, RustlsConfig}; use salvo::prelude::*; use tokio::sync::oneshot; - mod app_error; +{{#if need_db_conn}} +{{else}} +#[allow(dead_code)] +{{/if}} +mod app_response; mod config; +{{#if need_db_conn}} +mod db; +mod dtos; +mod services; +mod models; +mod utils; +{{/if}} mod middleware; -{{#if is_web_site}} - -#[derive(Template)] -#[template(path = "hello.html")] -struct HelloTemplate<'a> { - name: &'a str, -} - -#[endpoint] -async fn hello(req: &mut Request, res: &mut Response)->AppResult<()>{ - let hello_tmpl = HelloTemplate { - name: req.param::<&str>("name").unwrap_or("World"), - }; - res.render(Text::Html(hello_tmpl.render().unwrap())); - Ok(()) -} - -#[derive(Template)] -#[template(path = "404.html")] -struct Handle404 { -} - -#[handler] -async fn handle404(&self, _req: &Request, _depot: &Depot, res: &mut Response, ctrl: &mut FlowCtrl) { - if let Some(StatusCode::NOT_FOUND) = res.status_code { - let handle_404 = Handle404{}; - res.render(Text::Html(handle_404.render().unwrap())); - ctrl.skip_rest(); - } -} -{{else}} -#[endpoint] -async fn hello() -> AppResult<&'static str> { - Ok("Hello World from salvo") -} +mod routers; -#[handler] -async fn handle404(&self, _req: &Request, _depot: &Depot, res: &mut Response, ctrl: &mut FlowCtrl) { - if let Some(StatusCode::NOT_FOUND) = res.status_code { - res.render(Json("404 not found")); - ctrl.skip_rest(); - } -} -{{/if}} #[tokio::main] async fn main() { //{{main_log_message}} init_log(); - + {{#if need_db_conn}} + init_db_conn().await; + {{/if}} let (tx, rx) = oneshot::channel(); let router = router(); let service: Service = router.into(); - let service = service.catcher(Catcher::default().hoop(handle404)); - println!("💨 {} is staring ",&CFG.server.name); - println!(" listen on {}",&CFG.server.address.replace("0.0.0.0", "127.0.0.1")); + let service = service.catcher(Catcher::default().hoop(handle_404)); + println!("💨 {} is staring ", &CFG.server.name); + println!(" listen on {}", &CFG.server.address); + match CFG.server.ssl { true => { + println!( + "swagger-ui: https://{}/swagger-ui", + &CFG.server.address.replace("0.0.0.0", "127.0.0.1") + ); let config = RustlsConfig::new( Keycert::new() .cert(CERT_KEY.cert.clone()) @@ -85,6 +64,10 @@ async fn main() { tokio::task::spawn(server); } false => { + println!( + "swagger-ui: http://{}/swagger-ui", + &CFG.server.address.replace("0.0.0.0", "127.0.0.1") + ); let acceptor = TcpListener::new(&CFG.server.address).bind().await; let server = Server::new(acceptor).serve_with_graceful_shutdown( service, @@ -102,18 +85,6 @@ async fn main() { let _ = tx.send(()); } -fn router() -> Router { - let router = Router::new() - .hoop(Logger::new()) - .hoop(CatchPanic::new()) - .hoop(jwt_hoop()) - .get(hello); - let doc = OpenApi::new("salvo web api", "0.0.1").merge_router(&router); - router - .push(doc.into_router("/api-doc/openapi.json")) - .push(SwaggerUi::new("/api-doc/openapi.json").into_router("swagger-ui")) -} - fn init_log() { let _guard = clia_tracing_config::build() .filter_level(&CFG.log.filter_level) diff --git a/src/template/src/middleware/handle_404.hbs b/src/template/src/middleware/handle_404.hbs new file mode 100644 index 0000000..ad00be8 --- /dev/null +++ b/src/template/src/middleware/handle_404.hbs @@ -0,0 +1,29 @@ +{{#if is_web_site}} +use askama::Template; +use salvo::{handler, prelude::StatusCode, writing::Text, Depot, FlowCtrl, Request, Response}; +{{else}} +use salvo::{handler, prelude::StatusCode, writing::Json, Depot, FlowCtrl, Request, Response}; +{{/if}} + +{{#if is_web_site}} +#[derive(Template)] +#[template(path = "handle_404.html")] +struct Handle404; + +#[handler] +pub async fn handle_404(&self, _req: &Request, _depot: &Depot, res: &mut Response, ctrl: &mut FlowCtrl) { + if let Some(StatusCode::NOT_FOUND) = res.status_code { + let handle404 = Handle404; + res.render(Text::Html(handle404.render().unwrap())); + ctrl.skip_rest(); + } +} +{{else}} +#[handler] +pub async fn handle_404(&self, _req: &Request, _depot: &Depot, res: &mut Response, ctrl: &mut FlowCtrl) { + if let Some(StatusCode::NOT_FOUND) = res.status_code { + res.render(Json("404 not found")); + ctrl.skip_rest(); + } +} +{{/if}} \ No newline at end of file diff --git a/src/template/src/middleware/jwt.rs b/src/template/src/middleware/jwt.rs index 0fd553e..164e650 100644 --- a/src/template/src/middleware/jwt.rs +++ b/src/template/src/middleware/jwt.rs @@ -1,6 +1,6 @@ use anyhow::Result; use jsonwebtoken::EncodingKey; -use salvo::jwt_auth::{ConstDecoder, QueryFinder}; +use salvo::jwt_auth::{ConstDecoder, CookieFinder, HeaderFinder, QueryFinder}; use salvo::prelude::*; use serde::{Deserialize, Serialize}; use time::{Duration, OffsetDateTime}; @@ -13,21 +13,22 @@ pub struct JwtClaims { exp: i64, } +#[allow(dead_code)] pub fn jwt_hoop() -> JwtAuth { let auth_handler: JwtAuth = JwtAuth::new(ConstDecoder::from_secret( CFG.jwt.jwt_secret.to_owned().as_bytes(), )) .finders(vec![ - // Box::new(HeaderFinder::new()), + Box::new(HeaderFinder::new()), Box::new(QueryFinder::new("token")), - // Box::new(CookieFinder::new("jwt_token")), + Box::new(CookieFinder::new("jwt_token")), ]) - .force_passed(true); + .force_passed(false); auth_handler } #[allow(dead_code)] -pub fn get_token(username: String, user_id: String) -> Result { +pub fn get_token(username: String, user_id: String) -> Result<(String, i64)> { let exp = OffsetDateTime::now_utc() + Duration::seconds(CFG.jwt.jwt_exp); let claim = JwtClaims { username, @@ -39,5 +40,5 @@ pub fn get_token(username: String, user_id: String) -> Result { &claim, &EncodingKey::from_secret(CFG.jwt.jwt_secret.as_bytes()), )?; - Ok(token) + Ok((token, exp.unix_timestamp())) } diff --git a/src/template/src/middleware/mod.rs b/src/template/src/middleware/mod.rs index 417233c..9cc4bdd 100644 --- a/src/template/src/middleware/mod.rs +++ b/src/template/src/middleware/mod.rs @@ -1 +1,2 @@ +pub mod handle_404; pub mod jwt; diff --git a/src/template/src/models/mod.hbs b/src/template/src/models/mod.hbs new file mode 100644 index 0000000..22d12a3 --- /dev/null +++ b/src/template/src/models/mod.hbs @@ -0,0 +1 @@ +pub mod user; diff --git a/src/template/src/models/user.hbs b/src/template/src/models/user.hbs new file mode 100644 index 0000000..025bf41 --- /dev/null +++ b/src/template/src/models/user.hbs @@ -0,0 +1,9 @@ +use serde::Serialize; +use sqlx::FromRow; + +#[derive(FromRow, Serialize, Debug)] +pub struct User { + pub id: String, + pub username: String, + pub password: String, +} diff --git a/src/template/src/routers/demo.hbs b/src/template/src/routers/demo.hbs new file mode 100644 index 0000000..dfdaf06 --- /dev/null +++ b/src/template/src/routers/demo.hbs @@ -0,0 +1,54 @@ +{{#if is_web_site}} +use askama::Template; +{{/if}} +{{#if is_web_site}} +use salvo::{endpoint, writing::Text, Request, Response}; +{{else}} +use salvo::endpoint; +{{/if}} +use crate::app_error::AppResult; +{{#if is_web_site}} + +#[derive(Template)] +#[template(path = "hello.html")] +struct HelloTemplate<'a> { + name: &'a str, +} + +#[endpoint] +pub async fn hello(req: &mut Request, res: &mut Response)->AppResult<()>{ + let hello_tmpl = HelloTemplate { + name: req.param::<&str>("name").unwrap_or("World"), + }; + res.render(Text::Html(hello_tmpl.render().unwrap())); + Ok(()) +} +{{else}} +#[endpoint] +pub async fn hello() -> AppResult<&'static str> { + Ok("Hello World from salvo") +} +{{/if}} + +#[allow(unused_imports)] +mod tests { + use salvo::test::{ResponseExt, TestClient}; + use salvo::Service; + use crate::config::CFG; + + #[tokio::test] + async fn test_hello_world() { + let service = Service::new(crate::routers::router()); + + let content = TestClient::get(format!( + "http://{}", + &CFG.server.address.replace("0.0.0.0", "127.0.0.1") + )) + .send(&service) + .await + .take_string() + .await + .unwrap(); + assert_eq!(content, "Hello World from salvo"); + } +} diff --git a/src/template/src/routers/mod.hbs b/src/template/src/routers/mod.hbs new file mode 100644 index 0000000..04ae14c --- /dev/null +++ b/src/template/src/routers/mod.hbs @@ -0,0 +1,61 @@ +{{#if need_db_conn}} +use crate::middleware::jwt::jwt_hoop; +{{/if}} +use salvo::{ + prelude::{CatchPanic, Logger, OpenApi, SwaggerUi}, + Router, +}; + +use self::{ + demo::hello, +{{#if need_db_conn}} +{{#if is_web_site}} + user::{ + delete_user,login_page, post_add_user, post_login, post_update_user, + user_list_page, + }, +{{else}} + user::{ + delete_user, get_users, post_add_user, post_login, post_update_user, + }, +{{/if}} +{{/if}} +}; +pub mod demo; +{{#if need_db_conn}} +pub mod user; +{{/if}} + +pub fn router() -> Router { + let router = Router::new() + .hoop(Logger::new()) + .hoop(CatchPanic::new()) + {{#if need_db_conn}} + .get(hello) + {{#if is_web_site}} + .push(Router::with_path("login").get(login_page).post(post_login)) + {{else}} + .push(Router::with_path("login").post(post_login)) + {{/if}} + .push(user_router().hoop(jwt_hoop())); + {{else}} + .get(hello); + {{/if}} + let doc = OpenApi::new("salvo web api", "0.0.1").merge_router(&router); + router + .push(doc.into_router("/api-doc/openapi.json")) + .push(SwaggerUi::new("/api-doc/openapi.json").into_router("swagger-ui")) +} +{{#if need_db_conn}} +pub fn user_router() -> Router { + Router::with_path("user") + .post(post_add_user) + .put(post_update_user) + .delete(delete_user) + {{#if is_web_site}} + .get(user_list_page) + {{else}} + .get(get_users) + {{/if}} +} +{{/if}} \ No newline at end of file diff --git a/src/template/src/routers/user.hbs b/src/template/src/routers/user.hbs new file mode 100644 index 0000000..8ad1089 --- /dev/null +++ b/src/template/src/routers/user.hbs @@ -0,0 +1,124 @@ +{{#if is_web_site}} +use askama::Template; +{{/if}} +{{#if is_web_site}} +use salvo::{ + endpoint, + http::cookie::Cookie, + oapi::extract::{FormBody, JsonBody}, + writing::{Redirect, Text}, + Response, +}; +use crate::{ + app_error::AppResult, + app_response::{ErrRes, Res}, + dtos::user::{ + UserAddRequest, UserDeleteRequest, UserLoginRequest, UserResponse, UserUpdateRequest, + }, + services::user, +}; +{{else}} +use salvo::{ + endpoint, + oapi::extract::{FormBody, JsonBody}, + Response, +}; +use crate::{ + app_response::{ErrRes, Res}, + dtos::user::{ + UserAddRequest, UserDeleteRequest, UserLoginRequest, UserUpdateRequest, + }, + services::user, +}; +{{/if}} +{{#if is_web_site}} +#[derive(Template)] +#[template(path = "login.html")] +struct LoginTemplate {} + +#[endpoint] +pub async fn login_page(res: &mut Response) -> AppResult<()> { + let hello_tmpl = LoginTemplate {}; + res.render(Text::Html(hello_tmpl.render().unwrap())); + Ok(()) +} +{{/if}} + +#[endpoint] +pub async fn post_add_user(req: JsonBody, res: &mut Response) { + let result = user::add_user(req.0).await; + match result { + Ok(data) => Res::with_data(data).into_response(res), + Err(e) => ErrRes::with_err(&e.to_string()).into_response(res), + } +} + +#[endpoint] +pub async fn post_update_user(req: JsonBody, res: &mut Response) { + let result = user::update_user(req.0).await; + match result { + Ok(data) => Res::with_data(data).into_response(res), + Err(e) => ErrRes::with_err(&e.to_string()).into_response(res), + } +} + +#[endpoint] +pub async fn delete_user(req: JsonBody, res: &mut Response) { + let result = user::delete_user(req.0).await; + match result { + Ok(_) => Res::with_data(()).into_response(res), + Err(e) => ErrRes::with_err(&e.to_string()).into_response(res), + } +} + +#[endpoint] +pub async fn get_users(res: &mut Response) { + let result = user::users().await; + match result { + Ok(data) => Res::with_data(data).into_response(res), + Err(e) => ErrRes::with_err(&e.to_string()).into_response(res), + } +} +{{#if is_web_site}} + +#[derive(Template)] +#[template(path = "user_list_page.html")] +pub struct UserListPageTemplate { + users: Vec, +} + +#[endpoint] +pub async fn user_list_page(res: &mut Response) -> AppResult<()> { + let users = user::users().await?; + let hello_tmpl = UserListPageTemplate { users }; + + res.render(Text::Html(hello_tmpl.render().unwrap())); + Ok(()) +} + +#[endpoint] +pub async fn post_login(req: FormBody, res: &mut Response) { + let result = user::login(req.0).await; + match result { + Ok(data) => { + let jwt_token = data.token.clone(); + let cookie = Cookie::build("jwt_token", jwt_token) + .path("/") + .http_only(true) + .finish(); + res.add_cookie(cookie); + res.render(Redirect::other("/user")); + } + Err(e) => ErrRes::with_err(&e.to_string()).into_response(res), + } +} +{{else}} +#[endpoint] +pub async fn post_login(req: FormBody, res: &mut Response) { + let result = user::login(req.0).await; + match result { + Ok(data) => Res::with_data(data).into_response(res), + Err(e) => ErrRes::with_err(&e.to_string()).into_response(res), + } +} +{{/if}} diff --git a/src/template/src/services/mod.hbs b/src/template/src/services/mod.hbs new file mode 100644 index 0000000..22d12a3 --- /dev/null +++ b/src/template/src/services/mod.hbs @@ -0,0 +1 @@ +pub mod user; diff --git a/src/template/src/services/user.hbs b/src/template/src/services/user.hbs new file mode 100644 index 0000000..ea1cf2d --- /dev/null +++ b/src/template/src/services/user.hbs @@ -0,0 +1,124 @@ +use crate::{ + app_error::AppResult, + db::DB, + dtos::user::{ + UserAddRequest, UserDeleteRequest, UserLoginRequest, UserLoginResponse, UserResponse, + UserUpdateRequest, + }, + middleware::jwt::get_token, + models::user::User, + utils::rand_utils, +}; +use uuid::Uuid; +{{#if is_sqlx}} +pub async fn add_user(req: UserAddRequest) -> AppResult { + let db = DB.get().ok_or(anyhow::anyhow!("{{database_connection_failed}}"))?; + let id = Uuid::new_v4().to_string(); + let hash_password = rand_utils::hash_password(req.password).await?; + let _ = sqlx::query!( + r#" + INSERT INTO users (id, username, password) + VALUES ($1, $2, $3) + "#, + id, + req.username, + hash_password, + ) + .execute(db) + .await?; + + Ok(UserResponse { + id, + username: req.username, + }) +} + +pub async fn login(req: UserLoginRequest) -> AppResult { + let db = DB.get().ok_or(anyhow::anyhow!("{{database_connection_failed}}"))?; + let user = sqlx::query_as!( + User, + r#" + SELECT id, username, password FROM users + WHERE username = $1 + "#, + req.username + ) + .fetch_optional(db) + .await?; + if user.is_none() { + return Err(anyhow::anyhow!("{{user_does_not_exist}}").into()); + } + let user = user.unwrap(); + if rand_utils::verify_password(req.password, user.password) + .await + .is_err() + { + return Err(anyhow::anyhow!("{{incorrect_password}}").into()); + } + let (token, exp) = get_token(user.username.clone(), user.id.clone())?; + let res = UserLoginResponse { + id: user.id, + username: user.username, + token, + exp, + }; + Ok(res) +} + +pub async fn update_user(req: UserUpdateRequest) -> AppResult { + let db = DB.get().ok_or(anyhow::anyhow!("{{database_connection_failed}}"))?; + let hash_password = rand_utils::hash_password(req.password).await?; + let _ = sqlx::query!( + r#" + UPDATE users + SET username = $1, password = $2 + WHERE id = $3 + "#, + req.username, + hash_password, + req.id, + ) + .execute(db) + .await?; + + Ok(UserResponse { + id: req.id, + username: req.username, + }) +} + +pub async fn delete_user(req: UserDeleteRequest) -> AppResult<()> { + let db = DB.get().ok_or(anyhow::anyhow!("{{database_connection_failed}}"))?; + sqlx::query!( + r#" + DELETE FROM users + WHERE id = $1 + "#, + req.id, + ) + .execute(db) + .await?; + + Ok(()) +} + +pub async fn users() -> AppResult> { + let db = DB.get().ok_or(anyhow::anyhow!("{{database_connection_failed}}"))?; + let users = sqlx::query_as!( + User, + r#" + SELECT id, username, password FROM users + "#, + ) + .fetch_all(db) + .await?; + let res = users + .into_iter() + .map(|user| UserResponse { + id: user.id, + username: user.username, + }) + .collect::>(); + Ok(res) +} +{{/if}} \ No newline at end of file diff --git a/src/template/src/utils/mod.hbs b/src/template/src/utils/mod.hbs new file mode 100644 index 0000000..c6c1b4b --- /dev/null +++ b/src/template/src/utils/mod.hbs @@ -0,0 +1 @@ +pub mod rand_utils; diff --git a/src/template/src/utils/rand_utils.hbs b/src/template/src/utils/rand_utils.hbs new file mode 100644 index 0000000..5668317 --- /dev/null +++ b/src/template/src/utils/rand_utils.hbs @@ -0,0 +1,39 @@ +use anyhow::Context; +use argon2::{password_hash::SaltString, Argon2, PasswordHash}; +use rand::Rng; +use std::iter; +/// {{generate_a_string_of_a_specified_length}} +#[allow(dead_code)] +#[inline] +pub fn random_string(limit: usize) -> String { + iter::repeat(()) + .map(|_| rand::thread_rng().sample(rand::distributions::Alphanumeric)) + .map(char::from) + .take(limit) + .collect() +} + +pub async fn verify_password(password: String, password_hash: String) -> anyhow::Result<()> { + tokio::task::spawn_blocking(move || -> anyhow::Result<()> { + let hash = PasswordHash::new(&password_hash) + .map_err(|e| anyhow::anyhow!("invalid password hash: {}", e))?; + let result = hash.verify_password(&[&Argon2::default()], password); + match result { + Ok(_) => Ok(()), + Err(_) => Err(anyhow::anyhow!("invalid password")), + } + }) + .await + .context("panic in verifying password hash")? +} + +pub async fn hash_password(password: String) -> anyhow::Result { + tokio::task::spawn_blocking(move || -> anyhow::Result { + let salt = SaltString::generate(rand::thread_rng()); + Ok(PasswordHash::generate(Argon2::default(), password, &salt) + .map_err(|e| anyhow::anyhow!("failed to generate password hash: {}", e))? + .to_string()) + }) + .await + .context("panic in generating password hash")? +} diff --git a/src/template/templates/404.html b/src/template/templates/404.hbs similarity index 84% rename from src/template/templates/404.html rename to src/template/templates/404.hbs index e52aac0..dd97fe6 100644 --- a/src/template/templates/404.html +++ b/src/template/templates/404.hbs @@ -3,7 +3,7 @@ - 404 Page Not Found + 404 {{page_not_found}} - Page not found + {{page_not_found}}
Go back home{{return_to_homepage}} Contact support {{contact_support}}
diff --git a/src/template/templates/hello.html b/src/template/templates/hello.hbs similarity index 100% rename from src/template/templates/hello.html rename to src/template/templates/hello.hbs diff --git a/src/template/templates/login.hbs b/src/template/templates/login.hbs new file mode 100644 index 0000000..53133a3 --- /dev/null +++ b/src/template/templates/login.hbs @@ -0,0 +1,111 @@ + + + + + + + salvo + + +
+
+
+

+ {{login}} +

+
+
+ +
+
+ + +
+
+ + +
+
+
+ +
+
+
+
+ + + + + + + + + diff --git a/src/template/templates/user_list.hbs b/src/template/templates/user_list.hbs new file mode 100644 index 0000000..f17a058 --- /dev/null +++ b/src/template/templates/user_list.hbs @@ -0,0 +1,96 @@ +
+
+
+
+

+ {{user_list}} +

+
+
+
+
+
+ + + + + + + + + + {% for user in users %} + + + + + + {% endfor %} + + +
+ {{username}} + + {{delete}} + {{operation}} +
+ [[user.username]] + {{delete}}{{delete}} +
+
+
+
+
+
diff --git a/src/template/templates/user_list_page.hbs b/src/template/templates/user_list_page.hbs new file mode 100644 index 0000000..d7f65dc --- /dev/null +++ b/src/template/templates/user_list_page.hbs @@ -0,0 +1,40 @@ + + + + + + + salvo + + {% include "user_list.html" %} + + + + + + + + diff --git a/src/utils/create_project.rs b/src/utils/create_project.rs index a22a3e8..68f14b8 100644 --- a/src/utils/create_project.rs +++ b/src/utils/create_project.rs @@ -1,6 +1,5 @@ use crate::Project; use anyhow::{Context, Result}; -use dialoguer::{console::Style, theme::ColorfulTheme, Select}; use handlebars::Handlebars; use print_util::success; use rust_i18n::t; @@ -14,25 +13,40 @@ use std::{ slice, }; -use super::{print_util, restricted_names, warning}; +use super::{ + get_selection::{get_user_selected, DbConnectionType, DbType, TemplateType, UserSelected}, + print_util, restricted_names, warning, +}; pub fn create_project(project: Project) -> Result<()> { check_name(&project.project_name)?; let project_name = &project.project_name; let project_path = Path::new(project_name); if project_path.exists() { - anyhow::bail!(t!("error_project_path_exist",path=project_path.to_string_lossy())) + anyhow::bail!(t!( + "error_project_path_exist", + path = project_path.to_string_lossy() + )) } check_path(project_path)?; - let config = init_config()?; + let config = get_user_selected()?; match config { Some(config) => { - write_project_file(project_path, config, project.clone())?; + write_project_file(project_path, config.clone(), project.clone())?; init_git(project_path)?; success(t!("create_success", project_name = project_name).replace(r"\n", "\n")); + if config.db_conn_type== DbConnectionType::Sqlx { + success(t!("create_success_sqlx", project_name = project_name).replace(r"\n", "\n")); + if config.db_type == DbType::Sqlite { + success(t!("create_success_sqlx_sqlite").replace(r"\n", "\n")); + } + else { + success(t!("create_success_mysql_or_pgsql").replace(r"\n", "\n")); + } + } } None => anyhow::bail!("cli quit!"), } @@ -40,10 +54,19 @@ pub fn create_project(project: Project) -> Result<()> { Ok(()) } -fn write_project_file(project_path: &Path, config: Config, project: Project) -> Result<()> { +fn write_project_file( + project_path: &Path, + user_selected: UserSelected, + project: Project, +) -> Result<()> { let handlebars = Handlebars::new(); - let is_web_site = config.template_type == TemplateType::SalvoWebSite; - let data = json!({ + let is_web_site = user_selected.template_type == TemplateType::SalvoWebSite; + let need_db_conn = user_selected.db_conn_type != DbConnectionType::Nothing; + let is_sqlx = user_selected.db_conn_type == DbConnectionType::Sqlx; + let is_mysql = user_selected.db_type == DbType::Mysql; + let is_postgres = user_selected.db_type == DbType::Postgres; + let is_sqlite = user_selected.db_type == DbType::Sqlite; + let mut data = json!({ "project_name": project.project_name, "dependencies": { "anyhow": "1.0.75", @@ -52,7 +75,7 @@ fn write_project_file(project_path: &Path, config: Config, project: Project) -> "once_cell": "1.18.0", "salvo": { "version": "*", - "features": ["anyhow", "logging", "cors", "oapi", "jwt-auth", "rustls", "catch-panic"] + "features": ["anyhow", "logging", "cors", "oapi", "jwt-auth", "rustls", "catch-panic","cookie"] }, "serde": "1.0.188", "thiserror": "1.0.48", @@ -65,71 +88,301 @@ fn write_project_file(project_path: &Path, config: Config, project: Project) -> "tracing": "0.1" }, "is_web_site":is_web_site, + "need_db_conn":need_db_conn, + "is_sqlx":is_sqlx, + "is_mysql":is_mysql, + "is_postgres":is_postgres, + "is_sqlite":is_sqlite, "main_log_message":t!("main_log_message"), "config_error_no_exits":t!("config_error_no_exits"), "config_error_read":t!("config_error_read"), "config_error_parse":t!("config_error_parse"), "config_error_read_failed":t!("config_error_read_failed"), + "generate_a_string_of_a_specified_length":t!("generate_a_string_of_a_specified_length"), + "username":t!("username"), + "password":t!("password"), + "incorrect_password":t!("incorrect_password"), + "login":t!("login"), + "user_list":t!("user_list"), + "are_you_sure_you_want_to_delete":t!("are_you_sure_you_want_to_delete"), + "page_not_found":t!("page_not_found"), + "contact_support":t!("contact_support"), + "return_to_homepage":t!("return_to_homepage"), + "delete":t!("delete"), + "yes":t!("yes"), + "cancel":t!("cancel"), + "operation":t!("operation"), + "create_success_mysql_or_pgsql_fist_use":t!("create_success_mysql_or_pgsql_fist_use").replace(r"\n", "\n"), }); + if is_sqlx { + // Add sqlx dependencies + let mut dependencies = data["dependencies"].clone(); + if is_mysql { + dependencies["sqlx"] = json!({ + "version": "0.7", + "features": ["runtime-tokio", "macros", "mysql"] + }); + } + if is_postgres { + dependencies["sqlx"] = json!({ + "version": "0.7", + "features": ["runtime-tokio", "macros", "postgres"] + }); + } + if is_sqlite { + dependencies["sqlx"] = json!({ + "version": "0.7", + "features": ["runtime-tokio", "macros", "sqlite"] + }); + } + //add uuid dependency + dependencies["uuid"] = json!({ + "version": "1.4.1", + "features": ["v4", "fast-rng", "macro-diagnostics"] + }); + //add rand dependency + dependencies["rand"] = json!({ + "version": "0.8.5", + }); + //add argon2 dependency + dependencies["argon2"] = json!({ + "version": "0.5.2", + }); + data["dependencies"] = dependencies; + } + std::fs::create_dir_all(project_path)?; let src_path = project_path.join("src"); + //src std::fs::create_dir_all(&src_path)?; - + //src/main.rs let main_file_path = src_path.join("main.rs"); let main_template = include_str!("../template/src/main_template.hbs"); let main_rendered = handlebars.render_template(main_template, &data)?; let mut main_file = File::create(main_file_path)?; main_file.write_all(main_rendered.as_bytes())?; + //src/Cargo.toml let cargo_file_path = project_path.join("Cargo.toml"); let cargo_template = include_str!("../template/src/cargo_template.hbs"); let cargo_rendered = handlebars.render_template(cargo_template, &data)?; let mut cargo_file = File::create(cargo_file_path)?; cargo_file.write_all(cargo_rendered.as_bytes())?; + //src/config.rs let config_template = include_str!("../template/src/config_template.hbs"); let config_rendered = handlebars.render_template(config_template, &data)?; let mut config_file = File::create(src_path.join("config.rs"))?; config_file.write_all(config_rendered.as_bytes())?; - let app_error_rs = include_bytes!("../template/src/app_error.rs"); + //src/app_error.rs + let app_error_template = include_str!("../template/src/app_error.hbs"); + let app_error_rendered = handlebars.render_template(app_error_template, &data)?; let mut app_error_file = File::create(src_path.join("app_error.rs"))?; - app_error_file.write_all(app_error_rs)?; + app_error_file.write_all(app_error_rendered.as_bytes())?; + if need_db_conn { + //src/db.rs + let db_template = include_str!("../template/src/db.hbs"); + let db_rendered = handlebars.render_template(db_template, &data)?; + let mut db_file = File::create(src_path.join("db.rs"))?; + db_file.write_all(db_rendered.as_bytes())?; + } + //src/app_response.rs + let app_response_template = include_str!("../template/src/app_response.hbs"); + let app_response_rendered = handlebars.render_template(app_response_template, &data)?; + let mut app_response_file = File::create(src_path.join("app_response.rs"))?; + app_response_file.write_all(app_response_rendered.as_bytes())?; + //src/middleware let middleware_path = src_path.join("middleware"); std::fs::create_dir_all(&middleware_path)?; let jwt_bytes = include_bytes!("../template/src/middleware/jwt.rs"); let mut jwt_file = File::create(middleware_path.join("jwt.rs"))?; jwt_file.write_all(jwt_bytes)?; + //src/middleware/mod.rs let mod_bytes = include_bytes!("../template/src/middleware/mod.rs"); let mut mod_file = File::create(middleware_path.join("mod.rs"))?; mod_file.write_all(mod_bytes)?; + //src/middleware/handle404.rs + let handle404_template = include_str!("../template/src/middleware/handle_404.hbs"); + let handle404_rendered = handlebars.render_template(handle404_template, &data)?; + let mut handle404_file = File::create(middleware_path.join("handle_404.rs"))?; + handle404_file.write_all(handle404_rendered.as_bytes())?; + //config let config_path = project_path.join("config"); std::fs::create_dir_all(&config_path)?; + //config/config.toml let config_template = include_str!("../template/config/config.hbs"); - let config_toml_rendered = handlebars.render_template(config_template, &data)?; let mut config_file = File::create(config_path.join("config.toml"))?; config_file.write_all(config_toml_rendered.as_bytes())?; - + //config/certs let cert_path = config_path.join("certs"); std::fs::create_dir_all(&cert_path)?; + //config/certs/cert.pem let cert_template = include_str!("../template/config/certs/cert.pem"); let mut cert_file = File::create(cert_path.join("cert.pem"))?; cert_file.write_all(cert_template.as_bytes())?; + //config/certs/key.pem let key_path = cert_path.join("key.pem"); let key_template = include_str!("../template/config/certs/key.pem"); let mut key_file = File::create(key_path)?; key_file.write_all(key_template.as_bytes())?; if is_web_site { - let template_path = project_path.join("template"); + //templates + let template_path = project_path.join("templates"); std::fs::create_dir_all(&template_path)?; - let hello_html_template = include_bytes!("../template/templates/hello.html"); - let mut hello_html_file = File::create(template_path.join("hello.html"))?; - hello_html_file.write_all(hello_html_template)?; - let handle_404_template = include_bytes!("../template/templates/404.html"); + + //template/hello.html + let hello_template = include_str!("../template/templates/hello.hbs"); + let mut hello_file = File::create(template_path.join("hello.html"))?; + hello_file.write_all(hello_template.as_bytes())?; + + //template/handle_404.html + let handle_404_template = include_str!("../template/templates/404.hbs"); + let handle_404_template_rendered = + handlebars.render_template(handle_404_template, &data)?; let mut handle_404_file = File::create(template_path.join("handle_404.html"))?; - handle_404_file.write_all(handle_404_template)?; + handle_404_file.write_all(handle_404_template_rendered.as_bytes())?; + + if need_db_conn { + //template/login.html + let login_template = include_str!("../template/templates/login.hbs"); + let login_template_rendered = handlebars.render_template(login_template, &data)?; + let mut login_file = File::create(template_path.join("login.html"))?; + login_file.write_all(login_template_rendered.as_bytes())?; + + //template/user_list.html + let user_list_template = include_str!("../template/templates/user_list.hbs"); + let user_list_template_rendered = + handlebars.render_template(user_list_template, &data)?; + let mut user_list_file = File::create(template_path.join("user_list.html"))?; + user_list_file.write_all( + user_list_template_rendered + .replace("[[", "{{") + .replace("]]", "}}") + .as_bytes(), + )?; + + //template/user_list_page.html + let user_list_page_template = include_str!("../template/templates/user_list_page.hbs"); + let user_list_page_template_rendered = + handlebars.render_template(user_list_page_template, &data)?; + let mut user_list_page_file = File::create(template_path.join("user_list_page.html"))?; + user_list_page_file.write_all(user_list_page_template_rendered.as_bytes())?; + } } + //src/router + let router_path = src_path.join("routers"); + std::fs::create_dir_all(&router_path)?; + //src/router/mod.rs + let router_mod_template = include_str!("../template/src/routers/mod.hbs"); + let router_mod_rendered = handlebars.render_template(router_mod_template, &data)?; + let mut router_mod_file = File::create(router_path.join("mod.rs"))?; + router_mod_file.write_all(router_mod_rendered.as_bytes())?; + //src/router/demo.rs + let router_demo_template = include_str!("../template/src/routers/demo.hbs"); + let router_demo_rendered = handlebars.render_template(router_demo_template, &data)?; + let mut router_demo_file = File::create(router_path.join("demo.rs"))?; + router_demo_file.write_all(router_demo_rendered.as_bytes())?; + if need_db_conn { + //src/router/user.rs + let router_user_template = include_str!("../template/src/routers/user.hbs"); + let router_user_rendered = handlebars.render_template(router_user_template, &data)?; + let mut router_user_file = File::create(router_path.join("user.rs"))?; + router_user_file.write_all(router_user_rendered.as_bytes())?; + } + + if need_db_conn { + //src/services + let services_path = src_path.join("services"); + std::fs::create_dir_all(&services_path)?; + //src/services/mod.rs + let services_mod_template = include_str!("../template/src/services/mod.hbs"); + let services_mod_rendered = handlebars.render_template(services_mod_template, &data)?; + let mut services_mod_file = File::create(services_path.join("mod.rs"))?; + services_mod_file.write_all(services_mod_rendered.as_bytes())?; + //src/services/user.rs + let services_user_template = include_str!("../template/src/services/user.hbs"); + let services_user_rendered = handlebars.render_template(services_user_template, &data)?; + let mut services_user_file = File::create(services_path.join("user.rs"))?; + services_user_file.write_all(services_user_rendered.as_bytes())?; + //src/utils + let utils_path = src_path.join("utils"); + std::fs::create_dir_all(&utils_path)?; + //src/utils/mod.rs + let utils_mod_template = include_str!("../template/src/utils/mod.hbs"); + let utils_mod_rendered = handlebars.render_template(utils_mod_template, &data)?; + let mut utils_mod_file = File::create(utils_path.join("mod.rs"))?; + utils_mod_file.write_all(utils_mod_rendered.as_bytes())?; + + //src/utils/rand_utils.rs + let rand_utils_template = include_str!("../template/src/utils/rand_utils.hbs"); + let rand_utils_rendered = handlebars.render_template(rand_utils_template, &data)?; + let mut rand_utils_file = File::create(utils_path.join("rand_utils.rs"))?; + rand_utils_file.write_all(rand_utils_rendered.as_bytes())?; + + //src/dtos + let dtos_path = src_path.join("dtos"); + std::fs::create_dir_all(&dtos_path)?; + //src/dtos/mod.rs + let dtos_mod_template = include_str!("../template/src/dtos/mod.hbs"); + let dtos_mod_rendered = handlebars.render_template(dtos_mod_template, &data)?; + let mut dtos_mod_file = File::create(dtos_path.join("mod.rs"))?; + dtos_mod_file.write_all(dtos_mod_rendered.as_bytes())?; + + //src/dtos/user.rs + let dtos_user_template = include_str!("../template/src/dtos/user.hbs"); + let dtos_user_rendered = handlebars.render_template(dtos_user_template, &data)?; + let mut dtos_user_file = File::create(dtos_path.join("user.rs"))?; + dtos_user_file.write_all(dtos_user_rendered.as_bytes())?; + + //src/models + let models_path = src_path.join("models"); + std::fs::create_dir_all(&models_path)?; + //src/models/mod.rs + let models_mod_template = include_str!("../template/src/models/mod.hbs"); + let models_mod_rendered = handlebars.render_template(models_mod_template, &data)?; + let mut models_mod_file = File::create(models_path.join("mod.rs"))?; + models_mod_file.write_all(models_mod_rendered.as_bytes())?; + + //src/models/user.rs + let models_user_template = include_str!("../template/src/models/user.hbs"); + let models_user_rendered = handlebars.render_template(models_user_template, &data)?; + let mut models_user_file = File::create(models_path.join("user.rs"))?; + models_user_file.write_all(models_user_rendered.as_bytes())?; + + if is_sqlx { + //data + let data_path = project_path.join("data"); + std::fs::create_dir_all(&data_path)?; + if is_sqlite { + //data/demo.db + let demo_db_bytes = include_bytes!("../template/data/demo.db"); + let mut demo_db_file = File::create(data_path.join("demo.db"))?; + demo_db_file.write_all(demo_db_bytes)?; + } + else{ + //data/init_sql.sql + let init_sql_templte = include_str!("../template/data/init_sql_sql.hbs"); + let init_sql_rendered = handlebars.render_template(init_sql_templte, &data)?; + let mut init_sql_file = File::create(data_path.join("init_sql.sql"))?; + init_sql_file.write_all(init_sql_rendered.as_bytes())?; + } + //migrations + let migrations_path = project_path.join("migrations"); + std::fs::create_dir_all(&migrations_path)?; + //migrations/2021-10-20-000000_create_users_table/up.sql + let up_sql_bytes = include_bytes!("../template/migrations/20231001143156_users.sql"); + let mut up_sql_file = File::create(migrations_path.join("20231001143156_users.sql"))?; + up_sql_file.write_all(up_sql_bytes)?; + //.env + let env_template = include_str!("../template/.env.hbs"); + let env_rendered = handlebars.render_template(env_template, &data)?; + let mut env_file = File::create(project_path.join(".env"))?; + env_file.write_all(env_rendered.as_bytes())?; + } + } + Ok(()) } @@ -209,38 +462,3 @@ fn _create_dir_all(p: &Path) -> Result<()> { .with_context(|| format!("failed to create directory `{}`", p.display()))?; Ok(()) } - -#[derive(Debug)] -pub struct Config { - pub template_type: TemplateType, -} - -fn init_config() -> Result> { - let theme = ColorfulTheme { - defaults_style: Style::new().blue(), - prompt_style: Style::new().green().bold(), - values_style: Style::new().yellow().dim(), - ..ColorfulTheme::default() - }; - let selections = &[ - t!("salvo_web_api"), - t!("salvo_web_site"), - // "custom", - ]; - let selection = Select::with_theme(&theme) - .with_prompt(t!("welcome_message").replace(r"\n", "\n")) - .default(0) - .items(&selections[..]) - .interact()?; - let template_type = match selection { - 0 => TemplateType::SalvoWebApi, - 1 => TemplateType::SalvoWebSite, - _ => anyhow::bail!("Invalid selection"), - }; - Ok(Some(Config { template_type })) -} -#[derive(Debug, PartialEq)] -pub enum TemplateType { - SalvoWebSite, - SalvoWebApi, -} diff --git a/src/utils/get_selection.rs b/src/utils/get_selection.rs new file mode 100644 index 0000000..3893bc7 --- /dev/null +++ b/src/utils/get_selection.rs @@ -0,0 +1,106 @@ +use anyhow::Result; +use dialoguer::{console::Style, theme::ColorfulTheme, Select}; +use rust_i18n::t; + +#[derive(Debug,Clone)] +pub struct UserSelected { + pub template_type: TemplateType, + pub db_type: DbType, + pub db_conn_type: DbConnectionType, +} + +pub fn get_user_selected() -> Result> { + let theme = ColorfulTheme { + defaults_style: Style::new().blue(), + prompt_style: Style::new().green().bold(), + active_item_style: Style::new().blue().bold(), + values_style: Style::new().blue().dim(), + ..ColorfulTheme::default() + }; + let selections = &[ + t!("salvo_web_api"), + t!("salvo_web_site"), + // "custom", + ]; + let selection = Select::with_theme(&theme) + .with_prompt(t!("welcome_message").replace(r"\n", "\n")) + .default(0) + .items(&selections[..]) + .interact()?; + let template_type = match selection { + 0 => TemplateType::SalvoWebApi, + 1 => TemplateType::SalvoWebSite, + _ => anyhow::bail!("Invalid selection"), + }; + let db_conn_types = &[ + t!("db_conn_types_sqlx"), + // t!("db_conn_types_diesel"), + t!("db_conn_types_sea_orm"), + // t!("db_conn_types_rbatis"), + t!("db_conn_types_nothing"), + // "custom", + ]; + let db_conn_type_selection = Select::with_theme(&theme) + .with_prompt(t!("select_db_conn_type").replace(r"\n", "\n")) + .default(0) + .items(&db_conn_types[..]) + .interact()?; + + let db_conn_type = match db_conn_type_selection { + 0 => DbConnectionType::Sqlx, + 1 => DbConnectionType::Diesel, + 2 => DbConnectionType::SeaOrm, + 3 => DbConnectionType::Rbatis, + 4 => DbConnectionType::Nothing, + _ => anyhow::bail!("Invalid db connection type selection"), + }; + if db_conn_type == DbConnectionType::Nothing { + return Ok(Some(UserSelected { + template_type, + db_type: DbType::Sqlite, + db_conn_type, + })); + } + let db_types = &[ + "sqlite", "mysql", "postgres", + // "custom", + ]; + let db_type_selection = Select::with_theme(&theme) + .with_prompt(t!("select_db_type").replace(r"\n", "\n")) + .default(0) + .items(&db_types[..]) + .interact()?; + let db_type = match db_type_selection { + 0 => DbType::Sqlite, + 1 => DbType::Mysql, + 2 => DbType::Postgres, + _ => anyhow::bail!("Invalid db type selection"), + }; + + Ok(Some(UserSelected { + template_type, + db_type, + db_conn_type, + })) +} +#[derive(Debug, PartialEq,Clone)] +pub enum TemplateType { + SalvoWebSite, + SalvoWebApi, +} + +#[derive(Debug, PartialEq,Clone)] +pub enum DbType { + Sqlite, + Mysql, + Postgres, +} + +#[derive(Debug, PartialEq,Clone)] +pub enum DbConnectionType { + Sqlx, + Diesel, + SeaOrm, + Rbatis, + Nothing, +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index ea1fae6..983ff57 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,4 +1,5 @@ pub mod create_project; +pub mod get_selection; mod print_util; mod restricted_names; pub use create_project::create_project;