Skip to content

Commit 3a8af45

Browse files
author
Matthew Valancy
committed
feat: add backend OAuth provider configuration API
- Add oauth_provider_config table to SQLite schema - Implement CRUD methods in SQLiteAuthStore: - getOAuthProviderConfig() - getAllOAuthProviderConfigs() - upsertOAuthProviderConfig() - deleteOAuthProviderConfig() - Add GraphQL schema types and operations: - OAuthProviderConfig type - OAuthProviderConfigInput input - Query: oauthProviderConfigs, oauthProviderConfig - Mutation: updateOAuthProviderConfig, deleteOAuthProviderConfig - Implement admin-only resolvers with proper authorization - All operations restricted to ADMIN role users
1 parent 2feb68f commit 3a8af45

3 files changed

Lines changed: 229 additions & 0 deletions

File tree

packages/server/src/auth/sqlite-auth.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,19 @@ class SQLiteAuthStore {
262262
)
263263
`);
264264

265+
// OAuth provider configuration table for admin panel
266+
db.run(`
267+
CREATE TABLE IF NOT EXISTS oauth_provider_config (
268+
provider TEXT PRIMARY KEY, -- 'google', 'linkedin', 'github'
269+
enabled BOOLEAN DEFAULT 0,
270+
clientId TEXT,
271+
clientSecret TEXT,
272+
callbackUrl TEXT,
273+
createdAt TEXT NOT NULL,
274+
updatedAt TEXT NOT NULL
275+
)
276+
`);
277+
265278
// Shareable link access log
266279
db.run(`
267280
CREATE TABLE IF NOT EXISTS shareable_link_access (
@@ -1501,6 +1514,100 @@ class SQLiteAuthStore {
15011514
);
15021515
});
15031516
}
1517+
1518+
async getOAuthProviderConfig(provider: 'google' | 'linkedin' | 'github'): Promise<any> {
1519+
const db = await this.getDb();
1520+
1521+
return new Promise((resolve, reject) => {
1522+
db.get(
1523+
'SELECT provider, enabled, clientId, clientSecret, callbackUrl, createdAt, updatedAt FROM oauth_provider_config WHERE provider = ?',
1524+
[provider],
1525+
(err, row) => {
1526+
if (err) {
1527+
reject(err);
1528+
} else {
1529+
resolve(row || null);
1530+
}
1531+
}
1532+
);
1533+
});
1534+
}
1535+
1536+
async getAllOAuthProviderConfigs(): Promise<any[]> {
1537+
const db = await this.getDb();
1538+
1539+
return new Promise((resolve, reject) => {
1540+
db.all(
1541+
'SELECT provider, enabled, clientId, clientSecret, callbackUrl, createdAt, updatedAt FROM oauth_provider_config',
1542+
[],
1543+
(err, rows) => {
1544+
if (err) {
1545+
reject(err);
1546+
} else {
1547+
resolve(rows || []);
1548+
}
1549+
}
1550+
);
1551+
});
1552+
}
1553+
1554+
async upsertOAuthProviderConfig(config: {
1555+
provider: 'google' | 'linkedin' | 'github';
1556+
enabled: boolean;
1557+
clientId: string;
1558+
clientSecret: string;
1559+
callbackUrl: string;
1560+
}): Promise<void> {
1561+
const db = await this.getDb();
1562+
const now = new Date().toISOString();
1563+
1564+
return new Promise((resolve, reject) => {
1565+
db.run(
1566+
`INSERT INTO oauth_provider_config (provider, enabled, clientId, clientSecret, callbackUrl, createdAt, updatedAt)
1567+
VALUES (?, ?, ?, ?, ?, ?, ?)
1568+
ON CONFLICT(provider) DO UPDATE SET
1569+
enabled = excluded.enabled,
1570+
clientId = excluded.clientId,
1571+
clientSecret = excluded.clientSecret,
1572+
callbackUrl = excluded.callbackUrl,
1573+
updatedAt = excluded.updatedAt`,
1574+
[
1575+
config.provider,
1576+
config.enabled ? 1 : 0,
1577+
config.clientId,
1578+
config.clientSecret,
1579+
config.callbackUrl,
1580+
now,
1581+
now,
1582+
],
1583+
(err) => {
1584+
if (err) {
1585+
reject(err);
1586+
} else {
1587+
resolve();
1588+
}
1589+
}
1590+
);
1591+
});
1592+
}
1593+
1594+
async deleteOAuthProviderConfig(provider: 'google' | 'linkedin' | 'github'): Promise<void> {
1595+
const db = await this.getDb();
1596+
1597+
return new Promise((resolve, reject) => {
1598+
db.run(
1599+
'DELETE FROM oauth_provider_config WHERE provider = ?',
1600+
[provider],
1601+
(err) => {
1602+
if (err) {
1603+
reject(err);
1604+
} else {
1605+
resolve();
1606+
}
1607+
}
1608+
);
1609+
});
1610+
}
15041611
}
15051612

15061613
export const sqliteAuthStore = new SQLiteAuthStore();

