sirius-cli 0.1.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {sirius_cli-0.1.0/sirius_cli.egg-info → sirius_cli-0.2.2}/PKG-INFO +21 -10
  2. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/README.md +20 -9
  3. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/pyproject.toml +1 -1
  4. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/cli.py +16 -4
  5. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/generator.py +27 -5
  6. sirius_cli-0.2.2/sirius_cli/templates/backend/auth.py.jinja2 +80 -0
  7. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/backend/main.py.jinja2 +20 -7
  8. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/backend/models.py.jinja2 +9 -0
  9. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/backend/requirements.txt.jinja2 +5 -0
  10. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/frontend/src/App.tsx.jinja2 +31 -0
  11. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/frontend/src/Dashboard.tsx.jinja2 +6 -6
  12. sirius_cli-0.2.2/sirius_cli/templates/frontend/src/Login.tsx.jinja2 +106 -0
  13. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/frontend/src/TableCrud.tsx.jinja2 +30 -6
  14. {sirius_cli-0.1.0 → sirius_cli-0.2.2/sirius_cli.egg-info}/PKG-INFO +21 -10
  15. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli.egg-info/SOURCES.txt +2 -0
  16. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/LICENSE +0 -0
  17. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/MANIFEST.in +0 -0
  18. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/setup.cfg +0 -0
  19. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/__init__.py +0 -0
  20. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/parser.py +0 -0
  21. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/backend/Dockerfile.jinja2 +0 -0
  22. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/backend/alembic/env.py.jinja2 +0 -0
  23. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/backend/alembic/script.py.mako.jinja2 +0 -0
  24. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/backend/database.py.jinja2 +0 -0
  25. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/backend/schemas.py.jinja2 +0 -0
  26. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/docker-compose.yml.jinja2 +0 -0
  27. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/frontend/.env.jinja2 +0 -0
  28. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/frontend/Dockerfile.jinja2 +0 -0
  29. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/frontend/index.html.jinja2 +0 -0
  30. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/frontend/package.json.jinja2 +0 -0
  31. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/frontend/postcss.config.js.jinja2 +0 -0
  32. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/frontend/src/index.css.jinja2 +0 -0
  33. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/frontend/src/main.tsx.jinja2 +0 -0
  34. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/frontend/tailwind.config.js.jinja2 +0 -0
  35. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/frontend/tsconfig.json.jinja2 +0 -0
  36. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli/templates/frontend/vite.config.ts.jinja2 +0 -0
  37. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli.egg-info/dependency_links.txt +0 -0
  38. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli.egg-info/entry_points.txt +0 -0
  39. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli.egg-info/requires.txt +0 -0
  40. {sirius_cli-0.1.0 → sirius_cli-0.2.2}/sirius_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sirius-cli
3
- Version: 0.1.0
3
+ Version: 0.2.2
4
4
  Summary: A rapid prototyping tool for generating containerized backend + frontend CRUD stacks from CSV/SQLite files.
5
5
  Requires-Python: >=3.8
6
6
  Description-Content-Type: text/markdown
@@ -36,11 +36,13 @@ A rapid prototyping CLI tool designed to take multiple CSV files, Excel spreadsh
36
36
 
37
37
  ## Installation
38
38
 
39
- Install the package locally in editable mode:
39
+ Install the package directly from PyPI:
40
40
  ```bash
41
- pip install -e .
41
+ pip install sirius-cli
42
42
  ```
43
43
 
44
+ *(For local development: clone the repository and run `pip install -e .`)*
45
+
44
46
  ---
45
47
 
46
48
  ## Usage: Creating a New Project (`sirius-init`)
@@ -48,16 +50,16 @@ pip install -e .
48
50
  ### 1. Generating from CSVs or Excel
49
51
  ```bash
50
52
  # From CSVs
51
- sirius-init store_system --csv examples/users.csv --csv examples/orders.csv --theme violet
53
+ sirius-init init store_system --csv examples/users.csv --csv examples/orders.csv --theme violet
52
54
 
53
55
  # From Excel
54
- sirius-init store_system --excel examples/products.xlsx --theme amber
56
+ sirius-init init store_system --excel examples/products.xlsx --theme amber
55
57
  ```
56
58
 
57
59
  ### 2. Target Production Databases
58
60
  By default, the stack uses an embedded SQLite file (`app.db`). You can target Postgres or MySQL instead:
59
61
  ```bash
60
- sirius-init billing_system --config schema.json --pg
62
+ sirius-init init billing_system --config schema.json --pg
61
63
  ```
62
64
  *(The generated `docker-compose.yml` and `database.py` will expect a `DATABASE_URL` environment variable).*
63
65
 
@@ -90,17 +92,23 @@ Specify database schemas, relationships, and colors in a JSON file:
90
92
  ```
91
93
  Then run:
92
94
  ```bash
93
- sirius-init --config examples/billing-config.json
95
+ sirius-init init billing_system --config examples/billing-config.json
96
+ ```
97
+
98
+ ### 4. Generating with JWT Authentication
99
+ Pass the `--auth` flag to scaffold a secure project. This generates an `app_users` table, a beautiful Login screen, and protects all FastAPI routes with JWT Bearer tokens automatically.
100
+ ```bash
101
+ sirius-init init secure_system --csv examples/users.csv --auth --admin-user "superadmin" --admin-pass "securepass123"
94
102
  ```
95
103
 
96
104
  ---
97
105
 
98
- ## Usage: Updating an Existing Project (`sirius-update`)
106
+ ## Usage: Updating an Existing Project (`update`)
99
107
 
100
- If your data requirements change (e.g., adding a `reviews` table to your store), you don't need to start from scratch. Use `sirius-update` to merge new schemas into an existing project.
108
+ If your data requirements change (e.g., adding a `reviews` table to your store), you don't need to start from scratch. Use the `update` command to merge new schemas into an existing project.
101
109
 
102
110
  ```bash
