Set up production-grade JWT authentication middleware in Deno with OAuth2 provider integration and role-based access control. Learn to secure API endpoints with proper token validation, user management, and enterprise-ready authentication flows.
Prerequisites
- Deno runtime installed
- Basic TypeScript knowledge
- Understanding of JWT concepts
- OAuth2 provider applications (Google/GitHub)
What this solves
Modern web applications require secure authentication mechanisms that can scale with growing user bases and integrate with existing identity providers. This tutorial shows you how to implement JWT (JSON Web Token) authentication in Deno applications with OAuth2 integration, enabling secure API development with role-based access control and session management.
Step-by-step installation
Install Deno runtime
Install the latest Deno runtime with security features enabled. Deno provides built-in support for modern web standards including JWT handling.
curl -fsSL https://deno.land/install.sh | sh
echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
curl -fsSL https://deno.land/install.sh | sh
echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
Create project structure
Set up the directory structure for your Deno JWT authentication project with proper organization for middleware, routes, and configuration.
mkdir -p ~/deno-jwt-auth/{middleware,routes,config,utils,types}
cd ~/deno-jwt-auth
Create JWT configuration
Configure JWT settings including secret keys, expiration times, and OAuth2 provider settings. Use environment variables for sensitive configuration.
export interface AuthConfig {
jwtSecret: string;
jwtExpiration: string;
refreshTokenExpiration: string;
oauth2: {
google: {
clientId: string;
clientSecret: string;
redirectUri: string;
};
github: {
clientId: string;
clientSecret: string;
redirectUri: string;
};
};
}
export const authConfig: AuthConfig = {
jwtSecret: Deno.env.get("JWT_SECRET") || "your-super-secret-jwt-key-change-in-production",
jwtExpiration: "1h",
refreshTokenExpiration: "7d",
oauth2: {
google: {
clientId: Deno.env.get("GOOGLE_CLIENT_ID") || "",
clientSecret: Deno.env.get("GOOGLE_CLIENT_SECRET") || "",
redirectUri: Deno.env.get("GOOGLE_REDIRECT_URI") || "http://localhost:8000/auth/google/callback"
},
github: {
clientId: Deno.env.get("GITHUB_CLIENT_ID") || "",
clientSecret: Deno.env.get("GITHUB_CLIENT_SECRET") || "",
redirectUri: Deno.env.get("GITHUB_REDIRECT_URI") || "http://localhost:8000/auth/github/callback"
}
}
};
Define TypeScript interfaces
Create type definitions for users, JWT payloads, and OAuth2 responses to ensure type safety throughout your application.
export interface User {
id: string;
email: string;
name: string;
roles: string[];
provider: 'local' | 'google' | 'github';
providerId?: string;
createdAt: Date;
lastLogin?: Date;
}
export interface JWTPayload {
sub: string; // user id
email: string;
name: string;
roles: string[];
iat: number;
exp: number;
}
export interface RefreshToken {
token: string;
userId: string;
expiresAt: Date;
createdAt: Date;
}
export interface OAuth2UserInfo {
id: string;
email: string;
name: string;
picture?: string;
}
export interface AuthResponse {
accessToken: string;
refreshToken: string;
user: User;
}
Create JWT utilities
Implement JWT token generation, validation, and refresh functionality with proper error handling and security measures.
import { create, verify, decode } from "https://deno.land/x/djwt@v3.0.2/mod.ts";
import { authConfig } from "../config/auth.ts";
import type { JWTPayload, User } from "../types/auth.ts";
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(authConfig.jwtSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign", "verify"]
);
export async function generateAccessToken(user: User): Promise
Implement OAuth2 providers
Create OAuth2 integration handlers for Google and GitHub authentication with proper token exchange and user information retrieval.
import { authConfig } from "../config/auth.ts";
import type { OAuth2UserInfo } from "../types/auth.ts";
export class OAuth2Provider {
static getGoogleAuthUrl(state: string): string {
const params = new URLSearchParams({
client_id: authConfig.oauth2.google.clientId,
redirect_uri: authConfig.oauth2.google.redirectUri,
response_type: "code",
scope: "openid email profile",
state: state
});
return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
}
static getGithubAuthUrl(state: string): string {
const params = new URLSearchParams({
client_id: authConfig.oauth2.github.clientId,
redirect_uri: authConfig.oauth2.github.redirectUri,
scope: "user:email",
state: state
});
return `https://github.com/login/oauth/authorize?${params.toString()}`;
}
static async exchangeGoogleCode(code: string): Promise
Create JWT authentication middleware
Implement middleware to validate JWT tokens, extract user information, and enforce authentication requirements on protected routes.
import type { Context, Next } from "https://deno.land/x/oak@v15.0.0/mod.ts";
import { verifyAccessToken } from "../utils/jwt.ts";
import type { JWTPayload } from "../types/auth.ts";
declare module "https://deno.land/x/oak@v15.0.0/mod.ts" {
interface Context {
user?: JWTPayload;
}
}
export async function authenticateToken(ctx: Context, next: Next) {
const authHeader = ctx.request.headers.get("Authorization");
const token = authHeader?.split(" ")[1]; // Bearer TOKEN
if (!token) {
ctx.response.status = 401;
ctx.response.body = { error: "Access token required" };
return;
}
const payload = await verifyAccessToken(token);
if (!payload) {
ctx.response.status = 401;
ctx.response.body = { error: "Invalid or expired token" };
return;
}
// Check token expiration
if (payload.exp < Math.floor(Date.now() / 1000)) {
ctx.response.status = 401;
ctx.response.body = { error: "Token expired" };
return;
}
ctx.user = payload;
await next();
}
export function requireRoles(...roles: string[]) {
return async (ctx: Context, next: Next) => {
if (!ctx.user) {
ctx.response.status = 401;
ctx.response.body = { error: "Authentication required" };
return;
}
const hasRole = roles.some(role => ctx.user!.roles.includes(role));
if (!hasRole) {
ctx.response.status = 403;
ctx.response.body = { error: "Insufficient permissions" };
return;
}
await next();
};
}
export async function optionalAuth(ctx: Context, next: Next) {
const authHeader = ctx.request.headers.get("Authorization");
const token = authHeader?.split(" ")[1];
if (token) {
const payload = await verifyAccessToken(token);
if (payload && payload.exp >= Math.floor(Date.now() / 1000)) {
ctx.user = payload;
}
}
await next();
}
Create user management utilities
Implement user storage, retrieval, and management functions. This example uses in-memory storage, but you should replace with a database in production.
import type { User, RefreshToken } from "../types/auth.ts";
// In-memory storage (replace with database in production)
const users = new Map
Create authentication routes
Implement API endpoints for OAuth2 login, token refresh, logout, and user profile management with proper error handling.
import { Router } from "https://deno.land/x/oak@v15.0.0/mod.ts";
import { OAuth2Provider } from "../utils/oauth2.ts";
import { generateAccessToken, generateRefreshToken } from "../utils/jwt.ts";
import { UserService } from "../utils/users.ts";
import { authenticateToken } from "../middleware/auth.ts";
import type { AuthResponse } from "../types/auth.ts";
const router = new Router({ prefix: "/auth" });
// Generate OAuth2 authorization URLs
router.get("/google", (ctx) => {
const state = crypto.randomUUID();
// Store state in session or cache for validation
const authUrl = OAuth2Provider.getGoogleAuthUrl(state);
ctx.response.body = { authUrl, state };
});
router.get("/github", (ctx) => {
const state = crypto.randomUUID();
const authUrl = OAuth2Provider.getGithubAuthUrl(state);
ctx.response.body = { authUrl, state };
});
// OAuth2 callback handlers
router.get("/google/callback", async (ctx) => {
const code = ctx.request.url.searchParams.get("code");
const state = ctx.request.url.searchParams.get("state");
if (!code) {
ctx.response.status = 400;
ctx.response.body = { error: "Authorization code required" };
return;
}
const userInfo = await OAuth2Provider.exchangeGoogleCode(code);
if (!userInfo) {
ctx.response.status = 400;
ctx.response.body = { error: "Failed to get user information" };
return;
}
let user = await UserService.findByProvider("google", userInfo.id);
if (!user) {
user = await UserService.createUser({
email: userInfo.email,
name: userInfo.name,
roles: ["user"],
provider: "google",
providerId: userInfo.id
});
}
await UserService.updateLastLogin(user.id);
const accessToken = await generateAccessToken(user);
const refreshToken = await generateRefreshToken();
await UserService.storeRefreshToken(refreshToken, user.id);
const response: AuthResponse = {
accessToken,
refreshToken,
user
};
ctx.response.body = response;
});
router.get("/github/callback", async (ctx) => {
const code = ctx.request.url.searchParams.get("code");
if (!code) {
ctx.response.status = 400;
ctx.response.body = { error: "Authorization code required" };
return;
}
const userInfo = await OAuth2Provider.exchangeGithubCode(code);
if (!userInfo) {
ctx.response.status = 400;
ctx.response.body = { error: "Failed to get user information" };
return;
}
let user = await UserService.findByProvider("github", userInfo.id);
if (!user) {
user = await UserService.createUser({
email: userInfo.email,
name: userInfo.name,
roles: ["user"],
provider: "github",
providerId: userInfo.id
});
}
await UserService.updateLastLogin(user.id);
const accessToken = await generateAccessToken(user);
const refreshToken = await generateRefreshToken();
await UserService.storeRefreshToken(refreshToken, user.id);
const response: AuthResponse = {
accessToken,
refreshToken,
user
};
ctx.response.body = response;
});
// Token refresh endpoint
router.post("/refresh", async (ctx) => {
const body = await ctx.request.body({ type: "json" }).value;
const refreshToken = body.refreshToken;
if (!refreshToken) {
ctx.response.status = 400;
ctx.response.body = { error: "Refresh token required" };
return;
}
const userId = await UserService.validateRefreshToken(refreshToken);
if (!userId) {
ctx.response.status = 401;
ctx.response.body = { error: "Invalid or expired refresh token" };
return;
}
const user = await UserService.findById(userId);
if (!user) {
ctx.response.status = 404;
ctx.response.body = { error: "User not found" };
return;
}
const newAccessToken = await generateAccessToken(user);
const newRefreshToken = await generateRefreshToken();
await UserService.revokeRefreshToken(refreshToken);
await UserService.storeRefreshToken(newRefreshToken, user.id);
ctx.response.body = {
accessToken: newAccessToken,
refreshToken: newRefreshToken
};
});
// Logout endpoint
router.post("/logout", authenticateToken, async (ctx) => {
const body = await ctx.request.body({ type: "json" }).value;
const refreshToken = body.refreshToken;
if (refreshToken) {
await UserService.revokeRefreshToken(refreshToken);
}
ctx.response.body = { message: "Logged out successfully" };
});
// Get current user profile
router.get("/me", authenticateToken, async (ctx) => {
const user = await UserService.findById(ctx.user!.sub);
if (!user) {
ctx.response.status = 404;
ctx.response.body = { error: "User not found" };
return;
}
ctx.response.body = { user };
});
export default router;
Create protected API routes
Implement example API endpoints with role-based access control to demonstrate how to secure different parts of your application.
import { Router } from "https://deno.land/x/oak@v15.0.0/mod.ts";
import { authenticateToken, requireRoles, optionalAuth } from "../middleware/auth.ts";
const router = new Router({ prefix: "/api" });
// Public endpoint (no authentication required)
router.get("/public", (ctx) => {
ctx.response.body = {
message: "This is a public endpoint",
timestamp: new Date().toISOString()
};
});
// Endpoint with optional authentication
router.get("/optional", optionalAuth, (ctx) => {
const message = ctx.user
? `Welcome back, ${ctx.user.name}!`
: "This endpoint works for both authenticated and anonymous users";
ctx.response.body = {
message,
user: ctx.user || null,
timestamp: new Date().toISOString()
};
});
// Protected endpoint (authentication required)
router.get("/protected", authenticateToken, (ctx) => {
ctx.response.body = {
message: `Hello ${ctx.user!.name}, you are authenticated!`,
user: {
id: ctx.user!.sub,
email: ctx.user!.email,
rAutomated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Global variables
PROJECT_NAME="deno-jwt-auth"
PROJECT_DIR=""
CURRENT_USER=""
DENO_VERSION="latest"
# Usage function
usage() {
echo "Usage: $0 [OPTIONS]"
echo "Install and configure Deno JWT authentication with OAuth2 integration"
echo ""
echo "Options:"
echo " -d, --dir DIR Project directory (default: \$HOME/deno-jwt-auth)"
echo " -u, --user USER User to run as (default: current user)"
echo " -h, --help Show this help message"
echo ""
echo "Example:"
echo " $0 --dir /opt/deno-jwt-auth --user deno"
}
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Cleanup function
cleanup() {
local exit_code=$?
if [ $exit_code -ne 0 ]; then
log_error "Installation failed. Cleaning up..."
if [ -d "$PROJECT_DIR" ]; then
rm -rf "$PROJECT_DIR"
log_info "Removed project directory: $PROJECT_DIR"
fi
fi
exit $exit_code
}
# Set trap for cleanup
trap cleanup ERR
# Parse command line arguments
parse_args() {
while [[ $# -gt 0 ]]; do
case $1 in
-d|--dir)
PROJECT_DIR="$2"
shift 2
;;
-u|--user)
CURRENT_USER="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
log_error "Unknown option: $1"
usage
exit 1
;;
esac
done
}
# Auto-detect distribution and package manager
detect_distro() {
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf check-update || true"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum check-update || true"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
log_success "Detected distribution: $PRETTY_NAME using $PKG_MGR"
else
log_error "Cannot detect distribution. /etc/os-release not found."
exit 1
fi
}
# Check prerequisites
check_prerequisites() {
log_info "[1/8] Checking prerequisites..."
# Check if running as root or with sudo
if [[ $EUID -ne 0 ]]; then
if ! command -v sudo &> /dev/null; then
log_error "This script requires root privileges or sudo"
exit 1
fi
SUDO="sudo"
else
SUDO=""
fi
# Set default values
if [ -z "$CURRENT_USER" ]; then
CURRENT_USER="${SUDO_USER:-$(whoami)}"
fi
if [ -z "$PROJECT_DIR" ]; then
PROJECT_DIR="/home/$CURRENT_USER/$PROJECT_NAME"
fi
# Check if user exists
if ! id "$CURRENT_USER" &>/dev/null; then
log_error "User $CURRENT_USER does not exist"
exit 1
fi
log_success "Prerequisites check completed"
}
# Install dependencies
install_dependencies() {
log_info "[2/8] Installing system dependencies..."
$SUDO $PKG_UPDATE
$SUDO $PKG_INSTALL curl unzip
log_success "System dependencies installed"
}
# Install Deno
install_deno() {
log_info "[3/8] Installing Deno runtime..."
# Install Deno for the target user
if [ "$CURRENT_USER" != "$(whoami)" ]; then
$SUDO -u "$CURRENT_USER" bash -c 'curl -fsSL https://deno.land/install.sh | sh'
else
curl -fsSL https://deno.land/install.sh | sh
fi
# Add Deno to PATH for the user
local bashrc_path="/home/$CURRENT_USER/.bashrc"
if ! grep -q '.deno/bin' "$bashrc_path" 2>/dev/null; then
echo 'export PATH="$HOME/.deno/bin:$PATH"' >> "$bashrc_path"
chown "$CURRENT_USER:$CURRENT_USER" "$bashrc_path"
fi
log_success "Deno runtime installed"
}
# Create project structure
create_project_structure() {
log_info "[4/8] Creating project structure..."
# Create main project directory
$SUDO mkdir -p "$PROJECT_DIR"
# Create subdirectories
$SUDO mkdir -p "$PROJECT_DIR"/{middleware,routes,config,utils,types,tests}
# Set ownership
$SUDO chown -R "$CURRENT_USER:$CURRENT_USER" "$PROJECT_DIR"
# Set permissions
find "$PROJECT_DIR" -type d -exec chmod 755 {} \;
log_success "Project structure created at $PROJECT_DIR"
}
# Create configuration files
create_config_files() {
log_info "[5/8] Creating configuration files..."
# Create auth configuration
cat > "$PROJECT_DIR/config/auth.ts" << 'EOF'
export interface AuthConfig {
jwtSecret: string;
jwtExpiration: string;
refreshTokenExpiration: string;
oauth2: {
google: {
clientId: string;
clientSecret: string;
redirectUri: string;
};
github: {
clientId: string;
clientSecret: string;
redirectUri: string;
};
};
}
export const authConfig: AuthConfig = {
jwtSecret: Deno.env.get("JWT_SECRET") || "your-super-secret-jwt-key-change-in-production",
jwtExpiration: "1h",
refreshTokenExpiration: "7d",
oauth2: {
google: {
clientId: Deno.env.get("GOOGLE_CLIENT_ID") || "",
clientSecret: Deno.env.get("GOOGLE_CLIENT_SECRET") || "",
redirectUri: Deno.env.get("GOOGLE_REDIRECT_URI") || "http://localhost:8000/auth/google/callback"
},
github: {
clientId: Deno.env.get("GITHUB_CLIENT_ID") || "",
clientSecret: Deno.env.get("GITHUB_CLIENT_SECRET") || "",
redirectUri: Deno.env.get("GITHUB_REDIRECT_URI") || "http://localhost:8000/auth/github/callback"
}
}
};
EOF
# Create TypeScript interfaces
cat > "$PROJECT_DIR/types/auth.ts" << 'EOF'
export interface User {
id: string;
email: string;
name: string;
roles: string[];
provider: 'local' | 'google' | 'github';
providerId?: string;
createdAt: Date;
lastLogin?: Date;
}
export interface JWTPayload {
sub: string; // user id
email: string;
name: string;
roles: string[];
iat: number;
exp: number;
}
export interface RefreshToken {
token: string;
userId: string;
expiresAt: Date;
createdAt: Date;
}
export interface OAuth2UserInfo {
id: string;
email: string;
name: string;
picture?: string;
}
export interface AuthResponse {
accessToken: string;
refreshToken: string;
user: User;
}
EOF
# Create JWT utilities
cat > "$PROJECT_DIR/utils/jwt.ts" << 'EOF'
import { create, verify, decode } from "https://deno.land/x/djwt@v3.0.2/mod.ts";
import { authConfig } from "../config/auth.ts";
import type { JWTPayload, User } from "../types/auth.ts";
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(authConfig.jwtSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign", "verify"]
);
export async function generateAccessToken(user: User): Promise<string> {
const payload: JWTPayload = {
sub: user.id,
email: user.email,
name: user.name,
roles: user.roles,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (60 * 60) // 1 hour
};
return await create({ alg: "HS256", typ: "JWT" }, payload, key);
}
export async function verifyToken(token: string): Promise<JWTPayload | null> {
try {
const payload = await verify(token, key);
return payload as JWTPayload;
} catch {
return null;
}
}
EOF
# Create environment template
cat > "$PROJECT_DIR/.env.example" << 'EOF'
# JWT Configuration
JWT_SECRET=your-super-secret-jwt-key-change-in-production
# Google OAuth2
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GOOGLE_REDIRECT_URI=http://localhost:8000/auth/google/callback
# GitHub OAuth2
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
GITHUB_REDIRECT_URI=http://localhost:8000/auth/github/callback
# Server Configuration
PORT=8000
HOST=localhost
EOF
# Set file permissions and ownership
chown -R "$CURRENT_USER:$CURRENT_USER" "$PROJECT_DIR"
find "$PROJECT_DIR" -type f -exec chmod 644 {} \;
chmod 600 "$PROJECT_DIR/.env.example"
log_success "Configuration files created"
}
# Create sample middleware and routes
create_sample_code() {
log_info "[6/8] Creating sample middleware and routes..."
# Create JWT middleware
cat > "$PROJECT_DIR/middleware/auth.ts" << 'EOF'
import { verifyToken } from "../utils/jwt.ts";
import type { JWTPayload } from "../types/auth.ts";
export interface AuthContext {
user?: JWTPayload;
}
export async function authMiddleware(
req: Request,
ctx: AuthContext
): Promise<Response | void> {
const authHeader = req.headers.get("Authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return new Response("Unauthorized", { status: 401 });
}
const token = authHeader.slice(7);
const payload = await verifyToken(token);
if (!payload) {
return new Response("Invalid token", { status: 401 });
}
ctx.user = payload;
}
EOF
# Create basic auth routes
cat > "$PROJECT_DIR/routes/auth.ts" << 'EOF'
import { generateAccessToken } from "../utils/jwt.ts";
import type { User, AuthResponse } from "../types/auth.ts";
export async function loginHandler(req: Request): Promise<Response> {
try {
const { email, password } = await req.json();
// Placeholder user validation - implement your own logic
const user: User = {
id: "1",
email,
name: "Test User",
roles: ["user"],
provider: "local",
createdAt: new Date(),
};
const accessToken = await generateAccessToken(user);
const refreshToken = crypto.randomUUID();
const response: AuthResponse = {
accessToken,
refreshToken,
user,
};
return new Response(JSON.stringify(response), {
headers: { "Content-Type": "application/json" },
});
} catch (error) {
return new Response("Login failed", { status: 400 });
}
}
EOF
chown -R "$CURRENT_USER:$CURRENT_USER" "$PROJECT_DIR"
find "$PROJECT_DIR" -type f -name "*.ts" -exec chmod 644 {} \;
log_success "Sample code created"
}
# Create systemd service
create_systemd_service() {
log_info "[7/8] Creating systemd service..."
cat > /tmp/deno-jwt-auth.service << EOF
[Unit]
Description=Deno JWT Authentication Service
After=network.target
[Service]
Type=simple
User=$CURRENT_USER
Group=$CURRENT_USER
WorkingDirectory=$PROJECT_DIR
Environment=PATH=/home/$CURRENT_USER/.deno/bin:\$PATH
ExecStart=/home/$CURRENT_USER/.deno/bin/deno run --allow-net --allow-env --allow-read main.ts
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
$SUDO mv /tmp/deno-jwt-auth.service /etc/systemd/system/
$SUDO chmod 644 /etc/systemd/system/deno-jwt-auth.service
$SUDO systemctl daemon-reload
log_success "Systemd service created"
}
# Verification
verify_installation() {
log_info "[8/8] Verifying installation..."
# Check if Deno is installed
if $SUDO -u "$CURRENT_USER" bash -c 'export PATH="$HOME/.deno/bin:$PATH" && deno --version' &>/dev/null; then
log_success "Deno runtime is working"
else
log_warning "Deno runtime verification failed"
fi
# Check project structure
if [ -d "$PROJECT_DIR" ] && [ -f "$PROJECT_DIR/config/auth.ts" ]; then
log_success "Project structure is valid"
else
log_warning "Project structure verification failed"
fi
# Check systemd service
Review the script before running. Execute with: bash install.sh