packages/server/src/resolvers/sqlite-auth.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,51 @@ export const sqliteAuthResolvers = {
271271
console.error('❌ Get folder graphs error:', error);
272272
throw new GraphQLError('Failed to get folder graphs');
273273
}
274+
},
275+
276+
// Get all OAuth provider configurations (Admin only)
277+
oauthProviderConfigs: async (_: any, __: any, context: any) => {
278+
if (!context.user || context.user.role !== 'ADMIN') {
279+
throw new GraphQLError('Unauthorized', {
280+
extensions: { code: 'FORBIDDEN' }
281+
});
282+
}
283+
284+
try {
285+
const configs = await sqliteAuthStore.getAllOAuthProviderConfigs();
286+
return configs.map((config: any) => ({
287+
...config,
288+
enabled: Boolean(config.enabled),
289+
configured: Boolean(config.clientId && config.clientSecret)
290+
}));
291+
} catch (error: any) {
292+
console.error('❌ Get OAuth provider configs error:', error);
293+
throw new GraphQLError('Failed to get OAuth provider configurations');
294+
}
295+
},
296+
297+
// Get specific OAuth provider configuration (Admin only)
298+
oauthProviderConfig: async (_: any, { provider }: { provider: string }, context: any) => {
299+
if (!context.user || context.user.role !== 'ADMIN') {
300+
throw new GraphQLError('Unauthorized', {
301+
extensions: { code: 'FORBIDDEN' }
302+
});
303+
}
304+
305+
try {
306+
const config = await sqliteAuthStore.getOAuthProviderConfig(provider as any);
307+
if (!config) {
308+
return null;
309+
}
310+
return {
311+
...config,
312+
enabled: Boolean(config.enabled),
313+
configured: Boolean(config.clientId && config.clientSecret)
314+
};
315+
} catch (error: any) {
316+
console.error('❌ Get OAuth provider config error:', error);
317+
throw new GraphQLError('Failed to get OAuth provider configuration');
318+
}
274319
}
275320
},
276321

@@ -794,6 +839,55 @@ export const sqliteAuthResolvers = {
794839
console.error('❌ Reorder graphs error:', error);
795840
throw new GraphQLError('Failed to reorder graphs in folder');
796841
}
842+
},
843+
844+
// Update OAuth provider configuration (Admin only)
845+
updateOAuthProviderConfig: async (_: any, { input }: { input: { provider: string; enabled: boolean; clientId: string; clientSecret: string; callbackUrl: string } }, context: any) => {
846+
if (!context.user || context.user.role !== 'ADMIN') {
847+
throw new GraphQLError('Unauthorized', {
848+
extensions: { code: 'FORBIDDEN' }
849+
});
850+
}
851+
852+
try {
853+
await sqliteAuthStore.upsertOAuthProviderConfig({
854+
provider: input.provider as any,
855+
enabled: input.enabled,
856+
clientId: input.clientId,
857+
clientSecret: input.clientSecret,
858+
callbackUrl: input.callbackUrl
859+
});
860+
861+
const config = await sqliteAuthStore.getOAuthProviderConfig(input.provider as any);
862+
return {
863+
...config,
864+
enabled: Boolean(config.enabled),
865+
configured: Boolean(config.clientId && config.clientSecret)
866+
};
867+
} catch (error: any) {
868+
console.error('❌ Update OAuth provider config error:', error);
869+
throw new GraphQLError('Failed to update OAuth provider configuration');
870+
}
871+
},
872+
873+
// Delete OAuth provider configuration (Admin only)
874+
deleteOAuthProviderConfig: async (_: any, { provider }: { provider: string }, context: any) => {
875+
if (!context.user || context.user.role !== 'ADMIN') {
876+
throw new GraphQLError('Unauthorized', {
877+
extensions: { code: 'FORBIDDEN' }
878+
});
879+
}
880+
881+
try {
882+
await sqliteAuthStore.deleteOAuthProviderConfig(provider as any);
883+
return {
884+
success: true,
885+
message: 'OAuth provider configuration deleted successfully'
886+
};
887+
} catch (error: any) {
888+
console.error('❌ Delete OAuth provider config error:', error);
889+
throw new GraphQLError('Failed to delete OAuth provider configuration');
890+
}
797891
}
798892
}
799893
};

packages/server/src/schema/auth-schema.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,26 @@ export const authTypeDefs = gql`
141141
code: String!
142142
}
143143
144+
# OAuth Provider Configuration (Admin Only)
145+
type OAuthProviderConfig {
146+
provider: String!
147+
enabled: Boolean!
148+
clientId: String
149+
clientSecret: String
150+
callbackUrl: String!
151+
configured: Boolean!
152+
createdAt: String
153+
updatedAt: String
154+
}
155+
156+
input OAuthProviderConfigInput {
157+
provider: String!
158+
enabled: Boolean!
159+
clientId: String!
160+
clientSecret: String!
161+
callbackUrl: String!
162+
}
163+
144164
type Query {
145165
# Get current user from JWT token
146166
me: User
@@ -172,6 +192,10 @@ export const authTypeDefs = gql`
172192
173193
# Get OAuth providers for current user
174194
myOAuthProviders: [OAuthProvider!]!
195+
196+
# Get OAuth provider configurations (Admin only)
197+
oauthProviderConfigs: [OAuthProviderConfig!]!
198+
oauthProviderConfig(provider: String!): OAuthProviderConfig
175199
}
176200
177201
type Mutation {
@@ -239,6 +263,10 @@ export const authTypeDefs = gql`
239263
# OAuth mutations
240264
oauthLogin(input: OAuthLoginInput!): AuthPayload!
241265
unlinkOAuthProvider(provider: String!): MessageResponse!
266+
267+
# OAuth provider configuration mutations (Admin only)
268+
updateOAuthProviderConfig(input: OAuthProviderConfigInput!): OAuthProviderConfig!
269+
deleteOAuthProviderConfig(provider: String!): MessageResponse!
242270
}
243271
244272
input GraphOrderInput {

0 commit comments

Comments
 (0)