103
- sirius-update ./store_system --csv examples/new_reviews.csv
111
+ sirius-init update ./store_system --csv examples/new_reviews.csv
104
112
  ```
105
113
 
106
114
  Sirius-CLI will:
@@ -125,6 +133,9 @@ Sirius-CLI will:
125
133
  | `--port` | `-p` | `8000` | Backend port (used in Dockerfile, docker-compose, .env) |
126
134
  | `--api-url` | | `http://localhost:<port>` | Override the frontend VITE_API_URL |
127
135
  | `--no-seed` | | `false` | Skip seeding the DB from source CSV/Excel files |
136
+ | `--auth` | | `false` | Generate JWT authentication logic and a React Login screen |
137
+ | `--admin-user` | | `admin` | The default admin username to seed if `--auth` is enabled |
138
+ | `--admin-pass` | | `admin` | The default admin password to seed if `--auth` is enabled |
128
139
  | `--pg` | | `false` | Generate Postgres connection pool and drivers |
129
140
  | `--mysql` | | `false` | Generate MySQL connection pool and drivers |
130
141
 
@@ -20,11 +20,13 @@ A rapid prototyping CLI tool designed to take multiple CSV files, Excel spreadsh
20
20
 
21
21
  ## Installation
22
22
 
23
- Install the package locally in editable mode:
23
+ Install the package directly from PyPI:
24
24
  ```bash
25
- pip install -e .
25
+ pip install sirius-cli
26
26
  ```
27
27
 
28
+ *(For local development: clone the repository and run `pip install -e .`)*
29
+
28
30
  ---
29
31
 
30
32
  ## Usage: Creating a New Project (`sirius-init`)
@@ -32,16 +34,16 @@ pip install -e .
32
34
  ### 1. Generating from CSVs or Excel
33
35
  ```bash
34
36
  # From CSVs
35
- sirius-init store_system --csv examples/users.csv --csv examples/orders.csv --theme violet
37
+ sirius-init init store_system --csv examples/users.csv --csv examples/orders.csv --theme violet
36
38
 
37
39
  # From Excel
38
- sirius-init store_system --excel examples/products.xlsx --theme amber
40
+ sirius-init init store_system --excel examples/products.xlsx --theme amber
39
41
  ```
40
42
 
41
43
  ### 2. Target Production Databases
42
44
  By default, the stack uses an embedded SQLite file (`app.db`). You can target Postgres or MySQL instead:
43
45
  ```bash
44
- sirius-init billing_system --config schema.json --pg
46
+ sirius-init init billing_system --config schema.json --pg
45
47
  ```
46
48
  *(The generated `docker-compose.yml` and `database.py` will expect a `DATABASE_URL` environment variable).*
47
49
 
@@ -74,17 +76,23 @@ Specify database schemas, relationships, and colors in a JSON file:
74
76
  ```
75
77
  Then run:
76
78
  ```bash
77
- sirius-init --config examples/billing-config.json
79
+ sirius-init init billing_system --config examples/billing-config.json
80
+ ```
81
+
82
+ ### 4. Generating with JWT Authentication
83
+ Pass the `--auth` flag to scaffold a secure project. This generates an `app_users` table, a beautiful Login screen, and protects all FastAPI routes with JWT Bearer tokens automatically.
84
+ ```bash
85
+ sirius-init init secure_system --csv examples/users.csv --auth --admin-user "superadmin" --admin-pass "securepass123"
78
86
  ```
79
87
 
80
88
  ---
81
89
 
82
- ## Usage: Updating an Existing Project (`sirius-update`)
90
+ ## Usage: Updating an Existing Project (`update`)
83
91
 
84
- If your data requirements change (e.g., adding a `reviews` table to your store), you don't need to start from scratch. Use `sirius-update` to merge new schemas into an existing project.
92
+ If your data requirements change (e.g., adding a `reviews` table to your store), you don't need to start from scratch. Use the `update` command to merge new schemas into an existing project.
85
93
 
86
94
  ```bash
87
- sirius-update ./store_system --csv examples/new_reviews.csv
95
+ sirius-init update ./store_system --csv examples/new_reviews.csv
88
96
  ```
89
97
 
90
98
  Sirius-CLI will:
@@ -109,6 +117,9 @@ Sirius-CLI will:
109
117
  | `--port` | `-p` | `8000` | Backend port (used in Dockerfile, docker-compose, .env) |
110
118
  | `--api-url` | | `http://localhost:<port>` | Override the frontend VITE_API_URL |
111
119
  | `--no-seed` | | `false` | Skip seeding the DB from source CSV/Excel files |
120
+ | `--auth` | | `false` | Generate JWT authentication logic and a React Login screen |
121
+ | `--admin-user` | | `admin` | The default admin username to seed if `--auth` is enabled |
122
+ | `--admin-pass` | | `admin` | The default admin password to seed if `--auth` is enabled |
112
123
  | `--pg` | | `false` | Generate Postgres connection pool and drivers |
113
124
  | `--mysql` | | `false` | Generate MySQL connection pool and drivers |
114
125
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sirius-cli"
7
- version = "0.1.0"
7
+ version = "0.2.2"
8
8
  description = "A rapid prototyping tool for generating containerized backend + frontend CRUD stacks from CSV/SQLite files."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -97,7 +97,10 @@ def init(
97
97
  api_url: Optional[str] = typer.Option(None, "--api-url", help="Override the frontend VITE_API_URL (default: http://localhost:<port>)"),
98
98
  no_seed: bool = typer.Option(False, "--no-seed", help="Skip seeding the database from CSV/Excel files"),
99
99
  pg: bool = typer.Option(False, "--pg", help="Use PostgreSQL instead of SQLite"),
100
- mysql: bool = typer.Option(False, "--mysql", help="Use MySQL instead of SQLite")
100
+ mysql: bool = typer.Option(False, "--mysql", help="Use MySQL instead of SQLite"),
101
+ auth: bool = typer.Option(False, "--auth", help="Generate JWT authentication logic"),
102
+ admin_user: str = typer.Option("admin", "--admin-user", help="Default admin username if --auth is used"),
103
+ admin_pass: str = typer.Option("admin", "--admin-pass", help="Default admin password if --auth is used")
101
104
  ):
