navojit-auth 1.0.0
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.
- package/dist/index.d.mts +100 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.js +322 -0
- package/dist/index.mjs +293 -0
- package/package.json +62 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify';
|
|
2
|
+
|
|
3
|
+
interface User {
|
|
4
|
+
id: string;
|
|
5
|
+
email: string;
|
|
6
|
+
passwordHash: string;
|
|
7
|
+
orgId: string;
|
|
8
|
+
role: string;
|
|
9
|
+
}
|
|
10
|
+
interface AuthAdapter {
|
|
11
|
+
getUserByEmail(email: string, orgId: string): Promise<User | null>;
|
|
12
|
+
createUser(data: Omit<User, "id">): Promise<User>;
|
|
13
|
+
}
|
|
14
|
+
interface AuthProvider {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
handle(body: any): Promise<User | null>;
|
|
18
|
+
handleAction?(action: string, body: any): Promise<any>;
|
|
19
|
+
getRedirectUrl?(): string;
|
|
20
|
+
}
|
|
21
|
+
interface NavojitAuthConfig {
|
|
22
|
+
adapter: AuthAdapter;
|
|
23
|
+
jwtSecret: string;
|
|
24
|
+
providers: AuthProvider[];
|
|
25
|
+
}
|
|
26
|
+
declare module "@fastify/jwt" {
|
|
27
|
+
interface FastifyJWT {
|
|
28
|
+
payload: {
|
|
29
|
+
sub: string;
|
|
30
|
+
role: string;
|
|
31
|
+
orgId: string;
|
|
32
|
+
};
|
|
33
|
+
user: {
|
|
34
|
+
sub: string;
|
|
35
|
+
role: string;
|
|
36
|
+
orgId: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 1. Credentials Provider
|
|
43
|
+
* Standard Email/Password authentication using Argon2 hashing.
|
|
44
|
+
*/
|
|
45
|
+
declare class CredentialsProvider implements AuthProvider {
|
|
46
|
+
private adapter;
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
constructor(adapter: AuthAdapter);
|
|
50
|
+
handle(body: any): Promise<User | null>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 2. Google Social Provider
|
|
54
|
+
* Handles One-Click login/registration using Google OAuth2.
|
|
55
|
+
*/
|
|
56
|
+
declare class GoogleProvider implements AuthProvider {
|
|
57
|
+
private adapter;
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
private client;
|
|
61
|
+
constructor(adapter: AuthAdapter);
|
|
62
|
+
handle(body: any): Promise<User | null>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 3. OTP Provider
|
|
66
|
+
* Passwordless login with 6-digit codes sent via Real Email (Resend).
|
|
67
|
+
*/
|
|
68
|
+
declare class OTPProvider implements AuthProvider {
|
|
69
|
+
private adapter;
|
|
70
|
+
id: string;
|
|
71
|
+
name: string;
|
|
72
|
+
private otpStore;
|
|
73
|
+
constructor(adapter: AuthAdapter);
|
|
74
|
+
handleAction(action: string, body: any): Promise<any>;
|
|
75
|
+
handle(body: any): Promise<User | null>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
declare class DrizzleAdapter implements AuthAdapter {
|
|
79
|
+
getUserByEmail(email: string, orgId: string): Promise<User | null>;
|
|
80
|
+
createUser(data: Omit<User, "id">): Promise<User>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
declare const authorize: (allowedRoles: string[]) => (request: FastifyRequest, reply: FastifyReply) => Promise<undefined>;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* NavojitAuth Engine
|
|
87
|
+
* Attach this class to your Fastify instance to enable multi-tenant auth.
|
|
88
|
+
*/
|
|
89
|
+
declare class NavojitAuth {
|
|
90
|
+
private config;
|
|
91
|
+
constructor(config: NavojitAuthConfig);
|
|
92
|
+
/**
|
|
93
|
+
* Automatically registers all auth routes (login, register, refresh, logout, etc.)
|
|
94
|
+
* @param server Fastify Instance
|
|
95
|
+
* @param prefix URL prefix for auth routes (default: /auth)
|
|
96
|
+
*/
|
|
97
|
+
attach(server: FastifyInstance, prefix?: string): void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { type AuthAdapter, type AuthProvider, CredentialsProvider, DrizzleAdapter, GoogleProvider, NavojitAuth, type NavojitAuthConfig, OTPProvider, type User, authorize };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify';
|
|
2
|
+
|
|
3
|
+
interface User {
|
|
4
|
+
id: string;
|
|
5
|
+
email: string;
|
|
6
|
+
passwordHash: string;
|
|
7
|
+
orgId: string;
|
|
8
|
+
role: string;
|
|
9
|
+
}
|
|
10
|
+
interface AuthAdapter {
|
|
11
|
+
getUserByEmail(email: string, orgId: string): Promise<User | null>;
|
|
12
|
+
createUser(data: Omit<User, "id">): Promise<User>;
|
|
13
|
+
}
|
|
14
|
+
interface AuthProvider {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
handle(body: any): Promise<User | null>;
|
|
18
|
+
handleAction?(action: string, body: any): Promise<any>;
|
|
19
|
+
getRedirectUrl?(): string;
|
|
20
|
+
}
|
|
21
|
+
interface NavojitAuthConfig {
|
|
22
|
+
adapter: AuthAdapter;
|
|
23
|
+
jwtSecret: string;
|
|
24
|
+
providers: AuthProvider[];
|
|
25
|
+
}
|
|
26
|
+
declare module "@fastify/jwt" {
|
|
27
|
+
interface FastifyJWT {
|
|
28
|
+
payload: {
|
|
29
|
+
sub: string;
|
|
30
|
+
role: string;
|
|
31
|
+
orgId: string;
|
|
32
|
+
};
|
|
33
|
+
user: {
|
|
34
|
+
sub: string;
|
|
35
|
+
role: string;
|
|
36
|
+
orgId: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 1. Credentials Provider
|
|
43
|
+
* Standard Email/Password authentication using Argon2 hashing.
|
|
44
|
+
*/
|
|
45
|
+
declare class CredentialsProvider implements AuthProvider {
|
|
46
|
+
private adapter;
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
constructor(adapter: AuthAdapter);
|
|
50
|
+
handle(body: any): Promise<User | null>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 2. Google Social Provider
|
|
54
|
+
* Handles One-Click login/registration using Google OAuth2.
|
|
55
|
+
*/
|
|
56
|
+
declare class GoogleProvider implements AuthProvider {
|
|
57
|
+
private adapter;
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
private client;
|
|
61
|
+
constructor(adapter: AuthAdapter);
|
|
62
|
+
handle(body: any): Promise<User | null>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 3. OTP Provider
|
|
66
|
+
* Passwordless login with 6-digit codes sent via Real Email (Resend).
|
|
67
|
+
*/
|
|
68
|
+
declare class OTPProvider implements AuthProvider {
|
|
69
|
+
private adapter;
|
|
70
|
+
id: string;
|
|
71
|
+
name: string;
|
|
72
|
+
private otpStore;
|
|
73
|
+
constructor(adapter: AuthAdapter);
|
|
74
|
+
handleAction(action: string, body: any): Promise<any>;
|
|
75
|
+
handle(body: any): Promise<User | null>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
declare class DrizzleAdapter implements AuthAdapter {
|
|
79
|
+
getUserByEmail(email: string, orgId: string): Promise<User | null>;
|
|
80
|
+
createUser(data: Omit<User, "id">): Promise<User>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
declare const authorize: (allowedRoles: string[]) => (request: FastifyRequest, reply: FastifyReply) => Promise<undefined>;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* NavojitAuth Engine
|
|
87
|
+
* Attach this class to your Fastify instance to enable multi-tenant auth.
|
|
88
|
+
*/
|
|
89
|
+
declare class NavojitAuth {
|
|
90
|
+
private config;
|
|
91
|
+
constructor(config: NavojitAuthConfig);
|
|
92
|
+
/**
|
|
93
|
+
* Automatically registers all auth routes (login, register, refresh, logout, etc.)
|
|
94
|
+
* @param server Fastify Instance
|
|
95
|
+
* @param prefix URL prefix for auth routes (default: /auth)
|
|
96
|
+
*/
|
|
97
|
+
attach(server: FastifyInstance, prefix?: string): void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { type AuthAdapter, type AuthProvider, CredentialsProvider, DrizzleAdapter, GoogleProvider, NavojitAuth, type NavojitAuthConfig, OTPProvider, type User, authorize };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
CredentialsProvider: () => CredentialsProvider,
|
|
34
|
+
DrizzleAdapter: () => DrizzleAdapter,
|
|
35
|
+
GoogleProvider: () => GoogleProvider,
|
|
36
|
+
NavojitAuth: () => NavojitAuth,
|
|
37
|
+
OTPProvider: () => OTPProvider,
|
|
38
|
+
authorize: () => authorize
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(index_exports);
|
|
41
|
+
|
|
42
|
+
// src/db/index.ts
|
|
43
|
+
var import_postgres_js = require("drizzle-orm/postgres-js");
|
|
44
|
+
var import_postgres = __toESM(require("postgres"));
|
|
45
|
+
|
|
46
|
+
// src/db/schema.ts
|
|
47
|
+
var schema_exports = {};
|
|
48
|
+
__export(schema_exports, {
|
|
49
|
+
organizations: () => organizations,
|
|
50
|
+
refreshTokens: () => refreshTokens,
|
|
51
|
+
roleEnum: () => roleEnum,
|
|
52
|
+
users: () => users
|
|
53
|
+
});
|
|
54
|
+
var import_pg_core = require("drizzle-orm/pg-core");
|
|
55
|
+
var roleEnum = (0, import_pg_core.pgEnum)("role", ["owner", "admin", "member", "guest"]);
|
|
56
|
+
var organizations = (0, import_pg_core.pgTable)("organizations", {
|
|
57
|
+
id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
|
|
58
|
+
name: (0, import_pg_core.varchar)("name", { length: 255 }).notNull(),
|
|
59
|
+
slug: (0, import_pg_core.varchar)("slug", { length: 255 }).unique().notNull(),
|
|
60
|
+
createdAt: (0, import_pg_core.timestamp)("created_at").defaultNow().notNull()
|
|
61
|
+
});
|
|
62
|
+
var users = (0, import_pg_core.pgTable)("users", {
|
|
63
|
+
id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
|
|
64
|
+
orgId: (0, import_pg_core.uuid)("org_id").references(() => organizations.id).notNull(),
|
|
65
|
+
email: (0, import_pg_core.varchar)("email", { length: 255 }).unique().notNull(),
|
|
66
|
+
passwordHash: (0, import_pg_core.varchar)("password_hash", { length: 255 }).notNull(),
|
|
67
|
+
role: roleEnum("role").default("member").notNull(),
|
|
68
|
+
createdAt: (0, import_pg_core.timestamp)("created_at").defaultNow().notNull()
|
|
69
|
+
});
|
|
70
|
+
var refreshTokens = (0, import_pg_core.pgTable)("refresh_tokens", {
|
|
71
|
+
id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
|
|
72
|
+
userId: (0, import_pg_core.uuid)("user_id").references(() => users.id).notNull(),
|
|
73
|
+
token: (0, import_pg_core.varchar)("token", { length: 500 }).unique().notNull(),
|
|
74
|
+
revoked: (0, import_pg_core.boolean)("revoked").default(false).notNull(),
|
|
75
|
+
expiresAt: (0, import_pg_core.timestamp)("expires_at").notNull()
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// src/db/index.ts
|
|
79
|
+
var dotenv = __toESM(require("dotenv"));
|
|
80
|
+
dotenv.config();
|
|
81
|
+
var client = (0, import_postgres.default)(process.env.DATABASE_URL, { max: 1 });
|
|
82
|
+
var db = (0, import_postgres_js.drizzle)(client, { schema: schema_exports });
|
|
83
|
+
|
|
84
|
+
// src/routes/auth.ts
|
|
85
|
+
var import_drizzle_orm = require("drizzle-orm");
|
|
86
|
+
var import_argon2 = __toESM(require("argon2"));
|
|
87
|
+
var import_zod = require("zod");
|
|
88
|
+
var RegisterSchema = import_zod.z.object({
|
|
89
|
+
email: import_zod.z.string().email(),
|
|
90
|
+
password: import_zod.z.string().min(8),
|
|
91
|
+
orgName: import_zod.z.string().min(2),
|
|
92
|
+
role: import_zod.z.enum(["owner", "admin", "member", "guest"]).optional()
|
|
93
|
+
});
|
|
94
|
+
function createAuthRoutes(config2) {
|
|
95
|
+
return async function authRoutes(server) {
|
|
96
|
+
server.post("/register", async (request, reply) => {
|
|
97
|
+
const result = RegisterSchema.safeParse(request.body);
|
|
98
|
+
if (!result.success)
|
|
99
|
+
return reply.code(400).send({ errors: result.error.format() });
|
|
100
|
+
const { email, password, orgName, role } = result.data;
|
|
101
|
+
try {
|
|
102
|
+
let [org] = await db.select().from(organizations).where((0, import_drizzle_orm.eq)(organizations.name, orgName)).limit(1);
|
|
103
|
+
if (!org) {
|
|
104
|
+
[org] = await db.insert(organizations).values({
|
|
105
|
+
name: orgName,
|
|
106
|
+
slug: orgName.toLowerCase().replace(/\s/g, "-")
|
|
107
|
+
}).returning();
|
|
108
|
+
}
|
|
109
|
+
const hash = await import_argon2.default.hash(password);
|
|
110
|
+
const user = await config2.adapter.createUser({
|
|
111
|
+
email,
|
|
112
|
+
passwordHash: hash,
|
|
113
|
+
orgId: org.id,
|
|
114
|
+
role: role || "member"
|
|
115
|
+
});
|
|
116
|
+
return reply.send({ success: true, userId: user.id, orgId: org.id });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return reply.code(500).send({ error: "Registration failed", details: err.message });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
server.post("/login/:providerId", async (request, reply) => {
|
|
122
|
+
const { providerId } = request.params;
|
|
123
|
+
const provider = config2.providers.find((p) => p.id === providerId);
|
|
124
|
+
if (!provider) return reply.code(400).send({ error: "Invalid Provider" });
|
|
125
|
+
const user = await provider.handle(request.body);
|
|
126
|
+
if (!user) return reply.code(401).send({ error: "Unauthorized" });
|
|
127
|
+
const accessToken = server.jwt.sign(
|
|
128
|
+
{ sub: user.id, role: user.role, orgId: user.orgId },
|
|
129
|
+
{ expiresIn: "15m" }
|
|
130
|
+
);
|
|
131
|
+
const refreshToken = server.jwt.sign(
|
|
132
|
+
{ sub: user.id, role: user.role, orgId: user.orgId },
|
|
133
|
+
{ expiresIn: "7d" }
|
|
134
|
+
);
|
|
135
|
+
await db.insert(refreshTokens).values({
|
|
136
|
+
userId: user.id,
|
|
137
|
+
token: refreshToken,
|
|
138
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3)
|
|
139
|
+
});
|
|
140
|
+
return reply.send({
|
|
141
|
+
success: true,
|
|
142
|
+
access_token: accessToken,
|
|
143
|
+
refresh_token: refreshToken
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
server.post("/logout", async (request, reply) => {
|
|
147
|
+
const { refresh_token } = request.body;
|
|
148
|
+
if (refresh_token) {
|
|
149
|
+
await db.update(refreshTokens).set({ revoked: true }).where((0, import_drizzle_orm.eq)(refreshTokens.token, refresh_token));
|
|
150
|
+
}
|
|
151
|
+
return reply.send({
|
|
152
|
+
success: true,
|
|
153
|
+
message: "Logged out from all devices"
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/core/types.ts
|
|
160
|
+
var import_jwt = require("@fastify/jwt");
|
|
161
|
+
|
|
162
|
+
// src/core/providers.ts
|
|
163
|
+
var import_argon22 = __toESM(require("argon2"));
|
|
164
|
+
var import_google_auth_library = require("google-auth-library");
|
|
165
|
+
var import_resend = require("resend");
|
|
166
|
+
var resend = new import_resend.Resend(process.env.RESEND_API_KEY);
|
|
167
|
+
var CredentialsProvider = class {
|
|
168
|
+
constructor(adapter) {
|
|
169
|
+
this.adapter = adapter;
|
|
170
|
+
}
|
|
171
|
+
adapter;
|
|
172
|
+
id = "credentials";
|
|
173
|
+
name = "Email/Password";
|
|
174
|
+
async handle(body) {
|
|
175
|
+
const { email, password, orgId } = body;
|
|
176
|
+
const user = await this.adapter.getUserByEmail(email, orgId);
|
|
177
|
+
if (user && await import_argon22.default.verify(user.passwordHash, password)) {
|
|
178
|
+
return user;
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
var GoogleProvider = class {
|
|
184
|
+
constructor(adapter) {
|
|
185
|
+
this.adapter = adapter;
|
|
186
|
+
}
|
|
187
|
+
adapter;
|
|
188
|
+
id = "google";
|
|
189
|
+
name = "Google";
|
|
190
|
+
client = new import_google_auth_library.OAuth2Client(process.env.GOOGLE_CLIENT_ID);
|
|
191
|
+
async handle(body) {
|
|
192
|
+
const { idToken, orgId } = body;
|
|
193
|
+
try {
|
|
194
|
+
const ticket = await this.client.verifyIdToken({
|
|
195
|
+
idToken,
|
|
196
|
+
audience: process.env.GOOGLE_CLIENT_ID
|
|
197
|
+
});
|
|
198
|
+
const payload = ticket.getPayload();
|
|
199
|
+
if (!payload || !payload.email) return null;
|
|
200
|
+
let user = await this.adapter.getUserByEmail(payload.email, orgId);
|
|
201
|
+
if (!user) {
|
|
202
|
+
user = await this.adapter.createUser({
|
|
203
|
+
email: payload.email,
|
|
204
|
+
passwordHash: "SOCIAL_AUTH_EXTERNAL",
|
|
205
|
+
// Placeholder for social users
|
|
206
|
+
orgId,
|
|
207
|
+
role: "member"
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return user;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.error("[AUTH] Google Verification Failed:", error);
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
var OTPProvider = class {
|
|
218
|
+
constructor(adapter) {
|
|
219
|
+
this.adapter = adapter;
|
|
220
|
+
}
|
|
221
|
+
adapter;
|
|
222
|
+
id = "otp";
|
|
223
|
+
name = "OTP";
|
|
224
|
+
otpStore = /* @__PURE__ */ new Map();
|
|
225
|
+
async handleAction(action, body) {
|
|
226
|
+
if (action === "send") {
|
|
227
|
+
const otp = Math.floor(1e5 + Math.random() * 9e5).toString();
|
|
228
|
+
this.otpStore.set(body.email, { otp, expires: Date.now() + 3e5 });
|
|
229
|
+
if (process.env.RESEND_API_KEY) {
|
|
230
|
+
try {
|
|
231
|
+
await resend.emails.send({
|
|
232
|
+
from: "Navojit Auth <no-reply@navojit.io>",
|
|
233
|
+
// Apne verified domain se replace karein
|
|
234
|
+
to: body.email,
|
|
235
|
+
subject: "Your Security Code",
|
|
236
|
+
html: `<div style="font-family: sans-serif; padding: 20px;">
|
|
237
|
+
<h2>Verification Code</h2>
|
|
238
|
+
<p>Use the following code to sign in to your account:</p>
|
|
239
|
+
<h1 style="color: #4F46E5;">${otp}</h1>
|
|
240
|
+
<p>This code expires in 5 minutes.</p>
|
|
241
|
+
</div>`
|
|
242
|
+
});
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error("[AUTH] Email Delivery Failed:", error);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
console.log(`[AUTH] OTP for ${body.email}: ${otp}`);
|
|
248
|
+
return { success: true, message: "OTP sent successfully" };
|
|
249
|
+
}
|
|
250
|
+
return { success: false, message: "Invalid action" };
|
|
251
|
+
}
|
|
252
|
+
async handle(body) {
|
|
253
|
+
const { email, otp, orgId } = body;
|
|
254
|
+
const record = this.otpStore.get(email);
|
|
255
|
+
if (record && record.otp === otp && record.expires > Date.now()) {
|
|
256
|
+
this.otpStore.delete(email);
|
|
257
|
+
return await this.adapter.getUserByEmail(email, orgId);
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// src/adapters/drizzle.ts
|
|
264
|
+
var import_drizzle_orm2 = require("drizzle-orm");
|
|
265
|
+
var DrizzleAdapter = class {
|
|
266
|
+
async getUserByEmail(email, orgId) {
|
|
267
|
+
const result = await db.select().from(users).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(users.email, email), (0, import_drizzle_orm2.eq)(users.orgId, orgId))).limit(1);
|
|
268
|
+
return result[0] || null;
|
|
269
|
+
}
|
|
270
|
+
async createUser(data) {
|
|
271
|
+
const [newUser] = await db.insert(users).values({
|
|
272
|
+
email: data.email,
|
|
273
|
+
passwordHash: data.passwordHash,
|
|
274
|
+
orgId: data.orgId,
|
|
275
|
+
role: data.role
|
|
276
|
+
}).returning();
|
|
277
|
+
return newUser;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// src/middleware/rbac.ts
|
|
282
|
+
var authorize = (allowedRoles) => {
|
|
283
|
+
return async (request, reply) => {
|
|
284
|
+
try {
|
|
285
|
+
await request.jwtVerify();
|
|
286
|
+
const user = request.user;
|
|
287
|
+
if (!allowedRoles.includes(user.role)) {
|
|
288
|
+
return reply.code(403).send({
|
|
289
|
+
error: "Forbidden",
|
|
290
|
+
message: `Your role (${user.role}) does not have permission to access this resource.`
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
} catch (err) {
|
|
294
|
+
return reply.code(401).send({ error: "Unauthorized", message: "Invalid or expired token" });
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// src/index.ts
|
|
300
|
+
var NavojitAuth = class {
|
|
301
|
+
constructor(config2) {
|
|
302
|
+
this.config = config2;
|
|
303
|
+
}
|
|
304
|
+
config;
|
|
305
|
+
/**
|
|
306
|
+
* Automatically registers all auth routes (login, register, refresh, logout, etc.)
|
|
307
|
+
* @param server Fastify Instance
|
|
308
|
+
* @param prefix URL prefix for auth routes (default: /auth)
|
|
309
|
+
*/
|
|
310
|
+
attach(server, prefix = "/auth") {
|
|
311
|
+
server.register(createAuthRoutes(this.config), { prefix });
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
315
|
+
0 && (module.exports = {
|
|
316
|
+
CredentialsProvider,
|
|
317
|
+
DrizzleAdapter,
|
|
318
|
+
GoogleProvider,
|
|
319
|
+
NavojitAuth,
|
|
320
|
+
OTPProvider,
|
|
321
|
+
authorize
|
|
322
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/db/index.ts
|
|
8
|
+
import { drizzle } from "drizzle-orm/postgres-js";
|
|
9
|
+
import postgres from "postgres";
|
|
10
|
+
|
|
11
|
+
// src/db/schema.ts
|
|
12
|
+
var schema_exports = {};
|
|
13
|
+
__export(schema_exports, {
|
|
14
|
+
organizations: () => organizations,
|
|
15
|
+
refreshTokens: () => refreshTokens,
|
|
16
|
+
roleEnum: () => roleEnum,
|
|
17
|
+
users: () => users
|
|
18
|
+
});
|
|
19
|
+
import {
|
|
20
|
+
pgTable,
|
|
21
|
+
uuid,
|
|
22
|
+
varchar,
|
|
23
|
+
timestamp,
|
|
24
|
+
pgEnum,
|
|
25
|
+
boolean
|
|
26
|
+
} from "drizzle-orm/pg-core";
|
|
27
|
+
var roleEnum = pgEnum("role", ["owner", "admin", "member", "guest"]);
|
|
28
|
+
var organizations = pgTable("organizations", {
|
|
29
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
30
|
+
name: varchar("name", { length: 255 }).notNull(),
|
|
31
|
+
slug: varchar("slug", { length: 255 }).unique().notNull(),
|
|
32
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
33
|
+
});
|
|
34
|
+
var users = pgTable("users", {
|
|
35
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
36
|
+
orgId: uuid("org_id").references(() => organizations.id).notNull(),
|
|
37
|
+
email: varchar("email", { length: 255 }).unique().notNull(),
|
|
38
|
+
passwordHash: varchar("password_hash", { length: 255 }).notNull(),
|
|
39
|
+
role: roleEnum("role").default("member").notNull(),
|
|
40
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
41
|
+
});
|
|
42
|
+
var refreshTokens = pgTable("refresh_tokens", {
|
|
43
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
44
|
+
userId: uuid("user_id").references(() => users.id).notNull(),
|
|
45
|
+
token: varchar("token", { length: 500 }).unique().notNull(),
|
|
46
|
+
revoked: boolean("revoked").default(false).notNull(),
|
|
47
|
+
expiresAt: timestamp("expires_at").notNull()
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// src/db/index.ts
|
|
51
|
+
import * as dotenv from "dotenv";
|
|
52
|
+
dotenv.config();
|
|
53
|
+
var client = postgres(process.env.DATABASE_URL, { max: 1 });
|
|
54
|
+
var db = drizzle(client, { schema: schema_exports });
|
|
55
|
+
|
|
56
|
+
// src/routes/auth.ts
|
|
57
|
+
import { eq } from "drizzle-orm";
|
|
58
|
+
import argon2 from "argon2";
|
|
59
|
+
import { z } from "zod";
|
|
60
|
+
var RegisterSchema = z.object({
|
|
61
|
+
email: z.string().email(),
|
|
62
|
+
password: z.string().min(8),
|
|
63
|
+
orgName: z.string().min(2),
|
|
64
|
+
role: z.enum(["owner", "admin", "member", "guest"]).optional()
|
|
65
|
+
});
|
|
66
|
+
function createAuthRoutes(config2) {
|
|
67
|
+
return async function authRoutes(server) {
|
|
68
|
+
server.post("/register", async (request, reply) => {
|
|
69
|
+
const result = RegisterSchema.safeParse(request.body);
|
|
70
|
+
if (!result.success)
|
|
71
|
+
return reply.code(400).send({ errors: result.error.format() });
|
|
72
|
+
const { email, password, orgName, role } = result.data;
|
|
73
|
+
try {
|
|
74
|
+
let [org] = await db.select().from(organizations).where(eq(organizations.name, orgName)).limit(1);
|
|
75
|
+
if (!org) {
|
|
76
|
+
[org] = await db.insert(organizations).values({
|
|
77
|
+
name: orgName,
|
|
78
|
+
slug: orgName.toLowerCase().replace(/\s/g, "-")
|
|
79
|
+
}).returning();
|
|
80
|
+
}
|
|
81
|
+
const hash = await argon2.hash(password);
|
|
82
|
+
const user = await config2.adapter.createUser({
|
|
83
|
+
email,
|
|
84
|
+
passwordHash: hash,
|
|
85
|
+
orgId: org.id,
|
|
86
|
+
role: role || "member"
|
|
87
|
+
});
|
|
88
|
+
return reply.send({ success: true, userId: user.id, orgId: org.id });
|
|
89
|
+
} catch (err) {
|
|
90
|
+
return reply.code(500).send({ error: "Registration failed", details: err.message });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
server.post("/login/:providerId", async (request, reply) => {
|
|
94
|
+
const { providerId } = request.params;
|
|
95
|
+
const provider = config2.providers.find((p) => p.id === providerId);
|
|
96
|
+
if (!provider) return reply.code(400).send({ error: "Invalid Provider" });
|
|
97
|
+
const user = await provider.handle(request.body);
|
|
98
|
+
if (!user) return reply.code(401).send({ error: "Unauthorized" });
|
|
99
|
+
const accessToken = server.jwt.sign(
|
|
100
|
+
{ sub: user.id, role: user.role, orgId: user.orgId },
|
|
101
|
+
{ expiresIn: "15m" }
|
|
102
|
+
);
|
|
103
|
+
const refreshToken = server.jwt.sign(
|
|
104
|
+
{ sub: user.id, role: user.role, orgId: user.orgId },
|
|
105
|
+
{ expiresIn: "7d" }
|
|
106
|
+
);
|
|
107
|
+
await db.insert(refreshTokens).values({
|
|
108
|
+
userId: user.id,
|
|
109
|
+
token: refreshToken,
|
|
110
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3)
|
|
111
|
+
});
|
|
112
|
+
return reply.send({
|
|
113
|
+
success: true,
|
|
114
|
+
access_token: accessToken,
|
|
115
|
+
refresh_token: refreshToken
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
server.post("/logout", async (request, reply) => {
|
|
119
|
+
const { refresh_token } = request.body;
|
|
120
|
+
if (refresh_token) {
|
|
121
|
+
await db.update(refreshTokens).set({ revoked: true }).where(eq(refreshTokens.token, refresh_token));
|
|
122
|
+
}
|
|
123
|
+
return reply.send({
|
|
124
|
+
success: true,
|
|
125
|
+
message: "Logged out from all devices"
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/core/types.ts
|
|
132
|
+
import "@fastify/jwt";
|
|
133
|
+
|
|
134
|
+
// src/core/providers.ts
|
|
135
|
+
import argon22 from "argon2";
|
|
136
|
+
import { OAuth2Client } from "google-auth-library";
|
|
137
|
+
import { Resend } from "resend";
|
|
138
|
+
var resend = new Resend(process.env.RESEND_API_KEY);
|
|
139
|
+
var CredentialsProvider = class {
|
|
140
|
+
constructor(adapter) {
|
|
141
|
+
this.adapter = adapter;
|
|
142
|
+
}
|
|
143
|
+
adapter;
|
|
144
|
+
id = "credentials";
|
|
145
|
+
name = "Email/Password";
|
|
146
|
+
async handle(body) {
|
|
147
|
+
const { email, password, orgId } = body;
|
|
148
|
+
const user = await this.adapter.getUserByEmail(email, orgId);
|
|
149
|
+
if (user && await argon22.verify(user.passwordHash, password)) {
|
|
150
|
+
return user;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
var GoogleProvider = class {
|
|
156
|
+
constructor(adapter) {
|
|
157
|
+
this.adapter = adapter;
|
|
158
|
+
}
|
|
159
|
+
adapter;
|
|
160
|
+
id = "google";
|
|
161
|
+
name = "Google";
|
|
162
|
+
client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
|
|
163
|
+
async handle(body) {
|
|
164
|
+
const { idToken, orgId } = body;
|
|
165
|
+
try {
|
|
166
|
+
const ticket = await this.client.verifyIdToken({
|
|
167
|
+
idToken,
|
|
168
|
+
audience: process.env.GOOGLE_CLIENT_ID
|
|
169
|
+
});
|
|
170
|
+
const payload = ticket.getPayload();
|
|
171
|
+
if (!payload || !payload.email) return null;
|
|
172
|
+
let user = await this.adapter.getUserByEmail(payload.email, orgId);
|
|
173
|
+
if (!user) {
|
|
174
|
+
user = await this.adapter.createUser({
|
|
175
|
+
email: payload.email,
|
|
176
|
+
passwordHash: "SOCIAL_AUTH_EXTERNAL",
|
|
177
|
+
// Placeholder for social users
|
|
178
|
+
orgId,
|
|
179
|
+
role: "member"
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return user;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error("[AUTH] Google Verification Failed:", error);
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
var OTPProvider = class {
|
|
190
|
+
constructor(adapter) {
|
|
191
|
+
this.adapter = adapter;
|
|
192
|
+
}
|
|
193
|
+
adapter;
|
|
194
|
+
id = "otp";
|
|
195
|
+
name = "OTP";
|
|
196
|
+
otpStore = /* @__PURE__ */ new Map();
|
|
197
|
+
async handleAction(action, body) {
|
|
198
|
+
if (action === "send") {
|
|
199
|
+
const otp = Math.floor(1e5 + Math.random() * 9e5).toString();
|
|
200
|
+
this.otpStore.set(body.email, { otp, expires: Date.now() + 3e5 });
|
|
201
|
+
if (process.env.RESEND_API_KEY) {
|
|
202
|
+
try {
|
|
203
|
+
await resend.emails.send({
|
|
204
|
+
from: "Navojit Auth <no-reply@navojit.io>",
|
|
205
|
+
// Apne verified domain se replace karein
|
|
206
|
+
to: body.email,
|
|
207
|
+
subject: "Your Security Code",
|
|
208
|
+
html: `<div style="font-family: sans-serif; padding: 20px;">
|
|
209
|
+
<h2>Verification Code</h2>
|
|
210
|
+
<p>Use the following code to sign in to your account:</p>
|
|
211
|
+
<h1 style="color: #4F46E5;">${otp}</h1>
|
|
212
|
+
<p>This code expires in 5 minutes.</p>
|
|
213
|
+
</div>`
|
|
214
|
+
});
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.error("[AUTH] Email Delivery Failed:", error);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
console.log(`[AUTH] OTP for ${body.email}: ${otp}`);
|
|
220
|
+
return { success: true, message: "OTP sent successfully" };
|
|
221
|
+
}
|
|
222
|
+
return { success: false, message: "Invalid action" };
|
|
223
|
+
}
|
|
224
|
+
async handle(body) {
|
|
225
|
+
const { email, otp, orgId } = body;
|
|
226
|
+
const record = this.otpStore.get(email);
|
|
227
|
+
if (record && record.otp === otp && record.expires > Date.now()) {
|
|
228
|
+
this.otpStore.delete(email);
|
|
229
|
+
return await this.adapter.getUserByEmail(email, orgId);
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// src/adapters/drizzle.ts
|
|
236
|
+
import { eq as eq2, and } from "drizzle-orm";
|
|
237
|
+
var DrizzleAdapter = class {
|
|
238
|
+
async getUserByEmail(email, orgId) {
|
|
239
|
+
const result = await db.select().from(users).where(and(eq2(users.email, email), eq2(users.orgId, orgId))).limit(1);
|
|
240
|
+
return result[0] || null;
|
|
241
|
+
}
|
|
242
|
+
async createUser(data) {
|
|
243
|
+
const [newUser] = await db.insert(users).values({
|
|
244
|
+
email: data.email,
|
|
245
|
+
passwordHash: data.passwordHash,
|
|
246
|
+
orgId: data.orgId,
|
|
247
|
+
role: data.role
|
|
248
|
+
}).returning();
|
|
249
|
+
return newUser;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// src/middleware/rbac.ts
|
|
254
|
+
var authorize = (allowedRoles) => {
|
|
255
|
+
return async (request, reply) => {
|
|
256
|
+
try {
|
|
257
|
+
await request.jwtVerify();
|
|
258
|
+
const user = request.user;
|
|
259
|
+
if (!allowedRoles.includes(user.role)) {
|
|
260
|
+
return reply.code(403).send({
|
|
261
|
+
error: "Forbidden",
|
|
262
|
+
message: `Your role (${user.role}) does not have permission to access this resource.`
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
} catch (err) {
|
|
266
|
+
return reply.code(401).send({ error: "Unauthorized", message: "Invalid or expired token" });
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// src/index.ts
|
|
272
|
+
var NavojitAuth = class {
|
|
273
|
+
constructor(config2) {
|
|
274
|
+
this.config = config2;
|
|
275
|
+
}
|
|
276
|
+
config;
|
|
277
|
+
/**
|
|
278
|
+
* Automatically registers all auth routes (login, register, refresh, logout, etc.)
|
|
279
|
+
* @param server Fastify Instance
|
|
280
|
+
* @param prefix URL prefix for auth routes (default: /auth)
|
|
281
|
+
*/
|
|
282
|
+
attach(server, prefix = "/auth") {
|
|
283
|
+
server.register(createAuthRoutes(this.config), { prefix });
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
export {
|
|
287
|
+
CredentialsProvider,
|
|
288
|
+
DrizzleAdapter,
|
|
289
|
+
GoogleProvider,
|
|
290
|
+
NavojitAuth,
|
|
291
|
+
OTPProvider,
|
|
292
|
+
authorize
|
|
293
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "navojit-auth",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Blazing fast, multi-tenant global authentication engine for Fastify & Drizzle ORM.",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"dev": "tsx watch src/server.ts",
|
|
20
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
21
|
+
"prepublishOnly": "npm run build",
|
|
22
|
+
"db:generate": "drizzle-kit generate",
|
|
23
|
+
"db:push": "drizzle-kit push",
|
|
24
|
+
"db:studio": "drizzle-kit studio",
|
|
25
|
+
"lint": "tsc"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"fastify",
|
|
29
|
+
"auth",
|
|
30
|
+
"drizzle",
|
|
31
|
+
"multi-tenant",
|
|
32
|
+
"jwt",
|
|
33
|
+
"rbac",
|
|
34
|
+
"otp"
|
|
35
|
+
],
|
|
36
|
+
"author": "Kashish Singh",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"fastify": ">=4.0.0",
|
|
40
|
+
"drizzle-orm": ">=0.30.0",
|
|
41
|
+
"postgres": ">=3.4.0"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@fastify/cors": "^11.2.0",
|
|
45
|
+
"@fastify/helmet": "^13.0.2",
|
|
46
|
+
"@fastify/jwt": "^10.0.0",
|
|
47
|
+
"@fastify/rate-limit": "^10.3.0",
|
|
48
|
+
"argon2": "^0.44.0",
|
|
49
|
+
"dotenv": "^17.4.1",
|
|
50
|
+
"google-auth-library": "^10.6.2",
|
|
51
|
+
"resend": "^6.10.0",
|
|
52
|
+
"uuid": "^13.0.0",
|
|
53
|
+
"zod": "^4.3.6"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^22.0.0",
|
|
57
|
+
"drizzle-kit": "^0.31.10",
|
|
58
|
+
"tsup": "^8.3.0",
|
|
59
|
+
"tsx": "^4.19.0",
|
|
60
|
+
"typescript": "^5.7.0"
|
|
61
|
+
}
|
|
62
|
+
}
|