102
105
  """Initializes a new FastAPI backend and React frontend stack from data files or configuration."""
103
106
  # Ensure one and only one source parameter is provided
@@ -150,7 +153,10 @@ def init(
150
153
  typer.echo(f"Scaffolding project in: {dest_dir} (Theme: {resolved_theme}, Port: {port}, DB: {db_type})")
151
154
 
152
155
  try:
153
- generate_project(dest_dir, schemas, theme=resolved_theme, port=port, api_url=resolved_api_url, db_type=db_type)
156
+ generate_project(
157
+ dest_dir, schemas, theme=resolved_theme, port=port, api_url=resolved_api_url,
158
+ db_type=db_type, auth=auth, admin_user=admin_user, admin_pass=admin_pass
159
+ )
154
160
  except Exception as e:
155
161
  typer.secho(f"Error generating files: {e}", fg=typer.colors.RED, err=True)
156
162
  raise typer.Exit(code=1)
@@ -259,7 +265,10 @@ def update(
259
265
  port: int = typer.Option(8000, "--port", "-p", help="Backend server port (used in docker-compose and .env)"),
260
266
  api_url: Optional[str] = typer.Option(None, "--api-url", help="Override the frontend VITE_API_URL (default: http://localhost:<port>)"),
261
267
  pg: bool = typer.Option(False, "--pg", help="Use PostgreSQL instead of SQLite"),
262
- mysql: bool = typer.Option(False, "--mysql", help="Use MySQL instead of SQLite")
268
+ mysql: bool = typer.Option(False, "--mysql", help="Use MySQL instead of SQLite"),
269
+ auth: bool = typer.Option(False, "--auth", help="Generate JWT authentication logic"),
270
+ admin_user: str = typer.Option("admin", "--admin-user", help="Default admin username if --auth is used"),
271
+ admin_pass: str = typer.Option("admin", "--admin-pass", help="Default admin password if --auth is used")
263
272
  ):
264
273
  """Updates an existing project with new columns/tables."""
265
274
  if not os.path.exists(project_path):
@@ -295,7 +304,10 @@ def update(
295
304
 
296
305
  typer.echo(f"Updating project in: {project_path}")
297
306
  try:
298
- generate_project(project_path, schemas, theme=theme, port=port, api_url=resolved_api_url, db_type=db_type)
307
+ generate_project(
308
+ project_path, schemas, theme=theme, port=port, api_url=resolved_api_url,
309
+ db_type=db_type, auth=auth, admin_user=admin_user, admin_pass=admin_pass
310
+ )
299
311
  except Exception as e:
300
312
  typer.secho(f"Error generating files: {e}", fg=typer.colors.RED, err=True)
301
313
  raise typer.Exit(code=1)
@@ -16,7 +16,11 @@ def render_template(env, template_name, dest_path, **kwargs):
16
16
  with open(dest_path, "w", encoding="utf-8") as f:
17
17
  f.write(content)
18
18
 
19
- def generate_project(project_path: str, schemas: dict, theme: str = "blue", port: int = 8000, api_url: str = "http://localhost:8000", db_type: str = "sqlite"):
19
+ def generate_project(
20
+ project_path: str, schemas: dict, theme: str = "blue",
21
+ port: int = 8000, api_url: str = "http://localhost:8000", db_type: str = "sqlite",
22
+ auth: bool = False, admin_user: str = "admin", admin_pass: str = "admin"
23
+ ):
20
24
  """Generates complete FastAPI and React frontend files structure based on inferred schemas and theme."""
21
25
  env = get_env()
22
26
 
@@ -29,7 +33,10 @@ def generate_project(project_path: str, schemas: dict, theme: str = "blue", port
29
33
  theme=theme,
30
34
  port=port,
31
35
  api_url=api_url,
32
- db_type=db_type
36
+ db_type=db_type,
37
+ auth=auth,
38
+ admin_user=admin_user,
39
+ admin_pass=admin_pass
33
40
  )
34
41
 
35
42
  # 2. Backend FastAPI application files
@@ -44,6 +51,9 @@ def generate_project(project_path: str, schemas: dict, theme: str = "blue", port
44
51
  "backend/Dockerfile.jinja2": "Dockerfile"
45
52
  }
46
53
 
54
+ if auth:
55
+ backend_templates["backend/auth.py.jinja2"] = "auth.py"
56
+
47
57
  for t_path, dest_name in backend_templates.items():
48
58
  render_template(
49
59
  env,
@@ -53,7 +63,10 @@ def generate_project(project_path: str, schemas: dict, theme: str = "blue", port
53
63
  theme=theme,
54
64
  port=port,
55
65
  api_url=api_url,
56
- db_type=db_type
66
+ db_type=db_type,
67
+ auth=auth,
68
+ admin_user=admin_user,
69
+ admin_pass=admin_pass
57
70
  )
58
71
 
59
72
  # Write init file to make backend a python package
@@ -78,6 +91,9 @@ def generate_project(project_path: str, schemas: dict, theme: str = "blue", port
78
91
  "frontend/src/Dashboard.tsx.jinja2": "src/Dashboard.tsx"
79
92
  }
80
93
 
94
+ if auth:
95
+ frontend_templates["frontend/src/Login.tsx.jinja2"] = "src/pages/Login.tsx"
96
+
81
97
  for t_path, dest_name in frontend_templates.items():
82
98
  render_template(
83
99
  env,
@@ -87,7 +103,10 @@ def generate_project(project_path: str, schemas: dict, theme: str = "blue", port
87
103
  theme=theme,
88
104
  port=port,
89
105
  api_url=api_url,
90
- db_type=db_type
106
+ db_type=db_type,
107
+ auth=auth,
108
+ admin_user=admin_user,
109
+ admin_pass=admin_pass
91
110
  )
92
111
 
93
112
  # 4. Generate dynamic CRUD view pages for each table
@@ -103,7 +122,10 @@ def generate_project(project_path: str, schemas: dict, theme: str = "blue", port
103
122
  theme=theme,
104
123
  port=port,
105
124
  api_url=api_url,
106
- db_type=db_type
125
+ db_type=db_type,
126
+ auth=auth,
127
+ admin_user=admin_user,
128
+ admin_pass=admin_pass
107
129
  )
108
130
 
109
131
  def render_alembic_files(backend_path: str, schemas: dict):
@@ -0,0 +1,80 @@
1
+ from datetime import datetime, timedelta
2
+ from typing import Optional
3
+ from fastapi import APIRouter, Depends, HTTPException, status
4
+ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
5
+ from sqlalchemy.orm import Session
6
+ from passlib.context import CryptContext
7
+ from jose import JWTError, jwt
8
+ from pydantic import BaseModel
9
+ from .database import get_db
10
+ from .models import AppUser
11
+
12
+ SECRET_KEY = "super-secret-key-change-me-in-production"
13
+ ALGORITHM = "HS256"
14
+ ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days
15
+
16
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
17
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/token")
18
+
19
+ auth_router = APIRouter(prefix="/api/auth", tags=["auth"])
20
+
21
+ class Token(BaseModel):
22
+ access_token: str
23
+ token_type: str
24
+
25
+ def verify_password(plain_password, hashed_password):
26
+ return pwd_context.verify(plain_password, hashed_password)
27
+
28
+ def get_password_hash(password):
29
+ return pwd_context.hash(password)
30
+
31
+ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
32
+ to_encode = data.copy()
33
+ if expires_delta:
34
+ expire = datetime.utcnow() + expires_delta
35
+ else:
36
+ expire = datetime.utcnow() + timedelta(minutes=15)
37
+ to_encode.update({"exp": expire})
38
+ encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
39
+ return encoded_jwt
40
+
41
+ async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
42
+ credentials_exception = HTTPException(
43
+ status_code=status.HTTP_401_UNAUTHORIZED,
44
+ detail="Could not validate credentials",
45
+ headers={"WWW-Authenticate": "Bearer"},
46
+ )
47
+ try:
48
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
49
+ username: str = payload.get("sub")
50
+ if username is None:
51
+ raise credentials_exception
52
+ except JWTError:
53
+ raise credentials_exception
54
+ user = db.query(AppUser).filter(AppUser.username == username).first()
55
+ if user is None:
56
+ raise credentials_exception
57
+ return user
58
+
59
+ @auth_router.post("/token", response_model=Token)
60
+ def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
61
+ user = db.query(AppUser).filter(AppUser.username == form_data.username).first()
62
+ if not user or not verify_password(form_data.password, user.hashed_password):
63
+ raise HTTPException(
64
+ status_code=status.HTTP_401_UNAUTHORIZED,
65
+ detail="Incorrect username or password",
66
+ headers={"WWW-Authenticate": "Bearer"},
67
+ )
68
+ access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
69
+ access_token = create_access_token(
70
+ data={"sub": user.username}, expires_delta=access_token_expires
71
+ )
72
+ return {"access_token": access_token, "token_type": "bearer"}
73
+
74
+ def seed_admin_user(db: Session):
75
+ admin_user = "{{ admin_user }}"
76
+ admin_pass = "{{ admin_pass }}"
77
+ if not db.query(AppUser).first():
78
+ db_user = AppUser(username=admin_user, hashed_password=get_password_hash(admin_pass))
79
+ db.add(db_user)
80
+ db.commit()
@@ -7,8 +7,11 @@ from sqlalchemy import or_, cast, String as SAString, desc as sa_desc, asc as sa
7
7
  from sqlalchemy.orm import Session
8
8
  from typing import List, Optional
9
9
 
10
- from .database import engine, get_db
10
+ from .database import engine, get_db, SessionLocal
11
11
  from . import models, schemas
12
+ {% if auth %}
13
+ from .auth import auth_router, get_current_user, seed_admin_user
14
+ {% endif %}
12
15
 
13
16
  # Automatically create tables if they do not exist
14
17
  models.Base.metadata.create_all(bind=engine)
@@ -23,6 +26,14 @@ app.add_middleware(
23
26
  allow_headers=["*"],
24
27
  )
25
28
 
29
+ {% if auth %}
30
+ # Seed initial admin user if table is empty
31
+ with SessionLocal() as db:
32
+ seed_admin_user(db)
33
+
34
+ app.include_router(auth_router)
35
+ {% endif %}
36
+
26
37
  @app.get("/")
27
38
  def read_root():
28
39
  return {"message": "Welcome to the generated Proto-CLI API!"}
@@ -55,7 +66,8 @@ def list_{{ table_name }}(
55
66
  search: Optional[str] = Query(None, description="Search across all text columns"),
56
67
  order_by: Optional[str] = Query(None, description="Column name to sort by"),
57
68
  dir: Optional[str] = Query("asc", description="Sort direction: asc or desc"),
58
- db: Session = Depends(get_db)
69
+ db: Session = Depends(get_db){% if auth %},
70
+ current_user = Depends(get_current_user){% endif %}
59
71
  ):
60
72
  query = db.query(models.{{ model_name }})
61
73
 
@@ -79,7 +91,8 @@ def list_{{ table_name }}(
79
91
  @app.get("/api/{{ table_name }}/export")
80
92
  def export_{{ table_name }}(
81
93
  fmt: Optional[str] = Query("csv", description="Export format: csv or xlsx"),
82
- db: Session = Depends(get_db)
94
+ db: Session = Depends(get_db){% if auth %},
95
+ current_user = Depends(get_current_user){% endif %}
83
96
  ):
84
97
  records = db.query(models.{{ model_name }}).all()
85
98
  col_names = [c.name for c in models.{{ model_name }}.__table__.columns]
@@ -137,14 +150,14 @@ def export_{{ table_name }}(
137
150
  )
138
151
 
139
152
  @app.get("/api/{{ table_name }}/{id}", response_model=schemas.{{ schema_response }})
140
- def get_{{ table_name }}(id: int, db: Session = Depends(get_db)):
153
+ def get_{{ table_name }}(id: int, db: Session = Depends(get_db){% if auth %}, current_user = Depends(get_current_user){% endif %}):
141
154
  record = db.query(models.{{ model_name }}).filter(models.{{ model_name }}.id == id).first()
142
155
  if not record:
143
156
  raise HTTPException(status_code=404, detail="Record not found in {{ table_name }}")
144
157
  return record
145
158
 
146
159
  @app.post("/api/{{ table_name }}", response_model=schemas.{{ schema_response }}, status_code=status.HTTP_201_CREATED)
147
- def create_{{ table_name }}(payload: schemas.{{ schema_create }}, db: Session = Depends(get_db)):
160
+ def create_{{ table_name }}(payload: schemas.{{ schema_create }}, db: Session = Depends(get_db){% if auth %}, current_user = Depends(get_current_user){% endif %}):
148
161
  db_record = models.{{ model_name }}(**payload.model_dump())
149
162
  db.add(db_record)
150
163
  db.commit()
@@ -152,7 +165,7 @@ def create_{{ table_name }}(payload: schemas.{{ schema_create }}, db: Session =
152
165
  return db_record
153
166
 
154
167
  @app.put("/api/{{ table_name }}/{id}", response_model=schemas.{{ schema_response }})
155
- def update_{{ table_name }}(id: int, payload: schemas.{{ schema_update }}, db: Session = Depends(get_db)):
168
+ def update_{{ table_name }}(id: int, payload: schemas.{{ schema_update }}, db: Session = Depends(get_db){% if auth %}, current_user = Depends(get_current_user){% endif %}):
156
169
  db_record = db.query(models.{{ model_name }}).filter(models.{{ model_name }}.id == id).first()
157
170
  if not db_record:
158
171
  raise HTTPException(status_code=404, detail="Record not found in {{ table_name }}")
@@ -166,7 +179,7 @@ def update_{{ table_name }}(id: int, payload: schemas.{{ schema_update }}, db: S
166
179
  return db_record
167
180
 
168
181
  @app.delete("/api/{{ table_name }}/{id}", status_code=status.HTTP_204_NO_CONTENT)
169
- def delete_{{ table_name }}(id: int, db: Session = Depends(get_db)):
182
+ def delete_{{ table_name }}(id: int, db: Session = Depends(get_db){% if auth %}, current_user = Depends(get_current_user){% endif %}):
170
183
  db_record = db.query(models.{{ model_name }}).filter(models.{{ model_name }}.id == id).first()
171
184
  if not db_record:
172
185
  raise HTTPException(status_code=404, detail="Record not found in {{ table_name }}")
@@ -29,3 +29,12 @@ class {{ class_name }}(Base):
29
29
  {% endfor %}
30
30
 
31
31
  {% endfor %}
32
+
33
+ {% if auth %}
34
+ class AppUser(Base):
35
+ __tablename__ = "app_users"
36
+
37
+ id = Column(Integer, primary_key=True, index=True)
38
+ username = Column(String, unique=True, index=True, nullable=False)
39
+ hashed_password = Column(String, nullable=False)
40
+ {% endif %}
@@ -6,6 +6,11 @@ pydantic>=2.0.0
6
6
  pandas>=1.5.0
7
7
  openpyxl>=3.0.0
8
8
  email-validator>=2.0.0
9
+ {% if auth %}
10
+ passlib[bcrypt]>=1.7.4
11
+ python-jose[cryptography]>=3.3.0
12
+ python-multipart>=0.0.6
13
+ {% endif %}
9
14
  {% if db_type == 'pg' %}
10
15
  psycopg2-binary>=2.9.0
11
16
  {% elif db_type == 'mysql' %}
@@ -2,6 +2,11 @@ import React from 'react'
2
2
  import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom'
3
3
  import { Database, Table, LayoutDashboard, ChevronRight } from 'lucide-react'
4
4
  import Dashboard from './Dashboard'
5
+ {% if auth %}
6
+ import { useState } from 'react'
7
+ import Login from './pages/Login'
8
+ import { LogOut } from 'lucide-react'
9
+ {% endif %}
5
10
  {% for table_name, columns in schemas.items() %}
6
11
  import {{ table_name.replace('_', ' ').title().replace(' ', '') }}Crud from './pages/{{ table_name.replace('_', ' ').title().replace(' ', '') }}Crud'
7
12
  {% endfor %}
@@ -26,6 +31,23 @@ const SidebarLink = ({ to, children, icon: Icon }: { to: string; children: React
26
31
  }
27
32
 
28
33
  function AppContent() {
34
+ {% if auth %}
35
+ const [token, setToken] = useState<string | null>(localStorage.getItem('token'))
36
+
37
+ const handleLogin = (newToken: string) => {
38
+ localStorage.setItem('token', newToken)
39
+ setToken(newToken)
40
+ }
41
+
42
+ const handleLogout = () => {
43
+ localStorage.removeItem('token')
44
+ setToken(null)
45
+ }
46
+
47
+ if (!token) {
48
+ return <Login onLogin={handleLogin} />
49
+ }
50
+ {% endif %}
29
51
  return (
30
52
  <div className="flex min-h-screen bg-slate-950 text-slate-100 font-sans">
31
53
  {/* Sidebar */}
@@ -53,6 +75,15 @@ function AppContent() {
53
75
  </nav>
54
76
 
55
77
  <div className="mt-auto border-t border-slate-900 pt-4 px-2">
78
+ {% if auth %}
79
+ <button
80
+ onClick={handleLogout}
81
+ className="flex w-full items-center gap-2 px-4 py-2 text-sm text-red-400 hover:text-red-300 hover:bg-red-500/10 rounded-xl transition-all duration-200 mb-4"
82
+ >
83
+ <LogOut className="w-4 h-4" />
84
+ Logout
85
+ </button>
86
+ {% endif %}
56
87
  <p className="text-xs text-slate-500">FastAPI + React SPA</p>
57
88
  <p className="text-[10px] text-slate-600 mt-1">Status: <span className="text-emerald-500 font-semibold">Online</span></p>
58
89
  </div>
@@ -19,7 +19,7 @@ export default function Dashboard() {
19
19
  const fetchMetadata = async () => {
20
20
  try {
21
21
  setLoading(true)
22
- const res = await fetch(`${API_BASE}/api/metadata`)
22
+ const res = await fetch(`${API_BASE}/api/metadata`{% if auth %}, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }{% endif %})
23
23
  if (!res.ok) throw new Error('Failed to fetch system metadata')
24
24
  const data = await res.json()
25
25
  setMetadata(data)
@@ -100,7 +100,7 @@ export default function Dashboard() {
100
100
  </h3>
101
101
  <div className="h-72 w-full">
102
102
  <ResponsiveContainer width="100%" height="100%">
103
- <BarChart data={metadata} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
103
+ <BarChart data={metadata} margin={ { top: 10, right: 10, left: -20, bottom: 0 } }>
104
104
  <CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
105
105
  <XAxis
106
106
  dataKey="name"
@@ -118,10 +118,10 @@ export default function Dashboard() {
118
118
  tickFormatter={(val) => val >= 1000 ? `${(val / 1000).toFixed(1)}k` : val}
119
119
  />
120
120
  <Tooltip
121
- cursor={{ fill: '#0f172a' }}
122
- contentStyle={{ backgroundColor: '#0f172a', borderColor: '#1e293b', borderRadius: '0.75rem' }}
123
- itemStyle={{ color: '#38bdf8' }}
124
- labelStyle={{ color: '#94a3b8', textTransform: 'capitalize', marginBottom: '4px' }}
121
+ cursor={ { fill: '#0f172a' } }
122
+ contentStyle={ { backgroundColor: '#0f172a', borderColor: '#1e293b', borderRadius: '0.75rem' } }
123
+ itemStyle={ { color: '#38bdf8' } }
124
+ labelStyle={ { color: '#94a3b8', textTransform: 'capitalize', marginBottom: '4px' } }
125
125
  formatter={(value: number) => [value, 'Records']}
126
126
  labelFormatter={(label: string) => label.replace('_', ' ')}
127
127
  />
@@ -0,0 +1,106 @@
1
+ import React, { useState } from 'react';
2
+ import { Database, Lock, User, AlertCircle } from 'lucide-react';
3
+
4
+ export default function Login({ onLogin }: { onLogin: (token: str) => void }) {
5
+ const [username, setUsername] = useState('');
6
+ const [password, setPassword] = useState('');
7
+ const [error, setError] = useState('');
8
+ const [loading, setLoading] = useState(false);
9
+
10
+ const handleSubmit = async (e: React.FormEvent) => {
11
+ e.preventDefault();
12
+ setError('');
13
+ setLoading(true);
14
+
15
+ try {
16
+ const response = await fetch(`${import.meta.env.VITE_API_URL}/api/auth/token`, {
17
+ method: 'POST',
18
+ headers: {
19
+ 'Content-Type': 'application/x-www-form-urlencoded',
20
+ },
21
+ body: new URLSearchParams({
22
+ username,
23
+ password,
24
+ }),
25
+ });
26
+
27
+ if (!response.ok) {
28
+ throw new Error('Invalid credentials');
29
+ }
30
+
31
+ const data = await response.json();
32
+ onLogin(data.access_token);
33
+ } catch (err: any) {
34
+ setError(err.message || 'An error occurred during login');
35
+ } finally {
36
+ setLoading(false);
37
+ }
38
+ };
39
+
40
+ return (
41
+ <div className="min-h-screen bg-slate-950 flex items-center justify-center p-4">
42
+ <div className="w-full max-w-md">
43
+ <div className="bg-slate-900/50 backdrop-blur-xl border border-slate-800 rounded-3xl p-8 shadow-2xl">
44
+ <div className="flex flex-col items-center mb-8">
45
+ <div className="p-3 bg-brand-500/10 text-brand-400 rounded-2xl border border-brand-500/20 mb-4">
46
+ <Database className="w-8 h-8" />
47
+ </div>
48
+ <h1 className="text-2xl font-outfit font-bold text-white mb-2">Welcome Back</h1>
49
+ <p className="text-slate-400 text-sm text-center">Sign in to access your administrative dashboard.</p>
50
+ </div>
51
+
52
+ {error && (
53
+ <div className="mb-6 p-4 bg-red-500/10 border border-red-500/20 rounded-xl flex items-center gap-3 text-red-400 text-sm">
54
+ <AlertCircle className="w-5 h-5 flex-shrink-0" />
55
+ <p>{error}</p>
56
+ </div>
57
+ )}
58
+
59
+ <form onSubmit={handleSubmit} className="space-y-5">
60
+ <div>
61
+ <label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2 ml-1">Username</label>
62
+ <div className="relative">
63
+ <div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
64
+ <User className="w-5 h-5 text-slate-500" />
65
+ </div>
66
+ <input
67
+ type="text"
68
+ value={username}
69
+ onChange={(e) => setUsername(e.target.value)}
70
+ className="w-full pl-11 pr-4 py-3 bg-slate-950/50 border border-slate-800 rounded-xl text-white placeholder-slate-600 focus:outline-none focus:border-brand-500 focus:ring-1 focus:ring-brand-500 transition-all"
71
+ placeholder="Enter your username"
72
+ required
73
+ />
74
+ </div>
75
+ </div>
76
+
77
+ <div>
78
+ <label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2 ml-1">Password</label>
79
+ <div className="relative">
80
+ <div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
81
+ <Lock className="w-5 h-5 text-slate-500" />
82
+ </div>
83
+ <input
84
+ type="password"
85
+ value={password}
86
+ onChange={(e) => setPassword(e.target.value)}
87
+ className="w-full pl-11 pr-4 py-3 bg-slate-950/50 border border-slate-800 rounded-xl text-white placeholder-slate-600 focus:outline-none focus:border-brand-500 focus:ring-1 focus:ring-brand-500 transition-all"
88
+ placeholder="••••••••"
89
+ required
90
+ />
91
+ </div>
92
+ </div>
93
+
94
+ <button
95
+ type="submit"
96
+ disabled={loading}
97
+ className="w-full py-3 px-4 bg-gradient-to-r from-brand-600 to-brand-500 hover:from-brand-500 hover:to-brand-400 text-white rounded-xl font-medium shadow-lg shadow-brand-500/25 transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
98
+ >
99
+ {loading ? 'Authenticating...' : 'Sign In'}
100
+ </button>
101
+ </form>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ );
106
+ }
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useState, useCallback } from 'react'
1
+ import React, { useEffect, useState, useCallback } from 'react'
2
2
  import { useNavigate } from 'react-router-dom'
3
3
  import { Plus, Edit2, Trash2, X, ChevronLeft, ChevronRight, Loader2, Search, Download, ArrowUpDown, ArrowUp, ArrowDown, FileSpreadsheet } from 'lucide-react'
4
4
 
@@ -78,7 +78,7 @@ export default function {{ table_name.replace('_', ' ').title().replace(' ', '')
78
78
  params.set('order_by', sortCol)
79
79
  params.set('dir', sortDir)
80
80
  }
81
- const res = await fetch(`${API_BASE}/api/{{ table_name }}?${params}`)
81
+ const res = await fetch(`${API_BASE}/api/{{ table_name }}?${params}`{% if auth %}, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }{% endif %})
82
82
  if (!res.ok) throw new Error('Failed to load data from server')
83
83
  const data = await res.json()
84
84
  setRecords(data)
@@ -99,7 +99,7 @@ export default function {{ table_name.replace('_', ' ').title().replace(' ', '')
99
99
  {% for col in columns %}
100
100
  {% if col.foreign_key %}
101
101
  {% set target_table = col.foreign_key.split('.')[0] %}
102
- fetch(`${API_BASE}/api/{{ target_table }}?limit=500`)
102
+ fetch(`${API_BASE}/api/{{ target_table }}?limit=500`{% if auth %}, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }{% endif %})
103
103
  .then(res => {
104
104
  if (!res.ok) throw new Error(`Server responded ${res.status}`)
105
105
  return res.json()
@@ -182,7 +182,10 @@ export default function {{ table_name.replace('_', ' ').title().replace(' ', '')
182
182
 
183
183
  const res = await fetch(url, {
184
184
  method,
185
- headers: { 'Content-Type': 'application/json' },
185
+ headers: {
186
+ 'Content-Type': 'application/json'{% if auth %},
187
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`{% endif %}
188
+ },
186
189
  body: JSON.stringify(payload)
187
190
  })
188
191
 
@@ -200,7 +203,8 @@ export default function {{ table_name.replace('_', ' ').title().replace(' ', '')
200
203
  const handleDelete = async (id: number) => {
201
204
  try {
202
205
  const res = await fetch(`${API_BASE}/api/{{ table_name }}/${id}`, {
203
- method: 'DELETE'
206
+ method: 'DELETE'{% if auth %},
207
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }{% endif %}
204
208
  })
205
209
  if (!res.ok) throw new Error('Failed to delete record')
206
210
  setDeleteConfirmId(null)
@@ -210,8 +214,28 @@ export default function {{ table_name.replace('_', ' ').title().replace(' ', '')
210
214
  }
211
215
  }
212
216
 
213
- const handleExport = (fmt: 'csv' | 'xlsx') => {
217
+ const handleExport = async (fmt: 'csv' | 'xlsx') => {
218
+ {% if auth %}
219
+ try {
220
+ const res = await fetch(`${API_BASE}/api/{{ table_name }}/export?fmt=${fmt}`, {
221
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
222
+ })
223
+ if (!res.ok) throw new Error('Export failed')
224
+ const blob = await res.blob()
225
+ const url = window.URL.createObjectURL(blob)
226
+ const a = document.createElement('a')
227
+ a.href = url
228
+ a.download = `{{ table_name }}.${fmt}`
229
+ document.body.appendChild(a)
230
+ a.click()
231
+ a.remove()
232
+ window.URL.revokeObjectURL(url)
233
+ } catch (err: any) {
234
+ alert(err.message || 'Export failed')
235
+ }
236
+ {% else %}
214
237
  window.open(`${API_BASE}/api/{{ table_name }}/export?fmt=${fmt}`, '_blank')
238
+ {% endif %}
215
239
  }
216
240
 
217
241
  const handleSort = (colName: string) => {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sirius-cli
3
- Version: 0.1.0
3
+ Version: 0.2.2
4
4
  Summary: A rapid prototyping tool for generating containerized backend + frontend CRUD stacks from CSV/SQLite files.
5
5
  Requires-Python: >=3.8
6
6
  Description-Content-Type: text/markdown
@@ -36,11 +36,13 @@ A rapid prototyping CLI tool designed to take multiple CSV files, Excel spreadsh
36
36
 
37
37
  ## Installation
38
38
 
39
- Install the package locally in editable mode:
39
+ Install the package directly from PyPI:
40
40
  ```bash
41
- pip install -e .
41
+ pip install sirius-cli
42
42
  ```
43
43
 
44
+ *(For local development: clone the repository and run `pip install -e .`)*
45
+
44
46
  ---
45
47
 
46
48
  ## Usage: Creating a New Project (`sirius-init`)
@@ -48,16 +50,16 @@ pip install -e .
48
50
  ### 1. Generating from CSVs or Excel
49
51
  ```bash
50
52
  # From CSVs
51
- sirius-init store_system --csv examples/users.csv --csv examples/orders.csv --theme violet
53
+ sirius-init init store_system --csv examples/users.csv --csv examples/orders.csv --theme violet
52
54
 
53
55
  # From Excel
54
- sirius-init store_system --excel examples/products.xlsx --theme amber
56
+ sirius-init init store_system --excel examples/products.xlsx --theme amber
55
57
  ```
56
58
 
57
59
  ### 2. Target Production Databases
58
60
  By default, the stack uses an embedded SQLite file (`app.db`). You can target Postgres or MySQL instead:
59
61
  ```bash
60
- sirius-init billing_system --config schema.json --pg
62
+ sirius-init init billing_system --config schema.json --pg
61
63
  ```
62
64
  *(The generated `docker-compose.yml` and `database.py` will expect a `DATABASE_URL` environment variable).*
63
65
 
@@ -90,17 +92,23 @@ Specify database schemas, relationships, and colors in a JSON file:
90
92
  ```
91
93
  Then run:
92
94
  ```bash
93
- sirius-init --config examples/billing-config.json
95
+ sirius-init init billing_system --config examples/billing-config.json
96
+ ```
97
+
98
+ ### 4. Generating with JWT Authentication
99
+ Pass the `--auth` flag to scaffold a secure project. This generates an `app_users` table, a beautiful Login screen, and protects all FastAPI routes with JWT Bearer tokens automatically.
100
+ ```bash
101
+ sirius-init init secure_system --csv examples/users.csv --auth --admin-user "superadmin" --admin-pass "securepass123"
94
102
  ```
95
103
 
96
104
  ---
97
105
 
98
- ## Usage: Updating an Existing Project (`sirius-update`)
106
+ ## Usage: Updating an Existing Project (`update`)
99
107
 
100
- If your data requirements change (e.g., adding a `reviews` table to your store), you don't need to start from scratch. Use `sirius-update` to merge new schemas into an existing project.
108
+ If your data requirements change (e.g., adding a `reviews` table to your store), you don't need to start from scratch. Use the `update` command to merge new schemas into an existing project.
101
109
 
102
110
  ```bash
103
- sirius-update ./store_system --csv examples/new_reviews.csv
111
+ sirius-init update ./store_system --csv examples/new_reviews.csv
104
112
  ```
105
113
 
106
114
  Sirius-CLI will:
@@ -125,6 +133,9 @@ Sirius-CLI will:
125
133
  | `--port` | `-p` | `8000` | Backend port (used in Dockerfile, docker-compose, .env) |
126
134
  | `--api-url` | | `http://localhost:<port>` | Override the frontend VITE_API_URL |
127
135
  | `--no-seed` | | `false` | Skip seeding the DB from source CSV/Excel files |
136
+ | `--auth` | | `false` | Generate JWT authentication logic and a React Login screen |
137
+ | `--admin-user` | | `admin` | The default admin username to seed if `--auth` is enabled |
138
+ | `--admin-pass` | | `admin` | The default admin password to seed if `--auth` is enabled |
128
139
  | `--pg` | | `false` | Generate Postgres connection pool and drivers |
129
140
  | `--mysql` | | `false` | Generate MySQL connection pool and drivers |
130
141
 
@@ -14,6 +14,7 @@ sirius_cli.egg-info/requires.txt
14
14
  sirius_cli.egg-info/top_level.txt
15
15
  sirius_cli/templates/docker-compose.yml.jinja2
16
16
  sirius_cli/templates/backend/Dockerfile.jinja2
17
+ sirius_cli/templates/backend/auth.py.jinja2
17
18
  sirius_cli/templates/backend/database.py.jinja2
18
19
  sirius_cli/templates/backend/main.py.jinja2
19
20
  sirius_cli/templates/backend/models.py.jinja2
@@ -31,6 +32,7 @@ sirius_cli/templates/frontend/tsconfig.json.jinja2
31
32
  sirius_cli/templates/frontend/vite.config.ts.jinja2
32
33
  sirius_cli/templates/frontend/src/App.tsx.jinja2
33
34
  sirius_cli/templates/frontend/src/Dashboard.tsx.jinja2
35
+ sirius_cli/templates/frontend/src/Login.tsx.jinja2
34
36
  sirius_cli/templates/frontend/src/TableCrud.tsx.jinja2
35
37
  sirius_cli/templates/frontend/src/index.css.jinja2
36
38
  sirius_cli/templates/frontend/src/main.tsx.jinja2
File without changes
File without changes
File without changes